Skip to content

Instantly share code, notes, and snippets.

@dmmusil
Forked from akhansari/EsBankAccount.cs
Created July 26, 2022 00:53
Show Gist options
  • Select an option

  • Save dmmusil/c103aff32b72dd9ebd77c00a4b984439 to your computer and use it in GitHub Desktop.

Select an option

Save dmmusil/c103aff32b72dd9ebd77c00a4b984439 to your computer and use it in GitHub Desktop.
C# prototype of Decider pattern. F# version: https://github.com/akhansari/EsBankAccount
namespace EsBankAccount
{
// events
public interface IBankAccountEvent { }
public record Transaction(decimal Amount, DateTime Date);
public record Deposited(Transaction Transaction) : IBankAccountEvent;
public record Withdrawn(Transaction Transaction) : IBankAccountEvent;
public record Closed(DateTime Date) : IBankAccountEvent;
// commands
public interface IBankAccountCommand { }
public record Deposit(decimal Amount, DateTime Date) : IBankAccountCommand;
public record Withdraw(decimal Amount, DateTime Date) : IBankAccountCommand;
public record Close(DateTime Date) : IBankAccountCommand;
public static class BankAccount
{
public static readonly IReadOnlyCollection<IBankAccountEvent> NoHistory = new List<IBankAccountEvent>();
public static IReadOnlyCollection<IBankAccountEvent> Singleton(this IBankAccountEvent e) =>
new List<IBankAccountEvent>(1) { e };
// handle state
public record State(decimal Balance, bool IsClosed);
private static readonly State InitialState = new(0, false);
private static State Evolve(State state, IBankAccountEvent @event) =>
@event switch
{
Deposited deposited => state with { Balance = state.Balance + deposited.Transaction.Amount },
Withdrawn withdrawn => state with { Balance = state.Balance - withdrawn.Transaction.Amount },
Closed _ => state with { IsClosed = true },
_ => state
};
public static bool IsTerminal(State state) => state.IsClosed;
public static State Fold(this IEnumerable<IBankAccountEvent> history, State state) =>
history.Aggregate(state, Evolve);
public static State Fold(this IEnumerable<IBankAccountEvent> history) =>
history.Fold(InitialState);
// handle commands
private static IReadOnlyCollection<IBankAccountEvent> Deposit(Deposit c) =>
new Deposited(new(c.Amount, c.Date)).Singleton();
private static IReadOnlyCollection<IBankAccountEvent> Withdraw(Withdraw c) =>
new Withdrawn(new(c.Amount, c.Date)).Singleton();
private static IReadOnlyCollection<IBankAccountEvent> Close(State state, Close c)
{
var events = new List<IBankAccountEvent>();
if (state.Balance > 0)
events.Add(new Withdrawn(new(state.Balance, c.Date)));
events.Add(new Closed(c.Date));
return events;
}
public static IReadOnlyCollection<IBankAccountEvent> Decide(this State state, IBankAccountCommand command) =>
command switch
{
Deposit c => Deposit(c),
Withdraw c => Withdraw(c),
Close c => Close(state, c),
_ => throw new NotImplementedException()
};
}
}
namespace EsBankAccount.Tests
{
public class BankAccount_Should
{
[Fact]
public void Make_a_deposit() => BankAccount
.NoHistory.Fold()
.Decide(new Deposit(5, DateTime.MinValue))
.Should()
.Equal(new Deposited(new(5, DateTime.MinValue)).Singleton());
[Fact]
public void Make_a_withdrawal() => BankAccount
.NoHistory.Fold()
.Decide(new Withdraw(5, DateTime.MinValue))
.Should()
.Equal(new Withdrawn(new(5, DateTime.MinValue)).Singleton());
[Fact]
public void Close_the_account_and_withdraw_the_remaining_amount() =>
new List<IBankAccountEvent>()
{
new Deposited(new(5, DateTime.MinValue)),
new Deposited(new(5, DateTime.MinValue))
}
.Fold()
.Decide(new Close(DateTime.MinValue))
.Should()
.Equal(new List<IBankAccountEvent>
{
new Withdrawn(new(10, DateTime.MinValue)),
new Closed(DateTime.MinValue)
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment