EF Core event store with discriminator mapping for event-sourced command services. Covers ApplicationDbContext, GenericEventConfiguration with Newtonsoft.Json Data conversion, EventConfiguration discriminator pattern, OutboxMessageConfiguration with Event FK, and unique index on AggregateId+Sequence. Trigger: event store, command database, EF Core events, discriminator.
From dotnet-ai-kitnpx claudepluginhub faysilalshareef/dotnet-ai-kit --plugin dotnet-ai-kitThis skill uses the workspace's default tool permissions.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Compares coding agents like Claude Code and Aider on custom YAML-defined codebase tasks using git worktrees, measuring pass rate, cost, time, and consistency.
Event entitiesEvent base table uses TPH (table-per-hierarchy) with a discriminator on EventTypeEventConfiguration sets up the discriminator mapping from EventType enum to concrete event classesGenericEventConfiguration<TEntity, TData> handles the Newtonsoft.Json conversion for the Data column(AggregateId, Sequence) prevents duplicate eventsOutboxMessage has a 1:1 relationship with Event via shared primary key (HasForeignKey<OutboxMessage>(e => e.Id))Type column is stored as a string with HasConversion<string>()Handles JSON serialization of the typed Data property for each concrete event type. Uses Newtonsoft.Json (not System.Text.Json).
using {Company}.{Domain}.Commands.Domain.Events;
using {Company}.{Domain}.Commands.Domain.Events.DataTypes;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Newtonsoft.Json;
namespace {Company}.{Domain}.Commands.Infra.Persistence.Configurations;
public class GenericEventConfiguration<TEntity, TData> : IEntityTypeConfiguration<TEntity>
where TEntity : Event<TData>
where TData : IEventData
{
public void Configure(EntityTypeBuilder<TEntity> builder)
{
var jsonSerializerSettings = new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore
};
jsonSerializerSettings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter());
builder.Property(e => e.Data).HasConversion(
v => JsonConvert.SerializeObject(v, jsonSerializerSettings),
v => JsonConvert.DeserializeObject<TData>(v)!
).HasColumnName("Data");
}
}
Key details:
TEntity (the concrete event class) and TData (the event data type)TEntity : Event<TData> and TData : IEventDataNullValueHandling.Ignore and StringEnumConverterDeserializeObject)"Data" explicitlyMaps the EventType enum to concrete event classes using EF Core's discriminator pattern.
using {Company}.{Domain}.Commands.Domain.Enums;
using {Company}.{Domain}.Commands.Domain.Events;
using {Company}.{Domain}.Commands.Domain.Events.Orders;
using {Company}.{Domain}.Commands.Domain.Events.Invoices;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace {Company}.{Domain}.Commands.Infra.Persistence.Configurations;
public class EventConfiguration : IEntityTypeConfiguration<Event>
{
public void Configure(EntityTypeBuilder<Event> builder)
{
builder.HasIndex(e => new { e.AggregateId, e.Sequence }).IsUnique();
builder.Property(e => e.Type)
.HasMaxLength(128)
.HasConversion<string>();
builder.HasDiscriminator(e => e.Type)
.HasValue<OrderCreated>(EventType.OrderCreated)
.HasValue<OrderUpdated>(EventType.OrderUpdated)
.HasValue<OrderItemsAdded>(EventType.OrderItemsAdded)
.HasValue<OrderItemsRemoved>(EventType.OrderItemsRemoved)
.HasValue<InvoiceGenerated>(EventType.InvoiceGenerated)
.HasValue<InvoiceUpdated>(EventType.InvoiceUpdated);
}
}
Key details:
(AggregateId, Sequence) prevents duplicate eventsType is stored as a string via HasConversion<string>() with max length 128EventType enum, mapping each enum value to a concrete event classusing {Company}.{Domain}.Commands.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace {Company}.{Domain}.Commands.Infra.Persistence.Configurations;
public class OutboxMessageConfiguration : IEntityTypeConfiguration<OutboxMessage>
{
public void Configure(EntityTypeBuilder<OutboxMessage> builder)
{
builder.HasOne(e => e.Event)
.WithOne()
.HasForeignKey<OutboxMessage>(e => e.Id)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
}
}
Key details:
HasOne(e => e.Event).WithOne() -- OutboxMessage has an Event navigation propertyHasForeignKey<OutboxMessage>(e => e.Id) -- the OutboxMessage's own Id is the FKusing {Company}.{Domain}.Commands.Domain.Entities;
using {Company}.{Domain}.Commands.Domain.Events;
using {Company}.{Domain}.Commands.Domain.Events.DataTypes;
using {Company}.{Domain}.Commands.Domain.Events.Orders;
using {Company}.{Domain}.Commands.Domain.Events.Invoices;
using {Company}.{Domain}.Commands.Infra.Persistence.Configurations;
using Microsoft.EntityFrameworkCore;
namespace {Company}.{Domain}.Commands.Infra.Persistence;
public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : DbContext(options)
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Base configurations
modelBuilder.ApplyConfiguration(new EventConfiguration());
modelBuilder.ApplyConfiguration(new OutboxMessageConfiguration());
// GenericEventConfiguration for each concrete event type
modelBuilder.ApplyConfiguration(new GenericEventConfiguration<OrderCreated, OrderCreatedData>());
modelBuilder.ApplyConfiguration(new GenericEventConfiguration<OrderUpdated, OrderUpdatedData>());
modelBuilder.ApplyConfiguration(new GenericEventConfiguration<OrderItemsAdded, OrderItemsAddedData>());
modelBuilder.ApplyConfiguration(new GenericEventConfiguration<OrderItemsRemoved, OrderItemsRemovedData>());
modelBuilder.ApplyConfiguration(new GenericEventConfiguration<InvoiceGenerated, InvoiceGeneratedData>());
modelBuilder.ApplyConfiguration(new GenericEventConfiguration<InvoiceUpdated, InvoiceUpdatedData>());
base.OnModelCreating(modelBuilder);
}
public DbSet<Event> Events { get; set; }
public DbSet<OutboxMessage> OutboxMessages { get; set; }
}
Key details:
DbSet<Event> for all events (TPH pattern)EventConfiguration AND a GenericEventConfiguration registrationbase.OnModelCreating(modelBuilder) is called at the end{ get; set; } (not expression-bodied => Set<T>())namespace {Company}.{Domain}.Commands.Infra.Persistence.Repositories;
public class EventRepository : AsyncRepository<Event>, IEventRepository
{
private readonly ApplicationDbContext _appDbContext;
public EventRepository(ApplicationDbContext appDbContext) : base(appDbContext)
{
_appDbContext = appDbContext;
}
public async Task<IEnumerable<Event>> GetAllByAggregateIdAsync(
Guid aggregateId, CancellationToken cancellationToken)
=> await _appDbContext.Events
.AsNoTracking()
.Where(e => e.AggregateId == aggregateId)
.OrderBy(e => e.Sequence)
.ToListAsync(cancellationToken);
}
| Anti-Pattern | Correct Approach |
|---|---|
| Updating existing events | Events are immutable -- append only |
| Missing discriminator mapping | Every concrete event needs HasValue<> in EventConfiguration |
| Missing GenericEventConfiguration | Every concrete event also needs its Data JSON conversion registered |
| Using System.Text.Json for Data column | Use Newtonsoft.Json (project convention) |
| Separate tables per event type | Use TPH with single Events table and discriminator |
| OutboxMessage with its own independent Id | OutboxMessage.Id IS the Event.Id (shared PK/FK) |
# Find ApplicationDbContext
grep -r "class ApplicationDbContext" --include="*.cs" src/
# Find GenericEventConfiguration registrations
grep -r "GenericEventConfiguration" --include="*.cs" src/
# Find discriminator setup
grep -r "HasDiscriminator" --include="*.cs" src/
# Find EventConfiguration
grep -r "class EventConfiguration" --include="*.cs" src/
# Find OutboxMessageConfiguration
grep -r "OutboxMessageConfiguration" --include="*.cs" src/
EventConfiguration (HasValue<NewEvent>(EventType.NewEvent))ApplicationDbContext.OnModelCreating(AggregateId, Sequence) exists