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

Last Updated: January 27, 2021 | 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.

Update: Version 2.0.0 – new features

1. Supports async event handlers

You can now define async Before/After event handlers. These only work when you call SaveChangesAsync.

NOTE: If you want to have a sync and async version of the same event handler, then you can if you follow a simple naming rule – give your two event handler the same name, but the async one has “Async” on the end . GenericEventRunner will run the async if SaveChangesAsync is called and won’t run the sync version.

2. Now supports clean code architecture

There is a very small (one class, one enum, and one interface) library called EfCore.GenericEventRunner.DomainParts that you use in the domain project. This contains the class you need to inherit in an entity class to create events.

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 three separate lists: one for Before events, one for During events, and one for After events. The names give you a clue to when the handler is run: the Before events run before SaveChanges is called, the During events runs in an transaction where SaveChanges is called, and After events are run after SaveChanges is called. See the diagram below for a picture of this.

NOTE: As well as the three events the GenericEventRunner allows you to run some code just after the DetectChanges method. This is useful for detecting changes and do something to an entity, e.g. adding a LastUpdated DateTime to every entity class that has been changed.

I cover the three 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.

Before 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.

During events and handlers

DuringSave events are used for synchronizing your database update to another system that is separate from the database – if the other system fails then you database fails. This works by creating a transaction on your EF Core database, then call SaveChanges, if the SaveChanges was successful it then calls the other system. The Commit to the database is only called if both SaveChanges and the other system call works.

As an example, in my book “Entity Framework Core in Action, second edition” I use a During event to update a Cosmos DB database whenever the information about a book has changed. (I show this approach in an older article here – I didn’t have the GenericEventRunner then).

The need for during events are rare, but when you want to make sure your database is only updated if another system confirms it is OK to go ahead, then during events are the robust way to achieve this.

NOTE: Jimmy Bogard wrote an excellent series called “Life Beyond Distributed Transactions: An Apostate’s Implementation” in the 8th article in the series he talks about using a transaction in a SQL database to ensure the second update is done before exiting. Jimmy is very clear that too many people ignore these errors – as he says in his tweet “hope is not a strategy”!

Here is an example of a DuringSave event handler that updates the Cosmos DB when a book’s information changes. This is an async handler because Cosmos DB only works reliably with async calls.

public class BookChangeHandlerAsync 
    : IDuringSaveEventHandlerAsync<BookChangedEvent> 
{
    private readonly IBookToCosmosBookService _service; 

    public BookChangeHandlerAsync(
        IBookToCosmosBookService service)
    {
        _service = service;
    }

    public async Task<IStatusGeneric> HandleAsync( 
        object callingEntity, BookChangedEvent domainEvent, 
        Guid uniqueKey)
    {
        var bookId = ((Book)callingEntity).BookId; 
        switch (domainEvent.BookChangeType) 
        {
            case BookChangeTypes.Added:
                await _service.AddCosmosBookAsync(bookId);
                break;
            //... the update and delete versions are left out
        }

        return null; 
    }
}

The During event handler returns a IStatusGeneric result like the Before event – if it returns null or a status with no errors then the Commit will be called, otherwise the transaction will be rolled back, so the database is not updated.

After 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 or DuringSave event handlers send back an error?

As I said earlier if the Before or During event handlers return an error it does not update the database, but you most likely want to get the error message, 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. If there isn’t a During event, then the database is up to date. But if there are During events the database, then it depends on whether then During events are successful.
  • Exceptions in During events. Any exceptions within the transaction will stop the database update.

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).
  • See the documentation on registering the GenericEventRunner for more info.

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.

The third article in this series is “A technique for building high-performance databases with EF Core” which uses this event library to improve the the performance of my “book app”. I build a similar version in chapter 13 in my book “Entity Framework Core in Action”, but in my opinion the new version that uses events is much better (You can see the original article from the book called “Entity Framework Core performance tuning – a worked example”).

Happy coding.

0 0 votes
Article Rating
Subscribe
Notify of
guest
8 Comments
Newest
Oldest Most Voted
Inline Feedbacks
View all comments
Trung
Trung
2 years ago

I found a problem with this nuget, i just test on DuringEvent:

Function A
{
   ExecuteEvent1

   ExecuteEvent2 (have a SaveAsync inside this event2)

   SaveAsync()
}

—-
Result:
Event1 and Event2 loop infinite.

Is it a bugs or something else ?

Last edited 2 years ago by Trung
Koen
Koen
2 years ago

Great article! I stumbled on this (and in particular the previous article in this series) while writing my own article on a very similar subject, namely the use of Triggers with EFCore. Excellent write up on the topic and hard to add much to It, I may just link to to it and move ahead.

I recently published a library that overlaps in many functionalities: EntityFrameworkCore.Triggered. This library was developed as a result of many years of using some form of triggers/events in the field. The main difference from EfCore.GenericEventRunner is that EFCore.Triggered generates events implicitly based on the actual entity as well as the change type as tracked within the ChangeTracker. I’ve used many different variants including raising explicit events but I found that the
additional code and discipline required to consistently raise events was a
hassle, as often consumers of events are not known or predictable until much
later in the project. Triggers are hence named after database triggers which are
guaranteed to run for all changes within your database. By bringing them to the
level of EF, we then have (almost) the same predictable power as database
triggers while maintaining the luxury of running within your app.

I’m curious, have you considered this approach, and if so, why did you decide to go for explicit events?

Koen
Koen
2 years ago
Reply to  Jon P Smith

I totally agree with your mention of having control, with events you can attach (or remove) as much data as you need.

Having to capture the Id is covered in EFCore.Triggered by implementing IBeforeSaveTrigger as well as IAfterSaveTrigger within a trigger. Since this trigger is resolved through DI, registering it as scoped service gives the trigger the ability to run both events while maintaining state internally within the trigger (so that you can do your stuff before the Id is known as well as after the Id is known).

I’m curious as to your comment on DDD. I’m not sure I follow as to why raising explicit events has advantages over raising implicit events (with the exception of being able to control/attach additional data). Implicit events (triggers) contain both the state of the entity before being modified as well as the state of the entity after the modifications. In a DDD world, both approaches presumable share similar state, the only difference being when they are raised. For triggers, there is no need to test for raising triggers, only the need for testing the handling of such raised triggers. Perhaps you can elaborate. I guess I’m unsure what you meant with: ‘where to put events

Looking forward to your book update though I have not red previous iterations, will see to change that! And indeed, EF is awesome and has so much more potential then it currently shows. I’m prepping a series of posts on how I use EF in production and the lessons learned there, the more we put out there the better.

Montgomery Beltz
Montgomery Beltz
3 years ago

Great article Jon! This is exactly what I was hoping to find.

Jon P Smith
3 years ago

I’m glad it is useful. I’m using this event-driven technique quite a bit now. Watch out for another article using this library to improve the read performance of a database.

Montgomery Beltz
Montgomery Beltz
3 years ago
Reply to  Jon P Smith

Alright awesome! I just bought your Entity framework core in action book, I’ve got quite a bit of reading to do lol