Oskar Dudycz

Pragmatic about programming

How to build a simple event pipeline

2022-04-27 oskar dudyczEvent Sourcing

cover

Today we will deal with event consumption. I’ll also explain how I wrote the new version of the MediatR library. Curious? We’ll get to that later on.

If you read my article Integrating Marten with other systems, you should be already familiar with the concept of the subscriptions. If not, it works like the Outbox Pattern. Stored events are then published asynchronously by a background process. We subscribe to incoming events notifications and consume them. This pattern also applies to other event-based systems, e.g .:

Okay, but what to do with those events? Typically, we update read models (more about it in How to build event-driven projections with Entity Framework). We can also trigger a workflow. For example, the orders module can start processing the order by subscribing to a shopping cart confirmation event. After we start the process, we can store a follow-up OrderIntiated event. The financial module can take it from there, and after getting the event, issue a proforma invoice. Then the payment and shipment processes can start accordingly.

A single event can often cause a chain reaction and trigger changes in many places.

How do we connect those workflows? It all depends on the guarantees we need in our system. Where I need the 100% delivery guarantee, I’m using an outbox pattern. If I can leave with the state being out of sync sometimes and the risk is low, then in-memory processing is acceptable. I often also join those two approaches.

Subscriptions are implementations of the outbox pattern. They have a built-in retry mechanism when the event handler fails. Therefore, usually, when I get an event from them, I send it to the internal, in-memory bus. Even if an exception is thrown, it will be retried anyway, so I lose nothing. I have to only ensure that the service is idempotent.

The MediatR library is the basis for such implementations in .NET. It allows you to separate the logic related to the handling of the command or event, thus creating loosely related components. It all happens in memory.

MediatR broke into the world of .NET as the basis for the implementation of CQRS. I used it also. Some people love it; others hate it. It’s indeed a straightforward tool to use and reduces boilerplate. The downside is that most people do not use its capabilities like pipelines and behaviours. Without them, it may be overkill, which will just blur the application flow. It might be an unnecessary overhead if we don’t have more complex flows but just push events through it. It also forces marker interfaces for message classes (IRequest for the query or command, INotification for events) and their handlers. It sometimes obscures the picture and makes it challenging to analyze the flow. And some magic related to dependency injection and reflection.

Some time ago, I was hooked by a colleague asking if MediatR could do more extensive event handling flows. It turned out that it couldn’t. It can do this for command and query, but it’s just broadcast for events.

We often need to make a more complex flow. For instance, having the UserAdded event:

public record UserAdded(
    string FirstName,
    string LastName,
    bool IsAdmin
);

We may want to create a pipeline that will at first filter admin users:

public static bool IsAdmin(UserAdded @event) =>
    @event.IsAdmin;

Then map events to a dedicated AdminAdded event:

public record AdminAdded(
    string FirstName,
    string LastName
);

public static AdminAdded ToAdminAdded(UserAdded @event) =>
    new(@event.FirstName, @event.LastName);

Then handle mapped events storing information about new admins:

public static void Handle(AdminAdded @event) =>
    GlobalAdmins.Add(@event);

And distribute global admins to all tenants:

public static List<AdminGrantedInTenant> SendToTenants(UserAdded @event) =>
    TenantNames
        .Select(tenantName =>
            new AdminGrantedInTenant(@event.FirstName, @event.LastName, tenantName)
        )
        .ToList();

public record AdminGrantedInTenant(
    string FirstName,
    string LastName,
    string TenantName
);

public static void Handle(AdminGrantedInTenant @event) =>
    AdminsInTenants.Add(@event);
}

WithMediatR, doing something like this would be overwhelming. As an exercise, I decided to write my EventBus, which would allow me to compose it as follows:

serviceCollection
  .AddEventBus()
  .Filter<UserAdded>(AdminPipeline.IsAdmin)
  .Transform<UserAdded, AdminAdded>(AdminPipeline.ToAdminAdded)
  .Handle<AdminAdded>(AdminPipeline.Handle)
  .Transform<UserAdded, List<AdminGrantedInTenant>>(AdminPipeline.SendToTenants)
  .Handle<AdminGrantedInTenant>(AdminPipeline.Handle);

or without DI container:

var builder = EventHandlersBuilder
  .Setup()
  .Filter<UserAdded>(AdminPipeline.IsAdmin)
  .Transform<UserAdded, AdminAdded>(AdminPipeline.ToAdminAdded)
  .Handle<AdminAdded>(AdminPipeline.Handle)
  .Transform<UserAdded, List<AdminGrantedInTenant>>(AdminPipeline.SendToTenants)
  .Handle<AdminGrantedInTenant>(AdminPipeline.Handle);

var eventBus = new EventBus(builder);

I have roughly achieved MedtiatR with superpowers for events. Additionally, the composition is sprinkled with syntactic sugar without the magic of reflection.

So far, it can:

  • filter events,
  • transform them,
  • NOT requiring marker interfaces for events,
  • NOT requiring marker interfaces for handlers,
  • enables composition through regular functions,
  • allows using interfaces and classes if you want to,
  • can be used with Dependency Injection, but also without through builder,
  • integrates with MediatR if you want to.

See the full sample in my Event Sourcing in .NET repository.

Cheers!

Oskar

p.s. if you liked this article, then check also similar:

p.s.2. Ukraine is still under brutal Russian invasion. A lot of Ukrainian people are hurt, without shelter and need help. You can help in various ways, for instance, directly helping refugees, spreading awareness, putting pressure on your local government or companies. You can also support Ukraine by donating e.g. to Red Cross, Ukraine humanitarian organisation or donate Ambulances for Ukraine.

👋 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