c# - Saving Domain Events on entity in Entity Framework (CosmosDB) - Stack Overflow

Following domain-driven design, I'm trying to implement an outbox pattern, that will save domain e

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:

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.

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/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.

Share Improve this question edited Mar 21 at 5:40 marc_s 756k184 gold badges1.4k silver badges1.5k bronze badges asked Mar 20 at 23:27 Phil GoldingPhil Golding 2213 silver badges12 bronze badges
Add a comment  | 

2 Answers 2

Reset to default 2

I have got this working with the help of the approach here: https://github/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",
}

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/microsoftarchive/cqrs-journey/blob/master/source/Infrastructure/Sql/Infrastructure.Sql/EventSourcing/SqlEventSourcedRepository.cs

发布者:admin,转转请注明出处:http://www.yc00.com/questions/1744378973a4571329.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信