بنقرة واحدة
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 ويثبّتها لك.
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.
استنادا إلى تصنيف SOC المهني
| 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