Skip to content
bitzorcas
中EN

Guide

EF Core Isomorphic Integration & Interceptor Guide

Practical guide: configuring and utilizing EF Core in BitzOrcas.Modern, exploring Audit interceptors, PII encryption, global query filters, and CAP outbox integration.

Last updated

EF Core Isomorphic Integration & Interceptor Guide

For teams accustomed to Microsoft’s official ORM ecosystem, BitzOrcas.Modern provides production-grade support within src/Framework/BitzOrcas.Infrastructure.EfCore. Beyond implementing ICommandRepository<T, TId>, the adapter leverages SaveChanges Interceptors to automate audit metadata injection, Personally Identifiable Information (PII) field-level encryption, and CAP transactional outbox publication during the database commit lifecycle.


1. Fluent Entity Mapping: IEntityTypeConfiguration<T>

Domain aggregate roots defined in the Contracts tier are mapped inside independent configuration classes in Infrastructure, keeping domain code free of database-specific dependencies:

using BitzOrcas.Sandbox.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace BitzOrcas.Sandbox.Infrastructure.Mapping;
/// <summary>
/// EF Core configuration mapping for the Note aggregate root.
/// </summary>
public sealed class NoteConfiguration : IEntityTypeConfiguration<Note>
{
public void Configure(EntityTypeBuilder<Note> builder)
{
// Map physical table name; magic T_ prefixes are strictly prohibited
builder.ToTable("SandboxNote");
builder.HasKey(x => x.Id);
builder.Property(x => x.Id)
.ValueGeneratedNever(); // Snowflake IDs generated by application layer
// Map column alias Name and enforce length constraint
builder.Property(x => x.Title)
.HasColumnName("Name")
.HasMaxLength(Note.TitleMaxLength)
.IsRequired();
builder.Property(x => x.TenantId)
.HasMaxLength(64)
.IsRequired();
// Optimistic concurrency token and soft delete
builder.Property(x => x.Version)
.IsConcurrencyToken();
builder.Property(x => x.IsDeleted)
.IsRequired();
}
}

2. Core Interceptors: Audit Backfill & PII Encryption

BitzOrcasDbContext mounts two primary interceptors:

2.1 Audit Metadata Injection: AuditSaveChangesInterceptor

Injects the current authenticated tenant user and timestamp into entities implementing IAuditableEntity:

using System;
using System.Threading;
using System.Threading.Tasks;
using BitzOrcas.Application.Abstractions.Tenancy;
using BitzOrcas.Domain.Contracts;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
namespace BitzOrcas.Infrastructure.EfCore;
/// <summary>
/// Interceptor backfilling audit metadata on mutating entities.
/// </summary>
public sealed class AuditSaveChangesInterceptor(ICurrentTenant currentTenant) : SaveChangesInterceptor
{
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
if (eventData.Context is null)
{
return ValueTask.FromResult(result);
}
var now = DateTimeOffset.UtcNow;
var tenant = currentTenant.Tenant;
var currentUserId = tenant.IsAvailable ? tenant.EffectiveTenantId : "system";
foreach (var entry in eventData.Context.ChangeTracker.Entries<IAuditableEntity>())
{
if (entry.State == EntityState.Added)
{
entry.Entity.CreateTime = now;
entry.Entity.CreateId = currentUserId;
}
else if (entry.State == EntityState.Modified)
{
entry.Entity.ModifyTime = now;
entry.Entity.ModifyId = currentUserId;
}
}
return ValueTask.FromResult(result);
}
}

2.2 PII Protection: PiiSaveChangesInterceptor

Prior to database persistence, interceptors inspect entity properties marked with [PiiEncrypted], applying AES-GCM encryption on write and decrypting transparently on read, ensuring database breaches never expose plaintext confidential data.


3. Global Multitenancy Query Filters

In BitzOrcasDbContext.OnModelCreating, the framework automatically registers global query filters on all types implementing ITenantEntity:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Scan and apply all IEntityTypeConfiguration classes in the assembly
modelBuilder.ApplyConfigurationsFromAssembly(typeof(BitzOrcasDbContext).Assembly);
// Apply tenant scoping filters automatically
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
if (typeof(ITenantEntity).IsAssignableFrom(entityType.ClrType))
{
var parameter = System.Linq.Expressions.Expression.Parameter(entityType.ClrType, "e");
var filter = System.Linq.Expressions.Expression.Lambda(
System.Linq.Expressions.Expression.Equal(
System.Linq.Expressions.Expression.Property(parameter, nameof(ITenantEntity.TenantId)),
System.Linq.Expressions.Expression.Property(
System.Linq.Expressions.Expression.Constant(_tenantContext),
nameof(ITenantContext.TenantId))),
parameter);
entityType.SetQueryFilter(filter);
}
}
}

4. Transactional Outbox Coordination (EfCoreUnitOfWork)

To ensure physical atomicity between database mutations and domain event publishing, EfCoreUnitOfWork joins EF Core transactions with the DotNetCore.CAP transactional outbox:

public async Task<int> CommitAsync(CancellationToken cancellationToken = default)
{
// Begin physical transaction attached to CAP publisher
using var transaction = await _dbContext.Database.BeginTransactionAsync(_capPublisher, autoCommit: false, cancellationToken);
try
{
var writtenCount = await _dbContext.SaveChangesAsync(cancellationToken);
await transaction.CommitAsync(cancellationToken);
return writtenCount;
}
catch
{
await transaction.RollbackAsync(cancellationToken);
throw;
}
}

100%

Scroll or use controls to zoom · drag when enlarged · double-click for 100% / 200%