From 3e9f0b56c93690be45dff4fa6646b212e509316e Mon Sep 17 00:00:00 2001 From: Oli Sturm Date: Tue, 21 Apr 2026 14:43:06 +0100 Subject: [PATCH] initial --- .gitignore | 58 +++++++++++++++++++ .../Application/WithdrawMoneyHandler.cs | 39 +++++++++++++ csharp-oop/Contracts/WithdrawMoneyCommand.cs | 8 +++ csharp-oop/Domain/Account.cs | 35 +++++++++++ csharp-oop/Domain/AccountId.cs | 4 ++ csharp-oop/Domain/AggregateRoot.cs | 7 +++ csharp-oop/Domain/IAccountRepository.cs | 8 +++ csharp-oop/Domain/Money.cs | 33 +++++++++++ .../InMemoryAccountRepository.cs | 27 +++++++++ csharp-oop/Program.cs | 30 ++++++++++ csharp-oop/csharp-oop.csproj | 9 +++ 11 files changed, 258 insertions(+) create mode 100644 .gitignore create mode 100644 csharp-oop/Application/WithdrawMoneyHandler.cs create mode 100644 csharp-oop/Contracts/WithdrawMoneyCommand.cs create mode 100644 csharp-oop/Domain/Account.cs create mode 100644 csharp-oop/Domain/AccountId.cs create mode 100644 csharp-oop/Domain/AggregateRoot.cs create mode 100644 csharp-oop/Domain/IAccountRepository.cs create mode 100644 csharp-oop/Domain/Money.cs create mode 100644 csharp-oop/Infrastructure/InMemoryAccountRepository.cs create mode 100644 csharp-oop/Program.cs create mode 100644 csharp-oop/csharp-oop.csproj diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..30fc14b --- /dev/null +++ b/.gitignore @@ -0,0 +1,58 @@ +## A streamlined .gitignore for modern .NET projects +## including temporary files, build results, and +## files generated by popular .NET tools. If you are +## developing with Visual Studio, the VS .gitignore +## https://github.com/github/gitignore/blob/main/VisualStudio.gitignore +## has more thorough IDE-specific entries. +## +## Get latest from https://github.com/github/gitignore/blob/main/Dotnet.gitignore + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg + +# Others +~$* +*~ +CodeCoverage/ + +# MSBuild Binary and Structured Log +*.binlog + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +.idea +.vscode + diff --git a/csharp-oop/Application/WithdrawMoneyHandler.cs b/csharp-oop/Application/WithdrawMoneyHandler.cs new file mode 100644 index 0000000..26af40f --- /dev/null +++ b/csharp-oop/Application/WithdrawMoneyHandler.cs @@ -0,0 +1,39 @@ +using CsharpOop.Contracts; +using CsharpOop.Domain; + +namespace CsharpOop.Applications; + +/// Application-layer handler orchestrating the use case +public sealed class WithdrawMoneyHandler +{ + private readonly IAccountRepository _repository; + + public WithdrawMoneyHandler(IAccountRepository repository) + { + _repository = repository; + } + + public void Handle(WithdrawMoneyCommand command) + { + Console.WriteLine( + $"[App] Handling WithdrawMoneyCommand for account {command.AccountId}, amount {command.Amount:0.00}" + ); + + var accountId = new AccountId(command.AccountId); + var amount = new Money(command.Amount); + + var account = + _repository.GetById(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.Amount:0.00}..."); + + account.Withdraw(amount); + + Console.WriteLine($"[App] Withdrawal applied. New balance: {account.Balance.Amount:0.00}"); + + _repository.Save(account); + Console.WriteLine("[App] Account persisted."); + } +} diff --git a/csharp-oop/Contracts/WithdrawMoneyCommand.cs b/csharp-oop/Contracts/WithdrawMoneyCommand.cs new file mode 100644 index 0000000..65bf0ee --- /dev/null +++ b/csharp-oop/Contracts/WithdrawMoneyCommand.cs @@ -0,0 +1,8 @@ +namespace CsharpOop.Contracts; + +/// Command DTO representing the requested use case +public sealed class WithdrawMoneyCommand +{ + public Guid AccountId { get; init; } + public decimal Amount { get; init; } +} diff --git a/csharp-oop/Domain/Account.cs b/csharp-oop/Domain/Account.cs new file mode 100644 index 0000000..0996839 --- /dev/null +++ b/csharp-oop/Domain/Account.cs @@ -0,0 +1,35 @@ +namespace CsharpOop.Domain; + +/// Domain entity / aggregate root representing a bank account +public sealed class Account : AggregateRoot +{ + public Money Balance { get; private set; } + + // Often present to satisfy serializers / ORMs. + private Account() + { + Balance = new Money(0); + } + + public Account(AccountId id, Money openingBalance) + { + if (openingBalance.Amount < 0) + throw new ArgumentOutOfRangeException(nameof(openingBalance)); + + Id = id; + + Balance = openingBalance; + } + + // Domain behaviour attached to the entity. + public void Withdraw(Money amount) + { + if (amount.Amount <= 0) + throw new InvalidOperationException("Withdrawal amount must be positive."); + + if (Balance.Amount - amount.Amount < 0) + throw new InvalidOperationException("Balance cannot go below zero."); + + Balance = Balance.Subtract(amount); + } +} diff --git a/csharp-oop/Domain/AccountId.cs b/csharp-oop/Domain/AccountId.cs new file mode 100644 index 0000000..39450ea --- /dev/null +++ b/csharp-oop/Domain/AccountId.cs @@ -0,0 +1,4 @@ +namespace CsharpOop.Domain; + +/// Value object used to wrap the aggregate identity +public sealed record AccountId(Guid Value); diff --git a/csharp-oop/Domain/AggregateRoot.cs b/csharp-oop/Domain/AggregateRoot.cs new file mode 100644 index 0000000..e4e083b --- /dev/null +++ b/csharp-oop/Domain/AggregateRoot.cs @@ -0,0 +1,7 @@ +namespace CsharpOop.Domain; + +/// Conventional DDD base type for aggregates +public abstract class AggregateRoot +{ + public TId Id { get; protected set; } = default!; +} diff --git a/csharp-oop/Domain/IAccountRepository.cs b/csharp-oop/Domain/IAccountRepository.cs new file mode 100644 index 0000000..47cbfbb --- /dev/null +++ b/csharp-oop/Domain/IAccountRepository.cs @@ -0,0 +1,8 @@ +namespace CsharpOop.Domain; + +/// Repository abstraction used to load and save accounts +public interface IAccountRepository +{ + Account? GetById(AccountId id); + void Save(Account account); +} diff --git a/csharp-oop/Domain/Money.cs b/csharp-oop/Domain/Money.cs new file mode 100644 index 0000000..f4b8021 --- /dev/null +++ b/csharp-oop/Domain/Money.cs @@ -0,0 +1,33 @@ +namespace CsharpOop.Domain; + +/// Value object used to represent money and enforce simple invariants. +/// Note that this implementation uses immutable patterns for the data +/// by returning a new instance for each modification. This is an early +/// recommendation for DDD with OO, but not necessarily the common practice +/// in many real-world implementations. +public sealed class Money +{ + // Potentially with a setter - see note above + public decimal Amount { get; } + + public Money(decimal amount) + { + Amount = amount; + } + + // In many existing DDD/OO codebases you may actually see the use + // of mutable value types. + // + // public void Add(Money other) + // { + // this.Amount += other.Amount; + // } + + // On the other hand, sometimes these helpers may be left out + // and operations encoded directly "from the outside": + // newBalance = new Money(oldBalance.Amount - charge.Amount) + // + public Money Add(Money other) => new(Amount + other.Amount); + + public Money Subtract(Money other) => new(Amount - other.Amount); +} diff --git a/csharp-oop/Infrastructure/InMemoryAccountRepository.cs b/csharp-oop/Infrastructure/InMemoryAccountRepository.cs new file mode 100644 index 0000000..8b6ff1c --- /dev/null +++ b/csharp-oop/Infrastructure/InMemoryAccountRepository.cs @@ -0,0 +1,27 @@ +using CsharpOop.Domain; + +namespace CsharpOop.Infrastructure; + +/// Simple in-memory repository for demonstration +public class InMemoryAccountRepository : IAccountRepository +{ + private readonly Dictionary _accounts = new(); + + public Account? GetById(AccountId id) + { + var found = _accounts.TryGetValue(id, out var account); + Console.WriteLine( + found ? $"[Repo] Loaded account {id.Value}" : $"[Repo] Account {id.Value} not found" + ); + + return found ? account : null; + } + + public void Save(Account account) + { + _accounts[account.Id] = account; + Console.WriteLine( + $"[Repo] Saved account {account.Id.Value} with balance {account.Balance.Amount:0.00}" + ); + } +} diff --git a/csharp-oop/Program.cs b/csharp-oop/Program.cs new file mode 100644 index 0000000..39cb2a8 --- /dev/null +++ b/csharp-oop/Program.cs @@ -0,0 +1,30 @@ +using CsharpOop.Applications; +using CsharpOop.Contracts; +using CsharpOop.Domain; +using CsharpOop.Infrastructure; + +namespace CsharpOop; + +public class Program +{ + public static void Main() + { + Console.WriteLine("[Program] Starting withdraw money demo..."); + + var repository = new InMemoryAccountRepository(); + var handler = new WithdrawMoneyHandler(repository); + + var accountId = Guid.NewGuid(); + Console.WriteLine($"[Program] Seeding account {accountId} with opening balance 200.00"); + repository.Save(new Account(new AccountId(accountId), new Money(200m))); + + var command = new WithdrawMoneyCommand { AccountId = accountId, Amount = 100m }; + Console.WriteLine( + $"[Program] Dispatching command: withdraw {command.Amount:0.00} from account {command.AccountId}" + ); + + handler.Handle(command); + + Console.WriteLine("[Program] Demo completed."); + } +} diff --git a/csharp-oop/csharp-oop.csproj b/csharp-oop/csharp-oop.csproj new file mode 100644 index 0000000..481f3d6 --- /dev/null +++ b/csharp-oop/csharp-oop.csproj @@ -0,0 +1,9 @@ + + + Exe + net10.0 + csharp_oop + enable + enable + +