The TaskHub.Shared.Persistence.EntityFramework module is a sophisticated abstraction over EF Core designed for high-performance, observable microservices. It enforces architectural consistency by integrating the Outbox pattern and Media management directly into the database transaction lifecycle.
ContextBase<TEntity> is the primary abstract class that your service-specific DbContext must inherit from. It handles several critical responsibilities:
OnModelCreating, it calls builder.ApplyConfigurationsFromAssembly(settings.Assembly). This ensures that all IEntityTypeConfiguration<T> implementations in your project are automatically applied without manual registration.OutboxMessage and MediaMetadata based on the provided PersistenceOptions.TEntity generic acts as a primary entry point or marker for the contextβs main responsibility.The most powerful feature of this module is how it bridges the Domain Layer and the Persistence Layer. The UnitOfWorkBase class overrides the standard saving mechanism:
SaveChangesAsync, it scans the EF Core Change Tracker for all entities implementing IAggregate.IDomainEvents from these aggregates.OutboxMessage records using the IOutboxMessageFactory.OutboxMessage records are committed in a single database transaction. This guarantees that an event is never missed if the data change succeeds.ClearEvents() is called on the aggregates to prevent duplicate event creation.ContextBase<TEntity>| Method | Description |
| :β | :β |
| protected override void OnModelCreating(ModelBuilder builder) | Orchestrates assembly scanning and conditional feature configuration. |
| DbSet<TEntity> Data | A shortcut to the primary entity set. |
UnitOfWorkBase<TContext>| Method | Description |
| :β | :β |
| public async Task SaveAsync(CancellationToken ct) | The main entry point for transactional saves. Handles Outbox integration. |
PersistenceOptions| Property | Type | Description |
| :β | :β | :β |
| ConnectionString | string | The database connection string. |
| Outbox | OutboxOptions | Settings for the Outbox pattern. |
| Media | MediaOptions | Settings for media metadata tracking. |
using Microsoft.EntityFrameworkCore;
using TaskHub.Shared.Persistence.EntityFramework.Context;
using TaskHub.Shared.Persistence.EntityFramework.Options;
namespace TaskHub.ProjectService.Infrastructure.Persistence;
public class ProjectDbContext(ContextOptions settings) : ContextBase<Project>(settings)
{
// Primary set 'Data' inherited from base
public DbSet<TaskItem> TaskItems => Set<TaskItem>();
}
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
public class ProjectConfiguration : IEntityTypeConfiguration<Project>
{
public void Configure(EntityTypeBuilder<Project> builder)
{
builder.ToTable("Projects");
builder.HasKey(x => x.Id);
// Automatic Value Object mapping example
builder.Property(x => x.Title)
.HasConversion(x => x.Value, x => new Title(x))
.HasMaxLength(200);
builder.HasMany(x => x.Tasks)
.WithOne()
.HasForeignKey(x => x.ProjectId);
}
}
public class CompleteProjectHandler(IUnitOfWork unitOfWork, IProjectRepository repository)
: ICommandHandler<CompleteProjectCommand, Result>
{
public async Task<Result> HandleAsync(CompleteProjectCommand cmd, CancellationToken ct)
{
var project = await repository.GetAsync(cmd.Id, ct);
if (project == null) return ResultFactory.OnFailed(404, "Project not found");
project.MarkAsComplete(); // This internally adds a Domain Event
// This single call will save the Project AND the OutboxMessage for the event
await unitOfWork.SaveAsync(ct);
return ResultFactory.OnSuccess();
}
}
"Persistence": {
"ConnectionString": "Host=localhost;Database=TaskHub;Username=admin;Password=secret",
"Outbox": {
"IsEnabled": true,
"TableName": "OutboxMessages",
"BatchSize": 100,
"MaxRetryCount": 5,
"ProcessingInterval": "00:00:10"
},
"Media": {
"IsEnabled": true,
"TableName": "MediaMetadata"
}
}
false, domain events are ignored by the Unit of Work.The module utilizes standard EF Core instrumentation, creating spans for:
db.query: The actual SQL query executed.db.transaction: The lifecycle of the database transaction.db.system: e.g., postgresql or sqlserver.db.name: The database name.db.statement: The SQL command (sanitized for security).outbox.event_count: Number of events captured in the current save operation.IUnitOfWork: Avoid calling DbContext.SaveChangesAsync() directly to ensure the Outbox integration is triggered.DbContext clean of logic; use IEntityTypeConfiguration for mappings.UnitOfWork can manage them effectively.UnitOfWork handle it from domain events.IUnitOfWork across multiple DbContext types unless explicitly configured for distributed transactions.