ProductContext-EventSourcing icon indicating copy to clipboard operation
ProductContext-EventSourcing copied to clipboard

CorrelationId/CausationId implementation

Open osoykan opened this issue 6 years ago • 6 comments

Ideas are;

Request

using(AspnetCore Middleware (Generates CorrelationId))
{
 Captured by CommandContextAccessor.CorrelationId
 Set Command.CorrelationId with MediatR pipeline hook
 Set EventMetadata.CausationId
 AppendToStream
}

osoykan avatar May 29 '18 17:05 osoykan

Ideas are welcomed as well.

osoykan avatar May 29 '18 18:05 osoykan

Hello.

Firstly, I'm noob when it comes to ES and CQRS and stuff, but I feel I may have an idea.

I understand correlation id as identifier that links commands to events. Basically it gives you a clue for a question: "What caused this event to be created?"

I may be completely off what correlation id means, so correct me if I'm wrong, but based on that understanding I have an idea.

So you have a command. Let's add CorrelationId

public class CreateProduct : IRequest
{
    public Guid CorrelationId { get; set; }

    public string Code { get; set; }

    public int BrandId { get; set; }

    public string ProductId { get; set; }

    public int BusinessUnitId { get; set; }
}

In order to link it to event we have to add same property to event.

public class ProductCreated : INotification
{
    public readonly int BrandId;
    public readonly int BusinessUnitId;
    public readonly string ProductCode;
    public readonly string ProductId;
    public readonly Guid CorrelationId;

    public ProductCreated(string productId, string productCode, int brandId, int businessUnitId, Guid correlationId)
    {
        ProductId = productId;
        ProductCode = productCode;
        BrandId = brandId;
        BusinessUnitId = businessUnitId;
        CorrelationId = correlationId;
    }
}

Next we have to transfer it to event.

public static Product Create(string id, int brandId, string code, int businessUnitId, Guid correlationId)
{
    Product aggregate = Factory();
    aggregate.ApplyChange(
        new Events.V1.ProductCreated(id, code, brandId, businessUnitId, Guid correlationId)
        );

    return aggregate;
}

Challenge is not linking events to commands, but creating a design that enforces that. Currently this approach is not feasible.

Correlation id must be created with every command and all events must be created with correlation id.

What do you think about creating abstract base classes that enforce events to have correlation id?

Commands would become:

public abstract class CommandBase : IRequest
{
    public Guid CorrelationId { get; } = Guid.NewGuid();
}

public class CreateProduct : CommandBase
{
    public string Code { get; set; }

    public int BrandId { get; set; }

    public string ProductId { get; set; }

    public int BusinessUnitId { get; set; }
}

And events become:

public abstract class EventBase : INotification
{
    public readonly Guid CorrelationId;
    public EventBase(Guid correlationId)
    {
        CorrelationId = correlationId;
    }
}

public class ProductCreated : EventBase
{
    public readonly int BrandId;
    public readonly int BusinessUnitId;
    public readonly string ProductCode;
    public readonly string ProductId;

    public ProductCreated(string productId, string productCode, int brandId, int businessUnitId, Guid correlationId) : base(correlationId)
    {
        ProductId = productId;
        ProductCode = productCode;
        BrandId = brandId;
        BusinessUnitId = businessUnitId;
    }
}

Currently event carries that id, but as you described your idea, it should belong to event meta data.

siimhs avatar Jun 07 '18 12:06 siimhs

Hi @siimhs,

Thank you for your idea and pointing the implementation.

I've been thinking something similiar. CommandBase thought is reasonable for collecting these stuff one base class.

What I'm thinking on CausationId is not directly related with Event's contract. As you mentioned it is not feasible and it is related with Event's metadata that's why expanding Event's ctor can cause deseralization or schema change problems in the future and it loads extra burden to Events.

What we can do is might be add some CommandContext which I've already implemented in a not event-sourced project and doing Evets are totally context agnostic. Maybe we can bring this idea in here with proper way or enhance it.

osoykan avatar Jun 09 '18 10:06 osoykan

At first sight correlation and causation felt pretty much the same thing for me. This article helped me to understand them a bit clearer.

What I'm thinking on CausationId is not directly related with Event's contract.

As I understand now causation id is another event's id that caused new event to be raised. In that sense as you said, causationId should not be part of events contract.

The implementation you pointed towards uses something similar as EventMetaData.

However if we look where EventMetaData is created then we see that they are created independently from events. EventMetaData is created in RepositoryExtensions and in EventStoreSnapshotter. Currently I think we can put snapshotter aside for a moment and look RepositoryExtensions.

RepositoryExtensions is creating and appending EventData to stream. Something makes me itch here.

Essence of event is queried from aggregate, but yet we want to know what caused it. How come aggregate doesn't know what caused event to be fired?

As you said you don't like to add causation id to event's contract, but I sense that once event is created, then reason why it was created should be passed on.

What do you think?

siimhs avatar Jun 11 '18 07:06 siimhs

Essence of event is queried from aggregate, but yet we want to know what caused it. How come aggregate doesn't know what caused event to be fired?

Let's clarify our understanding about Aggregates.

My opinion is; Aggregate is a cluster of Entities and Value Objects, Aggregates are business driven, use-case aware and persistence/infra agnostic parts of domain. Hence an aggregate should not know about Infra concerns, in this scenario CausationId is an Infra concern for logging, tracing or whatever. That's why I'm thinking on creation of CausationId should be separated from Event creation/new-ing. Events should stay pure for Aggregate's state change. If we implemented this way, this would probably add extra burden and knowledge to Aggregate which it should not know.

So, metadata seems the right place for doing this.

osoykan avatar Jun 11 '18 11:06 osoykan

Indeed. I need to understand this whole concept around DDD and ES a little more.

I'll come back if I have something.

siimhs avatar Jun 13 '18 18:06 siimhs