Galosys.Foundation.EntityFrameworkCore 26.10.8.1

There is a newer version of this package available.
See the version list below for details.
dotnet add package Galosys.Foundation.EntityFrameworkCore --version 26.10.8.1
                    
NuGet\Install-Package Galosys.Foundation.EntityFrameworkCore -Version 26.10.8.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Galosys.Foundation.EntityFrameworkCore" Version="26.10.8.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Galosys.Foundation.EntityFrameworkCore" Version="26.10.8.1" />
                    
Directory.Packages.props
<PackageReference Include="Galosys.Foundation.EntityFrameworkCore" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Galosys.Foundation.EntityFrameworkCore --version 26.10.8.1
                    
#r "nuget: Galosys.Foundation.EntityFrameworkCore, 26.10.8.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Galosys.Foundation.EntityFrameworkCore@26.10.8.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Galosys.Foundation.EntityFrameworkCore&version=26.10.8.1
                    
Install as a Cake Addin
#tool nuget:?package=Galosys.Foundation.EntityFrameworkCore&version=26.10.8.1
                    
Install as a Cake Tool

Galosys.Foundation.EntityFrameworkCore

成熟度: 🟢 稳定 — 生产可用,测试充分,活跃维护

简介

Galosys.Foundation.EntityFrameworkCore 基于 Entity Framework Core 提供数据访问层集成,包含自动审计、多租户、软删除、仓储模式等企业级特性。

特性

  • 自动审计 - 自动填充创建者、修改者、时间戳
  • 多租户支持 - 租户数据隔离和全局过滤器
  • 软删除 - 标记删除而非物理删除
  • 全局查询过滤器 - 自动应用删除、租户、应用过滤条件
  • 雪花 ID 自动生成 - 基于 Snowflake 算法的 ID 生成
  • snake_case 列名映射 - 自动将 PascalCase 属性映射到 snake_case 列名
  • 分表支持 - 按年/月/日动态分表
  • 仓储模式 - 通用仓储接口和实现
  • 工作单元模式 - 事务管理
  • 动态查询 - 基于表达式的动态查询构建
  • 领域事件 - 保存时自动发布领域事件
  • 乐观并发控制 - 可选的 RowVersion 并发令牌,支持冲突自动重试
  • Outbox 批量处理 - EfCoreOutboxStore 实现 TryAcquireBatchAsync(PostgreSQL SKIP LOCKED)、MarkManySentAsync、MarkManyFailedAsync 批量 API
  • 多租户存储 - EfCoreTenantStore<TDbContext> 基于 EF Core 的只读租户元数据持久化存储(AddEfCoreTenantStore<TDbContext>() 覆写默认 InMemory)
  • 连接弹性 - 四种数据库自动重试瞬态故障
  • 读写分离 - DbCommandInterceptor 自动路由查询到副本
  • 支持多种数据库 - SQL Server、PostgreSQL、MySQL、SQLite、Oracle
  • Keyset 游标分页 - KeysetPageAsync<T, TOrderBy> 深翻页替代 OFFSET Skip/Take(常数时间翻页)
  • Filtered Include - IncludeWhere 子集合服务端过滤,避免"全量子表拉回内存"
  • 投影引导 - SelectTrimmed 字段裁剪(SQL 仅 SELECT 必要列)
  • 批量写入 - AddRangeOptimizedAsync / SaveChangesOptimizedAsync 分批 + 控追踪(10K-100K 行,8x 加速 / 77% 内存节省)
  • 动态查询 - ApplySort / ApplyFilters(12 操作符 + [Filterable] 白名单)/ SelectFields
  • Split Query 全局默认 - UseQuerySplittingBehavior(SplitQuery) 防多集合 Include 笛卡尔爆炸 + AsSplitQuerySafe
  • Untracked 只读入口 - PageUntrackedAsync / QueryUntracked / PageUntracked 免追踪查询

安装

<PackageReference Include="Galosys.Foundation.EntityFrameworkCore" Version="x.x.x" />

配置

1. 连接字符串配置

在 appsettings.json 中配置连接字符串,需包含 provider 信息:

{
  "ConnectionStrings": {
    "Default": "Server=localhost;Database=MyDb;User Id=sa;Password=xxx;Provider=Microsoft.Data.SqlClient",
    "Postgres": "Host=localhost;Database=mydb;Username=postgres;Password=xxx;Provider=Npgsql",
    "MySql": "Server=localhost;Database=mydb;User=root;Password=xxx;Provider=MySqlConnector",
    "Sqlite": "Data Source=mydb.db;Provider=Microsoft.Data.Sqlite"
  }
}

2. 注册 DbContext

使用 [DbContext] 特性标记 DbContext 类,模块会自动发现并注册:

[DbContext("Default")]
public class AppDbContext : DbContext<AppDbContext>
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

    public DbSet<User> Users => Set<User>();
    public DbSet<Order> Orders => Set<Order>();
}

3. 模块自动注册

模块会自动扫描带有 [DbContext] 特性的类并注册到 DI 容器:

// 在 Program.cs 或 Startup.cs 中
services.AddDbContext(configuration);

前置约定:DbContext<T> 依赖 Galosys.Core 的 AddCore() 注册的上下文服务(ITenantContext/IAppContext/IUserContext/ITimeProvider/ApplicationMessagePublisher 等),宿主须先调用 AddCore()。EF Core 集成包(Actuator/Outbox/Authorization 等)采用"宿主泛型上下文 + 显式装配"约定,详见 docs/adr/0001-efcore-integration-conventions.md。

使用示例

实体定义

// 完整实体(含审计、软删除)
[Table("users")]
public class User : FullEntity<long>
{
    [SnowflakeId]
    public override long Id { get; protected set; }
    public string Name { get; set; }
    [CreatorId]
    public override long CreatorId { get; protected set; }
    [CreatedAt]
    public override DateTime CreatedAt { get; protected set; }
}

// 多租户实体
[Table("orders")]
public class Order : MtMaEntity
{
    [SnowflakeId]
    public override long Id { get; protected set; }
    public string OrderNo { get; set; }
    public decimal Amount { get; set; }
}

DbContext 使用

public class UserService
{
    private readonly AppDbContext _context;

    public UserService(AppDbContext context)
    {
        _context = context;
    }

    public IQueryable<User> GetActiveUsers()
    {
        return _context.Query<User>(); // 自动过滤已删除记录
    }

    public async Task AddUserAsync(User user)
    {
        await _context.Entity<User>().AddAsync(user);
        await _context.SaveChangesAsync();
    }

    public async Task SaveWithEventsAsync()
    {
        await _context.SaveEntitiesAsync(); // 保存并发布领域事件
    }
}

仓储模式

定义业务接口,继承 IRepository<T, TID>,再用 [Repository] 标记实现类:

// 1. 定义业务仓储接口
public interface IUserRepository : IRepository<User, long>
{
    Task<User?> GetByNameAsync(string name);
}

// 2. 实现类继承 EfCoreRepository,用 [Repository] 标记自动注册
[Repository]
public class UserRepository : EfCoreRepository<AppDbContext, User, long>, IUserRepository
{
    public UserRepository(AppDbContext ctx) : base(ctx) { }

    public async Task<User?> GetByNameAsync(string name)
    {
        return await Query(u => u.Name == name).FirstOrDefaultAsync();
    }
}

// 3. 使用时注入业务接口
public class UserService
{
    private readonly IUserRepository _repository;

    public UserService(IUserRepository repository)
    {
        _repository = repository;
    }

    public async Task<User?> GetByIdAsync(long id) => await _repository.FindOneAsync(id);

    public async Task AddAsync(params User[] users)
    {
        await _repository.AddAsync(users);
        await _repository.SaveAsync();
    }
}

工作单元

行为变更: CommitAsync 现在会自动调用 SaveChangesAsync,无需手动保存。内部通过 _changesSaved 标志位防止重复保存,兼容已有的手动 SaveChangesAsync + CommitAsync 双调用模式。

简化写法(推荐):

public class OrderService
{
    private readonly IUnitOfWork _unitOfWork;
    private readonly AppDbContext _context;

    public OrderService(IUnitOfWork unitOfWork, AppDbContext context)
    {
        _unitOfWork = unitOfWork;
        _context = context;
    }

    public async Task CreateOrderAsync(Order order)
    {
        await _unitOfWork.BeginTransactionAsync();
        try
        {
            _context.Entity<Order>().Add(order);
            // CommitAsync 自动调用 SaveChangesAsync,无需手动保存
            await _unitOfWork.CommitAsync();
        }
        catch
        {
            await _unitOfWork.RollbackAsync();
            throw;
        }
    }
}

兼容写法(手动 SaveChanges + CommitAsync):

public async Task CreateOrderAsync(Order order)
{
    await _unitOfWork.BeginTransactionAsync();
    try
    {
        _context.Entity<Order>().Add(order);
        await _context.SaveChangesAsync();  // 手动保存
        await _unitOfWork.CommitAsync();     // 不会重复保存(_changesSaved 标志位)
    }
    catch
    {
        await _unitOfWork.RollbackAsync();
        throw;
    }
}

ITimeProvider 注入

DbContext<T> 通过构造函数注入 ITimeProvider(Core 模块已注册 Singleton),审计字段自动使用 ITimeProvider 获取时间,确保时区一致且可测试。无需额外配置。

// 单元测试中替换时间源
services.AddSingleton<ITimeProvider>(new TestTimeProvider(fixedTime));

乐观并发控制

继承 ConcurrencyEntity<TID> 即可启用 RowVersion 并发令牌:

[Table("products")]
public class Product : ConcurrencyEntity<long>
{
    public string Name { get; set; }
    public decimal Price { get; set; }
}

冲突时自动重试更新:

public async Task UpdatePriceAsync(Product product)
{
    await _repository.UpdateWithRetryAsync(product, maxRetries: 3);
}

更丰富的冲突处理(DatabaseWins / MergeFields / 三组值快照)见 § 并发冲突 3 策略。

执行策略配置

通过 [DbContext] 特性配置重试参数(默认 3 次 / 5 秒):

[DbContext("Default", MaxRetryCount = 5, MaxRetryDelaySeconds = 10)]
public class AppDbContext : DbContext<AppDbContext> { }
参数 默认值 说明
MaxRetryCount 3 瞬态故障最大重试次数
MaxRetryDelaySeconds 5 最大重试延迟(秒)

读写分离

1. 配置主库和副本

在 appsettings.json 中添加副本连接字符串({名称}.Replica{N} 格式):

{
  "ConnectionStrings": {
    "Default": "Server=master-host;Database=MyDb;User Id=sa;Password=xxx;Provider=Microsoft.Data.SqlClient",
    "Default.Replica1": "Server=replica1-host;Database=MyDb;User Id=sa;Password=xxx;Provider=Microsoft.Data.SqlClient",
    "Default.Replica2": "Server=replica2-host;Database=MyDb;User Id=sa;Password=xxx;Provider=Microsoft.Data.SqlClient"
  }
}

最小化配置仅需主库连接字符串,无副本时不启用读写分离。

2. 注册数据源
services.AddDataSources(configuration); // 自动识别副本,创建 HealthCheckedDataSourcePool
3. 使用 DataSourceContext 切换
// 定义业务仓储接口
public interface IOrderQueryRepository : IRepository<Order, long>
{
    Task<List<Order>> GetRecentOrdersAsync(int count);
}

[Repository]
public class OrderQueryRepository : EfCoreRepository<AppDbContext, Order, long>, IOrderQueryRepository
{
    public OrderQueryRepository(AppDbContext ctx) : base(ctx) { }

    public async Task<List<Order>> GetRecentOrdersAsync(int count)
    {
        using (DataSourceContext.SwitchTo(DataSourceType.Replica)) // SELECT 走副本
        {
            return await Query()
                .OrderByDescending(o => o.CreatedAt)
                .Take(count)
                .ToListAsync();
        }
    }
}

// 使用时注入业务接口
public class OrderAppService
{
    private readonly IOrderQueryRepository _queryRepo;
    private readonly IOrderRepository _orderRepo;

    public OrderAppService(IOrderQueryRepository queryRepo, IOrderRepository orderRepo)
    {
        _queryRepo = queryRepo;
        _orderRepo = orderRepo;
    }

    public async Task<Order> CreateOrderAsync(Order order)
    {
        await _orderRepo.AddAsync(order); // DML 自动走主库,无需手动切换
        await _orderRepo.SaveAsync();
        return order;
    }

    public Task<List<Order>> GetRecentOrdersAsync(int count)
    {
        return _queryRepo.GetRecentOrdersAsync(count); // 内部切换到副本
    }
}
场景 行为
默认(Master) 所有查询走主库
SwitchTo(Replica) SELECT 走副本
DML 操作 强制走主库,忽略上下文
事务内 不切换,保持主库连接
using 结束 自动恢复之前的上下文
4. 嵌套切换
using (DataSourceContext.SwitchTo(DataSourceType.Master))
{
    await SaveAsync(); // 写操作走主库

    using (DataSourceContext.SwitchTo(DataSourceType.Replica))
    {
        var stats = await QueryStatsAsync(); // 读操作走副本
    }
    // 自动恢复为 Master
}

分表

通过 [Table] + [Sharding] 特性标记实体,框架自动拼接表名后缀:

[Table("orders")]
[Sharding(ShardingType.Month)] // 按月分表 → orders_202604
public class Order : FullEntity<long>
{
    public string OrderNo { get; set; }
    public decimal Amount { get; set; }
}

[Table("logs")]
[Sharding(ShardingType.Day)] // 按日分表 → logs_20260415
public class Log : FullEntity<long>
{
    public string Message { get; set; }
}

[Table("reports")]
[Sharding(ShardingType.Year)] // 按年分表 → reports_2026(默认)
public class AnnualReport : FullEntity<long>
{
    public string Title { get; set; }
}
ShardingType 示例表名 适用场景
Year(默认) orders_2026 年度汇总、报表
Month orders_202604 订单、交易流水
Day logs_20260415 日志、高频数据

表名后缀基于 DateTimeOffset.Now 自动生成,DbContext OnModelCreating 时自动路由到对应表。需配合 [DbContext(Dynamic = true)] 启用动态模型缓存。

读写分离 + 分表组合

[Table("order_items")]
[Sharding(ShardingType.Month)]
public class OrderItem : FullEntity<long>
{
    public long OrderId { get; set; }
    public string ProductName { get; set; }
    public int Quantity { get; set; }
}

[Repository]
public class OrderItemRepository : EfCoreRepository<AppDbContext, OrderItem, long>, IOrderItemRepository
{
    public OrderItemRepository(AppDbContext ctx) : base(ctx) { }

    public async Task<List<OrderItem>> GetItemsByOrderAsync(long orderId)
    {
        using (DataSourceContext.SwitchTo(DataSourceType.Replica)) // 副本读取 + 按月分表
        {
            return await Query(i => i.OrderId == orderId).ToListAsync();
        }
    }
}

EntityTypeConfiguration 接口驱动配置

通过继承 EntityTypeConfigurationBase,根据实体实现的接口自动获得对应的索引和字段约束配置:

实体实现的接口 自动配置
ICreator CreatedAt 索引 + CreatorId 索引 + CreatorName HasMaxLength(64)
ILastModifier LastModifierId 索引 + LastModifierName HasMaxLength(64)
IMultiTenancy(仅) TenantId 单列索引
IMultiApplication(仅) AppId 单列索引
IMultiTenancy + IMultiApplication (TenantId, AppId) 复合索引
IConcurrency RowVersion 乐观并发令牌
// 用户配置 — 实体实现 IMultiTenancy,自动获得 TenantId 索引
public class SysUserConfiguration : EntityTypeConfigurationBase<SysUser, long>
{
    // 基类自动配置:CreatedAt/CreatorId 索引 + LastModifierId 索引 + TenantId 索引
    // 可覆写 ConfigureCore 添加自定义配置
}

// 菜单配置 — 实体实现 IMultiApplication,自动获得 AppId 索引
public class SysMenuConfiguration : EntityTypeConfigurationBase<SysMenu, long> { }

// 租户应用配置 — 实体同时实现两个接口,自动获得 (TenantId, AppId) 复合索引
public class SysTenantAppConfiguration : EntityTypeConfigurationBase<SysTenantAppConfig, long> { }

// 全局数据 — 仅 FullEntity 审计字段索引
public class SysRegionConfiguration : EntityTypeConfigurationBase<SysRegion, long> { }

说明:全局查询过滤器(软删除、租户、应用)由 DefaultGlobalFilterProvider 基于 IDeletable/IMultiTenancy/IMultiApplication 接口统一应用;蛇形命名映射由 EntityTypeConfigurationBase 处理;IConcurrency 自动检测并配置并发令牌。索引配置完全由接口检测驱动,无需为不同实体类型选择不同基类。

模块化异常处理

EFCore 模块提供 DbUpdateExceptionHandler(绑定 Microsoft.EntityFrameworkCore.DbUpdateException,[ExceptionHandler(100)]),优先于框架默认 DbExceptionHandler(0) 接管 SaveChanges / SaveChangesAsync 抛出的实体更新异常:

[ExceptionHandler(100)]
public sealed class DbUpdateExceptionHandler : TypedExceptionHandler<Microsoft.EntityFrameworkCore.DbUpdateException>
{
    public override ValueTask<ExceptionHandlingOutput> HandleAsync(
        Microsoft.EntityFrameworkCore.DbUpdateException exception,
        CancellationToken cancellationToken = default)
    {
        var entityName = exception.Entries.FirstOrDefault()?.Entity.GetType().Name ?? "实体";
        return ValueTask.FromResult(new ExceptionHandlingOutput(
            statusCode: 500,
            code: ErrorCode.Of("B", (short)BizErrorCode.Error),
            message: $"提示:数据库更新失败({entityName}),请联系管理员"));
    }
}

处理器继承 System.TypedExceptionHandler<T> 并标注 [ExceptionHandler(100)],经 AddAnnotationConfiguration 自动注册;当前仅暴露首个受影响实体类型名到提示文案,更详细的 entry.CurrentValues / InnerException 展开规划在未来 IExceptionExpander 专题中提供。

核心类

类/接口 说明
IRepository<T, TID> 通用仓储接口,定义增删改查
IUnitOfWork 工作单元接口,管理事务
DbContext<T> DbContext 基类,提供自动审计、过滤器等
[DbContext] 标记 DbContext 的特性,指定连接字符串名称
[Repository] 标记仓储实现类,自动注册到 DI
EfCoreRepository<TContext, T, TID> EF Core 仓储实现,继承并添加 UpdateWithRetryAsync
KeysetPagingExtensions Keyset 游标分页(KeysetPageAsync<T, TOrderBy> / EncodeCursor / TryDecodeCursor)
FilteredIncludeExtensions 子集合过滤 Include(IncludeWhere)
ProjectionExtensions 投影引导(SelectTrimmed)
BulkInsertExtensions 分批批量插入(AddRangeOptimizedAsync + BulkInsertOptions)
DbContextBulkExtensions 分批保存(SaveChangesOptimizedAsync)
ChangeTrackerReloadExtensions 重载已追踪实体(ReloadTrackedAsync<T>,ExecuteUpdate 后状态同步)
DynamicQueryExtensions 动态排序/过滤/字段选择(ApplySort / ApplyFilters / SelectFields)
EfCoreUnitOfWork<TContext> EF Core 工作单元实现
ConcurrencyEntity<TID> 乐观并发实体基类(RowVersion)
ConflictStrategy 并发冲突 3 策略枚举(ClientWins / DatabaseWins / MergeFields)
ConflictResult<T> 冲突结果(CurrentEntity + CurrentRowVersion),约束 T : class, IConcurrency
EfCoreRepositoryConcurrencyExtensions 并发 3 策略扩展(UpdateWithClientWinsAsync / TryUpdateWithDatabaseWinsAsync / MergeAndUpdateAsync)
DbUpdateConcurrencyExceptionExtensions 冲突三组值快照(CaptureSnapshotAsync → ConcurrencySnapshot)
ConcurrencySnapshot 冲突快照(Proposed / Original / Database 三组值)
ICrossDbContextTransaction 跨 DbContext 事务抽象(共享 DbConnection + DbTransaction)
CrossDbContextTransaction ICrossDbContextTransaction 默认实现
InMemoryMergeExtensions 跨 DbContext Join 的内存合并替代(MergeAsync)
MigrationsHistoryTableOptions 迁移历史表命名配置(Schema / TableName / UseDefault)
ReadWriteSplittingInterceptor 读写分离拦截器
ShardingAttribute 分表特性
ShardingExtensions 分表扩展方法(EnsureShardingTablesAsync)

手动建表

分表场景下,应用启动时需手动创建当前周期的物理表:

// 在 Program.cs 或 Startup.cs 中调用一次
using (var scope = app.Services.CreateScope())
{
    var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
    await db.EnsureShardingTablesAsync(); // CREATE TABLE IF NOT EXISTS 语义
}

EnsureShardingTablesAsync 扫描所有 [Sharding] 实体,为当前周期创建物理表。表已存在时不报错。

模型缓存策略

DynamicModelCacheKeyFactory 根据 [Sharding] 粒度优化模型缓存刷新频率:

粒度 缓存刷新周期 说明
Year 每年 1 月 1 日 表名年度不变,缓存年度有效
Month 每月 1 日 表名月度不变,缓存月度有效
Day 每天 表名每日变化,缓存每日有效

混合粒度取最细(Day > Month > Year)。未标记 [Sharding] 的 DbContext 不启用动态缓存。

审计属性

行为变更 (v3): 审计字段填充已从 SavingChanges 事件移至 SaveChanges/SaveChangesAsync 重写中,确保 ShardingCore 路由评估完成后再填充。对调用方无感知。

审计字段同时支持 DateTime 和 DateTimeOffset 类型,通过 ITimeProvider 获取时间。

属性 说明
[SnowflakeId] 雪花 ID 自动生成
[CreatedAt] 创建时间自动填充
[CreatorId] 创建者 ID 自动填充
[CreatorName] 创建者姓名自动填充
[LastModifiedAt] 最后修改时间自动填充
[LastModifierId] 最后修改者 ID 自动填充
[LastModifierName] 最后修改者姓名自动填充

全局过滤器

模块通过 IGlobalFilterProvider 接口自动为以下接口实现全局查询过滤器:

  • IDeletable - 过滤已删除记录 (e.Deleted == false)
  • IMultiTenancy - 租户数据隔离(ITenantContext.IsSystemContext 穿透 / 按 TenantId 过滤)
  • IMultiApplication - 应用数据隔离(IAppContext.AppId == 0 穿透 / 按 AppId 过滤)
  • IGlobalEntity - 跳过租户过滤(标记接口,静态系统表如 sys_dict/sys_menu)

fail-secure 默认行为

ITenantContext.TenantId 默认 null,过滤器采用 fail-secure 行为(依据 ADR-0004):

IsSystemContext TenantId 行为
false null SQL TenantId = NULL 自然返回零结果(未初始化阻断)
false >0 按 TenantId 过滤
true null 穿透,返回所有租户数据

运行时切换限制

过滤器表达式捕获 ITenantContext 对象引用,由 Expression.Condition 在 SQL 翻译时分支。 EF Core 在首次 query 时缓存 HasQueryFilter 参数值——同一 DbContext 实例内运行时切换 TenantId 不会立即生效。生产标准模式:每个 HTTP 请求 scoped DbContext, 中间件在 DbContext 创建前调用 SetTenant(...),filter 即正确。

系统模式入口

// 跨租户操作(仅系统后台使用)
using (ctx.EnterSystemContext())
{
    return await db.Orders.ToListAsync(); // 跨租户返回所有
}

插入穿透标记

public class SysUser : FullEntity<long>, IMultiTenancy, ITenantInsertOptOut
{
    public long TenantId { get; set; } // SaveChanges 不自动填充
}

public class ImportService(ITenantContext ctx, DbContext db)
{
    public async Task ImportUser(SysUser user)
    {
        user.TenantId = 42; // 显式指定
        db.Users.Add(user);
        await db.SaveChangesAsync(); // 不会覆盖为 ctx.TenantId
    }
}

Outbox 存储

services.AddEfCoreOutboxStore<MyDbContext>();

自动发现 OutboxMessageEntityTypeConfiguration(表 base.base_outbox_msg,列名 snake_case),一行注册即可,无需修改 DbContext 的 OnModelCreating。

列名 属性 说明
id Id 主键
transport_name TransportName 传输组件名
delivery_mode DeliveryMode 投递模式(Publish/Send)
exchange Exchange 交换机(发布模式使用)
routing_key RoutingKey 路由键
content_type ContentType 内容类型
body Body 消息体
message_type MessageType 消息类型
retry_count RetryCount 已重试次数
max_retry_count MaxRetryCount 最大重试次数
last_error LastError 最后一次错误
created_at CreatedAt 创建时间
scheduled_at ScheduledAt 预约发送时间
published_at PublishedAt 实际发送时间
trace_id TraceId 追踪 ID
correlation_id CorrelationId 关联 ID
status Status 状态(Pending/Processing/Sent/Failed)

OutboxSaveChangesInterceptor

AddEfCoreOutboxStore 自动注册 OutboxSaveChangesInterceptor 到 DI(Singleton)。该拦截器在 SaveChanges 时从 PendingMsgCol(AsyncLocal)取出待发送消息,同事务写入 base_outbox_msg 表。

业务代码无需额外配置:

await _db.Orders.AddAsync(order);
await _bus.PublishAsync("order.created", order);  // → PendingMsgCol,不入库
await _db.SaveChangesAsync();  // 拦截器同事务写入 orders + outbox_msg

DomainEventSaveChangesInterceptor

随 AddEfCoreOutboxStore<TDbContext>() 一并启用(无需额外注册):该方法在注册 OutboxSaveChangesInterceptor 的同时注册 DomainEventSaveChangesInterceptor(Singleton,EF 自动发现),并在容器缺少进程内发布器时兜底调 AddApplicationMessagePublisher()。

该拦截器在 SaveChanges 成功后扫描 ChangeTracker 中实现 IHasDomainEvents 的实体(框架 Entity 基类默认实现),把聚合根收集的领域事件经 ApplicationMessagePublisher 发布到进程内订阅者并清空。

一次启用,双通道对称:

拦截器 触发时机 通道 一致性
OutboxSaveChangesInterceptor SavingChanges(保存时同事务落库) Outbox → MQ 强(随事务回滚消失)
DomainEventSaveChangesInterceptor SavedChanges(保存成功后) 进程内 Channel 最终(事务回滚可能幽灵事件)
// 聚合根业务方法内收集事件
public Order Place(...) { ...; AddEvent(new OrderPlaced(this)); return this; }

// Handler 正常 SaveChangesAsync 即触发分发,无需任何标注
await _db.SaveChangesAsync(ct);

// 注册(一次启用双通道)
services.AddEfCoreOutboxStore<ApplicationDbContext>();

反模式:AddEvent 之后又在 Handler 手动 PublishAsync 同一事件 → 拦截器再次分发造成重复消费。进程内事件交给拦截器;手动 ApplicationMessagePublisher.PublishAsync 仅用于非 EF 场景。

EfCoreOutboxStore

PollAsync — LINQ AsNoTracking,跨数据库兼容(PostgreSQL 使用 FOR UPDATE SKIP LOCKED 原生 SQL)。标记操作 — 原生 SQL UPDATE,绕过 ChangeTracker。

测试覆盖

测试文件 组件 用例数
EfCoreOutboxStoreTests EfCoreOutboxStore 全部方法 12
OutboxSaveChangesInterceptorTests OutboxSaveChangesInterceptor 4
DomainEventSaveChangesInterceptorTest DomainEventSaveChangesInterceptor 4
EfCoreOutboxStoreRegistrationTests AddEfCoreOutboxStore 注册行为 4
dotnet test framework/test/Galosys.Foundation.EntityFrameworkCore.Tests/ --filter "FullyQualifiedName~EfCoreOutboxStoreTests"
dotnet test framework/test/Galosys.Foundation.EntityFrameworkCore.Tests/ --filter "FullyQualifiedName~OutboxSaveChangesInterceptor"
dotnet test framework/test/Galosys.Foundation.EntityFrameworkCore.Tests/ --filter "FullyQualifiedName~DomainEventSaveChangesInterceptorTest"
dotnet test framework/test/Galosys.Foundation.EntityFrameworkCore.Tests/ --filter "FullyQualifiedName~AddEfCoreOutboxStore"

性能防坑

本模块的性能优化契约(Keyset 分页 / Filtered Include / 批量写入 / 参数化 / 追踪控制)源自设计文档 docs/designs/efcore-performance-design.md。消费方按以下 5 招铁律书写查询与写入即可规避 90% 的 EF Core 性能陷阱。

招数 1:分页铁律

  • 深翻页(数据量大 / 页码深)→ Keyset:KeysetPageAsync<T, TOrderBy>
  • 浅翻页 → PagedRequest / PagedResult<T>(OFTSET)
  • 排序键必须唯一且建索引;重复时叠加第二键做 tie-break
// Keyset 深翻页(常数时间,不随页码增长)
var next = await db.Orders
    .KeysetPageAsync<Order, long>(
        new KeysetPagedRequest { Cursor = prev.NextCursor, Size = 30 },
        o => o.Id, ct);

// 浅翻页(PageQry/PageOutput 已被 PagedRequest/PagedResult<T> 取代)
var result = await db.Query<User>()
    .ApplySort(req.SortBy)                     // 动态排序
    .ApplyFilters(req.Filters)                 // 动态过滤
    .PageUntrackedAsync(req.PageNo, req.PageSize, ct); // 免追踪分页

消费方迁移指引:旧 PageQry → PagedRequest(PageNum→PageNo);旧 PageOutput → PagedResult<T>(Rows→Data、Total→TotalRecords、Pages→TotalPages、HasNext→HasNextPage、HasPrevious→HasPreviousPage)。PagedResult<T> 是数据载体层,API 返回用 UnifiedResponse.Succeed(pagedResult) 包装。

招数 2:导航铁律

  • Include 永远配 Where:FilteredIncludeExtensions.IncludeWhere
  • 能投影就别导航全取:ProjectionExtensions.SelectTrimmed
// 只加载未发货明细(服务端 SQL 过滤,杜绝全量子表拉回内存)
var orders = await db.Orders
    .Where(o => o.Status == "NEW")
    .IncludeWhere(o => o.Items, i => i.Shipped == false)
    .ToListAsync(ct);

// 只 SELECT 需要的列
var titles = await db.Blogs
    .SelectTrimmed(p => new { p.Id, p.Title })
    .ToListAsync(ct);

招数 3:写入铁律

规模 推荐方案
< 10K 插入 AddRange + SaveChanges
10K-100K 插入 AddRangeOptimizedAsync(分批 + 关追踪,8x 加速 / 77% 内存节省)
≥ 100K 导入 SqlBulkCopy / EFCore.BulkExtensions(旁路 EF Core,本模块 API 不适用百万级)
按条件更新 ExecuteUpdate(单 SQL, 3-5ms)
按条件删除 ExecuteDelete(单 SQL, 2-4ms)
软删除实体删除 ExecuteUpdate 设 IsDeleted=true + DeletedAt=now(勿用 ExecuteDelete,会绕过 IDeletable 全局过滤器)
// 10K-100K 分批插入:默认 BulkInsertOptions(BatchSize=5000 / AutoDetectChangesOff / ClearPerBatch / Atomic)
await db.Users.AddRangeOptimizedAsync(users, ct);

// 10K-100K 分批保存(DbContext 粒度)
await db.SaveChangesOptimizedAsync(ct);

// ExecuteUpdate 后重载已追踪实体,防止旧值覆盖
await db.ReloadTrackedAsync<User>(ct);

// 软删除批量"删除"推荐模式
await db.Users.Where(u => u.DeactivatedSince < cutoff)
    .ExecuteUpdateAsync(s => s
        .SetProperty(e => e.IsDeleted, true)
        .SetProperty(e => e.DeletedAt, DateTime.UtcNow), ct);

招数 4:参数化铁律

  • 永远让 EF 自动参数化(LINQ 表达式)
  • 禁止 FromSqlRaw 字符串拼接(SQL 注入 + 参数嗅探)
  • 动态条件用 FromSqlInterpolated

招数 5:追踪铁律

  • 只读查询:PageUntrackedAsync / QueryUntracked / PageUntracked(DbContext<T> 已全局 UseQueryTrackingBehavior(NoTracking),免追踪兜底)
  • ExecuteUpdate 后必须 ReloadTrackedAsync<T> 同步 ChangeTracker
  • Bulk 写入路径自动关 AutoDetectChangesEnabled + 每批 ChangeTracker.Clear()

Split Query 全局默认与代价

DbContext<T> 默认 UseQuerySplittingBehavior(SplitQuery),多集合 Include 自动拆多条 SQL,防笛卡尔爆炸(无需消费方配置)。可用 AsSplitQuerySafe() 显式标注意图。

代价:拆分后的查询不在同一事务;两个查询之间数据若变化会出现短暂不一致(仅只读场景可接受)。需要强一致读(事务内 / 刚写入后立即读)时显式 .AsSingleQuery() 切回单查询。

依赖

  • Microsoft.EntityFrameworkCore
  • Microsoft.EntityFrameworkCore.Relational
  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.EntityFrameworkCore.Sqlite
  • Npgsql.EntityFrameworkCore.PostgreSQL
  • Pomelo.EntityFrameworkCore.MySql
  • Oracle.EntityFrameworkCore
  • Galosys.Foundation.Core
  • Galosys.Foundation.Data

CDC EF Core 位置存储

本模块提供 EfOffsetStore<TDbContext>,基于 EF Core 持久化 CDC 位置到数据库(线程安全,使用 IDbContextFactory<TDbContext> 每操作新建上下文)。

// 注册 EF Core 位置存储替代默认的 InMemoryOffsetStore
services.RemoveAll<IOffsetStore>();
services.AddSingleton<IOffsetStore, EfOffsetStore<YourDbContext>>();

自动保存 CDC 最后读取的 LSN 位置,支持断点续传和多连接器 namespace 隔离。

旧版 EfPositionStore<TDbContext> 已标记 [Obsolete],请迁移到 EfOffsetStore<TDbContext>。

多租户存储(EfCoreTenantStore)

EfCoreTenantStore<TDbContext> 基于 EF Core 提供租户元数据的只读持久化存储,实现 ITenantStore(Core 层接口)。默认 AddMultiTenancyCore() 注册的是 InMemoryTenantStore,调用 AddEfCoreTenantStore<TDbContext>() 会用 RemoveAll + 作用域注册替换为 EF 存储。

1. 注册租户元数据表

在 DbContext 的 OnModelCreating 中应用租户实体映射(表名 mt_tenant,列 snake_case;Items 字典不持久化):

public class MyDbContext : DbContext
{
    public MyDbContext(DbContextOptions<MyDbContext> options) : base(options) { }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        modelBuilder.ApplyConfiguration(new TenantEntityTypeConfiguration()); // 或 ApplyConfigurationsFromAssembly(...)
    }
}

TenantEntityTypeConfiguration(命名空间 Microsoft.EntityFrameworkCore.Metadata.Builders)将 Tenant 映射到 MultiTenancyStoreConstants.TableName(mt_tenant),Id 为主键,Identifier 建立唯一索引;Items 字典被 Ignore 排除。P2 隔离抽象(spec multi-tenancy-p2-isolation-abstractions)新增的 SchemaTemplate / DatabaseTemplate / IsolationConnectionString 三个运行时元数据字段同样被 Ignore(不在 mt_tenant 建列),仅作内存中派生,不引入迁移。

2. 注册存储

// 默认(无参):覆写默认的 InMemoryTenantStore,改为从 MyDbContext 中查询租户元数据
builder.Services.AddEfCoreTenantStore<MyDbContext>();

// 可选(带 Options):显式覆盖目标表 schema/表名(如指向平台统一租户表 uc.uc_tenant)
builder.Services.AddEfCoreTenantStore<MyDbContext>(o =>
{
    o.Schema = "uc";
    o.TableName = "uc_tenant";
});
  • 作用域生命周期:每次请求作用域解析出绑定到该作用域 TDbContext 的存储实例
  • 覆写语义:调用会移除先前注册的 ITenantStore(含默认 InMemory),后一次调用生效
  • 只读:目前仅实现 GetByIdAsync / GetByIdentifierAsync(大小写不敏感,空/空串标识返回 null),不提供租户写入

2.1 可配置目标表(MultiTenancyStoreOptions)

MultiTenancyStoreOptions(命名空间 Microsoft.Extensions.MultiTenancy,位于 Galosys.Foundation.Core)允许消费方覆盖租户元数据表的目标 schema 与表名,默认值与历史行为一致(Schema = null,TableName = "mt_tenant"),零破坏。

字段 类型 默认值 说明
Schema string? null schema 名;null/空 → 单参 ToTable(name),非空 → 双参 ToTable(name, schema)
TableName string "mt_tenant" 表名;未配置兜底 MultiTenancyStoreConstants.TableName

典型用例:指向平台统一租户表 uc.uc_tenant

// Program.cs
builder.Services.AddEfCoreTenantStore<MyDbContext>(o =>
{
    o.Schema = "uc";
    o.TableName = "uc_tenant";
});
  • ADR-0001 规则 4 单一常量来源仍由 MultiTenancyStoreConstants 维护,Options 仅在显式配置时覆盖
  • uc.uc_tenant 表由平台侧(UC 团队)负责创建与迁移,本仓库只消费

2.2 ApplyEntityTypeConfigurations DI 优先发现

DbContext<T>.ApplyEntityTypeConfigurations 在自动发现 IEntityTypeConfiguration<T> 时优先从 ServiceProvider 解析,回退到 Activator.CreateInstance(parameterless)(默认行为保留)。

  • DI 命中:消费方在 DI 中显式注册 IEntityTypeConfiguration<T> 时,自动使用 DI 实例(可注入 IOptions<TOptions>、日志器等依赖)
  • DI 兜底:未注册时走 Activator.CreateInstance(parameterless),行为与历史一致
  • Design-time:ServiceProvider 为 null(如 EF Core CLI migration 工具)时跳过 DI 解析,走 Activator 兜底
  • 测试宿主:测试运行时(Microsoft.NET.Test.Sdk / xunit.core 已加载)允许把测试程序集纳入扫描面,使测试夹具中的配置类也能被发现

3. 使用

EfCoreTenantStore 由多租户中间件/服务自动解析,也可直接注入 ITenantStore:

public class TenantProbeService
{
    private readonly ITenantStore _store;

    public TenantProbeService(ITenantStore store) => _store = store;

    public async Task<Tenant?> FindAsync(string identifier)
        => await _store.GetByIdentifierAsync(identifier);
}

测试覆盖

测试文件 组件 用例数
EfCoreTenantStoreTests EfCoreTenantStore 只读查询 5
EfCoreTenantStoreRegistrationTests AddEfCoreTenantStore 无参注册行为 3
EfCoreTenantStoreOptionsRegistrationTests AddEfCoreTenantStore 带 Options 重载注册行为 2
TenantEntityTypeConfigurationTests TenantEntityTypeConfiguration 读取 MultiTenancyStoreOptions 3
DbContextApplyEntityTypeConfigurationsTests DI 优先 + Activator 兜底 + ServiceProvider null 3

P2 Schema 隔离

Schema 隔离是中规模 SaaS 的折中方案:共享 DB 实例 + 每租户独立 tenant_<id> schema,隔离强度高于 Row、迁移成本低于 Database(设计文档 § 五.A.2)。P2-1 已交付 Core 隔离抽象层(ISchemaTenantAccessor / ITenantIsolationContext),本节在 EFCore 层落地 Schema 路由约定与按租户迁移能力(spec multi-tenancy-p2-schema-isolation)。Row 模式零行为变化。

1. 注册

using Microsoft.EntityFrameworkCore.MultiTenancy;

builder.Services.AddEfCoreSchemaIsolation<MyDbContext>();
// 可选:覆盖模板 / DesignTime 兜底 schema
builder.Services.Configure<MultiTenancyIsolationOptions>(o =>
{
    o.DefaultSchemaTemplate = "tenant_{id:00000000}";
});

AddEfCoreSchemaIsolation<TDbContext>() 一站式注册:SchemaTenantRoutingConvention(IModelFinalizingConvention)+ SchemaTenantMigrator(IHostedService,启动时按租户循环迁移)+ DesignTimeSchemaProvider 静态兜底。Row 模式下约定不注入(if 分支保护),OnModelCreating 行为不变。

2. Schema 路由约定

SchemaTenantRoutingConvention 在 EF Core 完成 OnModelCreating 后由框架自动调,仅对 TenantEntity<TID> / TenantAppEntity<TID> 派生实体的 ToTable() 加 schema(从 ITenantIsolationContext.IsolationKey 取,模板替换 {id:00000000} → 8 位补零)。消费方无需在每个 IEntityTypeConfiguration 显式 ToTable(name, schema)。

// 消费方零改动
public class OrderEntityConfiguration : IEntityTypeConfiguration<Order>
{
    public void Configure(EntityTypeBuilder<Order> b)
    {
        b.HasKey(o => o.Id);
        // 无需 b.ToTable("order", schema) —— 约定在 ModelFinalizing 时统一注入
    }
}

模式行为:Row → 约定跳过;Schema / Hybrid → 注入 schema;Database → 约定跳过(走 P2-3 连接串路由)。

3. EF Tools 兼容(DesignTimeSchemaProvider)

dotnet ef migrations add 进程无 ITenantContext / IServiceProvider,通过静态服务兜底:

// 设计时显式指定(包装脚本设环境变量 DESIGNTIME_TENANT_ID)
DesignTimeSchemaProvider.Override("tenant_00000042");
// 或 Options 兜底:未 Override 时回退到 MultiTenancyIsolationOptions.DefaultSchemaTemplate

__EFMigrationsHistory 按 schema 分离(EF Core 默认行为),每个 schema 维护独立迁移历史。

4. Schema 迁移(SchemaTenantMigrator)

IHostedService 启动时遍历 ITenantStore.GetAllAsync(),对每个 IsolationMode = Schema 的租户在独立 schema 内 Database.MigrateAsync()。可与 SchemaTenantMigrator 并行(异步并行 + 并发上限);Schema 模式下禁用 EnsureCreated(),统一走 Migrator。

5. ITenantStore.GetAllAsync 接口扩展

P2-2 同步扩 ITenantStore.GetAllAsync(default 空实现,二进制兼容)。InMemoryTenantStore 返回全部 seed 租户;EfCoreTenantStore<TDbContext> 走现有 db.Tenants.ToListAsync()。

6. 测试覆盖

测试文件 组件 用例数
SchemaTenantRoutingConventionTests SchemaTenantRoutingConvention Row/Schema/Hybrid 分支 4
DesignTimeSchemaProviderTests Override + 兜底模板 2
SchemaTenantMigratorTests 按租户循环迁移 + 仅 Schema 模式触发 2

P2 Database/Hybrid 隔离

Database 隔离是 P2 隔离强度最高的模式:每租户独立 DB 实例(专属连接串),适合大客户 SLA 独立(设计文档 § 五.A.2)。Hybrid 模式 = Database 隔离 + Schema 切换,共享 DB 但按租户 schema 隔离。P2-1/P2-2 已交付 Core 抽象与 Schema 路由,本节落地连接路由 + 跨租户迁移 + 跨库迁移 CLI(spec multi-tenancy-p2-database-hybrid-isolation)。

1. 注册

using Microsoft.EntityFrameworkCore.MultiTenancy;
using Microsoft.EntityFrameworkCore;

builder.Services.AddEfCoreDatabaseIsolation<MyDbContext>();
// Database 模式下必须配置 DatabaseConnectionStringResolver
builder.Services.Configure<MultiTenancyIsolationOptions>(o =>
{
    o.DatabaseConnectionStringResolver = tenantId =>
        $"Server=tenant-{tenantId}.db.local;Database=app;Uid=app;Pwd=***";
});

AddEfCoreDatabaseIsolation<TDbContext>() 一站式注册:ITenantConnectionRouter(Scoped)+ MultiTenantDbConnectionFactory(EF Core IDbConnectionFactory 实现)+ DatabaseTenantMigrator(IHostedService 启动时按租户循环迁移)。fail-secure:未配置 DatabaseConnectionStringResolver 且 IsolationMode = Database/Hybrid 时,Router 抛 InvalidOperationException(继承 ADR-0004)。

2. 连接路由(ITenantConnectionRouter)

GetConnection() 返回已 Open 的 DbConnection(避免每次查询重解析 + 重打开),由 ITenantIsolationContext.Mode 决定:

  • Row / Schema:返回 master TDbContext 配置的共享连接
  • Database / Hybrid:返回 MultiTenancyIsolationOptions.DatabaseConnectionStringResolver(tenant.Id) 对应的专属连接

GetMasterConnection() 返回 ITenantStore 所在 master DB 的连接(uc.uc_tenant / mt_tenant 元数据),Database 模式下业务 DB 独立但 metadata 共享。

3. EF Core 集成

Database 模式需显式启用 IDbConnectionFactory:

services.AddDbContext<MyDbContext>(o =>
    o.UseSqlite().UseConnectionFactory<MultiTenantDbConnectionFactory>());

MultiTenantDbConnectionFactory.CreateDbConnection() 内部委托 ITenantConnectionRouter.GetConnection(),EF Core 自身连接管理与 Foundation 路由解耦。

4. Hybrid 模式 = Database + Schema(复用 P2-2)

Hybrid 模式下 ITenantConnectionRouter.GetConnection() 返回 Database 隔离专属连接,同时 ITenantIsolationContext.IsolationKey 提供 schema 名 → 复用 P2-2 的 SchemaTenantRoutingConvention,无需新增约定。两者职责正交:Router 管"连哪个库",Convention 管"用哪个 schema"。

5. DatabaseTenantMigrator

IHostedService 启动时遍历 ITenantStore.GetAllAsync(),过滤 IsolationMode in (Database, Hybrid),对每租户用专属 connection 调 Database.MigrateAsync()。并行受 SemaphoreSlim(parallel) 控制(默认 4),失败单租户不阻塞其它;__EFMigrationsHistory 按租户独立 DB 各自维护。Database 模式建议租户数 < 50(连接池开销 + 启动时间线性)。

6. 跨库迁移 CLI(multi-tenancy dotnet tool)

跨库迁移能力由独立 dotnet tool 提供(见 Galosys.Foundation.MultiTenancy.Tools):

# 安装(本地源 — MultiTenancy.Tools 已设为 IsPackable=false,需先手动 pack 到 ./nupkgs)
dotnet pack framework/tools/Galosys.Foundation.MultiTenancy.Tools/ -c Release -o ./nupkgs
dotnet tool install multi-tenancy --local --add-source ./nupkgs

# 迁移所有 Database/Hybrid 租户
multi-tenancy migrate --connection "Server=master;Database=uc" --tenant all

# 试运行(仅打印计划,不执行)
multi-tenancy migrate --dry-run --connection "Data Source=:memory:"

# 迁移指定租户
multi-tenancy migrate --tenant 1,2 --parallel 2 --max-retries 5

# 数据种子
multi-tenancy seed --tenant all

CLI 进程内组装最小 DI(MultiTenancyToolHost,无 ASP.NET Core),通过 --connection 指向 master DB(uc.uc_tenant / mt_tenant)读取租户列表,遍历调用 Database.MigrateAsync()。退出码:0 全成功 / 1 参数错 / 2 存在失败租户。

7. 测试覆盖

测试文件 组件 用例数
DatabaseIsolationRegistrationTests AddEfCoreDatabaseIsolation 注册 + 异常路径 4
TenantConnectionRouterTests Router Row/Schema/Database/Hybrid 分支 + fail-secure 4
DatabaseTenantMigratorTests 按租户循环迁移 + 并发控制 + 失败隔离 2
MultiTenancyMigratorCliTests CLI migrate / seed 子命令端到端 3

可靠性与多 DbContext(change-2)

1. 并发冲突 3 策略

ConflictStrategy(Galosys.Foundation.Core.Concurrency)提供 3 种冲突处理:

策略 行为 适用场景
ClientWins 最后写入者获胜(默认) 运营后台 / 低竞争
DatabaseWins 拒绝写入,返回当前值 用户决策类(如审批)
MergeFields 字段级融合 多端并发编辑

EfCoreRepositoryConcurrencyExtensions 提供对应扩展方法:

// ClientWins:等价于现有 UpdateWithRetryAsync
await repository.UpdateWithClientWinsAsync(product);

// DatabaseWins:冲突返回 ConflictResult<T>?(null = 成功)
var conflict = await repository.TryUpdateWithDatabaseWinsAsync(product);
if (conflict is not null)
{
    // 返回数据库当前值 + 最新 RowVersion,由 API 层决定后续动作
    return conflict.CurrentRowVersion;
}

// MergeFields:冲突时按字段融合后重试
await repository.MergeAndUpdateAsync(product, (db, client) =>
{
    client.Price = db.Price;      // 价格以数据库为准
    client.Name = client.Name;    // 名称以客户端为准
    return client;
});

冲突三组值快照(DbUpdateConcurrencyExceptionExtensions.CaptureSnapshotAsync):

catch (DbUpdateConcurrencyException ex)
{
    var entry = ex.Entries.First();
    var snapshot = await entry.CaptureSnapshotAsync(ct);
    // snapshot.Proposed   = entry.CurrentValues(客户端希望写入的值)
    // snapshot.Original   = entry.OriginalValues(追踪时的原值)
    // snapshot.Database   = await entry.GetDatabaseValuesAsync(ct)(数据库当前值,null = 已删除)
}

2. DbContext 池化(可选启用)

默认保持 AddDbContext<T>。高并发场景可选择性启用池化(吞吐 2-3x),但须先满足两个前置条件:

⚠️ 禁止在 DbContext 字段存请求级别状态。DbContext<T> 已注入 5 个 scoped 服务(Logger / Publisher / TimeProvider / TenantContext / IAppContext),池化实例跨请求复用会残留请求级状态。

  1. DbContext<T>.OnConfiguring 中动态修改 DbContextOptions 的配置项迁移到注册时调用(池化缓存 options,禁止 OnConfiguring 修改)
  2. 构造函数解析到字段的 scoped 服务改为惰性解析

AddDbContextPool<TContext, T> 候选已保留(MiAddDbContextPool 反射路径),前置条件满足即可切换。

3. MigrationsHistoryTable 独立命名

同库多 DbContext 默认使用独立迁移历史表 __XxxMigrationsHistory(Xxx = DbContext 类型名去 DbContext 后缀),避免互相覆盖。

选项 默认值 说明
UseDefault true 是否使用默认命名 __XxxMigrationsHistory
TableName 空 自定义表名(需 UseDefault=false)
Schema null 历史表所在 schema(默认与实体一致)
  • 与 SchemaTenantRoutingConvention 叠加:Schema 隔离模式下历史表按 schema 路由
  • 一次性破坏:已有迁移历史表需手动迁移(或 UseDefault=false 保留旧名 __EFMigrationsHistory)

4. 事务抽象选择

场景 抽象 说明
单 DbContext IUnitOfWork 现有工作单元,管理单上下文事务
跨 DbContext(同数据库,共享连接) ICrossDbContextTransaction change-2 新增,共享 DbConnection + DbTransaction,原子提交/回滚
跨数据库(分布式) Outbox Pattern / Saga 不在本模块范围(见 § Outbox 存储)
// ICrossDbContextTransaction:两个 DbContext 共享连接,同一事务内原子提交
await using var xact = scope.ServiceProvider.GetRequiredService<ICrossDbContextTransaction>();
await xact.BeginAsync(orderDbContext, userDbContext);

await orderDbContext.Orders.AddAsync(order);
await userDbContext.Users.UpdateAsync(user);

await xact.CommitAsync(); // 两个 DbContext 的变更同一事务内提交

判断准则:① 是否跨多个 DbContext?否 → IUnitOfWork;② 是否同一数据库?是 → ICrossDbContextTransaction;否 → 分布式(Outbox / Saga)。

5. 跨 DbContext Join 替代(InMemoryMergeExtensions)

EF Core 不支持跨 DbContext Join。MergeAsync 并行加载两边数据后按 key 内存合并:

IReadOnlyList<OrderWithStatus> result = await orderQuery
    .MergeAsync(
        statusQuery,
        l => l.Id,          // 左侧 key
        r => r.OrderId,     // 右侧 key
        (l, r) => new OrderWithStatus { Order = l, Status = r.Status });

⚠️ 铁律:频繁跨实体 Join 应合并到单 DbContext。MergeAsync 是"分别查询 + 内存合并"(O(N×M)),数据量 > 10K + 频繁 Join 时推荐合并到单 DbContext 或使用物化视图。

6. 多 DbContext 拆分决策矩阵

问题 建议拆分 建议合并
实体属于不同数据库? ✅ 必须拆 —
实体由不同团队/模块维护? ✅ 拆 可能保持单一
需要读副本? ✅ 拆(读写分离) —
实体之间频繁 join? — ✅ 保持单一(跨 DbContext join 不可行)
DbContext 有 100+ 实体,启动慢? ✅ 拆 —
模块化单体有限界上下文? ✅ 拆 —

判断流程:是否不同数据库 / 不同团队 / 需要读副本 / 100+ 实体 / 有限界上下文 → 拆;频繁 join → 合并。

7. 多数据库 RowVersion 适配表

数据库 类型 EF Core 配置 备注
SQL Server rowversion(8 bytes) IsRowVersion() 自动递增
PostgreSQL xmin 系统列(uint32) EF Core 9+ UseXminAsConcurrencyToken() 无需迁移
MySQL 无原生 自定义 DateTime6 列 + trigger 需 trigger 自增
SQLite 无 手动 INTEGER 列 手动维护

PostgreSQL xmin 自动检测:EF Core 9+ 使用 UseXminAsConcurrencyToken() 自动配置并发令牌。

8. DbUpdateException 全局捕获(Q5 占位)

⚠️ 占位:DbUpdateException 全局捕获展开(区分主键冲突 / 唯一约束 / 级联依赖)暂未实现,路径选型中(Q5 决策:A 拦截器 / B 仓储包装 / C 全局异常中间件),待后续 change 承接。当前需显式 catch DbUpdateException 并检查 InnerException。


性能防坑章节索引(change-1/2/3/4)

Phase 主分类 章节锚点
change-1 查询层 / 写入层 § 性能防坑(招数 1-5 + Split Query)
change-2 可靠性 § 可靠性与多 DbContext(并发冲突 3 策略 / 跨上下文事务 / 决策矩阵)
change-3 可观测性 / 合规 / 架构 § 监控(OTel 指标) + § 合规运维铁律 + § JSON 列自动映射
change-4 慢查询 / 审计增强内化 § 慢查询日志(主模块内置,需手动启用) + § 审计增强(change-4 起内置,默认启用)

监控(可观测性)OpenTelemetry

主模块内置 EfCoreInstrumentation(OpenTelemetry Source "Galosys.Foundation.EntityFrameworkCore"),注册后自动暴露 4 类指标:

指标 类型 来源
efcore.queries.count Counter DbCommandExecuting 拦截
efcore.queries.duration Histogram DbCommandExecuted.ElapsedMilliseconds
efcore.savechanges.duration Histogram SaveChanges / SaveChangesAsync
efcore.tracked_entities ObservableGauge ChangeTracker.Entries().Count()

接入

services.AddEfCoreInstrumentation();   // 注册 OpenTelemetry Meter Provider
// 配合 Galosys.Foundation.OpenTelemetry 或 OpenTelemetry.Exporter.Prometheus 暴露 /metrics

Grafana 面板

可使用社区面板 ID 14968(ASP.NET Core / EF Core) 作为起点,替换指标名称为上表;或自建面板关注 4 类指标的 P99 / 速率告警。

慢查询日志(主模块内置,需手动启用)

