Oskar Dudycz

Pragmatycznie o programowaniu

How to build event-driven projections with Entity Framework

2021-09-29 oskar dudyczEvent Sourcing

cover

Let’s assume that you’re building an Event-Driven system. You may be using Event Sourcing or microservices integrated with message bus or monolith with modules triggering each other by events.

In such systems, meaningful business operations end up with related events. Those events might be stored (in Event Sourcing) or just published. They can trigger other business workflows or be used to build read models. We’re going to focus today on the latter.

Read models typically are used for user queries. You get the most out of them if you design them to handle a specific query. In most of the systems, reads are much more frequent than writes. Thus, it’s usually worth selecting slower writes and faster queries when you have to do the tradeoff. You can also use them to build the local interpretation of the external state from other modules. The patterns that I describe below apply to both approaches.

Let’s say that we’re using an in-memory event bus defined by such interface:

public interface IEventBus
{
    Task Publish<TEvent>(TEvent @event, CancellationToken ct);

    Task Publish(object @event, CancellationToken ct);
}

See sample implementations:

Each event published to it will be handled by the event handler defined by:

public interface IEventHandler<in TEvent>
{
    Task Handle(TEvent @event, CancellationToken ct);
}

Event bus can:

The critical part of the event bus call from hosted service is to ensure that event bus event handlers are resolved in the new scope. That makes sure that services that require scoping are correctly resolved and disposed.

Projection is an interpretation of the sequence of the events. Using left fold approach, we can apply events one by one to the state (read more in “How to get the current entity state from events?”).

For the following set of events representing the ECommerce shopping cart lifecycle:

public record ShoppingCartInitialized(
    Guid ShoppingCartId,
    Guid ClientId,
    ShoppingCartStatus ShoppingCartStatus
);

public record ProductItemAddedToShoppingCart(
    Guid ShoppingCartId,
    PricedProductItem ProductItem
);

public record ProductItemRemovedFromShoppingCart(
    Guid ShoppingCartId,
    PricedProductItem ProductItem
);

public record ShoppingCartConfirmed(
    Guid ShoppingCartId,
    DateTime ConfirmedAt
);

We could define a projection for the list view as:

public record ShoppingCartShortInfo
{
    public Guid Id { get; set; }
    public Guid ClientId { get; set; }
    public int TotalItemsCount { get; set; }
    public decimal TotalPrice { get; set; }
    public ShoppingCartStatus Status { get; set; }
    public int Version { get; set; }
}

public class ShoppingCartShortInfoProjection
{
    public static ShoppingCartShortInfo Handle(ShoppingCartInitialized @event)
    {
        var (shoppingCartId, clientId, shoppingCartStatus) = @event;

        return new ShoppingCartShortInfo
        {
            Id = shoppingCartId,
            ClientId = clientId,
            TotalItemsCount = 0,
            Status = shoppingCartStatus,
            Version = 0
        };
    }

    public static void Handle(ShoppingCartConfirmed @event, ShoppingCartShortInfo view)
    {
        view.Status = ShoppingCartStatus.Confirmed;
        view.Version++;
    }

    public static void Handle(ProductItemAddedToShoppingCart @event, ShoppingCartShortInfo view)
    {
        view.TotalItemsCount += @event.ProductItem.Quantity;
        view.TotalPrice += @event.ProductItem.TotalPrice;
        view.Version++;
    }

    public static void Handle(ProductItemRemovedFromShoppingCart @event, ShoppingCartShortInfo view)
    {
        view.TotalItemsCount -= @event.ProductItem.Quantity;
        view.TotalPrice -= @event.ProductItem.TotalPrice;
        view.Version++;
    }
}

As you see, projection can be defined by a set of functions. They may either take:

  • an event as a parameter and return a new read model instance,
  • an event and read model instance and apply the new state to it.

Defining it as functions without reference to any infrastructure code makes testing by unit or integration tests easy. They’re also cleaner, readable and, thanks to that, easier to maintain.

The more advanced, Shopping Cart details projection can be represented as:

public static class ShoppingCartDetailsProjection
{
    public static ShoppingCartDetails Handle(ShoppingCartInitialized @event)
    {
        var (shoppingCartId, clientId, shoppingCartStatus) = @event;

        return new ShoppingCartDetails
        {
            Id = shoppingCartId,
            ClientId = clientId,
            Status = shoppingCartStatus,
            Version = 0
        };
    }

    public static void Handle(ShoppingCartConfirmed @event, ShoppingCartDetails view)
    {
        view.Status = ShoppingCartStatus.Confirmed;
        view.Version++;
    }

    public static void Handle(ProductItemAddedToShoppingCart @event, ShoppingCartDetails view)
    {
        var productItem = @event.ProductItem;
        var existingProductItem = view.ProductItems
            .FirstOrDefault(x => x.ProductId == @event.ProductItem.ProductId);

        if (existingProductItem == null)
        {
            view.ProductItems.Add(new ShoppingCartDetailsProductItem
            {
                ProductId = productItem.ProductId,
                Quantity = productItem.Quantity,
                UnitPrice = productItem.UnitPrice
            });
        }
        else
        {
            existingProductItem.Quantity += productItem.Quantity;
        }

        view.Version++;
    }

    public static void Handle(ProductItemRemovedFromShoppingCart @event, ShoppingCartDetails view)
    {
        var productItem = @event.ProductItem;
        var existingProductItem = view.ProductItems
            .Single(x => x.ProductId == @event.ProductItem.ProductId);

        if (existingProductItem.Quantity == productItem.Quantity)
        {
            view.ProductItems.Remove(existingProductItem);
        }
        else
        {
            existingProductItem.Quantity -= productItem.Quantity;
        }

        view.Version++;
    }
}

Above projection contains nested objects and more sophisticated logic on updating the state. However, it’s still clean and self-explanatory. In general, projections shouldn’t contain a lot of business logic. Its place is in the write model.

OK, but how to use them to update the data? Let’s use Entity Framework as an example of how to deal with that. In general, the relational model is not ideal for the read models. By its nature, normalisation brings additional overhead with table joins, etc. Nevertheless, relational databases are the most popular storage, and ORMs are the most popular way to use them in an object-oriented world. We should also use the way that helps us to integrate with our other technology stack or our team skills.

For the majority of cases, we can use two main patterns:

  • create, that can be described by code as:
var view = handler(@event);

await dbContext.AddAsync(view, ct);
await dbContext.SaveChangesAsync(ct);
  • update:
var viewId = getViewId(@event);
var view = await dbContext.FindAsync<TView>(new [] {viewId}, ct);

prepare?.Invoke(dbContext.Entry(view), ct);

handler(@event, view);

await dbContext.SaveChangesAsync(ct);

Where for handlers are projection functions defined above.

The DbContext is defined as:

public class ECommerceDbContext: DbContext
{
    public ECommerceDbContext(DbContextOptions<ECommerceDbContext> options)
        : base(options)
    {

    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder
            .Entity<ShoppingCartShortInfo>();

        modelBuilder
            .Entity<ShoppingCartDetails>()
            .OwnsMany(e => e.ProductItems, a =>
            {
                a.WithOwner().HasForeignKey("ShoppingCardId");
                // we cannot use the ProductId here
                // we need the autogenerated id
                // to support the automatic add/update/remove
                a.Property<int>("Id").ValueGeneratedOnAdd();
                a.HasKey("Id");
            });
    }
}

How to connect all the dots (event bus, event handlers, projections, Entity Framework)? Let’s start by defining what we’d like to achieve.

My motivations:

  • easy to use and self-explanatory API for registering projections. A fluent pattern is usually helpful for that needs,
  • extendible approach, with possible fallbacks,
  • registering everything into regular DI containers to integrate with other application services.

Sample API could look as follows:

public static class Configuration
{
    public static IServiceCollection AddShoppingCartsModule(this IServiceCollection services) => 
        services
            .AddDbContext<ECommerceDbContext>()
            .For<ShoppingCartDetails, ECommerceDbContext>(
                builder => builder
                    .AddOn<ShoppingCartInitialized>(ShoppingCartDetailsProjection.Handle)
                    .UpdateOn<ProductItemAddedToShoppingCart>(
                        e => e.ShoppingCartId,
                        ShoppingCartDetailsProjection.Handle,
                        (entry, ct) => entry.Collection(x => x.ProductItems).LoadAsync(ct)
                    )
                    .UpdateOn<ProductItemRemovedFromShoppingCart>(
                        e => e.ShoppingCartId,
                        ShoppingCartDetailsProjection.Handle,
                        (entry, ct) => entry.Collection(x => x.ProductItems).LoadAsync(ct)
                    )
                    .UpdateOn<ShoppingCartConfirmed>(
                        e => e.ShoppingCartId,
                        ShoppingCartDetailsProjection.Handle
                    )
            )
            .For<ShoppingCartShortInfo, ECommerceDbContext>(
                builder => builder
                    .AddOn<ShoppingCartInitialized>(ShoppingCartShortInfoProjection.Handle)
                    .UpdateOn<ProductItemAddedToShoppingCart>(
                        e => e.ShoppingCartId,
                        ShoppingCartShortInfoProjection.Handle
                    )
                    .UpdateOn<ProductItemRemovedFromShoppingCart>(
                        e => e.ShoppingCartId,
                        ShoppingCartShortInfoProjection.Handle
                    )
                    .UpdateOn<ShoppingCartConfirmed>(
                        e => e.ShoppingCartId,
                        ShoppingCartShortInfoProjection.Handle
                    )
            );
}

It could be even more brilliant, but the key to the right design is to be careful not to overdo it. I think that it’s self-explanatory, explicit. You can read it as decent documentation.

How to build such an API? Let’s start with defining our generic event handlers classes.

Add new item projection:

public class AddProjection<TView, TEvent, TDbContext>: IEventHandler<TEvent>
    where TView: class
    where TDbContext: DbContext
{
    private readonly TDbContext dbContext;
    private readonly Func<TEvent, TView> create;

    public AddProjection(
        TDbContext dbContext,
        Func<TEvent, TView> create
    )
    {
        this.dbContext = dbContext;
        this.create = create;
    }

    public async Task Handle(TEvent @event, CancellationToken ct)
    {
        var view = create(@event);

        await dbContext.AddAsync(view, ct);
        await dbContext.SaveChangesAsync(ct);
    }
}

Update existing item projection:

public class UpdateProjection<TView, TEvent, TDbContext>: IEventHandler<TEvent>
    where TView: class
    where TDbContext: DbContext
{
    private readonly TDbContext dbContext;
    private readonly Func<TEvent, object> getViewId;
    private readonly Action<TEvent, TView> update;
    private readonly Func<EntityEntry<TView>, CancellationToken, Task>? prepare;

    public UpdateProjection(
        TDbContext dbContext,
        Func<TEvent, object> getViewId,
        Action<TEvent, TView> update,
        Func<EntityEntry<TView>, CancellationToken, Task>? prepare = null)
    {
        this.dbContext = dbContext;
        this.getViewId = getViewId;
        this.update = update;
        this.prepare = prepare;
    }

    public async Task Handle(TEvent @event, CancellationToken ct)
    {
        var viewId = getViewId(@event);
        var view = await dbContext.FindAsync<TView>(new [] {viewId}, ct);

        prepare?.Invoke(dbContext.Entry(view), ct);

        update(@event, view);

        await dbContext.SaveChangesAsync(ct);
    }
}

Having them, we can define the main fluent API starting point to orchestrate our registration:

public static class EntityFrameworkProjection
{
    public static IServiceCollection For<TView, TDbContext>(
        this IServiceCollection services,
        Action<EntityFrameworkProjectionBuilder<TView, TDbContext>> setup
    )
        where TView: class
        where TDbContext: DbContext
    {
        setup(new EntityFrameworkProjectionBuilder<TView, TDbContext>(services));
        return services;
    }
}

And the main builder that glues all together:

public class EntityFrameworkProjectionBuilder<TView, TDbContext>
    where TView : class
    where TDbContext : DbContext
{
    public readonly IServiceCollection services;

    public EntityFrameworkProjectionBuilder(IServiceCollection services)
    {
        this.services = services;
    }

    public EntityFrameworkProjectionBuilder<TView, TDbContext> AddOn<TEvent>(Func<TEvent, TView> handler)
    {
        services.AddSingleton(handler);
        services.AddTransient<IEventHandler<TEvent>, AddProjection<TView, TEvent, TDbContext>>();

        return this;
    }

    public EntityFrameworkProjectionBuilder<TView, TDbContext> UpdateOn<TEvent>(
        Func<TEvent, object> getViewId,
        Action<TEvent, TView> handler,
        Func<EntityEntry<TView>, CancellationToken, Task>? prepare = null)
    {
        services.AddSingleton(getViewId);
        services.AddSingleton(handler);
        services.AddTransient<IEventHandler<TEvent>, UpdateProjection<TView, TEvent, TDbContext>>();

        if (prepare != null)
        {
            services.AddSingleton(prepare);
        }

        return this;
    }

    public EntityFrameworkProjectionBuilder<TView, TDbContext> QueryWith<TQuery>(
        Func<IQueryable<TView>, TQuery, CancellationToken, Task<TView>> handler
        )
    {
        services.AddEntityFrameworkQueryHandler<TDbContext, TQuery, TView>(handler);

        return this;
    }

    public EntityFrameworkProjectionBuilder<TView, TDbContext> QueryWith<TQuery>(
        Func<IQueryable<TView>, TQuery, CancellationToken, Task<IReadOnlyList<TView>>> handler
    )
    {
        services.AddEntityFrameworkQueryHandler<TDbContext, TQuery, TView>(handler);

        return this;
    }
}

It may look a bit scary, but if you have a look, then it’s pretty straightforward. It’s just a bunch of the registration to dependency injection containers. We’re registering projection functions that are then injected into projection classes and registered as event handlers.

We don’t have to use marker interfaces, as we’re pushing the check to the edges: the registration code. We still are safe and have the compiler check if we didn’t put too many params or inverted their order. If we need more sophisticated or customised logic, we can still fall back and define the custom event handler that will do the magic.

Before I finish, please have a look at the ShoppingCartDetailsProjection definition. As you see, I’m not splitting projections per table. I’m breaking it per read model. For the shopping cart details, the read model is built from the cart data and added product items. It’s an implementation detail that they’re split into two separate tables. Thus we should still keep the centralised business logic. For more modelling bits of advice, read “How to create projections of events for nested object structures?”.

As you see, with proper composability, we can create straightforward and maintainable code without head-scratching inheritance problems and generic or reflection magic. I hope this article will help you design better Event-Driven processing, as those patterns can be applied not only to Entity Framework but also to general projections handling.

Cheers!

Oskar

p.s. you can check the whole solution in the Pull Request.

👋 If you found this article helpful and want to get notification about the next one, subscribe to Architecture Weekly.

✉️ Join over 11500 subscribers, get the best resources to boost your skills, and stay updated with Software Architecture trends!

Loading...
Event-Driven by Oskar Dudycz

cover

Through my window, I see the result of good plans but poor execution. Opposite my flat, there is a partially completed construction place. Buildings were supposed to be eye-catching Mediterranean style apartments. Delivery date? Two years ago. Actual? More and more unknown.

Some time ago, I heard that using Event Sourcing makes creating Event-Driven Architecture easier. The arguments were correct, that if we’re already publishing events to trigger business workflows, then at some point, we may want to also store events to not lose information. Agreed. However, I also heard that keeping the state as events will simplify things. We’ll have a source of truth with a record of the system behaviour. This will allow, e.g. to confront the results of the operations with the recorded state. I’d agree with that, with one distinction. It’s easier as long as you already know Event Sourcing.

Many people in the DDD community claim that the essential is to properly break down the system into autonomous parts called bounded contexts. Once we have it, the rest is secondary and will sort itself out. For sure.

Many seasoned programmers speak similarly about new technologies. They claim that they can translate past experience into new technologies. That’s true that by analogy, they can catch the big picture quicker. But isn’t it a bold assumption to say that Win.Forms specialist will learn Angular quickly?

The end result may differ a lot from the initial ideas. I saw the plan of those buildings next to me. Now I can see the effects of the execution. Or actually, the lack.

I believe that we should carefully acknowledge not only the point of view of our authorities but also their seating point. If we want to find out how to form a wall, do we ask an architect or a foreman? An architect may know the theory, but the practice is what we’re looking for. On the other hand, if you want to know where to put the wall, you prefer the architect to do measurements. At least if you don’t want to have the roof falling to your head.

After I had torn a ligament in my knee, I went to two qualified orthopedists. One said I should have surgery and do a reconstruction. The second stated that there is no need for that; rehabilitation should be enough. Guess which one had a specialization in surgery and which in rehabilitation?

People usually give us advice from the point where they’re currently standing. They are entitled to a biased view. An architect who rarely does programming will tend to downplay the value of implementation and tactical patterns. Midlevel developers will focus on technicalities instead of the global system impact. The team manager or consultant will emphasize the importance of soft skills (or esoteric techniques known only to them).

The truth is that we need all of them. The excellent plan will fall on the bad execution. The best execution for the wrong case will be just a waste of time. We should carefully evaluate the advice considering what we need and what an expert can give us.

Therefore, when we’re reading an article, watching a talk, let’s also pay attention to the place where the person is standing. The perspective from there may be much different from where we are right now. That can be good, as it may push us in the right direction. But it may also be misleading, as we accidentally take biases of this person without understanding the tradeoffs. Personally, I prefer to follow not only people from pedestal but also those that are closer to my position. A bit further in the journey, but not too far. That helps me to calibrate my view as those people are more relative to my daily struggles.

Polish historical leader Józef Piłsudzki reportedly used to say: “Right is like an ass, everyone has its own”.

Cheers!

Oskar