diff --git a/wolverine/a-frame-architecture/FinacialApi/FinancialApi.http b/wolverine/a-frame-architecture/FinacialApi/FinancialApi.http deleted file mode 100644 index 59806ef..0000000 --- a/wolverine/a-frame-architecture/FinacialApi/FinancialApi.http +++ /dev/null @@ -1,6 +0,0 @@ -@FinancialApi_HostAddress = https://localhost:7010 - -GET {{FinancialApi_HostAddress}}/weatherforecast/ -Accept: application/json - -### diff --git a/wolverine/a-frame-architecture/FinacialApi/Program.cs b/wolverine/a-frame-architecture/FinacialApi/Program.cs deleted file mode 100644 index 124a24e..0000000 --- a/wolverine/a-frame-architecture/FinacialApi/Program.cs +++ /dev/null @@ -1,45 +0,0 @@ -using FinancialApi.ServiceDefaults; - -var builder = WebApplication.CreateBuilder(args); - -// Add services to the container. -// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi -builder.Services.AddOpenApi(); - -builder.AddServiceDefaults(); - -var app = builder.Build(); - -// Configure the HTTP request pipeline. -if (app.Environment.IsDevelopment()) -{ - app.MapOpenApi(); -} - -app.UseHttpsRedirection(); - -var summaries = new[] -{ - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" -}; - -app.MapGet("/weatherforecast", () => - { - var forecast = Enumerable.Range(1, 5).Select(index => - new WeatherForecast - ( - DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - Random.Shared.Next(-20, 55), - summaries[Random.Shared.Next(summaries.Length)] - )) - .ToArray(); - return forecast; - }) - .WithName("GetWeatherForecast"); - -app.Run(); - -record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) -{ - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); -} \ No newline at end of file diff --git a/wolverine/a-frame-architecture/FinancialApi.Application/Class1.cs b/wolverine/a-frame-architecture/FinancialApi.Application/Class1.cs new file mode 100644 index 0000000..2687d92 --- /dev/null +++ b/wolverine/a-frame-architecture/FinancialApi.Application/Class1.cs @@ -0,0 +1,16 @@ +namespace FinancialApi.Application; + +public record Account(int Id, decimal Balance) +{ + public Account ApplyFee(decimal amount) + { + if (amount <= 0) + throw new ArgumentException("Fee must be positive."); + + if (Balance - amount < 0) + throw new InvalidOperationException("Insufficient funds."); + + // Returns a brand new record with the updated balance + return this with { Balance = Balance - amount }; + } +} \ No newline at end of file diff --git a/wolverine/a-frame-architecture/FinancialApi.Application/FinancialApi.Application.csproj b/wolverine/a-frame-architecture/FinancialApi.Application/FinancialApi.Application.csproj new file mode 100644 index 0000000..237d661 --- /dev/null +++ b/wolverine/a-frame-architecture/FinancialApi.Application/FinancialApi.Application.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + diff --git a/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Class1.cs b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Class1.cs new file mode 100644 index 0000000..f0a2233 --- /dev/null +++ b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Class1.cs @@ -0,0 +1,24 @@ +using FinancialApi.Application; +using Microsoft.EntityFrameworkCore; + +namespace FinancialApi.Infrastructure; + +public class AccountDbContext : DbContext +{ + public AccountDbContext(DbContextOptions options) : base(options) + { + } + + public DbSet Accounts => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasKey(x => x.Id); + + modelBuilder.Entity().HasData( + new Account(1, 100.00m), + new Account(2, 500.00m) + ); + } +} \ No newline at end of file diff --git a/wolverine/a-frame-architecture/FinancialApi.Infrastructure/FinancialApi.Infrastructure.csproj b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/FinancialApi.Infrastructure.csproj new file mode 100644 index 0000000..5a597d6 --- /dev/null +++ b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/FinancialApi.Infrastructure.csproj @@ -0,0 +1,21 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + + + + diff --git a/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/20260713152041_InitialCreate.Designer.cs b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/20260713152041_InitialCreate.Designer.cs new file mode 100644 index 0000000..b101970 --- /dev/null +++ b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/20260713152041_InitialCreate.Designer.cs @@ -0,0 +1,50 @@ +// +using FinancialApi.Infrastructure; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace FinancialApi.Infrastructure.Migrations +{ + [DbContext(typeof(AccountDbContext))] + [Migration("20260713152041_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.9"); + + modelBuilder.Entity("FinancialApi.Application.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Balance") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Accounts"); + + b.HasData( + new + { + Id = 1, + Balance = 100.00m + }, + new + { + Id = 2, + Balance = 500.00m + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/20260713152041_InitialCreate.cs b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/20260713152041_InitialCreate.cs new file mode 100644 index 0000000..b5cfec2 --- /dev/null +++ b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/20260713152041_InitialCreate.cs @@ -0,0 +1,45 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace FinancialApi.Infrastructure.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Accounts", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Balance = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Accounts", x => x.Id); + }); + + migrationBuilder.InsertData( + table: "Accounts", + columns: new[] { "Id", "Balance" }, + values: new object[,] + { + { 1, 100.00m }, + { 2, 500.00m } + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Accounts"); + } + } +} diff --git a/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/AccountDbContextModelSnapshot.cs b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/AccountDbContextModelSnapshot.cs new file mode 100644 index 0000000..1f84a63 --- /dev/null +++ b/wolverine/a-frame-architecture/FinancialApi.Infrastructure/Migrations/AccountDbContextModelSnapshot.cs @@ -0,0 +1,47 @@ +// +using FinancialApi.Infrastructure; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace FinancialApi.Infrastructure.Migrations +{ + [DbContext(typeof(AccountDbContext))] + partial class AccountDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.9"); + + modelBuilder.Entity("FinancialApi.Application.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Balance") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Accounts"); + + b.HasData( + new + { + Id = 1, + Balance = 100.00m + }, + new + { + Id = 2, + Balance = 500.00m + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/wolverine/a-frame-architecture/FinancialApi/ApplyFeeHandler.cs b/wolverine/a-frame-architecture/FinancialApi/ApplyFeeHandler.cs new file mode 100644 index 0000000..47adfd2 --- /dev/null +++ b/wolverine/a-frame-architecture/FinancialApi/ApplyFeeHandler.cs @@ -0,0 +1,25 @@ +using FinancialApi.Application; +using FinancialApi.Infrastructure; +using Microsoft.EntityFrameworkCore; + +namespace FinancialApi; + +public static class ApplyFeeHandler +{ + public static Task LoadAsync(ApplyFeeCommand cmd, AccountDbContext db) + => db.Set().AsNoTracking() + .FirstOrDefaultAsync(x => x.Id == cmd.AccountId); + + public static (Account UpdatedAccount, FeeAppliedEvent Event) Handle(ApplyFeeCommand cmd, Account account) + { + var updatedAccount = account.ApplyFee(cmd.Amount); + + var @event = new FeeAppliedEvent(account.Id, cmd.Amount); + + return (updatedAccount, @event); + } +} + +public record ApplyFeeCommand(int AccountId, decimal Amount); + +public record FeeAppliedEvent(int AccountId, decimal Amount); \ No newline at end of file diff --git a/wolverine/a-frame-architecture/FinacialApi/FinacialApi.csproj b/wolverine/a-frame-architecture/FinancialApi/FinancialApi.csproj similarity index 54% rename from wolverine/a-frame-architecture/FinacialApi/FinacialApi.csproj rename to wolverine/a-frame-architecture/FinancialApi/FinancialApi.csproj index aad38d9..510d782 100644 --- a/wolverine/a-frame-architecture/FinacialApi/FinacialApi.csproj +++ b/wolverine/a-frame-architecture/FinancialApi/FinancialApi.csproj @@ -8,9 +8,14 @@ + + + + + diff --git a/wolverine/a-frame-architecture/FinancialApi/FinancialApi.http b/wolverine/a-frame-architecture/FinancialApi/FinancialApi.http new file mode 100644 index 0000000..c4c3e4c --- /dev/null +++ b/wolverine/a-frame-architecture/FinancialApi/FinancialApi.http @@ -0,0 +1,17 @@ +@FinancialApi_HostAddress = https://localhost:7010 + +GET {{FinancialApi_HostAddress}}/weatherforecast/ +Accept: application/json + +### + +POST {{FinancialApi_HostAddress}}/accounts/apply-fee +Accept: application/json +Content-Type: application/json + +{ + "accountId": 1, + "amount": 5.0 +} + +### diff --git a/wolverine/a-frame-architecture/FinancialApi/Program.cs b/wolverine/a-frame-architecture/FinancialApi/Program.cs new file mode 100644 index 0000000..4a512a0 --- /dev/null +++ b/wolverine/a-frame-architecture/FinancialApi/Program.cs @@ -0,0 +1,50 @@ +using FinancialApi; +using FinancialApi.Infrastructure; +using FinancialApi.ServiceDefaults; +using JasperFx.CodeGeneration; +using Microsoft.EntityFrameworkCore; +using Wolverine; +using Wolverine.EntityFrameworkCore; +using Wolverine.Sqlite; + + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddDbContext(options => + options.UseSqlite("Data Source=demo.db")); + +// Add services to the container. +// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi +builder.Services.AddOpenApi(); + +builder.AddServiceDefaults(); + +builder.Host.UseWolverine(opts => +{ + opts.PersistMessagesWithSqlite("Data Source=demo.db"); + opts.UseEntityFrameworkCoreTransactions(); +}); + +var app = builder.Build(); + +using (var scope = app.Services.CreateScope()) +{ + var db = scope.ServiceProvider.GetRequiredService(); + await db.Database.MigrateAsync(); +} + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +app.UseHttpsRedirection(); + +app.MapPost("/accounts/apply-fee", async (ApplyFeeCommand cmd, IMessageBus bus) => +{ + await bus.InvokeAsync(cmd); + return Results.Accepted(); +}); + +app.Run(); diff --git a/wolverine/a-frame-architecture/FinacialApi/Properties/launchSettings.json b/wolverine/a-frame-architecture/FinancialApi/Properties/launchSettings.json similarity index 100% rename from wolverine/a-frame-architecture/FinacialApi/Properties/launchSettings.json rename to wolverine/a-frame-architecture/FinancialApi/Properties/launchSettings.json diff --git a/wolverine/a-frame-architecture/FinacialApi/appsettings.Development.json b/wolverine/a-frame-architecture/FinancialApi/appsettings.Development.json similarity index 100% rename from wolverine/a-frame-architecture/FinacialApi/appsettings.Development.json rename to wolverine/a-frame-architecture/FinancialApi/appsettings.Development.json diff --git a/wolverine/a-frame-architecture/FinacialApi/appsettings.json b/wolverine/a-frame-architecture/FinancialApi/appsettings.json similarity index 100% rename from wolverine/a-frame-architecture/FinacialApi/appsettings.json rename to wolverine/a-frame-architecture/FinancialApi/appsettings.json diff --git a/wolverine/a-frame-architecture/FinancialApi/demo.db b/wolverine/a-frame-architecture/FinancialApi/demo.db new file mode 100644 index 0000000..4410bda Binary files /dev/null and b/wolverine/a-frame-architecture/FinancialApi/demo.db differ diff --git a/wolverine/a-frame-architecture/FinancialApi/demo.db-shm b/wolverine/a-frame-architecture/FinancialApi/demo.db-shm new file mode 100644 index 0000000..beb79bc Binary files /dev/null and b/wolverine/a-frame-architecture/FinancialApi/demo.db-shm differ diff --git a/wolverine/a-frame-architecture/FinancialApi/demo.db-wal b/wolverine/a-frame-architecture/FinancialApi/demo.db-wal new file mode 100644 index 0000000..0c41723 Binary files /dev/null and b/wolverine/a-frame-architecture/FinancialApi/demo.db-wal differ diff --git a/wolverine/a-frame-architecture/a-frame-architecture.sln b/wolverine/a-frame-architecture/a-frame-architecture.sln index 6b57ec0..ab94823 100644 --- a/wolverine/a-frame-architecture/a-frame-architecture.sln +++ b/wolverine/a-frame-architecture/a-frame-architecture.sln @@ -2,7 +2,11 @@ Microsoft Visual Studio Solution File, Format Version 12.00 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinancialApi.ServiceDefaults", "FinancialApi.ServiceDefaults\FinancialApi.ServiceDefaults.csproj", "{D9767D77-0B2D-4653-9B9C-A0E6A3850EA3}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinacialApi", "FinacialApi\FinacialApi.csproj", "{9EFD8279-869B-485C-BA02-C983E96348A0}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinancialApi", "FinancialApi\FinancialApi.csproj", "{9EFD8279-869B-485C-BA02-C983E96348A0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinancialApi.Application", "FinancialApi.Application\FinancialApi.Application.csproj", "{B7EB1ECE-9F7F-440B-8818-28A127A99915}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FinancialApi.Infrastructure", "FinancialApi.Infrastructure\FinancialApi.Infrastructure.csproj", "{6AB36AB5-05BD-41C8-9498-63EBDFAF8114}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -18,5 +22,13 @@ Global {9EFD8279-869B-485C-BA02-C983E96348A0}.Debug|Any CPU.Build.0 = Debug|Any CPU {9EFD8279-869B-485C-BA02-C983E96348A0}.Release|Any CPU.ActiveCfg = Release|Any CPU {9EFD8279-869B-485C-BA02-C983E96348A0}.Release|Any CPU.Build.0 = Release|Any CPU + {B7EB1ECE-9F7F-440B-8818-28A127A99915}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B7EB1ECE-9F7F-440B-8818-28A127A99915}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B7EB1ECE-9F7F-440B-8818-28A127A99915}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B7EB1ECE-9F7F-440B-8818-28A127A99915}.Release|Any CPU.Build.0 = Release|Any CPU + {6AB36AB5-05BD-41C8-9498-63EBDFAF8114}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6AB36AB5-05BD-41C8-9498-63EBDFAF8114}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6AB36AB5-05BD-41C8-9498-63EBDFAF8114}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6AB36AB5-05BD-41C8-9498-63EBDFAF8114}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal