Event sourcing in .NET

Did you ever wonder how banks are storing information about all the events are going on on their site, how the changes that were made on your bank account are stored and retrieved and how it could be integrated with other systems?

To be clear – I’ve never worked on bank systems but when I first time heard about Event sourcing it was like a revelation – that’s how they do this!

Just a quick and quite nice explanation about what the event sourcing is:

We can query an application’s state to find out the current state of the world, and this answers many questions. However there are times when we don’t just want to see where we are, we also want to know how we got there.

Event Sourcing ensures that all changes to application state are stored as a sequence of events. Not just can we query these events, we can also use the event log to reconstruct past states, and as a foundation to automatically adjust the state to cope with retroactive changes.

Martin Fowler – https://martinfowler.com/eaaDev/EventSourcing.html

To write this post I was inspired by the presentation of Jakub Pilimon (http://pillopl.github.io/) about refactoring to code Event Sourcing. After watching this video I already knew that I want to try to replicate this in C# for two reasons:

  1. Only watching videos doesn’t really help me with memorizing the knowledge,
  2. I was curious how it would look like in C#, what changes it would require and if there are similar tools that can be so easily used.

Let’s start coding! Let’s prepare the tests!

Jakub in his presentation used an example of Credit Card. As we know credit card is being used to withdraw money and it has to be repaid in future (that’s the worst part 😥). Let’s draw some requirements, just like Jakub has prepared it before the lecture.

Scenario: Can assign a limit to card
	Given new credit card
	When user assign a limit to 50
	Then limit should be 50

Scenario: Can not assign a limit to card second time
	Given new credit card
	When user assign a limit to 50
	And user assign a limit to 70
	Then should throw an illegal exception

Scenario: Can not withdraw when the amount is not in the limit
	Given new credit card
	When user assign a limit to 50
	And user withdraw 70
	Then should throw an illegal exception

Scenario: Can not withdraw after 45th in one cycle
	Given new credit card
	When the user withdraw 1 for 45 times
	And user withdraw 1
	Then should throw an illegal exception

Scenario: Can withdraw from the card
	Given new credit card
	When user assign a limit to 100
	And user withdraw 50
	Then available limit should equal 50

Scenario: Can withdraw in the next cycle
	Given new credit card
	When user assign a limit to 50
	And user withdraw 1 for 45 times
	And there is a new cycle
	And user withdraw 1
	Then limit should be 49

Scenario: Can repay the card
	Given new credit card
	When user assign a limit to 50
	And user withdraw 10
	And user repay 10
	Then limit should be 50

Okay, now as we have our requirements we can start coding. For starting I made some additional requirement that I would like to try as well – BDD.

I didn’t have an experience before with BDD, so I had to start from beginner level, however, SpecFlow – Getting Started is a great resource how you can start and integrate behaviour testing to your solution.

After creating a solution and SpecFlow project, just copy the requirements to CreditCard.feature file. SpecFlow should generate an implementation file for you behind the scenes and you should see something like on the picture. You could already spot the difference in font colour between Scenario and any other line. This is because the SpecFlow didn’t found the implementation for those lines. To fix that you need to manually create a new item – SpecFlow Step Definitions. If you can’t find this then you might be missing SpecFlow extension to Visual Studio (unfortunately at this moment Rider doesn’t have this extension).

Here is minimal boilerplate for Steps Definitions

using TechTalk.SpecFlow;

namespace Bank.BDD.Steps
{
     [Binding]
     public sealed class StepDefinition
     {
          private readonly ScenarioContext _scenarioContext;
          public StepDefinition(ScenarioContext scenarioContext)
          {
               _scenarioContext = scenarioContext;
          }
     }
}

At this point you are ready to implement logic for the the requirements. There are two ways of doing that of which I’m aware

  1. Manually create methods with attributes – Given/When/Then
  2. (preferred) Generate methods based on feature file.

For the first one you can just create a method and add attribue, just like on example below:

          [Given(@"new credit card")]
          public void GivenNewCreditCard()
          {
               _creditCard = new CreditCard(Guid.NewGuid());
          }

Generating methods is much simpler and it will automatically pick up any variables like numbers from your requirements.

This is how easily you can generate the implementation logic using a SpecFlow

Here is my implementation for the required behaviour. Besides SpecFlow I installed NUnit and Should packages.

using System;
using Bank.Models;
using Should;
using TechTalk.SpecFlow;

namespace Bank.BDD.Steps
{
     [Binding]
     public sealed class CreditCardSteps
     {
          private readonly ScenarioContext _scenarioContext;
          private CreditCard _creditCard;
          private Exception _exception;

          public CreditCardSteps(ScenarioContext scenarioContext)
          {
               _scenarioContext = scenarioContext;
          }

          [Given(@"new credit card")]
          public void GivenNewCreditCard()
          {
               _creditCard = new CreditCard(Guid.NewGuid());
          }

          [When(@"user assign a limit to (.*)")]
          public void WhenUserAssignLimitTo(int limit)
          {

               HandleInvalidOperationException(() => _creditCard.AssignLimit(limit));
          }

          [Then(@"limit should be (.*)")]
          public void ThenLimitShouldBe(int limit)
          {
               _creditCard.AvailableLimit().ShouldEqual(limit);
          }

          [Then(@"should throw an illegal exception")]
          public void ThenShouldThrowIllegalException()
          {
               _exception.ShouldBeType<InvalidOperationException>();
               _exception = null;
          }

          [When(@"user withdraw (.*)")]
          public void WhenUserWithdraw(int amount)
          {
               GivenUserWithdrawForTimes(amount, 1);
          }

          [When(@"the user withdraw (.*) for (.*) times")]
          public void GivenUserWithdrawForTimes(int withdrawAmount, int numberOfTimes)
          {
               HandleInvalidOperationException(()
                    => numberOfTimes.Times(()
                         => _creditCard.Withdraw(withdrawAmount)));
          }

          [Then(@"available limit should equal (.*)")]
          public void ThenAvailableLimitShouldEqual(int limit)
          {
               _creditCard.AvailableLimit().ShouldEqual(limit);
          }

          [When(@"there is a new cycle")]
          public void WhenThereIsNewCycle()
          {
               _creditCard.CycleClose();
          }

          [When(@"user repay (.*)")]
          public void WhenUserRepay(int amount)
          {
               _creditCard.Repay(amount);
          }

          private void HandleInvalidOperationException(Action action)
          {
               try
               {
                    action();
               } catch(InvalidOperationException e)
               {
                    _exception = e;
               }
          }
        
          [AfterScenario]
          public void AfterScenarioHook()
          {
               if(_exception != null)
               {
                    throw _exception;
               }
          }
     }
}

If you took a look into the above code you could spot private method HandleInvalidOperationException. This is unfortunately required because SpecFlow doesn’t have any way that I’m aware to handle exceptions from code, so if there is an exception thrown it will stop the execution. This is a worth to mention drawback of using this library, because if you have implemented some global handlers to the exception then it can be a bit tricky to get it working with SpecFlow.

At this point your code can still not compile because it will need a small extension method I’ve created to simplify code. You can find it below.

using System;

namespace Bank.BDD.Steps
{
     public static class NumberExtensions{
          public static void Times<T>(this T input, Action action )
               where T : struct, IComparable, IComparable<T>,
               IConvertible, IEquatable<T>, IFormattable
          {
               var number = input as int? ?? 0;
               for(var i = 0; i < number; i++)
               {
                    action();
               }
          }
     }
}

At this point generate CreditCard class with all used methods, build the solution and run tests. The solution should build without any issues and tests should fail.

Your CreditCard model should look like this at this moment

 public class CreditCard
     {
          private Guid _id;

          public CreditCard(Guid id)
          {
               _id = id;
          }

          public void Repay(in decimal amount)
          {
               throw new NotImplementedException();
          }

          public void CycleClose()
          {
               throw new NotImplementedException();
          }

          public decimal AvailableLimit()
          {
               throw new NotImplementedException();
          }

          public void Withdraw(in decimal withdrawAmount)
          {
               throw new NotImplementedException();
          }

          public void AssignLimit(in decimal limit)
          {
               throw new NotImplementedException();
          }
     }

Now try to implement CreditCard logic to pass all tests, if you stuck take a look at Jakub presentation. I will add my implementation at part 2 of this article.

Leave a comment

Your email address will not be published. Required fields are marked *