3

Following domain-driven design, I'm trying to implement an outbox pattern, that will save domain events on an AggregateRoot derived entity in the same "transaction" in CosmosDb.

I'm aware I could do this using batching in the CosmosDb SDK, however there is no way to do that currently with Entity Framework, and looks like it's not coming in version 10 either: https://github.com/dotnet/efcore/issues/17308

AggregateRoot base class:

public abstract class AggregateRoot<TAggregate> : IDomainEventAccumulator where TAggregate : AggregateRoot<TAggregate>
{
    public Guid Id { get; protected set; } = Guid.NewGuid();

    public ICollection<IDomainEvent> DomainEvents { get; } = new List<IDomainEvent>();

    protected void AddDomainEvent(IDomainEvent domainEvent)
    {
        DomainEvents.Add(domainEvent);
    }
    ...
}

Derived class:

public class Partner : AggregateRoot<Partner>
{
    public Partner(string name)
    {
        Name = name;
        AddDomainEvent(new PartnerCreatedEvent(Id));
    }
}

As you can see, IDomainEvent may have multiple implementations, and needs to be serialized/de-serialized to the correct types. I have had this working on the single entity by doing the following in the EntityTypeConfiguration:

public class PartnerConfiguration : IEntityTypeConfiguration<Partner>
{
    public void Configure(EntityTypeBuilder<Partner> builder)
    {
        builder.ToContainer(nameof(CosmosDbContext.Partners));

        builder.HasPartitionKey(d => d.Id);

        var assembly = Assembly.Load("MyApplication.Domain");
        var domainEventTypes = assembly.GetTypes().Where(t => typeof(IDomainEvent)
                .IsAssignableFrom(t) && !t.IsAbstract)
            .ToArray();

        var serializerOptions = new JsonSerializerOptions()
        {
            TypeInfoResolver = new EventTypeResolver(domainEventTypes)
        };

        builder.Property(c => c.DomainEvents).HasConversion(
            v => JsonSerializer.Serialize(v, serializerOptions),
            v => JsonSerializer.Deserialize<List<IDomainEvent>>(v, serializerOptions));
    }
}

Ideally I would like to blanket apply this to all classes derived from AggregateRoot base class, but cannot find a way of doing this. I have also tried using the following on the DbContext, but cannot find a way to pass the TypeInfoResolver in for JsonSerializer:

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Properties<IDomainEvent>(c => c.HaveConversion<DomainEventConversion>());
}

Has anyone got an idea how I can achieve the goal I can guarantee saving these events at the same time as calling SaveChanges() on the DbContext? This does not have to include the use of JsonSerializer, but this is as close as I've gotten so far.

2 Answers 2

2

I have got this working with the help of the approach here: https://github.com/dotnet/efcore/issues/23103#issuecomment-720662870

Tackling Serialization of events

First of all I created and registered my EventTypeResolver that will be used when serializing a derived IDomainEvent, to add a discriminator to the resultant JSON. It will then use this discriminator to deserialize to the correct derived event.

EventTypeResolver:

public class EventTypeResolver : DefaultJsonTypeInfoResolver
{
    private readonly Type[] _types;

    public EventTypeResolver(Type[] types)
    {
        _types = types;
    }

    public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options)
    {
        var jsonTypeInfo = base.GetTypeInfo(type, options);

        if (jsonTypeInfo.Type != typeof(IDomainEvent))
            return jsonTypeInfo;

        jsonTypeInfo.PolymorphismOptions = new JsonPolymorphismOptions
        {
            TypeDiscriminatorPropertyName = "$discriminator",
            IgnoreUnrecognizedTypeDiscriminators = true,
            UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization,
        };

        AddDerivedTypes(jsonTypeInfo.PolymorphismOptions, _types);

        return jsonTypeInfo;
    }

    private void AddDerivedTypes(JsonPolymorphismOptions jsonPolymorphismOptions, Type[] types)
    {
        foreach (var type in _types)
        {
            var discriminator = type.FullName.ToLower();
            jsonPolymorphismOptions.DerivedTypes.Add(new JsonDerivedType(type, discriminator));
        }
    }
}

I then used reflection to find all implementors of IDomainEvent, and register the EventTypeResolver in the DI container:

public static IServiceCollection AddDomainEventSerialization(this IServiceCollection services)
{
    var assembly = Assembly.Load("MyApplication.Domain");
    var domainEventTypes = assembly.GetTypes().Where(t => typeof(IDomainEvent)
            .IsAssignableFrom(t) && !t.IsAbstract
                                 && !t.IsGenericTypeDefinition)
        .ToArray();

    services.AddSingleton(new EventTypeResolver(domainEventTypes));

    return services;
}

Entity Configurations

I then created a new set of abstract classes that my Entity Configurations will now inherit from:

InjectedEntityConfiguration

public abstract class InjectedEntityConfiguration
{
    public abstract void Configure(ModelBuilder modelBuilder);
}

public abstract class InjectedEntityConfiguration<TEntity>: InjectedEntityConfiguration where TEntity : class
{
    public override void Configure(ModelBuilder modelBuilder)
        => Configure(modelBuilder.Entity<TEntity>());

    public abstract void Configure(EntityTypeBuilder<TEntity> modelBuilder);
}

