Galosys.Foundation.EntityFrameworkCore 26.9.3.1

dotnet add package Galosys.Foundation.EntityFrameworkCore --version 26.9.3.1
                    
NuGet\Install-Package Galosys.Foundation.EntityFrameworkCore -Version 26.9.3.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.9.3.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.9.3.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.9.3.1
                    
#r "nuget: Galosys.Foundation.EntityFrameworkCore, 26.9.3.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.9.3.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.9.3.1
                    
Install as a Cake Addin
#tool nuget:?package=Galosys.Foundation.EntityFrameworkCore&version=26.9.3.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)、MarkManySentAsyncMarkManyFailedAsync 批量 API
  • 多租户存储 - EfCoreTenantStore<TDbContext> 基于 EF Core 的只读租户元数据持久化存储(AddEfCoreTenantStore<TDbContext>() 覆写默认 InMemory)
  • 连接弹性 - 四种数据库自动重试瞬态故障
  • 读写分离 - DbCommandInterceptor 自动路由查询到副本
  • 支持多种数据库 - SQL Server、PostgreSQL、MySQL、SQLite、Oracle

安装

<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);
}

执行策略配置

通过 [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 自动检测并配置并发令牌。索引配置完全由接口检测驱动,无需为不同实体类型选择不同基类。

核心类

类/接口 说明
IRepository<T, TID> 通用仓储接口,定义增删改查
IUnitOfWork 工作单元接口,管理事务
DbContext<T> DbContext 基类,提供自动审计、过滤器等
[DbContext] 标记 DbContext 的特性,指定连接字符串名称
[Repository] 标记仓储实现类,自动注册到 DI
EfCoreRepository<TContext, T, TID> EF Core 仓储实现,继承并添加 UpdateWithRetryAsync
EfCoreUnitOfWork<TContext> EF Core 工作单元实现
ConcurrencyEntity<TID> 乐观并发实体基类(RowVersion)
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 路由评估完成后再填充。对调用方无感知。

审计字段同时支持 DateTimeDateTimeOffset 类型,通过 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"

依赖

  • 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.TableNamemt_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 = nullTableName = "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-timeServiceProvidernull(如 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>() 一站式注册:SchemaTenantRoutingConventionIModelFinalizingConvention)+ SchemaTenantMigratorIHostedService,启动时按租户循环迁移)+ 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 实现)+ DatabaseTenantMigratorIHostedService 启动时按租户循环迁移)。fail-secure:未配置 DatabaseConnectionStringResolverIsolationMode = Database/Hybrid 时,Router 抛 InvalidOperationException(继承 ADR-0004)。

2. 连接路由(ITenantConnectionRouter

GetConnection() 返回已 OpenDbConnection(避免每次查询重解析 + 重打开),由 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):

# 安装(本地源)
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
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 (7)

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.9.3.1 53 9/3/2026
26.8.29.1 132 8/31/2026
26.8.26.1 147 8/26/2026
26.8.23.1 150 8/23/2026
26.8.21.1 149 8/21/2026
26.8.20.1 147 8/20/2026
26.8.18.1 154 8/18/2026
26.8.17.1 156 8/17/2026
26.8.13.2 151 8/13/2026
26.8.13.1 152 8/13/2026
26.8.12.2 154 8/12/2026
26.8.12.1 154 8/12/2026
26.8.10.1 152 8/10/2026
26.8.5.1 170 8/5/2026
26.8.4.1 151 8/4/2026
26.8.3.1 147 8/3/2026
26.7.31.1 163 7/31/2026
26.7.30.1 150 7/30/2026
26.7.29.1 141 7/29/2026
26.7.28.1 146 7/28/2026
Loading failed