一键导入
event-store
Use when configuring EF Core event store with discriminator mapping for event sourcing.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Use when configuring EF Core event store with discriminator mapping for event sourcing.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Use when designing gRPC services, proto files, or adding gRPC-Web or JSON transcoding.
Use when writing async code, propagating CancellationTokens, or fixing async/await pitfalls.
Use when applying or enforcing C# coding style — namespaces, sealed classes, var usage, XML docs.
Use when applying modern C# idioms — records, pattern matching, primary constructors, collection expressions.
Use when registering services, choosing lifetimes, or implementing DI patterns like decorator or keyed services.
Use when selecting or implementing design patterns in C# — factory, builder, strategy, decorator, or mediator.
| name | event-store |
| description | Use when configuring EF Core event store with discriminator mapping for event sourcing. |
| metadata | {"category":"microservice/command","agent":"command-architect"} |
| when_to_use | When configuring EF Core event store, discriminator mapping, or outbox message tables |
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>()The event-store, outbox, and event-routing pipelines deliberately use
Newtonsoft.Json (JsonConvert) instead of System.Text.Json. The
constraints that drive this choice:
IEventData — Newtonsoft handles
$type-discriminated polymorphism out of the box (TypeNameHandling
SerializationBinder). System.Text.Json only added similar
support in .NET 7+ via [JsonPolymorphic] + [JsonDerivedType], and
it requires every concrete type to be statically known at compile time —
incompatible with the event-store's open set of IEventData
implementations contributed by downstream features.[JsonConstructor].StringEnumConverter, contract resolver
for snake-case columns, and NullValueHandling.Ignore are all
Newtonsoft-native. The STJ analogues are partial and noisy.Cross-references: skills/microservice/command/outbox/SKILL.md (line 139
where the outbox publisher imports Newtonsoft.Json),
skills/microservice/processor/event-routing/SKILL.md (line ~200
anti-pattern row reaffirming Newtonsoft), and
knowledge/outbox-pattern.md (line ~251 worked example).
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