From 212684ccca72c707d0a2a8f72db38d25c08065c0 Mon Sep 17 00:00:00 2001 From: Oli Sturm Date: Wed, 22 Apr 2026 15:07:54 +0100 Subject: [PATCH] add fp2 sample --- csharp-fp2/Application/AccountApplication.cs | 46 ++++++++++++++++++++ csharp-fp2/Domain/Account.cs | 42 ++++++++++++++++++ csharp-fp2/Domain/Money.cs | 8 ++++ csharp-fp2/Infrastructure/InMemoryAccount.cs | 41 +++++++++++++++++ csharp-fp2/Program.cs | 18 ++++++++ csharp-fp2/csharp-fp2.csproj | 9 ++++ csharp.sln | 15 +++++++ 7 files changed, 179 insertions(+) create mode 100644 csharp-fp2/Application/AccountApplication.cs create mode 100644 csharp-fp2/Domain/Account.cs create mode 100644 csharp-fp2/Domain/Money.cs create mode 100644 csharp-fp2/Infrastructure/InMemoryAccount.cs create mode 100644 csharp-fp2/Program.cs create mode 100644 csharp-fp2/csharp-fp2.csproj diff --git a/csharp-fp2/Application/AccountApplication.cs b/csharp-fp2/Application/AccountApplication.cs new file mode 100644 index 0000000..7db2052 --- /dev/null +++ b/csharp-fp2/Application/AccountApplication.cs @@ -0,0 +1,46 @@ +using CsharpFp2.Domain; +using CsharpFp2.Infrastructure; + +namespace CsharpFp2.Application; + +// delegate type is optional, but nice to illustrate +public delegate WithdrawResult WithdrawMoney(Guid accountId, decimal amount); + +public static class AccountApplication +{ + public static WithdrawMoney CreateWithdrawMoney( + LoadAccount loadAccount, + SaveAccount saveAccount + ) + { + return (accountId, amount) => + { + var account = + loadAccount(accountId) ?? throw new InvalidOperationException("Account not found."); + + Console.WriteLine( + $"[App] Account loaded. Current balance: {account.Balance.Amount:0.00}" + ); + Console.WriteLine($"[App] Executing withdrawal of {amount:0.00}..."); + + switch (AccountDomain.Withdraw(account, new Money(amount))) + { + case WithdrawResult.Success s: + { + Console.WriteLine( + $"[App] Withdrawal applied. New balance: {s.Account.Balance.Amount:0.00}" + ); + saveAccount(s.Account); + Console.WriteLine("[App] Account persisted."); + return s; + } + + case var other: + { + Console.Error.WriteLine($"[App Error] Withdrawal failed. Error: {other}"); + return other; + } + } + }; + } +} diff --git a/csharp-fp2/Domain/Account.cs b/csharp-fp2/Domain/Account.cs new file mode 100644 index 0000000..c1159fd --- /dev/null +++ b/csharp-fp2/Domain/Account.cs @@ -0,0 +1,42 @@ +namespace CsharpFp2.Domain; + +public sealed record Account(Guid Id, Money Balance); + +public abstract record WithdrawResult +{ + public sealed record Success(Account Account) : WithdrawResult; + + public sealed record AmountMustBePositive : WithdrawResult; + + public sealed record InsufficientBalance(Money balance, Money amount) : WithdrawResult; +} + +public static class AccountDomain +{ + public static Account Open(Guid id, Money openingBalance) + { + // Assuming we don't intend to allow invalid open balances to be passed + // here, we can view this as a technical (i.e. dev-time) error and + // stick to an exception. + if (openingBalance.Amount < 0) + throw new ArgumentOutOfRangeException(nameof(openingBalance)); + + return new Account(id, openingBalance); + } + + public static WithdrawResult Withdraw(Account account, Money amount) + { + if (amount.Amount <= 0) + return new WithdrawResult.AmountMustBePositive(); + + if (account.Balance.Amount < amount.Amount) + return new WithdrawResult.InsufficientBalance(account.Balance, amount); + + return new WithdrawResult.Success( + account with + { + Balance = account.Balance.Subtract(amount), + } + ); + } +} diff --git a/csharp-fp2/Domain/Money.cs b/csharp-fp2/Domain/Money.cs new file mode 100644 index 0000000..b99df38 --- /dev/null +++ b/csharp-fp2/Domain/Money.cs @@ -0,0 +1,8 @@ +namespace CsharpFp2.Domain; + +public sealed record Money(decimal Amount) +{ + public Money Add(Money other) => new(Amount + other.Amount); + + public Money Subtract(Money other) => new(Amount - other.Amount); +} diff --git a/csharp-fp2/Infrastructure/InMemoryAccount.cs b/csharp-fp2/Infrastructure/InMemoryAccount.cs new file mode 100644 index 0000000..fbbc270 --- /dev/null +++ b/csharp-fp2/Infrastructure/InMemoryAccount.cs @@ -0,0 +1,41 @@ +using CsharpFp2.Domain; + +namespace CsharpFp2.Infrastructure; + +// If we don't like working with generic delegates directly, we can +// create custom named delegate types. +public delegate Account? LoadAccount(Guid id); + +public delegate void SaveAccount(Account accunt); + +// If we don't want to use tuples, or you really miss the idea of a combined "interface", +// we can create a named container +// public sealed record AccountPersistence(LoadAccount Load, SaveAccount Save); + +public static class InMemoryAccount +{ + public static (LoadAccount, SaveAccount) Create() + { + Dictionary store = new(); + + Account? GetById(Guid id) + { + var found = store.TryGetValue(id, out var account); + Console.WriteLine( + found ? $"[Repo] Loaded account {id}" : $"[Repo] Account {id} not found" + ); + + return found ? account : null; + } + + void Save(Account account) + { + store[account.Id] = account; + Console.WriteLine( + $"[Repo] Saved account {account.Id} with balance {account.Balance.Amount:0.00}" + ); + } + + return (GetById, Save); + } +} diff --git a/csharp-fp2/Program.cs b/csharp-fp2/Program.cs new file mode 100644 index 0000000..41dc490 --- /dev/null +++ b/csharp-fp2/Program.cs @@ -0,0 +1,18 @@ +using CsharpFp2.Application; +using CsharpFp2.Domain; +using CsharpFp2.Infrastructure; + +Console.WriteLine("[csharp-fp2] Starting withdraw money demo..."); + +var (loadAccount, saveAccount) = InMemoryAccount.Create(); +var withdrawMoney = AccountApplication.CreateWithdrawMoney(loadAccount, saveAccount); + +var accountId = Guid.NewGuid(); +Console.WriteLine($"[csharp-fp2] Seeding account {accountId} with opening balance 200.00"); +saveAccount(new Account(accountId, new Money(200m))); + +decimal amount = 100m; +Console.WriteLine($"[csharp-fp2] Executing withdrawal {amount:0.00} from account {accountId}"); +withdrawMoney(accountId, amount); + +Console.WriteLine("[csharp-fp2] Demo completed."); diff --git a/csharp-fp2/csharp-fp2.csproj b/csharp-fp2/csharp-fp2.csproj new file mode 100644 index 0000000..d888ae7 --- /dev/null +++ b/csharp-fp2/csharp-fp2.csproj @@ -0,0 +1,9 @@ + + + Exe + net10.0 + CsharpFp2 + enable + enable + + diff --git a/csharp.sln b/csharp.sln index f30bd40..e5de18a 100644 --- a/csharp.sln +++ b/csharp.sln @@ -17,6 +17,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "csharp-oop-simplified2", "c EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "csharp-fp1", "csharp-fp1\csharp-fp1.csproj", "{C9D46510-994A-4C43-BA8F-33CA9BED79D0}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "csharp-fp2", "csharp-fp2\csharp-fp2.csproj", "{10A6FD3E-117A-4C9E-98E2-DAC31E7CE78A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -87,6 +89,18 @@ Global {C9D46510-994A-4C43-BA8F-33CA9BED79D0}.Release|x64.Build.0 = Release|Any CPU {C9D46510-994A-4C43-BA8F-33CA9BED79D0}.Release|x86.ActiveCfg = Release|Any CPU {C9D46510-994A-4C43-BA8F-33CA9BED79D0}.Release|x86.Build.0 = Release|Any CPU + {10A6FD3E-117A-4C9E-98E2-DAC31E7CE78A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {10A6FD3E-117A-4C9E-98E2-DAC31E7CE78A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {10A6FD3E-117A-4C9E-98E2-DAC31E7CE78A}.Debug|x64.ActiveCfg = Debug|Any CPU + {10A6FD3E-117A-4C9E-98E2-DAC31E7CE78A}.Debug|x64.Build.0 = Debug|Any CPU + {10A6FD3E-117A-4C9E-98E2-DAC31E7CE78A}.Debug|x86.ActiveCfg = Debug|Any CPU + {10A6FD3E-117A-4C9E-98E2-DAC31E7CE78A}.Debug|x86.Build.0 = Debug|Any CPU + {10A6FD3E-117A-4C9E-98E2-DAC31E7CE78A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {10A6FD3E-117A-4C9E-98E2-DAC31E7CE78A}.Release|Any CPU.Build.0 = Release|Any CPU + {10A6FD3E-117A-4C9E-98E2-DAC31E7CE78A}.Release|x64.ActiveCfg = Release|Any CPU + {10A6FD3E-117A-4C9E-98E2-DAC31E7CE78A}.Release|x64.Build.0 = Release|Any CPU + {10A6FD3E-117A-4C9E-98E2-DAC31E7CE78A}.Release|x86.ActiveCfg = Release|Any CPU + {10A6FD3E-117A-4C9E-98E2-DAC31E7CE78A}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -97,5 +111,6 @@ Global {561BCA96-76DF-4A33-B767-4AD7CD3246B4} = {CEC0EC27-0CEC-90C6-CABA-E58AB278E4DA} {7237398A-2E8B-4161-BA15-DB090395A1F2} = {CEC0EC27-0CEC-90C6-CABA-E58AB278E4DA} {C9D46510-994A-4C43-BA8F-33CA9BED79D0} = {89C53051-77F9-4C1C-ABE5-6D54AB398471} + {10A6FD3E-117A-4C9E-98E2-DAC31E7CE78A} = {89C53051-77F9-4C1C-ABE5-6D54AB398471} EndGlobalSection EndGlobal