1

I am working on a project to help learn DDD and am trying to do CQRS and Event Sourcing. Code is in C#.

For this example, lets say I have 2 aggregates, Customer and Product. My aggregate repository has a get method Get<TAggregate>(Guid id) that loads all events for that ID, creates a blank TAggregate instance, and then replays all the events against that instance. The aggregate ignores events it doesn't know what to do with.

The below will work as expected, each aggregate is reconstituted from its events in the event store

Customer customer = repo.Get<Customer>(customerId);
Product product = repo.Get<Product>(productId);

However, if I try to reconstitute an aggregate from a collection of events from a different aggregate, this currently doesn't throw any errors, but the aggregate instance ignores any events that it doesn't know what to do with so it is left in this "clean" state as if zero events were passed to it.

Customer customer = repo.Get<Customer>(productId);
Product product = repo.Get<Product>(customerId);

I see two ways to solve this problem: - The aggregate itself checks to make sure it is in a valid state prior to allowing any domain opperations. - Event types are explicitly associated with specific aggregate types, events passed to the wrong aggregate type result in an exception.

The aggregate ensures it is in a valid state

Ex:

public class Product : AggregateRoot
{
    private Guid _id;
    private bool _isConstructed;

    public Product(Guid id, ...)
    {
        // enforce domain rules here

        ApplyChange(new ProductAddedEvent(id, ...));
    }

    public void UpdatePrice(decimal newPrice)
    {
        if(!_isConstructed)
            throw new Exception(...);

        // enforce domain rules here

        ApplyChange(new ProductPriceUpdatedEvent(_id, newPrice));
    }

    private void Apply(ProductAddedEvent e)
    {
        _id = e.Id;
        _isConstructed = true;
    }

    private void Apply(ProductPriceUpdatedEvent e)
    {
        ...
    }
}

This could work, but I feel like it can get out of hand very quickly, resulting in bulky, awkward code.

Event types are explicitly associated with specific aggregate types

Similar setup to the above example.

public abstract class Event<TAggregate> where TAggregate : AggregateRoot
{
    public bool IsValidFor(AggregateRoot aggregate)
    {
        return aggregate is TAggregate;
    }
}

public class ProductAddedEvent : Event<Product>
{
    ...
}

public abstract class AggregateRoot
{
    public void Reconstitute(Event[] events)
    {
        foreach(Event event in events)
        {
            if(!event.IsValidFor(this)
                throw new Exception(...);

            ApplyEvent(event);
        }
    }
}

This approach makes the most sense to me. Is there some potential smell here that I am not seeing? Is there something else I'm not considering?

Edit:

One other idea I've had is perhaps that command handler needs to validate the command, querying the read model to ensure that an aggregate of expected type exists with the specified ID. But even if this ends up being the correct approach, is there anything wrong with associating events to specific aggregate types?

3 Answers 3

1

Is there something else I'm not considering?

One thing you are not considering is a typesafe interface for your identifiers.

Customer customer = repo.Get<Customer>(productId);
Product product = repo.Get<Product>(customerId);

If this kind of error is what you are worried about, why are you using the same type for your id? Wrap the implementation around two different types, so that the compiler can tell them apart.

The implementation of the repository can pop the guid out of the type when the raw data is needed.

CustomerId customerId = CustomerId.from(...)
ProductId productId = ProductId.from(...)

Customer customer = repo.Get<Customer>(productId); // Type Checker can catch this
1
  • 1
    But what is stopping something like this from happening? ProductId productId = ProductId.From(customerGuid); Product product = repo.Get<Product>(productId);
    – Entith
    Commented Aug 23, 2018 at 19:27
1

What you are trying to prevent is strange because there is no difference between the events that are ignored intentionally and the wrong events.

One solution is to load the events based not only on the Aggregate's ID but also Aggregate's type (or class). For this you need to also store the Aggregate's type in the event stream and use it when you query the Aggregate's events.

Adding the Aggregate type to the event stream opens the door to multiple Aggregates sharing the same ID, which is a good thing, as you can use the same Event store instance for multiple bounded contexts.

0

Like VoiceOfUnreason said, one aspect of this is the primitive obsession with the IDs. There's basically no place in the code where the productGUID and customerGUID should be in scope at the same time.

Specifically, ProductId.From(customerGuid) wouldn't happen because you wouldn't be calling any ProductId.From() in a context where you have a raw customerGuid.

In addition to that, if you aren't doing lazy domain binding you could add a type to the events, as per Constantin Galbenu's suggestion, but perhaps in a slightly different way:

You are likely to have a certain event type or a certain enum of event types, in any case fewer than the total number of event types for the agregate, that can be the first event ever seen for this aggregate, starting with the banal ProductRegisteredEvent. This way, when you query and get >0 events back but the first event isn't a ProductRegisteredEvent, you can immediately throw without pulling any more events in.

This won't enable you to have multiple separate aggregates with the same ID, but thus allow you to not have any reference to the type in the further events.

Finally, I want to make sure I understand why you have the "Ignore returned but unknown events" policy in place. Are you doing this in projections as opposed to the business logic aggregate? Are the unknown events inconsequential to validating business logic?

Or do you already have different aggregates coexisting on the same ID by having mutually exclusive sets of event types? (Which would mean you already baked the 'aggregateType' property into the event type, in a way.

5
  • "There's basically no place in the code where the productGUID and customerGUID should be in scope at the same time." At the end of the day, the application is receiving and responding to user input. Surly we need to ensure that the commands we are receiving from the user are valid and make sense. What is stopping someone from submitting an UpdateProductPrice command with a product ID that is actually a customer ID, either due to a bug in the UI or malicious intent?
    – Entith
    Commented Aug 30, 2018 at 14:45
  • I have experimented with a few solutions since posting this question (including specifying what event types an aggregate type expects first). I have settled on maintaining a GUID to aggregate type lookup in my read model, and having each command handler verify that the GUIDs provided in the command do belong to the expected aggregate types prior to loading any aggregates and calling any domain functions. This seems like the most natural fit to me and it didn't have the awkward feel of the other solutions.
    – Entith
    Commented Aug 30, 2018 at 14:45
  • And regarding the "ignore returned but unknown events" policy, there isn't a particularly solid reason for it, I could just as easily have an aggregate throw an exception in the case of an unknown event. Ignoring unknown events does make it (slightly) easier to handle events that are no longer relevant to the domain because of changes to business rules, but I suppose this could be easily (though more verbosely) handled by accepting the event and doing nothing with it. Due to my lack of experience, I just went with what made sense to me at the time.
    – Entith
    Commented Aug 30, 2018 at 14:46
  • @Entith, regarding the wrong ID originating from a far away machine, such as the client - how is access control implemented? Surely, I am not entitled to the same command succeeding over every Product aggregate in existence? As such, the same layer that prevents me from succeeding with a state-compatible command to my neighbor's aggregate ID could also prevent me from doing that to something else that isn't my Product, for example my Customer. I'm glad you brought up malicious intent, since indeed issuing a command to a wrong type of aggregate is only a subset of possible malice Commented Aug 30, 2018 at 14:58
  • As for implicitly vs explicitly ignoring irrelevant events, I find the explicit route not only more robust but also more convenient, since it enables various compile-time goodies, most importantly Exhaustiveness Checks. Not sure if C# has those yet or not, it might not - fsharpforfunandprofit.com/posts/… Commented Aug 30, 2018 at 15:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.