Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@akhansari
Last active March 9, 2024 08:53
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save akhansari/c2d57470d10aacd04aae71258c52bfc1 to your computer and use it in GitHub Desktop.
Save akhansari/c2d57470d10aacd04aae71258c52bfc1 to your computer and use it in GitHub Desktop.
C# prototype of the Decider pattern. (F# version: https://github.com/akhansari/EsBankAccount)
namespace EsBankAccount.Account;
using Events = IReadOnlyCollection<IEvent>;
public record Transaction(decimal Amount, DateTime Date);
// events
public interface IEvent { } // used to mimic a discriminated union
public record Deposited(Transaction Transaction) : IEvent;
public record Withdrawn(Transaction Transaction) : IEvent;
public record Closed(DateTime Date) : IEvent;
// commands
public interface ICommand { }
public record Deposit(decimal Amount, DateTime Date) : ICommand;
public record Withdraw(decimal Amount, DateTime Date) : ICommand;
public record Close(DateTime Date) : ICommand;
public record State(decimal Balance, bool IsClosed)
{
public static readonly State Initial = new(0, false);
}
public static class Decider
{
// handle state
private static State Evolve(State state, IEvent @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 State Fold(this IEnumerable<IEvent> history, State state) =>
history.Aggregate(state, Evolve);
public static State Fold(this IEnumerable<IEvent> history) =>
history.Fold(State.Initial);
public static bool IsTerminal(this State state) => state.IsClosed;
// handle commands
public static Events Decide(this State state, ICommand command) =>
command switch
{
Deposit c => Deposit(c),
Withdraw c => Withdraw(c),
Close c => Close(state, c),
_ => throw new NotImplementedException()
};
private static Events Deposit(Deposit c) =>
new Deposited(new(c.Amount, c.Date)).Singleton();
private static Events Withdraw(Withdraw c) =>
new Withdrawn(new(c.Amount, c.Date)).Singleton();
private static Events Close(State state, Close c)
{
var events = new List<IEvent>();
if (state.Balance > 0)
events.Add(new Withdrawn(new(state.Balance, c.Date)));
events.Add(new Closed(c.Date));
return events;
}
// helpers
public static Events Singleton(this IEvent e) => new IEvent[1] { e };
}
namespace EsBankAccount.Tests;
using EsBankAccount.Account;
public class AccountShould
{
[Fact]
public void Make_a_deposit() =>
State.Initial
.Decide(new Deposit(5, DateTime.MinValue))
.Should()
.Equal(new Deposited(new(5, DateTime.MinValue)).Singleton());
[Fact]
public void Make_a_withdrawal() =>
State.Initial
.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 IEvent[]
{
new Deposited(new(5, DateTime.MinValue)),
new Deposited(new(5, DateTime.MinValue))
}
.Fold()
.Decide(new Close(DateTime.MinValue))
.Should()
.Equal(new IEvent[]
{
new Withdrawn(new(10, DateTime.MinValue)),
new Closed(DateTime.MinValue)
});
}
@cjjohansen
Copy link

cjjohansen commented Feb 26, 2024

Hi Amin From where do you get the .Should() method ? Fluent Assertions ?

@akhansari
Copy link
Author

Hi Christian, yes Fluent Assertions is used here. I forgot I had activated implicit usings.

@cjjohansen
Copy link

Thx Amin got it working. Terrific example!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment