EfCore.GenericEventRunner: an event-driven library that works with EF Core

Last Updated: December 2, 2019 | Created: December 1, 2019

In the first article I described an event-driven architecture that work with Entity Framework Core (EF Core). In this article I go into the details of how to use the EfCore.GenericEventRunner that implements this event-driven design. This article covers the specific details of why and how to use this library.

NOTE: The EfCore.GenericEventRunner is an open-source (MIT licence) NuGet library designed to work with EF Core 3 and above. You can also find the code in this GitHub repo.

The articles in this series are:

TL;DR; – summary

  • This article describes how to use the EfCore.GenericEventRunner library, available on NuGet  and on GitHub.
  • EfCore.GenericEventRunner adds a specific event-driven system to EF Core. See this article for a description of this event-driven design.
  • I break up the description into five sections
    • Code to allow your EF Core classes to send events
    • How to build event handlers
    • How the events are run when SaveChanges/SaveChangesAsync is called.
    • How to register your event handlers and GenericEventRunner itself.
    • How to unit test an application which uses GenericEventRunner.

Overview of EfCore.GenericEventRunner library

I’m going to go though the four parts of the EfCore.GenericEventRunner library (plus something on unit testing) to demonstrate how to use this library. I start with a diagram which will give you an idea of how you might use GenericEventRunner. Then I will dive into the four parts.

NOTE: If you haven’t read the first article, then I recommend you read/skim that article – it might make understanding what I am trying to do.

In the diagram the blue rectangles are classes mapped to the database, with the events shown in light color at the bottom. The orange rounded rectangle is an event handler.

Here are the four parts of the library, plus a section on unit testing:

  1. ForEntities: This has the code that allows a class to contain and create events.
  2. ForHandlers: This contains the interfaces for building handlers.
  3. ForDbContext: The DbContextWithEvents<T> which contains the overriding of the SaveChanges/ SaveChangesAsync.
  4. The code for registering your event handlers and GenericEventRunner’s EventsRunner.
  5. How to unit test an application which uses GenericEventRunner (and logging).

NOTE: This code in is article taken from the code in the EfCore.GenericEventRunner repo used to test the library. I suggest you look at that code and the unit tests to see how it works.

1. ForEntities: code for your entity classes (see DataLayer in GenericEventRunner repo)

For this example, I am going to show you how I built the “1. Create new Order” (LHS of last diagram). The purpose of this event is to query the stock part a) is there enough stock to manage this order, and b) allocate some stock ready for this order.

The first thing I needed is an “allocate” event. An event is class that inherits the IDomainEvent interface. Here is my “allocate” event.

public class AllocateProductEvent : IDomainEvent
{
    public AllocateProductEvent(string productName, int numOrdered)
    {
        ProductName = productName;
        NumOrdered = numOrdered;
    }

    public string ProductName { get; }
    public int NumOrdered { get; }
}

In this case this event is sent from a new order which hasn’t been saved to the database. Therefore, I have to send the ProductName (which in my system is a unique key) and the number ordered because its not (yet) in the main database. Even if the data is in the database, I recommend sending the data in the event, as a) it saves a database access and b) it reduces the likelihood of concurrency issues (I’ll talk more on concurrency issues in the next article).

The next thing is to add that event to the Order class. To be able to do that the Order class must inherit abstract class called EntityEvents, e.g.

public class Order : EntityEvents
{ 
       //… rest of class left out

The EntityEvents class provides an AddEvent method which allows you to add a new event to your entity. It also stores the events for the Event Runner to look at when SaveChanges is called. (note that the events aren’t saved to the database – they only hang around as long as the class exists).

Below is the Order constructor, with the focus on the AllocateProductEvent – see the highlighted lines 10 and 11

public Order(string userId, DateTime expectedDispatchDate,
    ICollection<BasketItemDto> orderLines)
{
    //… some other code removed

    TotalPriceNoTax = 0;
    foreach (var basketItem in orderLines)
    {
        TotalPriceNoTax += basketItem.ProductPrice * basketItem.NumOrdered;
        AddEvent(new AllocateProductEvent(
             basketItem.ProductName, basketItem.NumOrdered));
    }
}

If you don’t use DDD, then the typical way to create an event is to catch the setting of a property. Here is an example of doing that taken from the first article.

private string _county;
public decimal County
{
    get => _county;
    private set
    {
        if (value != _county)
            AddEvent(new LocationChangeEvent(value));
        _county = value;
    }
}

This works because the property Country is changed into an EF Core backing field, and the name of the column in the table is unchanged. But because it’s now a backing field EF Core 3 will (by default) will read/write the field, not the property, which is good otherwise the load could cause an event.

NOTE: EF Core 3 default action is to read/write the field, but before EF Core 3 the default was to set via the property, which would have generated an event.

Types of events

When it comes to adding an event there are two separate lists: one for BeforeSave events and one for AfterSave events. The names give you a clue to when the handler is run: the BeforeSave events run before SaveChanges is called, and AfterSave events are run after SaveChanges is called.

I cover the two types of events in the next section, but I can say that BeforeSave events are by far the most used type, so that is the default for the AddEvent method. If you want to send an event to be run after SaveChanges, then you need to add a second parameter with the type, e.g. AddEvent(…, EventToSend.AfterSave).

2. ForHandlers: Building the event handlers

You need to create the event handlers to handle the events that the entity classes sent out. There are two types of event handler IBeforeSaveEventHandler<TEvent> and IAfterSaveEventHandler<TEvent>. Let me explain why I have the two types.

BeforeSave events and handlers

The BeforeSave events and handlers are all about the database. The idea is the BeforeSave handlers can change the entity classes in the database, and those changes are saved with the original data that your normal (non-event trigger) code set up. As I explained in the first article saving the original data and any data changed by the event together in one transaction is safe, as the data can’t get out of step.

Typically, a BeforeSave event will be triggered when something changes, or an event happens. The handler then either does some calculation, maybe accessing the database and returns a result to be saved in the calling entity and/or it might create, update or remove (delete) some other entity classes. The data changes applied by the normal code and the data changes applied by the event handler are saved together.

BeforeSave event handlers also have two extra features:

1. Firstly, they can return an (optional) IStatusGeneric status, which can send back errors. If it returns null or a status with no errors then the SaveChanges will be called.

Here is an example of a BeforeSave event handler which was called by the AllocateProductEvent you saw before. This checks that there is enough stock to accept this order. If it returns a status with any errors, then that stops SaveChanges/ SaveChangesAsync from being called.

public class AllocateProductHandler : IBeforeSaveEventHandler<AllocateProductEvent>
{
    private readonly ExampleDbContext _context;

    public AllocateProductHandler(ExampleDbContext context)
    {
        _context = context;
    }

    public IStatusGeneric Handle(EntityEvents callingEntity,
          AllocateProductEvent domainEvent)
    {
        var status = new StatusGenericHandler();
        var stock = _context.Find<ProductStock>(domainEvent.ProductName);
        //… test to check it was found OK removed 

        if (stock.NumInStock < domainEvent.NumOrdered)
            return status.AddError(
                $"I could not accept this order because there wasn't enough {domainEvent.ProductName} in stock.");

        stock.NumAllocated += domainEvent.NumOrdered;
        return status;
    }
}

The lines of code to highlight are:

  • Lines 18 to 19. If there isn’t enough stock it adds an error to the status and returns it immediately. This will stop the SaveChanges from being called.

The default situation is the first BeforeSave event handler that returns an error will stop immediately. If you want all the BeforeSave events to continue, say to get all the possible error messages, then you can set the StopOnFirstBeforeHandlerThatHasAnError property to false in the GenericEventRunnerConfig class provided at setup time (see 4. ForSetup: Registering service on config).

If the returned status has errors, then all the events are cleared and SaveChanges/Async isn’t called (see section “3. ForDbContext” for how these errors are returned to the application).

NOTE: Only a few of your BeforeSave handlers will need a status so you can return null as a quick way to say there are no errors (or more precisely the handler is not looking for errors). You can return a status with no errors and update the statues’ success Message string which will mean that Message will be returned at the top level (assuming a later BeforeSave handler doesn’t overwrite it).

2. Secondly, BeforeSave handlers can raise more events directly or indirectly. For instance, say an event handler changed a property that raised another event we need to pick that new event too. For that reason, the BeforeSave handler runner keeps looping around checking for new events until there are no more.

NOTE:  There is a property in the GenericEventRunnerConfig class called MaxTimesToLookForBeforeEvents value to stop circular events, e.g. an event calls something that calls the same event, which would loop for ever. If the BeforeSave handler runner loops around more than the MaxTimesToLookForBeforeEvents value (default 6) it throws an exception. See section “4. For setup” on how to change the GenericEventRunner’s configuration.

AfterSave events and handlers

AfterSave events are there to do things once the SaveChanges is successful and you know the data is OK. Typical uses are clearing a cache because certain data has changed, or maybe use SignalR to update a screen with the changed data. Unlike the BeforeSave events the events runner only looks once at all the events in the entity classes, so AfterSave events handlers can’t trigger new events.

Here is an example of an AfterSaveEventHandler that would send an internal message to the dispatch department once an Order is successfully placed in the database.

public class OrderReadyToDispatchAfterHandler : 
    IAfterSaveEventHandler<OrderReadyToDispatchEvent>
{
    public void Handle(EntityEvents callingEntity,
         OrderReadyToDispatchEvent domainEvent)
    {
        //Send message to dispatch that order has been checked and is ready to go
    }
}

AfterSave event handers aren’t “safe” like the BeforeSave events in that if they fail the database update is already done and can’t be undone. Therefore, you want to make your AfterSave event handlers aren’t going to cause exceptions. They also shouldn’t update the database (that’s the job of the BeforeSave event handlers).

AfterSave event handers also don’t return any status so you can’t know if they worked on not (see one way around this in section “4. Setup” on how to check an AfterSave event handler ran).

3. ForDbContext: Overriding of EF Core’s base SaveChanges/SaveChangesAsync

To make this all work GenericEventRunner needs to override the base SaveChanges/ SaveChangesAsync methods. GenericEventRunner library provides a class called DbContextWithEvents<T>, which contains overrides for the SaveChanges/ SaveChangesAsync and two extra versions called SaveChangesWithStatus/ SaveChangesWithStatusAsync that return a status. Here is a my ExampleDbContext that I use for unit testing GenericEventRunner.

public class ExampleDbContext
    : DbContextWithEvents<ExampleDbContext>
{
    public DbSet<Order> Orders { get; set; }
    public DbSet<LineItem> LineItems { get; set; }
    public DbSet<ProductStock> ProductStocks { get; set; }
    public DbSet<TaxRate> TaxRates { get; set; }

    public ExampleDbContext(DbContextOptions<ExampleDbContext> options, 
        IEventsRunner eventRunner = null)
        : base(options, eventRunner)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<ProductStock>().HasKey(x => x.ProductName);
    }
}

Line 2 is the only change in your DbContext. Instead of inheriting DbContext, you inherit GenericEventRunner’s DbContextWithEvents<T>, where T is your class. This overrides the SaveChanges/ SaveChangesAsync and adds some other methods and the IStatusGeneric<int> StatusFromLastSaveChanges property.

For people who are already overriding SaveChanges you can either still layer DbContextWithEvents<T> class on top of your SaveChanges method, which GenericEventRunner will override, and call at the apporriate time. If you want to customise your DbContext then the methods used in the DbContextWithEvents<T> class are public, so you can use them directly. This allows you to reconfigure the GenericEventRunner SaveChanges/ SaveChangesAsync to suit your system.

What happens if BeforeSave event handler send back an error?

As I said earlier if the BeforeSave event handlers return an error it does not call SaveChanges/Async, but you most likely want to get the error messaged, which are designed to be shown to the user. I expect most developers to call SaveChanges/Async so the GenericEventRunner throws a GenericEventRunnerStatusException if the combined statuses of all the BeforeSave handlers has any errors. You can then get the errors in two ways:

  • The Message property of the GenericEventRunnerStatusException contains a string starting with an overall message and then each error (separated by the Environment.NewLine characters). This returns just the error text, not the full ValidationResult.
  • For a more detailed error response you can access the IStatusGeneric<int> StatusFromLastSaveChanges property in the DbContext. This provides you with access to the Errors list, where each error has an ErrorResult of type ValidationResult, where you can specify the exact property that caused a problem.

NOTE: The IStatusGeneric<int> StatusFromLastSaveChanges property will be null if SaveChanges hasn’t yet been called.

The alternative is to call the SaveChangesWithStatus/ SaveChangesWithStatusAsync methods directly. That way you can get the status directly without having to use a try/catch. This makes getting the status easier, but if you have a lot of existing code that already calls SaveChanges/SaveChangesAsync then its most likely best to stay with SaveChanges/Async and capture the exception where you need to.

What is the state of the current DbContext when there are exceptions?

We need to consider what state the DbContext is in when there are exceptions. Here is the list:

  • Exceptions before SaveChanges is called (other than GenericEventRunnerStatusException): In this state there may be changes in the database and any events are still there. Therefore, you need to be very careful if you want to call SaveChanges again (Note: this isn’t much different from what happens if you don’t have events – you don’t really know what state the DbContext is in after an exception and you should not try to call SaveChanges).
  • Exceptions during SaveChanges, e.g. DbUpdateConcurrencyException. If you get an exception during SaveChanges itself then it’s something about the database. The DbContext will have all the data ready to retry the SaveChanges, if you can “fix” the problem. If you call SaveChanges again (after fixing it) and it succeeds then all the BeforeEvents have been cleared (because they have already been applied to the DbContext), but any AfterSave events are still there and will run.
  • Exceptions after SaveChanges was called. The database is up to date. If an AfterSave event handler throws an exception then other AfterSave event handlers may be lost. As I said AfterSave event handlers are not robust.

4. ForSetup: Registering your event handlers

The final stage is to register all your event handlers, and the EventsRunner from the GenericEventRunner library. This is done using the extension method called RegisterGenericEventRunner. There are two signatures for this method. Both need an array of assemblies that is needs to scan to find your BeforeSave/AfterSave event handlers, but one starts with property of type IGenericEventRunnerConfig by which you can change the GenericEventRunner default configuration. Here is an example in ASP.NET Core without a config.

public void ConfigureServices(IServiceCollection services)
{
    //… other service registeration left out
       services.RegisterGenericEventRunner(
                Assembly.GetAssembly(typeof(OneOfMyEventHandlers)));
}

NOTES:

  • You can provide multiple assemblies to scan.
  • If you don’t provide any assemblies it will scan the calling assembly.
  • If its scan doesn’t find any AfterSave event handlers then it sets the NotUsingAfterSaveHandlers config property to false (saves time in the the SaveChanges/ SaveChangesAsync).

NOTE: If you send an event has hasn’t got a registered handler then you will get a GenericEventRunnerException at run time.

There are two ways to configure GenericEventRunner and the event handlers at startup.

  1. You can provide a GenericEventRunnerConfig class at the first parameter to the RegisterGenericEventRunner. You can change the default setting of various parts of GenericEventRunner (see the config class for what features it controls).
  2. There is an EventHandlerConfig Attribute which you can add to an event handler class. From this you can set the lifetime of the handler. The default is transient

NOTE: The ability to change the lifetime of an event handler is there in case you need to communicate to event handler in some way, e.g. to check that an AfterSave event handler has run properly. In this case you could set the event handler’s lifetime to “Scoped” and use DI to inject the same handler into your code. (This is advanced stuff! – be careful).

5. Unit Testing applications which use GenericEventRunner

I recommend unit testing your events system, as if you haven’t provided an event handler you will get a runtime exception. Setting up the system to test events is a little complex because GenericEventRunner uses dependency injection (DI). I have therefore built some code you might find useful in unit tests.

The class called SetupToTestEvents in the GenericEventRunner’s Test assembly that contains an extension method called CreateDbWithDiForHandlers that registers your event handlers and return an instance of your DbContext, with the required EventsRunner, to use in your unit tests. Here is an example of how you would use it in a unit test.

[Fact]
public void TestOrderCreatedHandler()
{
    //SETUP
    var options = SqliteInMemory.CreateOptions<ExampleDbContext>();
    var context = options.CreateDbWithDiForHandlers 
        <OrderCreatedHandler>();
    {
        context.Database.EnsureCreated();
        context.SeedWithTestData();

        var itemDto = new BasketItemDto
        {
            ProductName = context.ProductStocks.OrderBy(x => x.NumInStock).First().ProductName,
            NumOrdered = 2,
            ProductPrice = 123
        };

        //ATTEMPT
        var order = new Order("test", DateTime.Now, new List<BasketItemDto> { itemDto });
        context.Add(order);
        context.SaveChanges();

        //VERIFY
        order.TotalPriceNoTax.ShouldEqual(2 * 123);
        order.TaxRatePercent.ShouldEqual(4);
        order.GrandTotalPrice.ShouldEqual(order.TotalPriceNoTax * (1 + order.TaxRatePercent / 100));
        context.ProductStocks.OrderBy(x => x.NumInStock).First().NumAllocated.ShouldEqual(2);
    }
} 

The lines of code to highlight are:

  • Line 5: You create your database options. In this case I am using a method in my EfCore.TestSupport library to create an in-memory Sqlite database, but it could be any type of database.
  • Line 6 and 7: This is where I call the CreateDbWithDiForHandlers extension method which needs two types:
    • TContext: This is your DbContext class
    • THandler: This should be one of your event handlers. This is used for find an assembly which GenericEventRunner needs to scan to find all your event handlers are in so that it can register them in DI. (It also registers any event handlers in the executing assembly – that allows you to add extra handlers for unit testing).

The CreateDbWithDiForHandlers extension method has some useful optional parameters- have a look at the code to see what they provide.

NOTE: I didn’t include the SetupToTestEvents class in the EfCore.GeneriEventHandler because it uses code from my EfCore.TestSupport library. You will need to copy it by hand from the GitHub repo into your unit test assembly.

Logging

The GenericEventRunner Event Runner logs each event handler before it is run. The log message starts with a prefix:

  • First letter: ‘A’ for AfterSave event handlers and ‘B’ for BeforeSave event handlers
  • Then number: this show what loop was it run, e.g. 1, 2, 3 etc. (remember, BeforeHandlers can create new events, which needs another loop around to find them). This is generally useful to see what events are fired when.

Here is an example from one of m GenericEventRunner unit tests. Notice that the last log message starts with “B2”, which means it must have been triggered by a change caused by one of the event handlers that run in the first (i.e. “B1”) event loop.

"B1: About to run a BeforeSave event handler …OrderCreatedHandler."
"B1: About to run a BeforeSave event handler …AllocateProductHandler."
"B2: About to run a BeforeSave event handler …TaxRateChangedHandler."

Also, the unit test CreateDbWithDiForHandlers method allows you to capture logs, which can be useful in testing that events handlers run at the correct time.

Conclusions

Well done for getting here! It’s a long article but I hope it told you “why” and well as “how” to use the EfCore.GenericEventRunner library. If you are thinking of using this library I recommend you inspecting/cloning the EfCore.GenericEventRunner GitHub repo and look at the examples and the unit tests to see how it works.

While this library is new, I have been working on a similar system in my client’s application for some time. That means the features and approach of this library has been proven in the real-world. In fact, the AfterSave events have been added to help deal with some issues that cropped up in the client’s original implementation.

My aim (when I can find time!) is to use this event library to try another way to improve the “book app” that I build for chapter 13 in my book “Entity Framework Core in Action”. You can see the original article from the book called “Entity Framework Core performance tuning – a worked example” and I’m hoping to build the “cached values” version in a much easier way.

Happy coding.