MoneyTree.EFCore.VectorStore 1.0.3

MoneyTree.EFCore ORM 增强

📋 概述

MoneyTree.EFCore 是 EF Core 的深度集成增强模块,提供 IUnitOfWork 实现、审计自动填充、领域事件派发、多租户筛选与填充、向量存储抽象、分页扩展等开箱即用的能力。

属性 说明
NuGet 包 MoneyTree.EFCore
外部依赖 MoneyTree.CoreMicrosoft.EntityFrameworkCore
定位 EF Core 深度集成,提供拦截器和向量存储

📦 包体系结构

MoneyTree.EFCore                    ← 核心包:DbContext 基类、工作单元、审计、领域事件
    ├── MoneyTree.EFCore.MultiTenancy   ← 扩展包:多租户筛选/填充拦截器
    ├── MoneyTree.EFCore.VectorStore    ← 扩展包:向量存储抽象 + 各数据库实现
    ├── MoneyTree.EFCore.SqlServer      ← 扩展包:SQL Server 配置
    ├── MoneyTree.EFCore.PostgreSql     ← 扩展包:PostgreSQL 配置
    └── MoneyTree.EFCore.MySql          ← 扩展包:MySQL 配置

依赖关系

┌─────────────────────────────────────────────────────┐
│                   MoneyTree.Core                     │
│         Entity, AggregateRoot, IUnitOfWork           │
└────────────────────────┬────────────────────────────┘
                         │
┌────────────────────────┴────────────────────────────┐
│                 MoneyTree.EFCore                      │
│   MoneyTreeDbContext, UnitOfWork, AuditInterceptor    │
│   DomainEventInterceptor, PageQueryExtensions         │
└────┬───────────────┬───────────────┬─────────────────┘
     │               │               │
     ▼               ▼               ▼
┌─────────┐   ┌───────────┐   ┌──────────┐
│ SqlServer│   │ PostgreSql │   │  MySQL   │
│  扩展包  │   │   扩展包   │   │  扩展包  │
└─────────┘   └───────────┘   └──────────┘

     ▼               ▼               ▼
┌─────────────────────────────────────────────────────┐
│  可选扩展包                                          │
│  ├── MoneyTree.EFCore.MultiTenancy(多租户)          │
│  │   └── 依赖 MoneyTree.MultiTenancy                 │
│  └── MoneyTree.EFCore.VectorStore(向量存储)         │
└─────────────────────────────────────────────────────┘

🏗️ 核心包项目文件结构

MoneyTree.EFCore/
├── MoneyTree.EFCore.csproj
├── GlobalUsings.cs
├── Abstractions/
│   └── IDatabaseTimeConvention.cs       # 数据库时间约定抽象(跨数据库 UTC 函数)
├── Core/
│   ├── MoneyTreeDbContext.cs            # DbContext 基类(扫描 [TimeZoneAware] 特性)
│   ├── UnitOfWork.cs                    # IUnitOfWork 实现
│   ├── UtcDateTimeConverter.cs          # UTC DateTime 值转换器(PG/SQL Server)
│   ├── CrossTimeZoneConverter.cs        # 跨时区 DateTime 值转换器(MySQL)
├── Interceptors/
│   ├── AuditInterceptor.cs              # 审计自动填充
│   └── DomainEventInterceptor.cs        # 领域事件派发
└── Extensions/
    ├── EFCoreBuilder.cs                 # 流畅配置构造器
    ├── EFCoreExtensions.cs              # DI 注册扩展
    ├── PageQueryExtensions.cs           # 分页查询扩展(3 个重载)
    ├── UtcDateTimePropertyExtensions.cs # UseTimeZoneAware() Fluent API
    └── DatabaseProviderExtensions.cs    # 数据库提供者扩展

扩展包结构

MultiTenancy 多租户扩展包

MoneyTree.EFCore.MultiTenancy/
├── MoneyTree.EFCore.MultiTenancy.csproj
├── GlobalUsings.cs
├── MultiTenancyOptions.cs
├── MultiTenancyServiceExtensions.cs
├── Interceptors/
│   ├── TenantFilterInterceptor.cs      # 查询时自动添加租户筛选
│   └── TenantSaveChangesInterceptor.cs # 新增时自动填充 TenantId
└── Extensions/
    ├── MultiTenancyBuilder.cs
    └── MultiTenancyExtensions.cs

VectorStore 向量存储扩展包

MoneyTree.EFCore.VectorStore/
├── MoneyTree.EFCore.VectorStore.csproj
├── GlobalUsings.cs
├── IVectorStore.cs                      # 向量存储接口
├── VectorRecord.cs                      # 向量记录模型
├── VectorSearchResult.cs               # 向量搜索结果
├── DistanceFunctions.cs                # 距离函数
├── VectorStore.cs                      # 向量存储实现
└── Extensions/
    └── VectorStoreExtensions.cs

🎯 核心能力

能力 实现 说明
IUnitOfWork 实现 UnitOfWork<TDbContext> SaveChangesAsync 自动事务
审计自动填充 AuditInterceptor 自动设置创建时间/人、修改时间/人
领域事件派发 DomainEventInterceptor SaveChanges 后自动派发领域事件(需 UseMediatR 注册)
多租户筛选 TenantFilterInterceptor 全局查询过滤
多租户填充 MultiTenancySaveChangesInterceptor 新增时自动设置 TenantId
向量存储 IVectorStore / VectorStore 统一向量存储抽象
分页扩展 ToPageResultAsync(3 个重载,详见下方说明) 轻量分页 / 标准分页(锚点防漂移)/ 带汇总分页
时区保障 UtcDateTimeConverter + CrossTimeZoneConverter + [TimeZoneAware] 特性 + UseTimeZoneAware() Fluent API 按需配置,MySQL 跨时区转换,PG/SQL Server Kind=Utc 标记

⏰ UTC 时间字段策略

问题背景

MySQL 的 datetime 列不保存时区信息。EF Core 写入 DateTime.UtcNowKind=Utc)后,读取回来时 Kind 变为 Unspecified,导致时区信息丢失,可能引发时间计算错误。

解决方案(按需配置)

框架提供 UtcDateTimeConverter,写入时 ToUniversalTime(),读取时 SpecifyKind(Utc),防止时区丢失。

设计变更:框架早期对所有 DateTime 属性全量应用转换器。但全量应用对于日期型字段(如 BirthdayBusinessDate)和非 UTC 时间字段不友好。现已改为按需配置,提供两种方式:

方式一:[TimeZoneAware] 特性标记(声明式)

using MoneyTree.Core.Attributes;

public class User : SnowflakeEntity, IAuditable
{
    // 审计字段标记时区感知转换
    [TimeZoneAware]
    public DateTime CreatedAt { get; set; }

    [TimeZoneAware]
    public DateTime UpdatedAt { get; set; }

    [TimeZoneAware]
    public DateTime? PublishedAt { get; set; }  // 可空字段同样支持

    // 生日字段仅含日期,不需要时区转换
    public DateTime Birthday { get; set; }
}

MoneyTreeDbContext.OnModelCreating 自动扫描 [TimeZoneAware] 特性并应用转换器,无需额外配置。

转换器选择(根据 IDatabaseTimeConvention.UsesServerTimeZone 自动决策):

  • MySQL(UsesServerTimeZone=true):使用 CrossTimeZoneConverter — 处理服务器本地时间与 UTC 的跨时区转换
  • PostgreSQL / SQL Server(UsesServerTimeZone=false):使用 UtcDateTimeConverter — 仅标记 DateTime.Kind=Utc

Fluent API 的 UseTimeZoneAware() 仍使用 UtcDateTimeConverter。 MySQL 场景推荐使用 [TimeZoneAware] 特性(自动选择正确转换器), 或手动 HasConversion(new CrossTimeZoneConverter())

方式二:Fluent API(编程式)

IEntityTypeConfiguration<T> 中使用 UseTimeZoneAware() 扩展方法:

public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
    public void Configure(EntityTypeBuilder<Order> builder)
    {
        builder.Property(o => o.CreatedAt).UseTimeZoneAware();
        builder.Property(o => o.UpdatedAt).UseTimeZoneAware();
        builder.Property(o => o.PublishedAt).UseTimeZoneAware();  // 可空 DateTime? 同样支持

        // Birthday 不配置,保持原样
        builder.Property(o => o.Birthday);
    }
}

UtcDateTimeConverter 实现

/// <summary>
/// UTC DateTime 值转换器。
/// 写入时 ToUniversalTime(),读取时 SpecifyKind(Utc)。
/// </summary>
public class UtcDateTimeConverter : ValueConverter<DateTime, DateTime>
{
    public UtcDateTimeConverter()
        : base(
            v => v.ToUniversalTime(),                      // 写入:转换为 UTC
            v => DateTime.SpecifyKind(v, DateTimeKind.Utc)  // 读取:标记为 UTC
        )
    { }
}

数据库默认值

CreatedAt / UpdatedAt 字段配置数据库默认值(由各数据库扩展包通过 IDatabaseTimeConvention 提供),原生 SQL 插入时自动填充当前时间。

字段 类型 MySQL 默认值 PostgreSQL 默认值 SQL Server 默认值
CreatedAt datetime CURRENT_TIMESTAMP NOW() AT TIME ZONE 'UTC' SYSUTCDATETIME()
UpdatedAt datetime 同上 同上 同上

MySQL 8.0+ 不支持 UTC_TIMESTAMP(6) 语法,框架已改用 CURRENT_TIMESTAMP。 MySQL CURRENT_TIMESTAMP 返回数据库服务器本地时间,框架通过 CrossTimeZoneConverter 自动处理时区转换。 PostgreSQL/SQL Server 存储 UTC,使用 UtcDateTimeConverter 标记 Kind=Utc。

MySQL 时区配置(必需)

使用 MySQL 时需在应用启动时初始化 TimeZoneHelper,检测数据库时区并启用跨时区转换:

// Program.cs — 启动时检测 MySQL 时区
using (var scope = app.Services.CreateScope())
{
    var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
    using var connection = context.Database.GetDbConnection();
    connection.Open();
    using var cmd = connection.CreateCommand();
    cmd.CommandText = "SELECT TIMEDIFF(NOW(6), UTC_TIMESTAMP(6))";
    var databaseOffset = TimeSpan.Parse(cmd.ExecuteScalar()!.ToString()!);
    TimeZoneHelper.Initialize(databaseOffset);
}

未注册任何数据库扩展包时,跳过数据库默认值设置,由 AuditInterceptor 在应用层填充 CreatedAt

UpdatedAt 乐观锁机制

UpdatedAt 字段在 MoneyTreeDbContext.OnModelCreating 中统一配置:

  • IsConcurrencyToken():EF Core 在 UPDATE 时自动在 WHERE 子句包含 UpdatedAt 原始值,并发冲突时抛 DbUpdateConcurrencyException
  • ValueGeneratedOnAddOrUpdate():告诉 EF Core 该值由数据库生成,不在 SET 子句显式发送

MySQL 需要通过迁移 SQL 配置列的 ON UPDATE 行为(EF Core 的 HasDefaultValueSql 仅生成 DEFAULT 约束,不生成 ON UPDATE):

-- MySQL 迁移:配置 UpdatedAt 列的 ON UPDATE 行为
ALTER TABLE your_table 
MODIFY COLUMN updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;

关键规则:应用层不要手动设置 UpdatedAtAuditInterceptor 也不设置 UpdatedAt,仅设置 UpdatedByUpdatedAt 完全由数据库管理,确保乐观锁机制正确工作。

注意UtcDateTimeConverter 保障 DateTime.Kind 不丢失,无需改用 DateTimeOffset。对于审计字段,建议标记 [TimeZoneAware] 以保障时区正确。MySQL 场景框架自动选择 CrossTimeZoneConverter


🗃️ 数据库兼容性

数据库 多租户 向量检索 JSON 列 推荐版本
SQL Server ✅ (SqlVector) 2022+
MySQL ✅ (VECTOR) 9.0+
PostgreSQL ✅ (pgvector) 15+

🚀 使用示例

示例一:单租户 PostgreSQL 项目

// Program.cs
using MoneyTree.EFCore.Extensions;
using MoneyTree.EFCore.PostgreSql.Extensions;
using MyApp.Infrastructure.Data;

var builder = WebApplication.CreateBuilder(args);

// EF Core 配置
builder.Services.AddMoneyTreeEFCore<AppDbContext>(db =>
{
    db.UsePostgreSql<AppDbContext>(
        builder.Configuration.GetConnectionString("Default")!);
    db.UseMediatR(typeof(Program).Assembly);  // 注册 MediatR,启用领域事件派发
});

var app = builder.Build();
app.Run();

领域事件派发DomainEventInterceptor 依赖 IMediator,未调用 UseMediatR 时降级为不派发(无异常)。 启用 Outbox 派发服务时必须先调用 UseMediatR,否则 MediatrEventDispatcher 解析 IMediator 会抛异常。

示例二:多租户 SaaS 项目(PostgreSQL)

// AppDbContext.cs
using MoneyTree.EFCore.Core;
using MoneyTree.EFCore.MultiTenancy;
using MoneyTree.MultiTenancy.Abstractions;
using Microsoft.EntityFrameworkCore;

