The term “event-driven architecture” covers a wide range of distributed systems like MicroServices, Serverless etc. And this chapter from the book Software Architecture Patterns says event-driven architectures have many useful features, but they are hard to build and test. One reason for that is because designing a system that is robust, i.e. works in all situations, requires significant thought and code.
I came across a simple, but useful application of events to trigger code that accesses the database via EF Core. While this is a very small subset of the whole event-driven space I think it is very useful, especially as it is inherently robust. In fact, I like the approach so much I have built a library called EfCore.GenericEventRunner so I can easily add this feature to any projects I work on, and that’s described in the next article.
The articles in this series are:
- A robust event-driven architecture for using with Entity Framework Core (this article)
- EfCore.GenericEventRunner: an event-driven library that works with EF Core
- A technique for building high-performance databases with EF Core.
TL;DR; – summary
- This article is about a specific event-driven design that works with EF Core. It is limited in what it does -mainly database updates, but it does that in a robust way.
- The advantages for this design are:
- Using is event-driven design helps you to apply an Separation of Concerns in your application.
- The design is robust by design. It saves the original data that caused the events with the data updated by the event handler together in one transaction.
- The event-driven design helps apply a Domain-Driven Design to your code.
- The disadvantages for this design are:
- More difficult to follow the application flow
- Adds (some) complexity to your application.
- The rest of the article gives examples of how the event-driven design works with EF Core.
Setting the Scene – why is this EF Core event system useful?
While working for a client I came across an event-driven design that had initially come from a Jimmy Bogard article, but the client had extended it. There were some issues which I needed to help them fix, but I could see how useful it was. Let me explain what I liked (and disliked) about this approach.
First benefit: Separation of concerns.
My client’s company provides services across the USA and where they deliver their service effects the tax they need to charge. So, when you set, or change, the location you have to recalculate the tax for this job, or any job linked to that location. But for me changing the location’s address is a very different to a recalculation of the job’s tax.
From a design point of view the changing the location, which can be done by a simple update to the State and County properties, doesn’t naturally fit with recalculating the tax on an invoice. And the same location might be used in multiple invoices, which makes the simple setting of State and County into a much more complex piece of business logic.
Their solution is to kick off a “location State/County” event any time a location’s State/County properties change, or a “job location change” event if a different location is selected for a job. It is then down to the event handlers for these two events to recalculation of the tax for that job, or jobs. See the two different use cases below, with the events in red, the event handler in orange, and classes mapped to the database in blue.

So, in effect we have separated the “set/change location” logic from the “calculation of the tax” logic. That might seem small, but to me with my design hat on that is very good “separation of concerns”. And with my developer hat on it makes the setting of the location really simple, leaving the complex tax calculation to be run separately.
In fact, the client has loads of these linked business rules which benefit from using events like this.
Second benefit: Its robust by design
It is really easy to design applications when everything is normal and there are no competing updates. But designing systems that can handles error situations, concurrency issues, and database connection faults is way harder. For that reason, I’m always looking for designs that handle these errors by design, and this event approach does that for everything but some concurrency issues.
As you will see later in this article the data that triggered an event (in the last example the location), and any data that changed (in the last example the TaxRate) are saved together in one transaction by calling to EF Core’s SaveChanges method. That is important because either all the data is saved, or no data is saved. That means the original data and the data from the events will never get out of step.
And if you do have an exception on SaveChanges, such as a DbUpdateConcurrencyException, all the data is now in the DbContext and the events have been cleared. This means you can “fix” the problem and safely call SaveChanges again and it will save the original data and event-generated data to the database, with no extra event calls.
Third benefit: This event-driven design fits with Domain-Driven Design
Domain-Driven Design (DDD) is, to quote the dddcommunity.org site, “is an approach to developing software for complex needs by deeply connecting the implementation to an evolving model of the core business concepts.”. I for one try to use a DDD approach in anything I do.
Some DDD users advocate that the code inside the entity class should not know anything about the database (I’m not in that camp, but I respect their views). That means you need to do all the database work before you go to the entity class. But the event-driven design I am describing gives you another option – you can send an event to a handler that can access the database for the class.
Taking the example of the location effecting the tax, then using an event-driven approach allows the class to ask an external service to calculate the tax (which in this case needs to access the database). I think that keeps the separation of database from entity class while handling the business rule in an efficient and fast way.
Downsides of using the event-driven design.
It’s important to think about the downside of this event-driven design as well, as no extra feature comes with a price.
First downside: using events can make it difficult to under the code
The first problem is described by Martin Fowler in an excellent article called What do you mean by “Event-Driven”?. He says “The problem is that it can be hard to see such a flow as it’s not explicit in any program text.”
For instance, there is an example above there were two types of events (“location State/County” and “job location change”), but what do those events call? You can’t use the VS/VSCode “Go to Definition” (F12) feature to go to the handler code because its hidden behind layers of interfaces and DI. That can make things hard to follow.
My advice is, if you have business code where all the business rules sensibly belongs together then write one set of code, and don’t use events. Only use events where it makes sense, like decoupling the setting of the location from the recalculation of the tax rate. I also suggest you name of the event and the handler starts with the same name, e.g. LocationChangeEvent and LocationChangeHandler respectively. That makes it easier to work out what code is called.
Second downside: makes your application more complicated
Adding event handling to an application isn’t simple and it changes your DbContext, especially around the SaveChanges/ SaveChangesAsync. Complexity is bad, as it makes the application harder to understand and test. You have to weight up the usefulness of the event-driven design against the extra complexity it adds to your application.
NOTE: In the next article I describe my EfCore.GenericEventRunner library which provides you with a pre-build event-driven system. You can read that article and see if you think it is useful.
Implementing this in EF Core
I have spent a lot of time on the pros and cons of the approach so now we look at how it works. I start with a diagram which shows the three stages of the event handling.

This example gives a good idea of what is possible and the next three sections show the code you need at each stage.
Stage 1 – adding event triggers to your entity classes
An event is triggered in an entity class that you have read in and is tracked, i.e. it wasn’t loaded with a query that has the .AsNoTracking in it. This is because the event runner only looks for events in tracked entities.
You can send an event from anywhere, but the typical approach is to trigger an event when something changes. One way is to catch the setting of a property by using a backing field and testing if something changes in the property setter. Here is an example.
private string _county;
public decimal County
{
get => _county;
private set
{
if (value != _county)
AddEvent(new LocationChangeEvent(value));
_county = value;
}
}
The things to note are:
- Line 1: I’m using a private field so that I can add my own code in the property setter. Converting a normal property to this form is handled by EF Core via a backing field and the name of the column in the table is unchanged. NOTE: In EF Core 3 and above when EF Core loads data it puts it in the private field, not via the setter – that’s good otherwise the load could cause an event (before EF Core 3 the default was to set via the property, which would have generated an event).
- Lines 7 & 8: This is code that triggers an event if the Tax value has changed.
If you are using a Domain-Driven Design (DDD) then you can put the AddEvent call in your DDD method or constructors. Here is an example from the example code in the EfCore.GenericEventRunner code.
public Order(string userId, DateTime expectedDispatchDate, ICollection<BasketItemDto> orderLines)
{
UserId = userId;
DispatchDate = expectedDispatchDate;
AddEvent(new OrderCreatedEvent(expectedDispatchDate, SetTaxRatePercent));
var lineNum = 1;
_LineItems = new HashSet<LineItem>(orderLines
.Select(x => new LineItem(lineNum++, x.ProductName, x.ProductPrice, x.NumOrdered)));
TotalPriceNoTax = 0;
foreach (var basketItem in orderLines)
{
TotalPriceNoTax += basketItem.ProductPrice * basketItem.NumOrdered;
AddEvent(new AllocateProductEvent(basketItem.ProductName, basketItem.NumOrdered));
}
}
private void SetTaxRatePercent(decimal newValue)
{
TaxRatePercent = newValue;
}
The things to note are:
- Line 5: The event add here is given a method called SetTaxRatePercent (see lines 18 to 21) which allows the event to set the TaxRatePercent property which has a private setter. I do this because I using a DDD design where all the properties are read-only, but I hand the event handler, via the event a method to set that property.
- Line 15. I want to allocate each item of stock from this order and to do this I must send over the information in the event. That’s because the Order isn’t in the database yet, so the event handler can’t read the database to get it.
NOTE: If you trigger an event in a constructor make sure its not the constructor that EF Core will use when loading the data – check the EF Core documentation on how this works.
Stage 2 – Before SaveChanges
The EfCore.GenericEventRunner overrides the base SaveChanges/ SaveChangesAsync and has an event runner that will find all the events before SaveChanges/ SaveChangesAsync is called. It does this by looking for all the tracked entities (i.e. any classes loaded, Added, Attached etc.) that has inherited EfCore.GenericEventRunner’s EntityEvents class. This contains methods to get the events and then wipes the events (I do that to ensure an event isn’t run twice).
NOTE: To make it simpler to understand I talked about “events”, but in fact there are two types of events in EfCore.GenericEventRunner: the BeforeSave and the AfterSave events, which run before or after the call to SaveChanges/ SaveChangesAsync respectively. I will explain why I added the AfterSave events in the next article.
(Before) Handlers have to inherit the following interface, where the T part is the type of event the handler can process.
public interface IBeforeSaveEventHandler<in T> where T : IDomainEvent
{
IStatusGeneric Handle(EntityEvents callingEntity, T domainEvent);
}
Here is an example handler for working out the Tax value
public class OrderCreatedHandler : IBeforeSaveEventHandler<OrderCreatedEvent>
{
private readonly TaxRateLookup _rateFinder;
public OrderCreatedHandler(ExampleDbContext context)
{
_rateFinder = new TaxRateLookup(context);
}
public IStatusGeneric Handle(EntityEvents callingEntity,
OrderCreatedEvent domainEvent)
{
var tax = _rateFinder.GetTaxRateInEffect(domainEvent.ExpectedDispatchDate);
domainEvent.SetTaxRatePercent(tax);
return null;
}
}
The EfCore.GenericEventRunner library has an extension method called RegisterGenericEventRunner which scans the assemblies you provide to find all the handlers that have the IBeforeSaveEventHandler (and IAfterSaveEventHandler) interfaces. You should put this in your start-up code where the other dependency injection (DI) items are registered.
In the overridden SaveChanges/ SaveChangesAsync methods an event runner looks for event handlers in the DI services that match the full handler + event type. It then runs each event handler with the event data.
NOTE: I am not covering the inner workings of the event handler here as I want to give you a good overview of the approach. Suffice to say there is a lot going on in the event handler.
Stage 3 – Run SaveChanges
The final stage is saving the data to the database. Its simple to do because EF Core does all the complex stuff. SaveChanges will inspect all the tracked entities and work out what State each entity is in: either Added, Modified, Deleted, or Unchanged. It then builds the database commands to update the database.
As I said earlier the important thing is the original data and the new data added by the event handlers are saved together in one transaction. That means you can be sure all the data was written out, or if there was a problem the nothing is left out.
Conclusions
I have described an event-driven design which is very limited in its scope: it focuses on updating database data via EF Core. This approach isn’t a “silver bullet” that does everything, but I think it is a valuable tool in building applications. I expect to still be using my normal business rule (see this article on how a build my business logic), but this event-driven design now allows me to access external services (i.e. event handlers) while inside the entity class, which is something I have wanted to be able to do for a while.
I spent some time describing the design and its benefits because it wasn’t obvious to me how useful this event-driven design was until I saw the client’s system. Also, I felt it was best to describe how it works before describing the EfCore.GenericEventRunner library, which I do in the next article.
I want to recognise Jimmy Bogard Blog for his original article on an event-driven approach that my client used. I find Jimmy’s articles really good as he, like me, writes from his work with clients. I also want to thank my client for exposing me to this approach in a real-world system.
NOTE: My client is aware that I am building the EfCore.GenericEventRunner library, which is a complete rewrite done in my own time. This library also solves one outstanding problem in their implementation, so they benefit too.
Happy coding.