change-4-2 起,EF Core 慢查询日志能力由原独立扩展模块 Galosys.Foundation.EntityFrameworkCore.SlowQuery 内化合并到主模块 Galosys.Foundation.EntityFrameworkCore,扩展方法名同步从 UseEfCoreSlowQueryLogging 改为 AddEfCoreSlowQueryLogging(命名统一到主模块 Add* 约定)。

services.AddEfCoreSlowQueryLogging();              // 默认阈值 500ms
services.AddEfCoreSlowQueryLogging(thresholdMs: 100); // 自定义阈值

按类别 "EfCore.Slow"(Warning 级)写入 ILogger,含 TraceId / ElapsedMs / Sql;Serilog 业务方通过类别名桥接。能力内置但不自动启用——与 AddEfCoreInstrumentation 同模式,需手动调用。


审计增强(change-4 起内置,默认启用)

change-4-2 起,EF Core 审计增强能力由原独立扩展模块 Galosys.Foundation.EntityFrameworkCore.Audit 内化合并到主模块 Galosys.Foundation.EntityFrameworkCore;修订2 进一步恢复原拦截器类名 AuditSavingInterceptor(命名空间 Microsoft.EntityFrameworkCore.Diagnostics)并由基类 DbContext<T>.OnConfiguring 默认接线启用——与 ReadWriteSplittingInterceptor 接线方式一致。

public class AppDbContext : DbContext<AppDbContext>
{
    // 基类 OnConfiguring 已默认接线 AuditSavingInterceptor:
    //   GetService<IEnvironmentAttributeSource>() ?? new DefaultEnvironmentAttributeSource()
    //   + GetService<ILogger<AuditSavingInterceptor>>()
    //   → AddInterceptors(new AuditSavingInterceptor(auditSource, auditLogger))
}

被修改条目(Add / Update / Delete)按以下模板输出结构化日志:

EF审计: {Operation} {Entity}#{EntityId} IP={Ip} TraceId={TraceId}
  • IP 来源:DI 解析 IEnvironmentAttributeSource(AspNetCore 宿主由 HttpContextAttributeSource 实现;Worker / 后台服务未注册源时回退 "worker" 占位)。
  • TraceId:来自 Activity.Current?.TraceId,无 Activity 输出 "no-trace"。
  • 默认启用——无需任何注册代码即获得审计日志;如需关闭可从 DI 移除拦截器或重写 OnConfiguring。

合规运维铁律

1. 多 Provider 迁移目录组织

生产环境多 Provider 并存时,按 provider 分目录组织迁移文件:

Infrastructure/
└── Migrations/
    ├── SqlServer/         # Microsoft.EntityFrameworkCore.SqlServer 生成的迁移
    ├── Npgsql/            # Npgsql.EntityFrameworkCore.PostgreSQL 生成的迁移
    ├── MySqlConnector/    # Pomelo.EntityFrameworkCore.MySql 生成的迁移
    └── Oracle/            # Oracle.EntityFrameworkCore 生成的迁移

切换 provider 时用 --output-dir 隔离迁移:

dotnet ef migrations add InitialCreate --context AppDbContext --output-dir Migrations/Npgsql

所有迁移文件放入同一程序集时,需注意 MigrationsAssembly 一致 + DesignTimeDbContextFactory 按配置选择 provider。

2. Vault / Key Vault 集成

生产环境连接字符串禁止明文存放于 appsettings.json:

平台 工具
Azure Key Vault + Managed Identity
AWS Secrets Manager + IAM Role
自建 HashiCorp Vault Sidecar
本地 User Secrets(dotnet user-secrets)
dotnet user-secrets init
dotnet user-secrets set "ConnectionStrings:Default" "<conn>"

配置提供程序需先注入对应 Secret 注入源(Azure.Extensions.Configuration.Secrets / Amazon.Extensions.Configuration.SystemsManager),再做 AddEntityFrameworkCore。

3. GDPR 合规支持(占位,待独立 change)

本期仅文档化,完整实施在后续 OpenSpec change(业务规则挂钩):

  • 数据可移植性:租户数据导出 API(JSON / CSV)
  • 被遗忘权:软删除 + 定期物理清除流程

4. 跨分片聚合边界铁律

⚠️ 禁止业务库跨分片聚合

  • 高频报表:导入 ClickHouse / Elasticsearch
  • 实时看板:分析副本 + 物化视图
  • 跨租户统计:走 EnterSystemContext() 系统模式,且必须 ExecuteUpdate 非追踪聚合

性能灾难场景:N 租户 × 全表扫描 = O(N) 内存 + O(N) 延迟。

5. 副本延迟处理(读写分离场景)

  • ReplicaLagOptions:LagThresholdMs = 500, FallbackToMasterOnHighLag = true, MaxRetries = 3
  • ForceMaster():支付后立即查询"读己之写"场景:
using (db.ForceMaster())
{
    var order = await db.Orders.FindAsync(orderId);
}
  • 多数据库副本延迟监控:
    • SQL Server:sys.dm_os_performance_counters 的 Log Send Queue KB
    • MySQL:SHOW SLAVE STATUS 的 Seconds_Behind_Master
    • 超过阈值(如 5s)告警

JSON 列自动映射([OwnedJson])

[OwnedJson] 特性(Microsoft.EntityFrameworkCore.Metadata.OwnedJsonAttribute)标注 owned complex type 后,JsonColumnConvention 在模型构建末期自动执行 OwnsOne(...).ToJson() 等价映射。

启用

services.AddJsonColumnConvention();   // 注册 Scoped 约定(可空,可挂入 ConfigureConventions)

约定通过 ConfigureConventions 或 DI 注册后,对标注实体自动生效:

public class Customer
{
    public int Id { get; set; }

    [OwnedJson]
    public Address Address { get; set; } = null!;
}

public class Address
{
    public string Street { get; set; } = string.Empty;
    public string City { get; set; } = string.Empty;
}

适用场景

  • 内嵌对象图(收货地址 / 用户偏好 / 配置字典)随主实体一起读写
  • 几乎不按 JSON 内字段独立查询

不适用场景

  • 频繁按 JSON 内字段查询 / 范围检索 / 聚合 → 应拆独立表
  • 若需局部查询,推荐在 JSON 列上建计算列 + 索引

决策流程

  1. 对象是否仅由主实体读写?→ 否,拆独立表
  2. 是否按内嵌字段做查询/聚合?→ 是,拆独立表或计算列+索引
  3. 是 → 使用 [OwnedJson](顺带减少表的列数膨胀)

ADR 索引

ADR 主题 关联 change
ADR-0001 EF Core 集成约定 —
ADR-0003 多租户隔离策略 —
ADR-0004 系统上下文 —
ADR-0006 保留自研 [Sharding],不引入 ShardingCore change-3

⚠️ 决策:未来除非自研分表出现显著短板,否则不引入 ShardingCore。

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (8)

Showing the top 5 NuGet packages that depend on Galosys.Foundation.EntityFrameworkCore:

Package Downloads
Galosys.Foundation.ShardingCore

Galosys.Foundation快速开发库

Galosys.Foundation.Yarp.Database

Galosys.Foundation快速开发库

Galosys.Foundation.Actuator.EntityFrameworkCore

Galosys.Foundation快速开发库

Galosys.Foundation.DataPermission

Galosys.Foundation快速开发库

Galosys.Foundation.Agents.AI.EntityFrameworkCore

Galosys.Foundation快速开发库

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
26.10.9.1 55 10/9/2026
26.10.8.1 66 10/8/2026
26.9.23.1 162 9/23/2026
26.9.16.1 158 9/16/2026
26.9.15.1 152 9/15/2026
26.9.14.1 157 9/14/2026
26.9.10.1 146 9/10/2026
26.9.3.1 151 9/3/2026
26.8.29.1 154 8/31/2026
26.8.26.1 166 8/26/2026
26.8.23.1 173 8/23/2026
26.8.21.1 167 8/21/2026
26.8.20.1 168 8/20/2026
26.8.18.1 172 8/18/2026
26.8.17.1 176 8/17/2026
26.8.13.2 166 8/13/2026
26.8.13.1 171 8/13/2026
26.8.12.2 171 8/12/2026
26.8.12.1 172 8/12/2026
26.8.10.1 168 8/10/2026
Loading failed