namespace MyApp.Infrastructure.Data;

public class AppDbContext : MoneyTreeDbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
    {
    }

    public DbSet<Order> Orders => Set<Order>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        // 配置多租户全局筛选
        modelBuilder.ConfigureTenantFilters(
            () => this.GetService<ITenantContextAccessor>()?.TenantContext?.TenantId);
    }
}

// Program.cs
using MoneyTree.EFCore.Extensions;
using MoneyTree.EFCore.MultiTenancy.Extensions;
using MoneyTree.EFCore.PostgreSql.Extensions;
using MoneyTree.MultiTenancy.Extensions;

var builder = WebApplication.CreateBuilder(args);

// 多租户
builder.Services.AddMoneyTreeMultiTenancy(mt =>
{
    mt.UseHeader("X-Tenant-Id");
});

// EF Core + 多租户
builder.Services.AddEFCoreMultiTenancy(tenant =>
{
    tenant.WithColumnName("TenantId")
          .IgnoreTable("SystemConfigs");
});

// 数据库
builder.Services.AddMoneyTreeEFCore<AppDbContext>(db =>
{
    db.UsePostgreSql<AppDbContext>(
        builder.Configuration.GetConnectionString("Default")!);
});

var app = builder.Build();
app.UseMoneyTreeMultiTenancy();
app.Run();

服务层使用

