Skip to content
bitzorcas
中EN

Guide

EF Core 同构集成与拦截器实战

实战指南:在 BitzOrcas.Modern 中配置与使用 EF Core,深入 Audit 拦截器、PII 数据加密、全局租户过滤与 CAP 事务发件箱集成。

Last updated

EF Core 同构集成与拦截器实战 (EF Core Integration & Interceptors)

对于习惯使用 Microsoft 官方 ORM 技术栈的团队,BitzOrcas.Modern 在 src/Framework/BitzOrcas.Infrastructure.EfCore 中提供了完整的生产级支持。它不仅完美实现了领域命令仓储接口 ICommandRepository<T, TId>,更通过一系列精巧的 SaveChanges 拦截器(Interceptors),将审计元数据回填、敏感数据(PII)加密与 CAP 事务发件箱无缝嵌入数据保存生命周期。


1. 实体 Fluent 配置:IEntityTypeConfiguration<T>

在契约层定义的领域实体,在基础设施层通过独立的配置类进行映射,避免领域逻辑依赖具体的 ORM 包:

using BitzOrcas.Sandbox.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace BitzOrcas.Sandbox.Infrastructure.Mapping;
/// <summary>
/// Note 实体的 EF Core 映射配置
/// </summary>
public sealed class NoteConfiguration : IEntityTypeConfiguration<Note>
{
public void Configure(EntityTypeBuilder<Note> builder)
{
// 映射物理表名,严禁使用 T_ 前缀魔术命名
builder.ToTable("SandboxNote");
builder.HasKey(x => x.Id);
builder.Property(x => x.Id)
.ValueGeneratedNever(); // 使用雪花 ID 生成器,不依赖数据库自增
// 对应持久化别名列 Name 与最大长度
builder.Property(x => x.Title)
.HasColumnName("Name")
.HasMaxLength(Note.TitleMaxLength)
.IsRequired();
builder.Property(x => x.TenantId)
.HasMaxLength(64)
.IsRequired();
// 乐观并发锁版本列与软删除
builder.Property(x => x.Version)
.IsConcurrencyToken();
builder.Property(x => x.IsDeleted)
.IsRequired();
}
}

2. 核心拦截器机制:审计回填与 PII 保护

BitzOrcas 在 BitzOrcasDbContext 中挂载了两个关键拦截器:

2.1 审计属性自动填充:AuditSaveChangesInterceptor

自动为所有实现 IAuditableEntity 的实体注入当前租户操作人与时间戳,避免业务 Handler 中手写赋值:

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>
/// 审计元数据自动填充拦截器
/// </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 敏感数据自动脱敏加密:PiiSaveChangesInterceptor

在数据入库前,拦截打标了 [PiiEncrypted] 属性的实体字段,使用 AES-GCM 进行对称加密,并在读取时自动解密,确保落盘数据即使发生数据库泄露也无法被明文破解。


3. 全局租户隔离过滤器 (Global Query Filter)

在 BitzOrcasDbContext.OnModelCreating 中,框架为所有继承 TenantAggregateRoot 的实体自动注册全局租户隔离过滤器:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// 自动扫描当前程序集内的所有 IEntityTypeConfiguration
modelBuilder.ApplyConfigurationsFromAssembly(typeof(BitzOrcasDbContext).Assembly);
// 为多租户实体自动附加全局隔离过滤器
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. CAP 事务发件箱与工作单元 (EfCoreUnitOfWork)

为了保证领域事件派发与数据库写入的物理一致性,EfCoreUnitOfWork 将 EF Core 的 IDbContextTransaction 绑定至 CAP 事务:

public async Task<int> CommitAsync(CancellationToken cancellationToken = default)
{
// 开启物理数据库事务并接入 CAP 事务上下文
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%

滚轮或按钮缩放 · 放大后拖动画面 · 双击切换 100% / 200%