Specifically for my AggregateRoots, to make sure I didn't need to set up the deserialization in each configuration with the following:

AggregateRootConfiguration

public abstract class AggregateRootConfiguration<TEntity>
    : InjectedEntityConfiguration, IEntityTypeConfiguration<TEntity>
    where TEntity : AggregateRoot
{
    protected readonly EventTypeResolver _eventTypeResolver;

    protected AggregateRootConfiguration(EventTypeResolver eventTypeResolver)
    {
        _eventTypeResolver = eventTypeResolver;
    }

    public void Configure(EntityTypeBuilder<TEntity> builder)
    {
        var serializerOptions = new JsonSerializerOptions()
        {
            TypeInfoResolver = _eventTypeResolver,
        };

        builder.Property(c => c.DomainEvents).HasConversion(
            v => JsonSerializer.Serialize(v, serializerOptions),
            v => JsonSerializer.Deserialize<List<IDomainEvent>>(v, serializerOptions));

        DoConfigure(builder);
    }

    public abstract void DoConfigure(EntityTypeBuilder<TEntity> builder);

    public override void Configure(ModelBuilder modelBuilder)
        => Configure(modelBuilder.Entity<TEntity>());
}

then register these configurations in my DI container:

public static IServiceCollection AddEntityConfigurations(this IServiceCollection services)
{
    var entityConfigurations = typeof(CosmosDbContext).Assembly.DefinedTypes
        .Where(t => !t.IsAbstract
                    && !t.IsGenericTypeDefinition
                    && typeof(InjectedEntityConfiguration).IsAssignableFrom(t));

    foreach (var type in entityConfigurations)
    {
        services.AddSingleton(typeof(InjectedEntityConfiguration), type);
    }

    return services;
}

Database Context

To correctly register these InjectedEntityConfiguration classes, we must inject them into the DbContext:

public sealed class CosmosDbContext : DbContext, ICosmosDbContext
{
    private readonly IEnumerable<InjectedEntityConfiguration> _entityConfigurations;

    public CosmosDbContext(DbContextOptions<CosmosDbContext> options,
        IEnumerable<InjectedEntityConfiguration> entityConfigurations)
        : base(options)
    {
        _entityConfigurations = entityConfigurations;
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        foreach (var injectedEntityConfiguration in _entityConfigurations)
        {
            injectedEntityConfiguration.Configure(modelBuilder);
        }

        base.OnModelCreating(modelBuilder);
    }
}

Entity Setup

Then I added the Partner & Entity and it's configuration:

Partner

public class Partner : AggregateRoot<Partner>
{
    public Partner(string name)
    {
        Name = name;
        ((IDomainEventAccumulator)this).AddDomainEvent(new PartnerCreatedEvent(Id));
    }
}

PartnerConfiguration

public class PartnerConfiguration(EventTypeResolver eventTypeResolver)
    : AggregateRootConfiguration<Domain.Partner.Partner>(eventTypeResolver)
{
    public override void DoConfigure(EntityTypeBuilder<Domain.Partner.Partner> builder)
    {
        builder.ToContainer(nameof(CosmosDbContext.Partners))
            .HasNoDiscriminator()
            .HasKey(d => d.Id);

        builder.HasPartitionKey(d => d.Id);
    }
}

Results

Now when creating a new parter, the following is saved in Cosmos:

{
    "id": "9be9a1e0-1cd4-4a59-a078-4a2de3baeba8",
    "DomainEvents": "[{\"$discriminator\":\"myapplication.domain.partner.events.partnercreatedevent\",\"Partner\":\"9be9a1e0-1cd4-4a59-a078-4a2de3baeba8\"}]",
    "Name": "fsfa",
}
Sign up to request clarification or add additional context in comments.

Comments

0

What you should be doing is serializing event payload and storing it together with event class metadata like class name and namespace.
When reading events from db table you read all aggregate records and restore event class instances using metadata and serialized event payload.

Take a look how it's done in Microsoft patterns & pratices CQRS Journey sample application

https://github.com/microsoftarchive/cqrs-journey/blob/master/source/Infrastructure/Sql/Infrastructure.Sql/EventSourcing/SqlEventSourcedRepository.cs

3 Comments

This makes sense, as is ultimately my attempt in the PartnerConfiguration file. I do Serialize the event, along with a discriminator, to help deserialize. What I would like to do, is do this blanket in all AggregateRoot entity configurations, without having to use reflection in each configuration to get all possible domain events. The DomainEventConversion class needs to be aware of the possible IDomainEvent implementers in order to deserialize to the correct derived class (domain event).
I understand, but approach described doesn't need any configuration or code modification when e.g. new event is introduced. In your case, every new event (or AR?) needs new IEntityTypeConfiguration implementation, if I'm not mistaken.
IEntityTypeConfiguration is for each AggregateRoot to be configured in Entity Framework, we always create a new configuration for each AggregateRoot anyway, to define the CosmosDb document structure for that Entity. In the case above I also use it to tell EFCore how to (de)serialize the domain events, I'd like this define this globally. Reflection is used to prevent having to manually add to config when a new Domain Event is added - but I definitely don't want to do in every IEntityTypeConfiguration

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.