分页扩展方法(ToPageResultAsync

框架提供三个分页重载,按需选择:

重载 返回类型 时间戳锚点 适用场景
ToPageResultAsync(query, ct) IList<T> ❌ 不应用 轻量分页,仅返回分页数据,适用于无限滚动等不需要总页数的场景
ToPageResultAsync(query, timestampProperty?, ct) PageResult<T> ✅ 可选启用 标准分页,返回完整元数据(TotalCount/Timestamp),传入 timestampProperty 启用锚点防漂移。锚点通过 PageQuery.PageTime 获取(首页自动取当前时间,翻页回传还原)
ToPageResultAsync<T, TSummary>(query, summarySelector, timestampProperty?, ct) PageResult<T, TSummary> ✅ 可选启用 带汇总的标准分页,汇总与分页基于同一 PageTime 锚点视图
using MoneyTree.Core.Primitives;
using MoneyTree.EFCore.Extensions;
using MoneyTree.EFCore.MultiTenancy.Extensions;
using MyApp.Domain.Entities;
using Microsoft.EntityFrameworkCore;

namespace MyApp.Application.Services;

/// <summary>
/// 完整的订单服务,展示所有 EF Core 特性组合使用。
/// </summary>
public class OrderService 
{
    [Inject] public DbSet<Order> Orders { get; private set; } = default!;
    [Inject] public IUnitOfWork UnitOfWork { get; private set; } = default!;
    [Inject] public ICurrentUser CurrentUser { get; private set; } = default!;

    /// <summary>
    /// 创建订单。
    /// TenantId 由 TenantSaveChangesInterceptor 自动填充。
    /// CreatedAt/UpdatedAt 由 AuditInterceptor 自动填充。
    /// </summary>
    public async Task<ApiResult<Order>> CreateAsync(Order order)
    {
        await Orders.AddAsync(order);
        await UnitOfWork.SaveChangesAsync();
        // 领域事件由 DomainEventInterceptor 自动派发
        return ApiResult<Order>.Success(order);
    }

    /// <summary>
    /// 查询订单(自动租户隔离)。
    /// </summary>
    public async Task<ApiResult<Order?>> GetAsync(string id)
    {
        var order = await Orders.FirstOrDefaultAsync(o => o.Id == id);
        return ApiResult<Order?>.Success(order);
    }

    /// <summary>
    /// 标准分页查询(返回完整元数据,含 TotalCount/Timestamp 锚点防漂移)。
    /// 传入 timestampProperty 启用时间戳锚点筛选,防止翻页期间新增数据导致漂移。
    /// </summary>
    public async Task<PageResult<Order>> GetPagedAsync(PageQuery query)
    {
        return await Orders
            .Where(o => o.TotalAmount > 0)
            .OrderByDescending(o => o.TotalAmount)
            .ToPageResultAsync(query, o => o.CreatedAt);
    }

    /// <summary>
    /// 轻量分页查询(仅返回分页数据,不含元数据,不应用时间戳锚点)。
    /// 适用于无限滚动等不需要总页数的场景。
    /// </summary>
    public async Task<IList<Order>> GetPagedListAsync(PageQuery query)
    {
        return await Orders
            .Where(o => o.TotalAmount > 0)
            .OrderByDescending(o => o.TotalAmount)
            .ToPageResultAsync(query);
    }

    /// <summary>
    /// 带汇总的分页查询(含 Timestamp 锚点防漂移)。
    /// </summary>
    public async Task<PageResult<Order, OrderSummary>> GetPagedWithSummaryAsync(PageQuery query)
    {
        return await Orders.ToPageResultAsync(
            query,
            summarySelector: q => Task.FromResult(new OrderSummary
            {
                TotalAmount = q.Sum(o => o.TotalAmount),
                TotalCount = q.Count()
            }),
            timestampProperty: o => o.CreatedAt);
    }

    /// <summary>
    /// 管理后台:跨租户查询。
    /// </summary>
    public async Task<List<Order>> GetAdminOrdersAsync()
    {
        return await Orders
            .IgnoreTenantFilter()
            .OrderByDescending(o => o.TotalAmount)
            .Take(100)
            .ToListAsync();
    }

    /// <summary>
    /// 按指定租户查询。
    /// </summary>
    public async Task<List<Order>> GetByTenantAsync(string tenantId)
    {
        return await Orders
            .IgnoreTenantFilter()
            .Where(o => o.TenantId == tenantId)
            .ToListAsync();
    }
}

/// <summary>
/// 订单汇总模型。
/// </summary>
public class OrderSummary
{
    public decimal TotalAmount { get; set; }
    public int TotalCount { get; set; }
}

示例三:多租户 + 向量存储项目

using MoneyTree.EFCore.Extensions;
using MoneyTree.EFCore.MultiTenancy.Extensions;
using MoneyTree.EFCore.PostgreSql.Extensions;
using MoneyTree.EFCore.VectorStore.Extensions;
using MoneyTree.MultiTenancy.Extensions;

var builder = WebApplication.CreateBuilder(args);

// 多租户
builder.Services.AddMoneyTreeMultiTenancy(mt => mt.UseHeader("X-Tenant-Id"));
builder.Services.AddEFCoreMultiTenancy();

// 数据库
builder.Services.AddMoneyTreeEFCore<AppDbContext>(db =>
{
    db.UsePostgreSql<AppDbContext>(
        builder.Configuration.GetConnectionString("Default")!);
});

// 向量存储
builder.Services.AddEFCoreVectorStore<PostgreSqlVectorStore>();

var app = builder.Build();
app.UseMoneyTreeMultiTenancy();
app.Run();

AI 搜索服务

using MoneyTree.EFCore.VectorStore;

namespace MyApp.Application.Services;

public class SearchService
{
    private readonly IVectorStore _vectorStore;
    private readonly IEmbeddingGenerator _embeddingGenerator;

    public SearchService(IVectorStore vectorStore, IEmbeddingGenerator embeddingGenerator)
    {
        _vectorStore = vectorStore;
        _embeddingGenerator = embeddingGenerator;
    }

    /// <summary>
    /// 语义搜索。
    /// </summary>
    public async Task<IReadOnlyList<VectorSearchResult>> SemanticSearchAsync(
        string query, int topK = 10)
    {
        var queryVector = await _embeddingGenerator.GenerateAsync(query);
        return await _vectorStore.SearchAsync(queryVector, topK);
    }
}

🛡️ 拦截器说明

拦截器 触发时机 作用
AuditInterceptor SaveChanges 自动设置 CreatedAt/UpdatedAt/CreatedBy/UpdatedBy
DomainEventInterceptor SaveChanges 后 自动派发聚合根中收集的领域事件
TenantFilterInterceptor 查询时 自动为所有查询添加 WHERE TenantId = @currentTenant
TenantSaveChangesInterceptor SaveChanges 新增实体时自动填充 TenantId

Showing the top 20 packages that depend on MoneyTree.EFCore.VectorStore.

Packages Downloads
MoneyTree.AI
MoneyTree Framework AI 集成模块。基于 Microsoft.Extensions.AI 抽象层,提供嵌入生成、RAG 检索等功能。
2
MoneyTree.AI
MoneyTree Framework AI 集成模块。基于 Microsoft.Extensions.AI 抽象层,提供嵌入生成、RAG 检索等功能。
1

.NET 10.0

Version Downloads Last updated
1.0.4 1 7/21/2026
1.0.3 1 7/12/2026