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.
2 Answers
Reset to default 2I 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条)