Crping.EFCore 10.2.2

dotnet add package Crping.EFCore --version 10.2.2
                    
NuGet\Install-Package Crping.EFCore -Version 10.2.2
                    
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="Crping.EFCore" Version="10.2.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Crping.EFCore" Version="10.2.2" />
                    
Directory.Packages.props
<PackageReference Include="Crping.EFCore" />
                    
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 Crping.EFCore --version 10.2.2
                    
#r "nuget: Crping.EFCore, 10.2.2"
                    
#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 Crping.EFCore@10.2.2
                    
#: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=Crping.EFCore&version=10.2.2
                    
Install as a Cake Addin
#tool nuget:?package=Crping.EFCore&version=10.2.2
                    
Install as a Cake Tool

Crping.EFCore

基于 EntityFramework Core 的数据库操作工具包基础类,支持读写分离,支持SQLiteSQL Server等,包含WebApi的增、删、改、查、分页等常用方法实现!

版本更新说明


10.2.2

2026年9月15日 星期二

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 10.2.2 —— ct 全量复查修复批次:公开 API 零变更、升级零迁移、需重编译

ct(取消令牌)修复 — 全量复查

  • BaseDAL.UpdateOverallAsync 声明的 ct 此前被静默丢弃(硬编码 CancellationToken.None,自 params 兄弟方法复制带入),现正确透传至 SaveChangesAsync——单条整体更新可响应调用方取消
  • GetByIdProjectedAsync 投影详情缓存管道补 ct 透传:版本号读 / GET / SET / 续期 / 单飞等待全程可取消(六视角详情方法 GetDetailsAsync 等公开签名已有 ct,此前断在私有收口点)
  • TryCreateAsync / CreateModelsChunkedAsync catch 路径回滚改 CancellationToken.None:回滚不被调用方取消打断(与 BaseDomain.InTransactionAsync / GetOrCreate 族 / 批量更新族同口径),回滚自身失败仍告警留痕后重抛原始异常
  • TryCreateAsync 提交后缓存补失效 FlushAsyncCancellationToken.None:此前取消恰好落在 Commit 之后会跳过登记的失效(缓存陈旧至 TTL),现与 GetOrCreate 族 / BaseDomain 提交点同口径

10.2.1

2026年9月15日 星期二

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 10.2.1 —— 审计修复批次:公开 API 零变更、升级零迁移、需重编译

性能修复 — 级联逐层模式 N+1 消除

  • GetCascadeAsync 逐层模式(batchMode: false)由"每节点一查"改为广度优先按层查询:每层子节点合并为一条 ParentId IN 查询,L 层树从 N+1 次往返降为 L+1 次(65 节点 3 层树实测 66 → 4 次);condition 每层生效、父内排序、孤儿不入树、叶节点 Children 为 null 语义均与旧实现一致(CascadeLevelTests 树形等价 + 查询次数回归守卫)

性能修复 — 缓存读写 UTF8 字节直通

  • CacheManager 读写改 GetAsync/SetAsync(byte[])+ SerializeToUtf8Bytes/Deserialize(byte[])——旧实现大 payload(ListPage 全列表等)经 string 双重编码;存量缓存值(string 写入的 UTF8 JSON 字节)新旧互通,无需失效

性能修复 — 表达式指纹不再物化 JSON 字符串

  • CacheKeys.FormatCapture 非基元闭包捕获(如 ids.Contains(x.Id) 的集合)改 SerializeToUtf8Bytes 直出:未超限(≤128 UTF8 字节)转回可读 JSON 文本(纯 ASCII 键与旧版完全一致),超限直接对字节流 SHA256 指纹化——旧实现先整体序列化字符串再按 128 字符长度判定丢弃,大集合捕获每次构建缓存键数百 KB 无谓分配

性能修复 — 结构体主键比较装箱消除

  • BaseDAL.UpdateAsync 族 5 处 + ValidateUpdatePreCheck 1 处的 id.Equals(model.Id)EqualityComparer<TKey>.Default.Equals(不装箱;附带修复:引用型主键 id 为 null 时由 NRE 变为按参数不一致抛 ArgumentException

健壮性 — 事务回滚失败不再掩盖原始异常

  • BLL 5 处 catch 中的裸 RollbackAsync() 加保护(GetOrCreateAsync ×2、批量创建 ×2、批量更新 ×1):回滚自身失败(如连接已断)仅告警留痕后重抛原始异常(与 BaseDomain.InTransactionAsync 同口径);跟踪器清理不再依赖回滚成败

行为变更(bug 修复性质)

  • WhereLike 关键词按字面前缀匹配:转义 \ % _ 并经 EF.Functions.Like ESCAPE 重载声明转义符——此前关键词含通配符会改变匹配语义(如 "banana_split"_ 通配任意字符误命中 "banana split");不含通配符的关键词行为与缓存键完全不变
  • BaseDomain.Authorization 委托未设置抛 UnauthorizedAccessException(全局映射 401),与 CurrentUserId 同口径——此前 ArgumentNullException(映射 400);授权上下文缺失属认证问题而非参数错误
  • DeleteModelsAsync 批量数量超过 MaxBatchUpdateSize(默认 1000)返回 BadRequest——与 BatchSetAuditStatusAsync 同口径,超大集合的 IN 参数化不再触发数据库参数上限异常(SQL Server 2100)

可测试性 — GenerateId 接入 TimeProvider

  • 新增 EntityHelper.TimeProvider 静态可设属性(默认 TimeProvider.System 生产行为不变;静态类无法 DI 注入,与 PendingCacheInvalidations.Logger 同模式):时钟回拨(沿用旧块)与序列溢出(自旋进位)分支可测,补 2 个分支测试;固定时间路径(fixedTime 种子数据 / ToStringId)不经过该属性,种子 Id 不受影响

文档化设计立场(防误判,零代码行为)

  • EntityHelper XML 注释明示:默认 collation Latin1_General_BIN2 为库的默认数据库立场(未指定数据库时按 SQL Server 处理),非 SQL Server 消费方须显式传参(SQLite: "BINARY";PostgreSQL 不支持列级 collation)
  • WebApiRootController XML 注释明示 BLL 必须以 Scoped 注册(控制器每请求向 BLL 写 CacheEnable/CurrentUserIdResolver,单例误注册会跨请求互踩)

清理(零行为)

  • 删除 WebApiBaseBLL.Update 不可达 switch 兜底分支(入口已校验接口,改直接强转)
  • 删除 WebApiBaseBLL.InvalidateCacheAsync 不可达兜底 catch(CacheManager.InvalidateAsync 已内部降级)
  • 删除 Utils.cs 35 行注释旧实现;修正"数组代替Stack"注释(实为 List<char>

10.2.0

2026年9月14日 星期一

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 10.2.0 —— 评审批次:2 个正确性 bug 修复 + 契约改善

破坏性变更 — IDetailDAL 接口新增 FindNoTrackAsync 成员(自定义 IBaseDAL 实现者需补实现)

  • 新增 Task<TModel?> FindNoTrackAsync(TKey id, CancellationToken ct = default)AsNoTracking + 主键谓词查询,不进 ChangeTracker;BaseDAL 已提供默认实现

bug 修复 — BLL 状态切换族跟踪器陈旧快照

  • ToggleEnableStatusAsync / ToggleAuditStatusAsync / SetAuditStatusAsync / SetDeleteStatusAsync / SoftDeleteCoreAsync 的探测查询由 FindAsync(tracked)改 FindNoTrackAsync——此前 ExecuteUpdateAsync(绕过 ChangeTracker)后跟踪器残留更新前快照:同请求内连续 toggle 按旧值计算翻转目标致数据库值不再变化、enforceTransit 按陈旧状态判定流转(新增 ToggleStatusStaleTrackerTests 回归锁定)

bug 修复 — 写后读绑定(BindReadToWrite)跨 async 边界失效

  • ReadWriteRouting 由裸 AsyncLocal<bool> 改 AsyncLocal 持有者模式(引用对象变异不受 ExecutionContext 回滚影响),ReadWriteRoutingMiddleware 请求入口 seed 请求级状态对象——裸 AsyncLocal 写入发生在 DAL 写方法(async)内部,随方法返回被运行时回滚,"跨 BLL 编排的 read-your-writes"主打场景实际不生效;公共 API 零变化,未注册中间件的宿主行为与修复前一致

bug 修复 — 恢复软删除的 DeletedAt 落库 0001-01-01 而非 NULL

  • SoftDeleteCoreAsync 恢复分支的 restore ? default : GetLocalNow() 三元中 default 被推断为 DateTimeOffset(非可空)——恢复时 DeletedAt 写入 0001-01-01 而非 NULL,SQL Server datetime 列(最小 1753 年)会溢出抛异常;改 (DateTimeOffset?)null 恢复 NULL 清理语义(DeletedBy 同步改 default(TUserKey?)

行为改善

  • GetByIdAsync(id, options, …) 不再变异调用方 options 实例(补 id 条件改局部副本)——共享复用的 options 不再被残留条件污染
  • ct 透传补齐 3 处:DeleteModelAsync 前置 GetAsync、两个分批事务 BeginTransactionAsync

零行为重构/优化

  • 统计聚合族 16 处 condition == null ? … : Where(condition) 同构分支收敛到私有 StatisticSource(条件化查询源统一入口)
  • IgnoreSpecialProperties 接口匹配(GetInterfaces() 全扫描)按 (TModel, DAL类型) 进程级缓存一次——每更新(批量时每实体)重复反射收敛为一次解析;AdditionalSpecialProperties 覆写须返回类级静态映射(缓存约束已写入 XML 注释)

10.1.0

2026年9月13日 星期日

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 10.1.0

  • 无功能性变更,仅同步版本号(kebab-case 路由风格组件化 AddEFCoreKebabCaseRouting 为 Controller 层新功能,Core 契约未变);升级自 10.0.0 零迁移、需重编译

10.0.0

2026年9月11日 星期五

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 10.0.0 —— v10 破坏性收口线开启

破坏性变更 — 调参旋钮 init 化(v10 登记 ⑤ + MyDbContextFactory 可变旋钮同批收口)

  • CacheManager 三调参属性 { get; set; }{ get; init; }VersionLocalCacheTtl / MaxThrottleEntries / LogThrottleWindow
  • MyDbContextFactory 三旋钮 { get; set; }{ get; init; }ReadOnlyFailureCooldown / ValidateReadOnlyOnCreate / CreateInstance
  • 依据:六者均属「进程级调参、启动时设置一次」语义(原 XML 注明「运行期请勿修改」但无任何机制约束)——init 化把约束交给编译器;写入飞行中的调参本就无正确性保证(单飞分片锁、续期节流字典、故障冷却字典的中间状态),init 消除该类误用;EFCoreCacheOptions(配置载体 POCO,appsettings 绑定)保持可变不受影响
  • Demo 零改动(CreateInstance 注册示例本就是对象初始化器形态,init 下依然合法)

迁移指南(9.25.0 → 10.0.0)

  • 构造后属性赋值将编译失败(CS8852)→ 改为构造时赋值:
    • CacheManager:DI 场景经 AddEFCoreCache(o => o.LogThrottleWindow = ...) / appsettings Crping.EFCore.Cache 配置节(推荐,v9.13.0 起通道);手工构造场景 new CacheManager(cache, store, options: new EFCoreCacheOptions { ... }) 或对象初始化器
    • MyDbContextFactory:对象初始化器 new MyDbContextFactory<TDb>(config, name) { ReadOnlyFailureCooldown = ..., CreateInstance = ... }
  • 二进制需重编译:init setter 带 modreq(IsExternalInit),旧编译产物引用 10.0.0 调用 setter 会 MissingMethodException;升级后全量重新编译
  • 外部自定义 ICacheManager 实现者:移除三属性 setter(接口已收窄为 { get; },见 Common 包 Readme)

测试

  • 3 处构造后赋值点迁移:FactoryRoundRobinTests / FactoryFailoverTests 改对象初始化器(后者「冷却期置零」由测试中段变异改构造时赋值——冷却期计时跟随注入 TimeProvider 的测试路径此前已独立存在,运行期可变性依赖彻底解除);CacheOptionsTests 删除「构造后手动设置覆盖初值」过时断言(init 化后该语义不再存在);全量 693 / 0 失败 / 2 跳过,0 警告

v10 收口状态

  • 本批收口:⑤ CacheManager 旋钮 init 化 + MyDbContextFactory 可变旋钮(v9.22.0 评审遗留 ④)——本评审系列(v9.23.0 起)登记的 v10 清单全部清零
  • 10.0.0 收口线已开启:其余历史登记破坏性项(静态注册表下线、PUT {id}/enable / PUT {id}/audit / DELETE {id}/delete 端点删除、SoftDeleteAsync reset 参数删除、[Obsolete] 垫片移除、GetCurrentUserId 改名候选等)按登记并入后续 10.0.0 批次

第二批增补 — 删除状态切换 BLL 方法移除(路由收口 ② 的 Core 侧)

  • IDeleteBLL / WebApiBaseBLL 移除 ToggleDeleteStatusAsync(TKey, bool)(切换语义)——set 语义唯一形态 SetDeleteStatusAsync(id, isDeleted, selfOnly, ct)(v9.20.0 起);需切换语义时先查当前状态再取反调用
  • 迁移ToggleDeleteStatusAsync(id)SetDeleteStatusAsync(id, !current)(或直接改用 PUT {id}/deleted 端点传目标值——端点 v10.0.0 第六批起为 PUT 形态,见第六批增补)
  • 迁移(端点配置面)MethodsEnable.ToggleDeleteStatusAsync = trueSetDeleteStatusAsync = true(本版唯一配置面变化)

第二批增补 — 静态注册表下线(Core 侧无 API 变更)

  • Core 包不受影响(注册表全部位于 Controller 包);SetDeleteStatusAsync XML 文档同步去除切换版引用

第三批增补 — SoftDeleteAsync reset 遗留参数移除(路由评审收口清单 ③,v9.19.0 登记)

  • WebApiBaseBLL.SoftDeleteAsync(TKey, bool reset, bool selfOnly, ct)SoftDeleteAsync(TKey id, bool selfOnly = true, CancellationToken ct = default)——软删除仅保留删除语义(盖章 DeletedBy/DeletedAt);恢复语义唯一入口 ResetSoftDeleteAsync(恢复时清理审计字段),两者共用私有核心实现(布尔分叉收敛到私有层,公开 API 语义单一)
  • 迁移SoftDeleteAsync(id, reset: true, ...)ResetSoftDeleteAsync(id, ...)reset: false 调用点直接删参数
  • IDeleteBLL 接口签名同步

第三批增补 — WebApiBaseBLL.Cache 过时垫片移除(v9.21.0 登记)

  • 删除 [Obsolete]WebApiBaseBLL.Cache 属性——IWebApiBaseBLLICacheBLL.CacheManager 访问缓存门面(同一实例),双属性暴露收口为单入口
  • IKit<TCategoryName> 接口移除 Cache 成员(Common 包,与 ICacheBLL.CacheManager 的遗留重复;BaseDomain.Cache 为 Domain 自有属性不受影响)
  • 迁移bll.Cachebll.CacheManager

第四批增补 — 委托槽位同族收口改名(v9.21.0 评审登记 ③ 兑现,v10 登记清单全部清零)

  • 四个委托槽位属性(Func 属性)取名词形态改名,消除「属性名带 Get 前缀」与 Controller 侧同名方法的同名不同义混淆;与库内既有 ResolveSort / ResolveConfig / ResolveRoutes 的 Resolver 词族一致:
旧名 新名 声明位置
GetCurrentUserId CurrentUserIdResolver ICurrentUser<TUserKey>(BLL/Domain 共享)、WebApiBaseBLLBaseDomain
GetAuthorization AuthorizationResolver IDomain<TUserKey>BaseDomain
GetAuthHeaderValue AuthHeaderValueResolver IDomain<TUserKey>BaseDomain
GenerateId IdGenerator IGenerateId(BLL/Domain 共享)、WebApiBaseBLLBaseDomain
  • 不改名(语义分层的另外两层):Controller 侧 protected 方法 GetCurrentUserId() / GetAuthorization() / GetAuthHeaderValue()(动词短语,接线读作 _bll.CurrentUserIdResolver = GetCurrentUserId; 依然自然);IToolkit.GenerateId() 方法与 EntityHelper.GenerateId()(方法调用非槽位);值属性 CurrentUserId / Authorization / AuthHeaderValue
  • 行为零变更(纯改名):AsyncLocal 承载、SyncConfig 双向同步、漏配 WARN、401 决策均保持;SyncConfig 漏配诊断消息中的委托名同步更新为 CurrentUserIdResolver
  • 迁移(编译器逐点暴露,无静默行为差异):bll.GetCurrentUserId = ...bll.CurrentUserIdResolver = ...domain.GetCurrentUserId / GetAuthorization / GetAuthHeaderValue / GenerateIdCurrentUserIdResolver / AuthorizationResolver / AuthHeaderValueResolver / IdGenerator;自定义 ICurrentUser / IGenerateId 实现者同步成员名
  • 附带修复:BaseDomain 三处既有 XML cref 裸引用(WebApiBaseBLL 泛型类、IKit{TCategoryName}.Cache)CS1574 改 <c> 文本标注(增量构建掩盖、全量重建暴露,同 v9.20.0 批次 <c> 处理先例)

9.25.0

2026年9月11日 星期五

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 9.25.0

新功能 — Domain 事务编排助手 InTransactionAsync(落实 v9.23.0 登记 ④)

  • BaseDomain 新增 InTransactionAsync<T>(work, ct) 与无返回值形态:解析第一个 BLL(构造参数顺序)的写库上下文 → 开启事务 → 共享给所有 BLL(ShareDbContext)→ 执行 work → 提交并补缓存失效;异常自动回滚——收拢 DomainTransactionTests / Demo TopicDom 展示的手写样板(取上下文、开事务、共享、提交、回滚、清理跟踪器、补失效七件事一处声明)
  • 提交语义:work 正常返回即提交——经 ApiResult 错误码早退的业务失败视为正常完成(无待提交变更时提交为空操作);需要回滚的业务失败请在 work 内抛出异常
  • 提交补失效:提交后立即补失效该事务登记的模型版本号(PendingCacheInvalidations.FlushAsync,对齐 BLL 自管事务提交点语义),且补失效以 CancellationToken.None 保护——已提交数据的缓存失效不被调用方取消打断(失效失败降级告警,TTL 兜底);v9.8.0 事务感知失效对 Domain 编排场景由助手自动接线,不再依赖手动 CommitAndInvalidateCacheAsync
  • 回滚语义:异常时回滚(None 保护,不被调用方取消打断)并清理共享上下文的跟踪状态(防残留脏实体污染后续操作——对齐 Demo TopicDom 既文档化语义,DAL 层异常路径的 Clear 仅覆盖非共享场景),随后原样重抛;回滚自身失败仅告警留痕,不掩盖原始异常
  • 嵌套编排:当前上下文已有活动事务(外层助手或手动开启)时复用——只共享上下文并执行 work,不开新事务、不提交/回滚(生命周期归外层所有;对齐 GetOrCreateAsync 嵌套事务修复语义)
  • 兼容性:源码与二进制均兼容——InTransactionAsync 仅加在 BaseDomain(不加 IDomain,避免破坏外部实现者),写库解析复用既有 IDbBLL 成员(SetWriteDb + Db),零接口变更
// Demo TopicDom:约 30 行手写事务样板 → 声明式 work
public Task<ApiResult<TopicDto>> CreateAsync (TopicDto model)
    => InTransactionAsync(async ct =>
    {
        var (code, topic) = await topicBLL.CreateTopicAsync(model.Title, model.Contents, CurrentUserId);
        if (code != ResultCode.Created) return new ApiResult<TopicDto>(code);
        await topicImageBLL.CreateAsync(BuildImages(topic.Id, model.Images));
        return ApiResult.Created();   // 正常返回即提交;需回滚的业务失败在 work 内抛异常
    });

测试

  • DomainTransactionTests +5(SQLite 真实事务):跨 BLL 编排提交落库 / work 异常回滚且跟踪器清理 / 复用外层活动事务且不越权提交(外层回滚后内层写入一并消失)/ 提交后补失效事务登记的缓存版本号(mock ICacheManager Verify)/ 无 BLL 抛 InvalidOperationException;全量 693 / 0 失败 / 2 跳过,0 警告

9.24.0

2026年9月11日 星期五

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 9.24.0

变更 — BaseDomain 身份委托 AsyncLocal 化(落实 v9.23.0 登记 ①,范围扩展至三个委托)

  • GetCurrentUserId / GetAuthorization / GetAuthHeaderValue 三个身份委托由普通自动属性改为实例 AsyncLocal 承载(与 WebApiBaseBLL.GetCurrentUserId 同构);属性 get/set 形态不变,源码与二进制均兼容
  • 登记范围原为 GetCurrentUserId 一项,扩展至三个委托的依据:三者同属「WebApiRootController 构造函数每请求 ??= 注入」的身份委托家族——旧普通属性下单例 Domain 首个请求注入的委托存活于实例上,后续请求复用其捕获的控制器上下文(GetCurrentUserId 是错身份、Authorization 是错 Token 的跨请求身份串扰),单修一个会留下混合语义模型;GenerateId 保持普通属性(组合期一次性配置,无请求级语义)
  • 语义增量:① 同一实例跨并行分支(Parallel/Task.WhenAll)共享时各分支取值互相隔离(与 BLL 对齐);② 单例 Domain 跨请求身份串扰修复;③ 组合根一次性配置不再跨流生效——须在各消费流内设置(与 BLL 侧行为一致,XML remarks 注明)
  • 升级注意:Web 注入路径(Controller 每请求构造中 ??= 注入)行为不变——各请求流读到 null 后设置自己的委托;非 Web「启动时配置一次、多个流消费」的用法须改为在各消费流内设置(或每流经 SetCurrentUser 式注入),与 BLL 侧 AsyncLocal 的既有约束一致

新功能 — SyncConfig 漏配诊断(落实 v9.23.0 登记 ②)

  • 同步后 Domain 与全部 BLL 均无 GetCurrentUserId 委托时输出 WARN(消息含 Domain 类型、BLL 数量与后果指引),每 Domain 类型每进程仅提示一次(静态守卫,与 MethodsEnable 生效性自检同形态);logger 未注入时静默跳过——漏调 SyncConfig / 漏配委托的「运行期 401 静默失效」提前到同步点定位

变更 — BaseDomain.Cache / Logger 未注入归 InvalidOperationException(落实 v9.23.0 登记 ③)

  • 可选依赖(ICacheManager / ILogger<IDomain<TUserKey>>)未注入时访问由 ArgumentNullExceptionInvalidOperationException(消息指明 DI 注册路径 AddEFCoreRedisCache / AddEFCoreMemoryCache / 日志注册)——宿主 DI 配置缺失与调用参数错误语义分型,对齐 v9.22.0 MyDbContextFactory 配置错误口径;保留「构造不抛、首次访问才暴露」的惰性形态
  • 升级注意:catch (ArgumentNullException) 包住该场景的消费者需同步(HTTP 语义按正确方向变化:400「请求参数错误」→ 500「服务器内部错误」——DI 配置缺失归 500 更准确)

测试

  • 新增 BaseDomainTests +7:AsyncLocal 并行分支隔离 1、SyncConfig 漏配 WARN 3(一次且幂等 / 已配置无 WARN / 无 logger 静默)、Cache/Logger IOE 2、注入后返回原实例 1;全量 688 / 0 失败 / 2 跳过,0 警告

9.23.0

2026年9月11日 星期五

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 9.23.0

变更 — BaseDomain.CurrentUserId 委托未设置归 401(对齐 BLL 侧 v9.21.0 决策)

  • BaseDomain.CurrentUserIdGetCurrentUserId 委托未设置时抛 UnauthorizedAccessException(原 ArgumentNullException → 400「请求参数错误」),经全局异常映射为 401——与 WebApiBaseBLL v9.21.0 同语义决策对齐:身份未解析属认证问题而非参数错误;异常消息指明修复路径(直接设置委托,或构造后调用 SyncConfig 从已配置的 BLL 反向同步)
  • 触发面集中在非 Web 手工组装 + 漏调 SyncConfig 的场景(Web 注入路径 Controller 构造函数已自动下发委托,正常使用无感)
  • 兼容性:源码与二进制均兼容(无签名变化,仅异常类型);catch (ArgumentNullException) 包住该场景的消费者需同步为 UnauthorizedAccessException(HTTP 400 → 401)

加固 — ShareDbContext null 守卫

  • BaseDomain.ShareDbContext 入口补 ArgumentNullException.ThrowIfNull(dbContext):null 不再透传到各 BLL 的 SetDb,守卫先行、BLL 零触碰

文档 — BaseDomain XML 注释收口

  • 类级时序契约(Web 注入路径由 WebApiRootController 构造函数自动调用 SyncConfig;非 Web 场景——后台任务 / HostedService——须在首次使用前手动调用,漏调以 401 暴露)
  • SyncConfig remarks 补三点:时序契约与不自动化的原因(派生类可能在构造体中向 BLL 集合追加项,基类构造阶段自动同步会漏);顺序依赖(多个 BLL 均持有委托时,反向拉取取构造参数顺序的第一个);AsyncLocal 流语义(BLL 侧委托由 AsyncLocal 承载,本方法向 BLL 的赋值仅在调用 SyncConfig 的 async 流下游生效;Domain 侧为普通属性,不具备 BLL 的并行分支隔离)
  • 字段、全部属性与委托、构造、ShareDbContext(TDb 兼容性与部分应用状态说明)补 XML 注释

风格 — CacheManager(纯风格零行为)

  • TrimLastRefreshed 移除项改 TryRemove(替代 ICollection<KVP> 显式接口装箱转换)
  • _stripedLocks XML 注明 static 取舍(单例注册下与实例等价;多实例共享分片跨实例排队,正确性无损)

测试

  • 新增 BaseDomainTests +9(BaseDomain 首个专属测试文件):CurrentUserId 401 对齐 2、SyncConfig 双向同步语义 4(下发 / 反向拉取 / ??= 不覆盖既有委托 / 无 BLL 空操作)、ShareDbContext null 守卫与委托分发 2、AuthHeaderValue 委托透传 1;全量 681 / 0 失败 / 2 跳过,0 警告

v10 登记(本批识别,未动)

  • Domain 侧 GetCurrentUserId 委托 AsyncLocal 化(对齐 BLL 并行分支隔离能力,属性形态不变源码兼容)
  • SyncConfig 漏配诊断(Domain 与全部 BLL 均无委托时 WARN)
  • BaseDomain.Cache / Logger 异常类型 ANE → InvalidOperationException(宿主 DI 配置缺失语义更准)
  • Domain 事务编排助手(自动开事务 → 共享 DbContext → 提交补缓存失效 → 异常回滚)
  • CacheManager 三个调参属性(VersionLocalCacheTtl / MaxThrottleEntries / LogThrottleWindow)init 化(与 MyDbContextFactory 可变旋钮 init 化同批)

9.22.0

2026年9月11日 星期五

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 9.22.0

加固 — MyDbContextFactory 时间源统一(TimeProvider 注入)

  • 构造函数补尾参 TimeProvider? timeProvider = null(源码兼容,与 BaseDAL / WebApiBaseBLL / CacheManager 注入口径一致),故障摘除冷却期计时(IsInCooldown / MarkReadOnlyFailed)由 DateTimeOffset.UtcNow 改为注入 TimeProvider.GetUtcNow()
var fakeTime = new FakeTimeProvider(start);
var factory = new MyDbContextFactory<TestDbContext>(config, "EFCore_Test", timeProvider: fakeTime);
fakeTime.Advance(TimeSpan.FromSeconds(31));   // 冷却期精确推进,测试无需真实等待

变更 — 工厂配置错误异常类型统一归 InvalidOperationException

  • 配置节缺失/为空、只读配置缺失、只读配置项为空、读写配置缺失:原分别抛 ArgumentNullException(中文消息误作 paramName)/ 裸 Exception / ArgumentNullException,统一改 InvalidOperationException——配置状态错误与构造参数错误语义分型(真参数错误仍归 ArgumentNullException,见下节)
  • 兼容性:catch (ArgumentNullException)(或更宽的 catch (ArgumentException))/ catch (Exception) 中按异常类型分支的消费者需同步;MyDbContextFactory 为可选组件,未使用多副本轮询工厂的消费者行为零影响

加固 — 构造参数守卫与重复连接名检测

  • configuration 为 null 抛 ArgumentNullException(原 NRE);connectionName 空白抛 ArgumentException(原拖到 CreateDbContext 才报「未找到连接名称为 的…」)
  • 重复连接名:Config.Options.Add 撞键的 ArgumentException(.NET 默认消息含 Key,但为英文泛化文案且类型语义不符)改为指明重复 Name 与配置节的 InvalidOperationException

文档 — 启动期配置边界 / 良性竞态

  • Config 属性:构造一次性加载,属启动期配置(单例注册下运行时修改产生跨请求竞态)
  • TryValidateReadOnly remarks:成功路径 TryRemove 与并发标记写入的良性竞态(下一次探活自愈,无需加锁治理)
  • IsConnectionFaultInvalidOperationException 宽口径说明(仅作用于探活路径,可覆写 TryValidateReadOnly 精确化)

测试

  • 本批 +6(冷却期 FakeTimeProvider 计时、读写配置缺失、配置节缺失、构造参数守卫 ×2、重复连接名);存量 1 处异常类型断言同步;全量 672 / 0 失败 / 2 跳过,0 警告

升级注意(9.21.0 → 9.22.0)

  • 二进制需重编译MyDbContextFactory 构造签名新增可选尾参 timeProvider(源码兼容,调用代码不用改;同 v9.9.0 / v9.15.0 / v9.21.0 口径)
  • 异常类型变化仅影响工厂配置错误路径(见上);其余场景行为逐位一致

9.21.0

2026年9月12日 星期五

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 9.21.0

新功能 — BLL 全家族 CancellationToken 透传(评审遗留「另行批次」收口)

  • WebApiBaseBLL 全部非 params、非过时公开 async 方法补尾参 CancellationToken ct = default,透传至 DAL 既有 ct 管道与缓存管道(GetOrAddAsync(ct) / InvalidateCacheAsync(ct)),覆盖:SaveChanges、详情族(GetAsync / GetByIdAsync / FindAsync / 六视角方法)、列表/分页、统计聚合族、Kvp、Cascade、Create(单条/批量/GetOrCreate/TryCreate)、批量更新(UpdateModelsAsync / ExecuteUpdateAsync)、状态切换族(Toggle/Set enable-audit-delete、BatchSetAuditStatus、CanTransit)、删除/软删族(DeleteModelListAsync / ExecuteDeleteAsync / SoftDeleteAsync / ResetSoftDeleteAsync 等)
  • BLL 接口同步:IDbBLL / IDeleteBLL / IDetailBLL / ICreateBLL / IUpdateBLL / IListBLL / IStatisticsBLL / IKeyValuePairBLL + 视角接口 IGetDetails / IGetForEditor / IGetListItem / IGetSimple
  • 边界(对齐 DAL 侧口径):params 形态更新(UpdateModelAsync / UpdateSelectedPropsAsync / UpdateIgnoreAsync)与过时方法不补(C# 无法在 params 前插可选参数 / 随 v10 淘汰);GetOrCreateAsync 与批量分批的事务生命周期 API(Begin/Commit/Rollback)不透传,仅数据操作透传;TotalAsync() 零参 ApiResult 包装不补 ct——与全可选参数的 4 参 TotalAsync 并存时零参调用依赖「全部参数均有实参者优先」规则唯一命中,本重载若补 ct 将致零参调用二义(CS0121)
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var page   = await bll.GetOffsetPageAsync(query, page: 1, cts.Token);   // 查询可取消
var (code, model) = await bll.DeleteModelAsync(id, ct: cts.Token);      // 删除保存管道可取消

修复 — 内存路径 owner 校验 NRE(500 → 403)

  • DeleteModelAsync / ValidateUpdateCoreCreatedBy.Equals(CurrentUserId) 对 CreatedBy 为 null 的历史/种子数据抛 NRE → 500;改 EqualityComparer<TUserKey>.Default.Equals,null CreatedBy 按 Forbidden(403) 处理——与 SQL 路径(BuildSelfOnlyCondition 翻译后 NULL 比较不命中行)语义对齐
bll.VerifyOwner();
var (code, _) = await bll.DeleteModelAsync(id);   // CreatedBy = null 的行:403 Forbidden(此前 NRE → 500)

变更 — CurrentUserId 委托未设置归 401(原 400)

  • 委托未设置时抛 UnauthorizedAccessException(原 ArgumentNullException):经全局异常映射 401「未授权访问」而非 400「请求参数错误」,对齐 v9.16.0 Controller 侧 claim 缺失/转换失败归 401 的决策;消息指明需经 SetCurrentUser / 注入 ICurrentUser<TUserKey> 提供

过时 — Cache 属性垫片(v10 移除)

  • WebApiBaseBLL.Cache 标记 [Obsolete](warning 级):与 CacheManager 属性暴露同一实例,迁移至 CacheManager

文档 — AsyncLocal 流语义 / ID 生成优先级

  • IsVerifyOwner / GetCurrentUserId remarks 写明 AsyncLocal 语义:向下游传播、不向上游回流(方法内设置返回调用方即失效,须同一调用流内 bll.VerifyOwner().DeleteAsync(id))、并行分支(Parallel/Task.WhenAll)隔离、置位后无自动复位
  • SetCurrentUser null 委托静默 no-op;CheckAndSetStringId 优先级 IToolkit.GenerateId(构造注入)> GenerateId 委托

测试

  • 本批 +5(SaveChangesAsync / DeleteModelAsync ct 透传、DeleteModelAsync / UpdateAsync null CreatedBy → Forbidden、CurrentUserId 未设置 → UnauthorizedAccessException);存量 229 处测试调用点补 TestContext.Current.CancellationToken(xUnit1051),下游 DAL mock Setup/Verify 表达式同步实际令牌;全量 666 / 0 失败 / 2 跳过,0 警告

升级注意(9.20.0 → 9.21.0)

  • 二进制需重编译:BLL 方法签名新增可选尾参——源码兼容(调用代码不用改),旧编译产物引用 9.21.0 运行时会 MissingMethodException(与 v9.9.0 / v9.15.0 同口径)
  • 覆写 virtual GetByIdAsync(TKey, bool?, CachePolicy?) 的子类:覆写签名需同步补 ct 尾参(否则 CS0115)——本版唯一改签名的虚方法
  • catch (ArgumentNullException) 包住 CurrentUserId 未设置场景:异常类型已改为 UnauthorizedAccessException
  • ct 全家族为 opt-in:不传令牌运行行为与 9.20.0 逐位一致;Controller 端点尚未透传 RequestAborted,Web 链路取消行为不变

9.20.0

2026年9月12日 星期五

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 9.20.0

新功能 — SetDeleteStatusAsync 删除状态 set 语义方法(路由评审 v10 候选 ②)

  • WebApiBaseBLL.SetDeleteStatusAsync(id, isDeleted, selfOnly = false)IDeleteBLL 接口同步):精确设置 ISoftDelete.IsDeleted 目标值(幂等,区别于 ToggleDeleteStatusAsync 的取反语义),不写入/清理 DeletedBy / DeletedAt 审计字段——与切换版行为逐位一致,迁移安全
  • 语义矩阵:完整软删除(盖章 DeletedBy / DeletedAt)用 SoftDeleteAsync,完全恢复(清理审计字段)用 ResetSoftDeleteAsync,仅翻转标志用本方法
  • 配合 Controller 层 PATCH {id}/deleted 端点(独立开关),替代动词入路径的遗留 DELETE {id}/delete toggle 端点(v10 移除)
var result = await bll.SetDeleteStatusAsync(id, isDeleted: true);
// result.Code == ResultCode.NoContent;重复调用结果一致(幂等),审计字段不动

新功能 — 删除外键冲突 409 分类(BaseDAL 设计评审 P2-3)

  • WebApiBaseBLL 新增 protected virtual IsForeignKeyViolation(Exception):与 IsUniqueViolation 同风格的消息特征判定(SQLite FOREIGN KEY constraint failed / SQL Server conflicted with the REFERENCE constraint / PostgreSQL violates foreign key constraint / MySQL a foreign key constraint fails),子类可覆写补充第三方 provider(Firebird / Oracle / Dameng 等)
  • DeleteModelAsync 写入路径与 DeleteModelListAsync 硬删分支捕获外键冲突返回 Conflict(409)(Warn 日志 + 跟踪状态清理);DeleteAsync / DeleteListAsync / DeleteModelsAsync 经委托链自动获得——行为变更:走 BLL 的删除外键冲突从 500(冒泡全局异常处理器)→ 409;直接用 DAL 的消费者行为不变(异常仍冒泡)
  • BaseDAL.DeleteAsync 软删/硬删两分支补 catch (DbUpdateException) { ChangeTracker.Clear(); throw; }(对齐 Create 通道):写入失败后跟踪器不再残留,分类由上层负责
  • 实现要点ExecuteDeleteAsync / ExecuteUpdateAsync 为立即执行 API,不经 SaveChanges 管道——失败直接冒 provider 原生 DbException(无 DbUpdateException 包装);判定签名因此取 Exception(消息解包对两种形态均工作),批量路径 catch System.Data.Common.DbException(BCL 基类,零 provider 包依赖),连接故障等非约束异常因消息特征不匹配照常冒泡
// 硬删除仍被子表引用的行:409 Conflict(此前为 500)
var result = await bll.DeleteAsync(id);   // result.Code == ResultCode.Conflict

新功能 — WhereNotDeleted 软删除过滤扩展(P2-4')

  • EFCoreExt.WhereNotDeleted 四重载(IQueryable<T> / KeysetQuery / OffsetQuery / ListQuery),与 WhereAuditStatus 完全对称:仅保留 ISoftDelete.IsDeleted == false
  • 背景:库不内置全局查询过滤器HasQueryFilter),软删除后的行对所有读取可见(GetAsync / FindAsync / 列表查询均不自动排除)——需要隐藏已删行时在查询端显式叠加本扩展
var list = await bll.GetListAsync(
    ListQuery<Topic, string, TopicDto>.Create(rows: 50)
        .WhereNotDeleted()                       // 排除已软删行
        .Where(keyword.HasValue(), n => EF.Functions.Like(n.Title, $"{keyword}%")));

加固 — BaseDAL 设计评审 P1/P2 收口

  • 活动事务守卫:SetDb(bool) 读/写切换分支在释放旧上下文前检查 Database.CurrentTransaction,存在未提交事务即抛 InvalidOperationException——旧行为切换会 Dispose 上下文并静默回滚其上的活动事务,调用方无感知直至提交失败;库内既有路径不受影响(写方法先 SetWriteDb 后开事务),拦截面为消费者在自有事务块内显式切换读写库
  • 共享实例重绑守卫:SetDb(TDb) 重绑不同实例时经 GuardSharedDbContext 拦截(旧行为会 Dispose 旧的外部共享上下文,破坏跨 BLL 事务编排);入口补 ObjectDisposedException.ThrowIf 与类级契约对齐
  • GetListPageAsync 页码收敛:page = Math.Max(page, 1)——负/零页码的 Skip 跨 provider 行为不一致(InMemory/SQLite 静默当首页,SQL Server/PostgreSQL SQL 错误 500),与 GetOffsetPageAsync 对齐
  • DAL 全量 CancellationToken 透传:26 个非过时公开 async 方法补尾参 ct = default(Create 单条/批量、批量 Update、ExecuteUpdate、Delete 单条/条件/ExecuteDelete、Detail、First、Statistics 聚合族、SaveChangesAsync),6 个 DAL 接口签名同步,令牌逐层透传至 SaveChangesAsync(ct) / FindAsync([id], ct) / Execute*Async(…, ct);源码兼容(可选参数置尾),二进制需重编译。边界:3 个 params 形态更新方法因 C# 无法在 params 前插入可选参数暂不补(内部 CancellationToken.None,remarks 注明随 v10 签名重构补齐);过时方法(GetKvpsAsync / UpdateListAsync)不补

文档 — 软删除可见性语义明示与 P3 风格收尾

  • DeleteAsync / DeleteListAsync XML remarks 明示软删除契约:无全局查询过滤器、已删行对所有读取可见、批量软删(服务端 ExecuteUpdate)对命中行含已软删行无条件覆盖 DeletedAt 且行数含已删行(与单条 ??= 保留预置值语义不同)、重复软删幂等
  • SetDb(TDb?) remarks 注明与私有 SetDb(bool) 的双语义分工(直接指派外部实例 vs 按读写模式切换);ExecuteDeleteAsync remarks 注明纯值方法不分类口径(与 ExecuteUpdateAsync 同),需 409 语义时改用 DeleteModelListAsync
  • P3 风格收尾:"查询able" 笔误 ×3 修正为"查询数据源"、SetTrackAll() 多空格、SetReadDbIfNull 判空统一 is null_maxPageSize = 100 提取 DefaultMaxPageSize 常量、过时 GetKvpsAsync 注释修正(keySelector.Compile() 经 funcletizer 提升后为客户端逐行求值,非服务端投影)

测试:本批 +47(BaseDAL 加固:事务守卫 6、共享重绑守卫 4、页码收敛 3、取消透传 9;FK 409 分类 6、WhereNotDeleted 5;删除状态 set 语义批次:SetDeleteStatusTests 7、PatchToggleEndpointTests 7);全量 661 / 0 失败 / 2 跳过(口径含并行 Controller 批次)。测试基建注意:SQLite 外键默认关闭,FK 测试需连接串 Foreign Keys=True 且 FK 显式 DeleteBehavior.Restrict(EF required 关系默认 Cascade 会吞掉冲突场景);SQLite 对照组实体须实现 IDelete<string> 才命中 SoftDeleteAsync 盖章分支(仅 ISoftDelete 时只设 IsDeleted)


9.15.0

2026年9月12日 星期五

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 9.15.0

  • 无功能性变更,仅同步版本号(CacheEnable 缓存开关 Options 化——AddEFCoreCacheEnable() 集中配置、控制器构造链 CacheEnableOptions 可选参数透传、RegisterCacheEnable 标 Obsolete——均为 Controller 层变更,Core 契约未变)

9.14.0

2026年9月11日 星期四

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 9.14.0

  • 无功能性变更,仅同步版本号(MethodsEnable 端点开关治理——Options/DI 集中配置 AddEFCoreMethodsEnable()、启动期属性路由冲突检测 fail-fast、生效性自检等——均为 Controller 层变更,Core 透明;Common 层仅 CacheEnable.Clone() 防御性快照,Core 契约未变)

9.13.0

2026年9月2日 星期三

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 9.13.0

新功能 — 缓存滑动过期时间可配置化(EFCoreCacheOptions 全局配置 + 四层默认策略优先级)

  • 新增 EFCoreCacheOptions(Common 层纯 POCO,零新依赖):DefaultPolicy 默认缓存策略(滑动/绝对/ExpirationJitter/CacheNull 四属性整体)+ 四个进程级调参——RefreshThrottleFraction 续期节流比例(默认 0.5,使用处 clamp 至 [0.05, 1.0])、VersionLocalCacheTtl(默认 5s)、MaxThrottleEntries(默认 50k)、LogThrottleWindow(默认 30s)
  • 配置入口:AddEFCoreCache 双重载(委托配置(services.AddEFCoreCache(o => o.DefaultPolicy = new CachePolicy { SlidingExpiration = TimeSpan.FromHours(1) }))与 appsettings 节绑定(services.AddEFCoreCache(builder.Configuration.GetSection(EFCoreCacheOptions.ConfigSection)),节名常量 ConfigSection = "Crping.EFCore.Cache")),或两入口一站式绑定(AddEFCoreRedisCache(conn, section[, configure]) / AddEFCoreMemoryCache([section]),Redis 版未注册 IDistributedCache 时以同一连接串统一补注册);section.Get<T>() 为整体替换语义,节内未写的属性取类型默认值,配置节应给出完整内容
  • 默认策略四层优先级:方法 policy 参数 > BLL protected virtual CachePolicy? DefaultCachePolicy(按模型覆写,base 返回 null 跟随全局)> 全局配置 DefaultPolicy > 静态 CachePolicy.Default 兜底(滑动 4h + 绝对 12h 不变)——未配置时行为与旧版完全一致

appsettings 配置示例(TimeSpan"hh:mm:ss",Configuration Binder 原生支持):

"Crping.EFCore.Cache": {
  "DefaultPolicy": {
    "SlidingExpiration": "01:00:00",
    "AbsoluteExpirationRelativeToNow": "12:00:00",
    "CacheNull": false,
    "ExpirationJitter": "00:30:00"
  },
  "RefreshThrottleFraction": 0.5,
  "VersionLocalCacheTtl": "00:00:05",
  "MaxThrottleEntries": 50000,
  "LogThrottleWindow": "00:00:30"
}

消费者按模型覆写示例(低频字典数据长滑动,其余模型跟随全局):

public class DictBLL<TDb, TDict, TKey, TUserKey> : WebApiBaseBLL<...>
{
    protected override CachePolicy? DefaultCachePolicy => new()
    {
        SlidingExpiration = TimeSpan.FromHours(24),
        AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(48),
    };
}
  • 设计说明:不给 AddEFCoreRedisCacheAction<EFCoreCacheOptions> 重载(与既有 Action<RedisCacheOptions> 重载对无类型 lambda 产生 CS0121 二义),配置节绑定用 IConfigurationSection 参数(命名类型无二义);AddEFCoreRedisCache / AddEFCoreMemoryCache 内置 TryAddSingleton(new EFCoreCacheOptions()) 空注册兜底(无显式配置时类型默认值,行为与未配置一致);AddEFCoreCache 双重载与两入口的节绑定参数均为显式配置(Replace 语义,后到优先)——移除既有注册后注册新实例,注册顺序无关,无「配置节被空配置抢占静默失效」陷阱;Redis 一站式重载的 cacheOptions 参数为必填(可选时 configure: null 命名实参会与 2 参重载二义),位置传 null 仍会 CS0121(用 2 参重载或命名参数规避);CacheManager 主构造追加置尾可选参数 EFCoreCacheOptions? options = null(源码兼容,DI 自动注入)
  • CachePolicy.Default 静态兜底保持提交版 4h/12h(撤销工作区 4h→1h 本地修改),1h 等定制诉求由配置承载

修复 — 缓存命中续期与写入口径统一

  • GetOrAddAsync / GetAsync 入口解析 effective 策略一次,写入(ToEntryOptions)与命中续期(RefreshIfSlidingSafeAsync)共用同一口径——修复「policy=null 时写入兜底带滑动、命中却不续期」的不一致:投影详情 / Kvp / 直连 GetAsync 等传 null 的高频键原本过期不续期,现按默认策略链续期(节流后每键每实例约每滑动 TTL × RefreshThrottleFraction 一次往返)
  • 续期节流窗口由硬编码「滑动 TTL / 2」改为 × RefreshThrottleFraction 可配置;双空过期兜底(仅 CacheNull 的策略)改取全局配置 DefaultPolicy 的绝对过期,无配置时仍为静态 12h
  • 行为变更仅此一处(即本修复本身):API 零破坏(全部新参数可选置尾、新方法独立、ICacheManager 接口契约不变),未配置时默认值与提交版行为完全一致

测试:基线 507 → 525(+18:四层优先级 3 例、#5 续期口径 2 例、节流比例 0.25 与 clamp 2 例、调参属性读回与类型默认值 2 例、双空兜底取配置 1 例、DefaultCachePolicyBLLTests 虚属性传导 3 例、AddEFCoreCache 注册 5 例;原「无滑动不续期」场景改经 options 保留)


9.12.0

2026年8月31日 星期一

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 9.12.0

修复 — 缓存取消语义收窄(None 令牌下的传输层取消降级告警)

  • CacheManager 降级告警的取消语义收窄:仅当调用方令牌可取消(CancellationToken.CanBeCanceled,即调用方真实参与取消语义)时,观察到的取消(OperationCanceledException)向上传播;CancellationToken.None 调用下观察到的取消必然不是调用方发起的,属存储传输层故障,与「存储不可用」同策略降级告警,不再演变为调用方异常
  • 真实案例:后台任务(ct=None)CreateModelsAsync 落库成功后递增模型版本号,SE.Redis 连接瞬断把飞行中的 INCR 命令以取消状态完成(数据面 API 无取消令牌,堆栈中无 SE.Redis 帧),TaskCanceledException 顺「CacheManager → WebApiBaseBLL → 调用方」整条异步栈刷 Error——数据已入库、仅缓存失效未完成(下次写入或 TTL 兜底),无害但噪音大且具误导性
  • 收口点:CacheManager 六处降级 catch(缓存读/写/续期/删除、版本号读/递增)与 WebApiBaseBLL.InvalidateCacheAsync 统一改经 CacheManager.IsCallerCancellation(ex, ct) 判定;可取消令牌下的取消行为不变(照常传播),FlushAsync 等取消感知调用方不受影响(其登记动作以 CancellationToken.None 执行,传输层取消现已在 CacheManager 内部降级)
  • 影响:无 API 签名变化;仅 CancellationToken.None 调用下原本向上抛的传输层取消改为降级告警(限频策略不变)

测试:基线 504 → 507(+3:None 令牌 INCR 取消降级告警 1 例、None 令牌缓存读取取消降级回源 1 例、可取消令牌 INCR 取消照常传播 1 例)


9.11.0

2026年8月31日 星期一

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 9.11.0

加固 — 缓存键按需叶子求值 + 键长守卫(防误用巨型键)

  • CacheKeys 闭包捕获按需取值:表达式键对根部为闭包实例的成员访问链(如 video.AwemeId)仅求值叶子成员,不再整体序列化捕获对象——条件表达式捕获实体/DTO(误用,API 层无法禁止)时,键只含实际参与比较的值。真实案例:抖音搜索任务循环 ExistAsync(n => n.AwemeId == video.AwemeId, true),旧实现每个 AwemeInfo 对象(含 rawdata 整条原始 JSON)整体序列化入键,单键数十~上百 KB 且每条视频一键,GUI Redis 客户端打开键库即溢出
  • 链上逐级反射取值,中途失败(getter 抛异常等)返回上级已取值,最差退化类型全名(与旧实现一致);命中叶子即停止下钻,内层链与闭包常量不重复入键。键长大幅缩短,且"同值异例"请求互相命中(旧实现每对象一键,命中率≈0);对基元值捕获键格式不变,旧键由 TTL 兜底自然过渡
  • CacheKeys.FormatCapture 捕获段上限 128 字符:超长字符串/大集合捕获以 #sha256前32位(len) 指纹替代原文,键长有界且异值异键;新增公共 CacheKeys.Fingerprint 指纹工具
  • CacheManager.BuildKeyAsync 键长守卫:完整键超 2048 字符时后缀整体指纹化({modelKey}:v{version}:#hash(len))——异常覆写、巨型表达式等病态场景兜底,防巨型键撑爆 Redis 键空间与客户端枚举;守卫位于默认实现内,子类覆写键构建需自行保证键长
  • 对详情/列表等大投影缓存的影响面校准:改动只触碰键构建,缓存值(详情/列表的大 JSON)完全不受影响;键长阈值经实测校准——21 字段投影的详情键实测 619 字符(投影指纹为 Expression.ToString() 随字段数线性增长),阈值定为 2048,正常大投影键(约 0.6~1.5KB)原样保留,仅病态键(>2KB)指纹化;受影响的键(表达式捕获引用类型场景、原超 2048 的键)升级后首次访问一次性回源重写,旧键 TTL 兜底清理

测试:基线 497 → 504(+7:捕获实体叶子求值 1 例、同值异例同键 1 例、超长捕获指纹 1 例、求值失败回退 1 例、键长守卫指纹化 1 例、正常键不受影响 1 例、21 字段大投影键校准回归 1 例;旧「整对象 JSON 入键」断言随行为修订)


9.10.0

2026年8月30日 星期日

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 9.10.0

新增 — BLL/DAL 虚化扩展点(模板方法位,零行为变更)

  • BLL 创建/更新/所有权钩子:PrepareForCreate / CheckAndSetStringId / BuildSelfOnlyConditionprotected virtualValidateUpdatePreCheck / ValidateUpdateCore(原 private,与创建侧 protected 不对称)→ protected virtual——子类可挂默认值填充、状态机约束、租户/所有权语义定制
  • 缓存钩子:ModelCacheKeyprotected virtual(多应用共用 Redis 时追加应用级后缀 / 按租户分版本空间);InvalidateCacheAsyncprotected virtual(关联模型联动失效,事务延迟失效逻辑保留在 base)
  • 冲突判定:IsUniqueViolation / IsTrackingConflict(原 private static)→ protected virtual(第三方 provider 冲突消息特征补充)
  • 读取钩子:GetByIdAsync(TKey, bool?, CachePolicy?) 实体版 → public virtual(叠加二级本地缓存 / 缓存前附加 Include)
  • DAL:新增 AdditionalSpecialProperties 组合式扩展点(更新时自定义审计/只读属性被忽略,与内置映射合并);SetNoTracking / SetTrackAllpublic virtual
  • 基础设施:CacheManager.BuildKeyAsyncprotected virtual(键格式定制:多租户前缀 / Redis hashtag);MyDbContextFactory.TryValidateReadOnlyprotected virtual(探活故障分类定制)

修复 — 缓存正确性与探活

  • CacheManager.InvalidateAsync 本地版本号回写改 AddOrUpdate 单调保护:并发 INCR 下先返回旧新版本的线程不再覆盖已回写的更高版本,消除「版本号倒退 → ≤5s 窗口拼旧键读写前缓存」;与 GetVersionAsync 同保护
  • MyDbContextFactory 探活异常分类增强:AggregateException 自动扁平化解包,识别 TimeoutException(连接超时)与 InvalidOperationException(连接池耗尽)——此前仅按 DbException 分类,上述故障被误判「副本可用」得不到摘除;探活连接即开即关归还连接池
  • UseDefaultTime 默认 SQL getDate()SYSDATETIMEOFFSET():datetimeoffset 列精确匹配(服务器本地时间 + 偏移、100ns 精度),消除 datetime 隐式转换与精度截断;仅影响新生成迁移,既有数据库不变

测试:基线 464 → 497(+33:虚化扩展点覆写生效 14 例、探活异常分类 6 例、时间源 3 例、缓存版本号单调守卫 1 例、EFCoreExt 6 例、UseDefaultTime 默认值 1 例、BuildKeyAsync 覆写 1 例、Controller 覆写 1 例)


9.9.0

2026年8月29日 星期六

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 9.9.0

修复 — 分页信息与取消令牌

  • GetListPageAsync RecordCount 修复:改为分页前先 CountAsync(返回总数),不再返回当前页行数——多页数据分页信息错误;补多页 RecordCount 测试
  • GetListPageAsync 全链路补 CancellationToken(DAL / IListDAL / IListBLL / BLL 委托透传),与分页三态(Offset / Keyset)对齐;旧 API 签名以可选参数扩展,源码兼容

加固 — 并发与可观测性

  • PendingCacheInvalidations 补失效失败不再静默:新增静态 ILogger 注入(AddEFCoreRedisCache / AddEFCoreMemoryCache 注册时自动绑定),未注册 Logging 时降级 Debug 输出
  • BaseDAL.Dispose / DisposeAsyncInterlocked.Exchange 原子防重入,并发释放无竞态

优化 — 防御与文档

  • WebApiBaseBLL.SetDb 类型检查:传入的 DbContext 与 TDb 泛型参数不匹配时抛语义化 ArgumentException(含期望/实际类型),替代运行时 InvalidCastException
  • Db getter XML 文档写全副作用:首次访问自动初始化读库 + 写后读绑定消费请求写标志 + Dispose 后抛 ObjectDisposedException

(测试基线 461 → 464:GetListPageAsync 多页 RecordCount 2 例、SetDb 类型不匹配 1 例)


9.8.0

2026年8月28日 星期五

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 9.8.0

优化 — 缓存键命名空间前缀修订

  • 业务键前缀 crping:crping_efcore:,版本号键前缀 crping:ver:cache_ver:RedisCacheVersionStore 内部拼装)——crping:ver:crping:... 双段冗余可读性差,修订后 Redis 中一眼可辨用途;旧键由版本号 + TTL 兜底自然过渡

新功能 — 内存模式 DI 补齐

  • AddEFCoreMemoryCache() 消费者未注册 IDistributedCache 时自动补注册内存实现 MemoryDistributedCache(TryAdd 语义),一行注册即可解析 ICacheManager

加固 — 缓存健壮性与可观测性

  • CacheManager.GetAsync 未命中计入 Miss 统计(含版本号存储不可用降级路径),修复命中率分母缺失虚高
  • AddEFCoreRedisCache 连接强制 abortConnect=false:首次解析时 Redis 暂不可用不再抛异常打挂请求(后台自动重连),缓存故障不影响主流程
  • CacheKeys 表达式指纹健壮化:闭包捕获值固定文化格式化(跨文化部署键稳定)+ 引用类型 JSON 序列化区分实例内容(默认 ToString 仅返回类型名,不同实例会同键串数据)
  • RedisCacheVersionStore 发起调用前响应取消令牌(SE.Redis 数据面 API 无 CT 重载,飞行中调用无法中断)
  • 版本号本地缓存单调回写:GetVersionAsyncInvalidateAsync 并发时不再把新版本号倒退回旧值;VersionLocalCacheTtl 文档明示多实例部署的陈旧窗口(≤5s)与 useCache: false 写后读指引

加固二 — 并发与可观测性收尾

  • 单飞改分片锁:_locks 字典 + 50k 阈值 O(n) 清理替换为 1024 分片信号量(按键哈希取片),固定内存不随键数量增长,同键单飞语义不变
  • 续期节流字典加固:改只读字段 + 就地移除 + 超限概率触发;时间源从 Environment.TickCount64 换为注入 TimeProvider(FakeTimeProvider 测试可控,与版本号本地缓存一致)
  • 降级告警限频:新增 LogThrottleWindow(默认 30s),缓存读/写/续期/删除与版本号读/递增 6 类事件窗口内各只记一条 Warning,防 Redis 故障期间日志洪泛
  • 反序列化异常放宽:TryParseCached 从仅捕获 JsonException 放宽到全部非取消异常,一律按未命中回源重写
  • CacheEnable.GetSwitch 预编译 getter 委托,热路径零反射
  • 投影键新增类型段(detail:{id}:{条件}:{投影}:{投影类型}:{升序} 等):同结构签名但成员类型不同的投影不再同键串数据

修复 — 缓存失效时机与事务一致性

  • 失效早于事务提交修复:InvalidateCacheAsync 事务感知——处于活动外层事务(Domain 编排 / GetOrCreateAsync / TryCreateAsync 等)时,失效延迟登记(新增 PendingCacheInvalidationsConditionalWeakTable 弱引用键无泄漏),由新增扩展 transaction.CommitAndInvalidateCacheAsync() 或 BLL 自管事务提交点补失效——消除"提交前 INCR → 并发读者用新版本号缓存未提交旧数据,提交后旧数据以当前版本存活至 TTL(默认 12h)"的时序缺陷;回滚时登记随事务丢弃,忘记用扩展等同旧行为(TTL 兜底)不回归
  • 取消语义对齐:InvalidateCacheAsync 新增 CancellationToken 透传,任务取消(OperationCanceledException)向上传播(与"仅取消不降级"原则一致);事务延迟补失效路径用 CancellationToken.None 保护已提交事务的补失效不被中途取消打断

修复 — 缓存键策略与统计

  • 防垃圾键:CachePolicy 滑动与绝对过期双空时(如仅设 CacheNull)兜底 CachePolicy.Default 绝对过期(12h),避免 Redis 键永不过期、版本失效后残留为永久垃圾
  • GetOrAddAsync 版本号存储不可用降级路径计入 Miss 统计(与 GetAsync 口径一致,命中率分母不再缺失)
  • 投影详情新增 4 参 GetByIdAsync(id, options/selector, useCache, policy) 重载(保留 3 参接口实现,向后兼容):投影缓存策略可定制,与实体版对齐

加固 — 并发与文档

  • CacheEnableRegistry(Controller)_configs 由普通 Dictionary 改为 ConcurrentDictionary:不同控制器静态构造函数并发注册 + 请求线程并发读取不再有损坏风险
  • CacheKeys.BuildExpressionKey 表达式结构签名 CWT 记忆化(ConditionalWeakTable<Expression, string> 弱引用无泄漏):共享静态选择器(DetailsSelector / KvpsSelector 等)热路径免每请求 ToString();闭包捕获值仍逐请求提取(捕获值随请求变化,不可记忆化)
  • 多应用共享 Redis 隔离指引:crping_efcore: / cache_ver: 前缀只隔离业务自有键,不隔离其他 Crping.EFCore 应用;多应用共用同一 Redis 且模型 FullName 相同时会互相失效/串缓存,应给各应用设置不同 RedisCacheOptions.InstanceName 或为模型键追加应用级后缀
  • MaxThrottleEntries 注释明示软上限语义:1 小时内热键基数超限时续期节流字典可短时超限(有界于热键基数,误删无害)

(测试基线 439 → 461:AddEFCoreMemoryCache DI 2 例、GetAsync 统计 2 例、CacheKeys 指纹 4 例 + 投影类型 2 例、版本号存储 2 例、DegradeTests 3 例;本批 CacheTransactionInvalidationTests 事务感知失效 6 例、CacheTests 防垃圾键兜底 TTL 1 例,并删除临时探针 TempRecordCountProbeTests 1 例)


9.7.0

2026年8月26日 星期三

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 9.7.0

优化 — 缓存键可读性与集中构建

  • 缓存键命名空间统一加 crping: 前缀,版本号键独立为 crping:ver: 前缀(RedisCacheVersionStore 内部拼装)——Redis 中一眼可辨用途、与业务自有键隔离(此前模型段为裸模型全名,版本号键与业务键混在同一个库难辨)
  • 缓存键格式收敛至 CacheKeys 单一构建器:Detail / DetailProjected / Kvp / Total / Count / Exist / Cascade 具名方法 + 前缀常量——方法名即用途,4 个 partial 文件与测试中的 "detail:" 等魔法字符串全部消除,格式单一出处、防拼错

修复 — TotalAsync 时间敏感统计陈旧窗口

  • 缓存键新增统计日期段 total:{lastDays}:{yyyyMMdd}:跨天自动换键,时间敏感统计不再因滑动续期而长期陈旧(原键无日期,持续访问永不自然过期)

新功能 — 缓存防穿透与 DI 统一注册

  • GetByIdAsync(id, useCache, policy) 新增 CachePolicy? 重载(保留原两参重载实现 IDetailBLL,源码兼容):设 CacheNull=true 可缓存"未找到"的空值防穿透,配合模型版本号失效机制,实体后续创建后立即可见
  • AddEFCoreRedisCache(configuration, configure) 新增重载:同一连接串统一注册业务数据存储 IDistributedCache(TryAdd 语义,不覆盖消费者已注册存储),避免双轨配置不一致导致缓存失效静默失效
  • Core 新增包引用 Microsoft.Extensions.Caching.StackExchangeRedis(中央包管理统一版本)

(测试基线 437 → 439:新增 Total 跨天换键 1 例、GetByIdAsync 防穿透 2 例、DI 统一注册/TryAdd 语义 2 例)


9.6.1

2026年8月24日 星期日

修复 — DeleteModelListAsync 所有权验证条件构建崩溃

  • DeleteModelListAsync 开启 IsVerifyOwner 时,构建表达式树调用 typeof(TUserKey).GetMethod(nameof(object.Equals))TUserKey = string 场景下匹配到多个 Equals 重载(Equals(string) / Equals(object)),抛出 AmbiguousMatchException → 500
  • 修复为 GetMethod(nameof(object.Equals), new[] { typeof(object) }) 指定参数类型,消除歧义

(测试基线 333 → 430:新增 97 例补充 BLL 层未覆盖方法的单元测试——Detail/Kvp/List/Update/Delete/Statistics/Cascade/BaseMethods 八大类)


9.6.0

2026年8月24日 星期一

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 9.6.0

新功能 — 接口补齐

  • IKeyValuePairBLL 新增 GetCascadeAsyncbatchMode 重载(6 参数:keySelector/showZero/zeroKvp/options/batchMode/useCache),支持选择递归或批量加载模式

其他

  • IListBLL 补充 XML 文档注释(接口概述、Query 属性、SetMaxPageSize 参数、简单形态 GetListAsync

9.5.0

2026年8月21日 星期五

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 9.5.0

优化 — EntityHelper.GenerateId 采用 Snowflake 式位布局

  • 运行时 ID 由"同 Tick 计数器直接叠加在时间戳数值上"改为位域拼接:时间位(相对基准时间,4096 Tick ≈ 0.4ms 为一块)+ 12 位序列位互不重叠——同一时间块内多次调用的唯一性由构造保证(旧实现存在"计数器偏移渗漏进后续时间块"的理论碰撞窗口)
  • 序列溢出时自旋等待进入下一时间块(Snowflake tilNextMillis);时钟回拨时沿用上次时间块仅递增序列,避免重新生成已产出的值
  • 生成格式不变:12 字符、62 进制时间戳编码(前导零补齐),与旧版视觉一致;fixedTime 路径(种子数据 / ToStringId)输出与旧版逐字符一致
  • 内部重构:Utils 编码逻辑收敛为共享 Encode 核心,删除 CustomTimestampByTicks 死代码

异常处理与诊断日志

  • BaseDAL / MyDbContextFactory 构造函数新增可选 ILogger 参数(源码兼容,不注入则无日志):DAL 层 Create / Update / Delete 并发冲突与 NotFound 路径记录 LogWarning;工厂只读副本探活遇非连接类异常记录 LogDebug(不再静默吞掉)
  • WebApiBaseBLL.CreateModelAsync 创建异常分类:仅唯一键 / 主键冲突(UNIQUE constraint / duplicate key / Duplicate entry 消息特征)返回 Conflict(409),其余写入异常透传全局异常处理器——不再一律 409 掩盖真实错误;新增跟踪冲突兜底(同主键的另一实例已在跟踪器中)同样返回 Conflict
  • BaseDAL.CreateAsync 单实体与批量:DbUpdateExceptionChangeTracker.Clear() 再重新抛出——失败不再残留跟踪状态污染共享 DbContext(批量路径此前无任何异常处理)
  • GetOrCreateAsync 系列仅成功结果(Ok / Created)提交事务,Conflict 等失败结果回滚——统一跨数据库行为(SQL Server / PostgreSQL 语句失败后事务不可提交,SQLite / MySQL 可继续提交造成部分写入)
  • 共享 DbContext 编排约定:事务回滚不重置跟踪器,编排器应在回滚后统一 ChangeTracker.Clear()(见 Demo TopicDomdocs/EFCore问题与优化建议.md

(测试基线 298 → 302:新增 GenerateIdTests 4 例——顺序 5000 / 并发 50000 无重复、fixedTime 确定性、ToStringId 格式)

(测试基线 302 → 306:新增 CreateModelAsync 异常分类 2 例、GetOrCreateAsync 失败回滚 1 例、跟踪冲突兜底 1 例)

2026年8月22日 星期六

新功能 — 简单形态方法族

  • GetListAsync(rows, orderBy, ascending, condition, selector) — 不分页列表简单形态(rows 0 表示不限制),与 ListQuery 构建器同管道
  • GetByIdsAsync(ids, orderBy?, ascending?, condition?, selector?) — 按主键集合批量查询(ids 转 Contains 过滤条件),空集合直接返回空列表不访问 DAL
  • GetOffsetPageAsync(page, pageSize, orderBy?, ascending?, name?, condition?, selector?, ct) — 偏移分页简单形态,与构建器形态重载共存,排序名回显 PaginationInfo.OrderBy
  • ExistAsync(TKey id, bool? useCache) — 按主键存在性检查,复用 exist: 缓存键

新功能 — 批量写方法族(全原子分批事务)

  • CreateModelsAsync(models, chunkSize = 1000)(原 CreateModelListAsync 更名)— 批量创建:超 chunkSize 分批、整体包显式事务,唯一键冲突整批回滚返回 Conflict(409)
  • UpdateModelsAsync(models, chunkSize = 1000) — 批量更新:按主键整行更新,自动忽略审计/软删除特殊属性;任一主键不存在整批回滚返回 NotFound(404),唯一键冲突整批回滚返回 Conflict(409)(新增 BaseDAL.UpdateAsync(IEnumerable<TModel>) 批量原语)
  • DeleteModelsAsync(ids, isSoft = false) — 按主键集合批量删除,委托 DeleteModelListAsync(复用所有者验证 / 软删 / 缓存失效)

修复 — 审计字段泛型化与批量软删对齐

  • BaseDAL._specialPropertyMap 键改为泛型类型定义 ICreate<> / IDelete<>:任意键类型实体的 CreatedBy / DeletedBy / DeletedAt 在更新时自动忽略(原仅 int 键实体的非泛型接口命中,字符串键实体需手动 ignoreProps——单条更新同受影响)
  • DeleteModelListAsync 批量软删外层检查由非泛型 IDelete 改为 ISoftDeleteIDelete<TUserKey> 实体批量软删补全 DeletedBy / DeletedAt(原仅置 IsDeleted,与单条 SoftDeleteAsync 对齐)

Demo 示例

  • TopicBLL.GetListSimpleAsync(简单形态列表,GET test/Topic/list/simple)与 BatchCreateTopicsAsync(批量创建 + 唯一键冲突整批回滚演示,POST test/Topic/Batch/Create),新增 TopicBatchCreateDto

(测试基线 306 → 333:新增简单列表族 8 例、批量创建 8 例、批量更新/删除 9 例、ExistAsync(id) 2 例)


9.4.0

2026年8月18日 星期二

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 9.4.0

新功能 — IAuditStatus 审核状态(枚举三态)

  • EFCoreExt.WhereAuditStatus 条件式审核状态过滤:IQueryable / KeysetQuery / OffsetQuery / ListQuery 四重载,status 为 null 时跳过过滤(语义等同条件式 Where)
  • IUpdateBLL 新增三个方法(IAuditStatus 专用,运行时能力守卫):
    • SetAuditStatusAsync(id, status, selfOnly, enforceTransit) — 精确设置目标状态
    • BatchSetAuditStatusAsync(ids, status, enforceTransit) — 服务端批量设置(ExecuteUpdateAsync),返回实际受影响行数
    • CanTransitAsync(id, targetStatus) — 判断是否允许流转到目标状态(UI 预判)
  • IStatisticsBLL 新增 CountByStatusAsync(status = null) — 按审核状态统计记录数,null 统计全部
  • 流转规则提取为 protected virtual IsAuditTransitAllowed(current, target):禁止同状态重复流转;Approved / Rejected 为终态不可再流转;子类可覆写定制规则
  • 写入路径提供流转校验开关(enforceTransit,默认 false 允许任意跳变,与库既有 Toggle 系列行为一致;传 true 时强制校验):单条非法流转返回 Conflict(409);批量在 SQL 层追加来源状态约束防并发竞态,无合法来源状态(如目标为初始态 Pending)直接返回 0 且不执行更新
  • MaxBatchUpdateSize 批量更新上限(默认 1000,可配置,接口暴露):超限返回 BadRequest,防止 IN 子句参数超出数据库参数上限
  • BaseDAL._specialPropertyMap 挂接 IAuditStatus(更新/忽略审计字段走统一字典映射)

(测试基线 297 → 298:AuditStatusTests 共 22 例——正常设置 / NotFound / BadRequest / selfOnly 所有权、强制校验 Conflict、默认不校验跳变、批量无合法来源返回 0、批量超上限、CanTransit 流转矩阵、CountByStatus)


9.3.0

2026年8月18日 星期二

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 9.3.0

修复 — GetOrCreateAsync 嵌套事务冲突

  • GetOrCreateAsync / TryCreateAsync 系列方法内部调用 BeginTransactionAsync() 开启新事务,当调用方已开启事务时(如 Domain 层 ShareDbContext + 多 BLL 编排场景)会抛出 InvalidOperationException: The connection is already in a transaction
  • 修复方案:检测 Database.CurrentTransaction,已有活动事务时直接执行核心逻辑(GetOrCreateCoreAsync),复用调用方事务而非嵌套;无活动事务时行为不变
  • 影响范围:所有使用 GetOrCreateAsync 的 Domain 编排场景(如 CommunityDom.CreateAsync 中调用 cityBLL.GetOrCreateAsync
  • 向后兼容:无活动事务时行为与 9.2.0 完全一致

9.2.0

2026年8月18日 星期一

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 9.2.0

新功能 — ResultCode 新增 HTTP 5xx 状态码枚举

  • ResultCode 枚举新增五个服务器端状态码:
    • InternalServerError = 500 — 服务器内部错误
    • NotImplemented = 501 — 未实现的功能
    • BadGateway = 502 — 网关错误
    • ServiceUnavailable = 503 — 服务不可用
    • GatewayTimeout = 504 — 网关超时

新功能 — ApiResult.InternalError 工厂方法

  • ApiResult 新增两个静态工厂,用于构造 500 服务器内部错误结果:
    • InternalError(string? error = null) — 无数据载荷(字符串消息落 Error 槽)
    • InternalError<TData>(TData? data) — 携带数据载荷(泛型参数由 data 自动推断)
  • StatusCode 工厂的区别:InternalError 语义明确(500 错误),StatusCode 为通用逃生舱(任意状态码)

(测试基线 271 → 275:新增 ApiResultTests 4 例——InternalError 基本/带错误消息/带数据载荷/ToActionResult 映射)


9.1.2

2026年8月16日 星期日

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 9.1.2

新功能 — ApiResult.StatusCode 透传工厂(枚举之外的逃生舱)

  • ApiResult 新增两个静态工厂,透传 ResultCode 未覆盖的任意 HTTP 状态码(如 422 / 429 / 418):
    • StatusCode(int statusCode, string? error = null) — 无数据载荷(字符串消息落 Error 槽)
    • StatusCode<TData>(int statusCode, TData? data) — 携带数据载荷(泛型参数由 data 自动推断,须显式传状态码——单参数调用无法推断泛型)
  • 命名说明:工厂名取 StatusCode 而非 Code(记录结构体主构造参数 Code 已生成同名属性,Code 会 CS0102 编译冲突)
  • 状态码经 ToActionResult 兜底分支映射为 StatusCodeResult / ObjectResult,须在 100~599 范围内(ASP.NET Core 对越界值抛异常)

新功能 — ToActionResult 兜底分支携带响应体

  • ResultCode 未覆盖的状态码由裸 StatusCodeResult(丢弃载荷)改为:非泛型取 Error、泛型 Error ?? Data 回退,有载荷走 ObjectResult 设状态码、无载荷走 StatusCodeResult——与 400 / 404 / 409 分支行为对齐

优化 — OffsetQuery 排序键返回类型收窄(CA1859)

  • ResolveSort / BuildSortKeys 返回类型与 _sortKeys 缓存字段由 IReadOnlyList<OffsetSortKey<TModel>> 收窄为 List<OffsetSortKey<TModel>>(internal,公共 API 不变),与 Keyset 侧 KeysetSortPlan 具体类做法对齐,消除 CA1859 接口调用开销警告

(测试基线 264 → 271:新增 ApiResultTests 7 例——429/422 透传、Error 槽、无载荷 StatusCodeResult、带载荷 ObjectResult、隐式提升)


9.1.1

2026年8月16日 星期日

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 9.1.1

新功能 — ApiResult 错误/创建工厂带数据重载

  • ApiResult 新增四个与 Ok<TData>(data) 同构的泛型工厂:Created<TData>(data) / BadRequest<TData>(data) / NotFound<TData>(data) / Conflict<TData>(data)(泛型参数由 data 自动推断)——错误结果可携带数据载荷(如校验失败对象、未找到的标识),泛型 ToActionResultError ?? Data 回退输出
  • 字符串实参解析规则:BadRequest("msg") / NotFound("msg") 等字符串消息在恒等转换平局下非泛型重载胜出,自然落 Error 槽;字符串数据载荷(如 NotFound("id-001"))同样落 Error 槽,响应体经 Error ?? Data 回退后输出一致

破坏性变更 — 错误参数收窄为 string?(9.x 未发布,无兼容负担)

  • BadRequest / NotFound / Conflict 的错误参数由 object? 收窄为 string?Forbidden / Unauthorized 新增 string? error = null
  • 迁移指南:传入字符串错误消息的调用点零改动;传入非字符串对象(旧版落 Error 槽)的调用点需改为带数据重载 ApiResult.BadRequest(obj)(落 Data 槽,响应体输出一致)或 new ApiResult(ResultCode.BadRequest, obj)

新功能 — ToActionResult 错误响应体

  • Unauthorized(error) → 401 + UnauthorizedObjectResultForbidden(error) → 403 + ObjectResult(ASP.NET Core 无 ForbidObjectResult)
  • 泛型 ToActionResult 的 400 / 404 / 409 分支由 Error ?? Data 回退,错误消息与数据载荷均可渲染进响应体

(测试基线 264:新增 ApiResultTests 18 例——带数据工厂槽位 / 字符串落 Error 槽 / Error 优先级 / 201、400、401、403、404、409 响应体映射)


9.1.0

2026年8月15日 星期六

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 9.1.0

新功能 — ApiResult 静态工厂族(结果构造声明式化)

  • ApiResult(非泛型)新增与 ResultCode 一一对应的静态工厂:Ok() / Ok<TData>(data) / Created() / NoContent() / BadRequest(error) / Unauthorized() / Forbidden() / NotFound(error) / Conflict(error)
  • ApiResult<TData> 新增隐式提升转换(ApiResult → ApiResult<TData>):无数据载荷结果在泛型上下文中可直接 return(如方法返回 Task<ApiResult<object>> 时写 return ApiResult.NotFound();
  • 全部固定 ResultCode 调用点(约 30 处)迁移:new ApiResult<X>(ResultCode.Ok, data)ApiResult.Ok(data)new ApiResult(ResultCode.NotFound)ApiResult.NotFound();动态 code 透传(new ApiResult(code) 等 11 处)保持原样
  • 非破坏性增量:现有 new ApiResult(...) 构造写法全部兼容

新功能 — EFCoreExt.WhereLike 条件式前缀模糊查询

  • 新增 WhereLike 扩展(IQueryable<T> + KeysetQuery / OffsetQuery / ListQuery 四重载):.WhereLike(keyword, n => n.Field) 一行替代 .Where(keyword.HasValue(), n => EF.Functions.Like(n.Field, $"{keyword}%")) 组合;关键词为空时跳过过滤(语义等同条件式 Where)
  • Demo 9 处模糊查询全部迁移(TopicBLL / MemberBLL / CategoryBLL / ModuleBLL / TopicImageBLL)

(测试基线 245 不变:纯新增 API 与调用点等价改写)


9.0.0

2026年8月15日 星期六

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 9.0.0

破坏性变更 — BLL 去 MVC 依赖(分层解耦)

  • 新增 ApiResult / ApiResult<TData>ResultCode + 数据 + 错误):BLL 全部 Task<IActionResult> 方法改为返回 ApiResult(约 20 个方法 / 13 个接口),响应构造彻底移出 BLL
  • WebApiResponse / IWebApiResponse 响应构造迁至 Crping.EFCore.Controller 包;WebApiBaseBLL / BaseDomain 不再继承 WebApiResponseIWebApiBaseBLL / IDomain 不再继承 IWebApiResponse
  • DatabaseExtensionsIApplicationBuilder 扩展)迁至 Controller 包
  • 删除 Microsoft.AspNetCore.App FrameworkReference,改引 Microsoft.Extensions.Configuration.BinderMyDbContextFactoryGetSection(...).Get<T>() 依赖)
  • Controller 层新增 ApiResultExtensions.ToActionResult() 统一映射(ResultCodeIActionResult),WebApiBaseController 端点改为 (await BLL.Xxx(...)).ToActionResult()

破坏性变更 — 写后读绑定静态开关 Options 化

  • ReadWriteRouting.BindReadToWrite 静态属性移除,改为 ReadWriteRoutingOptionssealed classbool BindReadToWrite,默认关);BaseDAL 构造函数新增可选参数 ReadWriteRoutingOptions? options = null
  • 开启方式:builder.Services.AddSingleton(new ReadWriteRoutingOptions { BindReadToWrite = true }) + app.UseReadWriteRouting()RequestWritten AsyncLocal 与复位中间件保持不变)

优化

  • CacheManager.VersionLocalCacheTtl / MaxThrottleEntriesstatic 改实例属性(CacheManager 单例,语义等价)
  • MyDbContextFactory 提供 protected virtual CreateWriteInstance / CreateReadOnlyInstance 钩子(派生类可定制上下文创建,如注入拦截器/日志)
  • MyDbContextFactory 新增 Func<string, TDb>? CreateInstance 委托:复用 DbContextOptionsEnableRetryOnFailure / 拦截器 / 日志(默认走无参构造 + OnConfiguring 属性注入路径,向后兼容);探活 TryValidateReadOnly 改传连接串(委托路径下 ConnectionString 属性可能为空)
  • CacheManager 单飞锁竞态修复:去掉每次 Release 后立即 Remove 的竞态(同 key 双锁导致单飞失效),改为锁常驻 + 超 MaxLockEntries(默认 5 万)阈值批量清理空闲锁

破坏性变更 — 删除过时类型与兼容层(v9.0.0 未发布,无兼容负担)

  • 删除 BLL 别名类 WebApiForLongBLL / WebApiForIntBLL / IWebApiForLongBLL / IWebApiForIntBLL(旧 "For" 前缀命名)与转发类 EntityConfigure
  • 删除旧接口 IEnabled / IDisabled 的兼容逻辑(BaseDAL._specialPropertyMap 条目与 ToggleEnableStatusAsync 分支,统一 IIsEnabled / IIsDisabled);删除 ResolveSort 默认实现的冗余 "id" 分支

(测试基线 245:分层解耦后 Update/Create/Delete/StatusToggle/CacheEnable/ExceptionMiddleware 等测试的 IActionResult 断言迁移为 ApiResult 断言,用例数不变)


8.11.0

2026年8月13日 星期四

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 8.11.0

破坏性变更 — 依赖升级:Microsoft.AspNetCore.Mvc.Core → 框架引用

  • Microsoft.AspNetCore.Mvc.Core 由 2.3.11 包引用改为 <FrameworkReference Include="Microsoft.AspNetCore.App" />(该包 2.3.12 后停更,netcoreapp3.0+ 的 MVC 类型由共享框架提供)
  • 根除传递依赖 Newtonsoft.Json 9.0.1 高危漏洞(GHSA-5crp-9r3c-p9vr);同时移除未使用的 Newtonsoft.Json 直接引用与 Directory.Packages.props 一批死钉版(Mvc / Mvc.Abstractions / Mvc.ViewFeatures / Http 等 2.x)

新功能 — 写后读绑定请求边界复位

  • 新增 ReadWriteRoutingMiddleware + UseReadWriteRouting():开启 ReadWriteRouting.BindReadToWrite 时须在请求管道注册,请求进入 / 结束双重复位写标志,防跨请求残留(此前 ResetRequestWritten 无中间件接线,非 ASP.NET Core 场景 AsyncLocal 写标志会跨请求残留)

修复

  • ExecuteProjectionAsyncselectFunc(客户端投影)分支:由 exp.Select(n => selectFunc(n))(委托无法翻译,运行抛 InvalidOperationException)改为先 ToListAsync 物化再内存投影

优化

  • CacheManager 注入 TimeProvider(版本号本地缓存 TTL 改用 GetUtcNow,可测、与工厂冷却期语义一致)
  • MyDbContextFactory 连接名缺失改 TryGetValue + 友好 InvalidOperationException(原字典索引器抛裸 KeyNotFoundException
  • WebApiBaseBLL.Db / Query 移除冗余判空;MaxPageSize 魔法数提取常量,SetMaxPageSize 标记 [Obsolete](推荐 MaxPageSize 属性 / 链式方法)
  • MyDbContextFactory 设计取舍(运行时副本选择 vs DI 能力)与静态开关"启动时设置、运行期勿改"文档

(测试基线 241 → 245:新增中间件复位 3 例、SelectFunc 客户端投影 1 例)


8.10.0

2026年8月13日 星期四

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 8.10.0

破坏性变更 — 列表 API 三态对齐(分页三态 + ListQuery 不分页)

新功能 — ListQuery 构建器(不分页列表)

  • 新增 ListQuery<TModel, TKey, TResult> 查询构建器(与 KeysetQuery / OffsetQuery 同构):Create(rows) / Top / Where / Where(when, ...) / OrderBy / Select / Apply / TagWith
  • 新增 GetListAsync(ListQuery)(DAL / BLL / 接口三层):不分页列表查询,未声明排序键默认 Id 降序,rows 正数按 MaxPageSize 收敛、0 表示不限制;排序处理与 Keyset / Offset 共用同一缓存路径(按名 + 表达式指纹)
  • Demo 示例:TopicBLL.GetListByQueryAsync(标题前缀 + 软删过滤 + 投影)→ GET /api/Test/Topic/list/query

破坏性变更 — 分页三态收敛

  • 旧分页 GetListAsync(page, pageSize, options) 更名为 GetListPageAsync(DAL / BLL / 接口,返回 ListPage<TResult> 两段式契约不变)——分页三态:GetListPageAsync(ListPage 契约)/ GetKeysetPageAsync / GetOffsetPageAsync
  • 删除(未正式使用、无兼容负担):分页版 GetRawListAsync(page, pageSize, options)(与 GetListPageAsync 重复)、不分页 GetRawListAsync(options / condition 两个重载)、同步 GetRawList(两个重载)、同步 GetList(元组版)、obsolete 同步 FirstOrDefault
  • 旧方法族内部迁移:级联 / 键值对 / 详情 / 统计改走 GetListAsync(ListQuery.FromOptions(...))——ListQuery.FromOptions 为内部适配器,以 IQueryOptions 为参数的方法族(GetCascadeAsync / GetKvpsAsync / GetByIdAsync 等)公共签名不变

Keyset 游标格式定稿(破坏性,未发布无兼容约束)

  • 枚举游标值仅存底层数值(移除内嵌 AssemblyQualifiedName 类型名——游标更短、解码不再有 Type.GetType 类型加载路径)
  • KeysetCursor.Decode 移除双参数重载,expectedTypes 必填(按期望类型还原值,键值数量/类型强校验,无 legacy 纯格式解码路径)
  • Prev 方向 + 空游标按首页处理(修复前会反转排序、返回排序末端的"最后一页")

Keyset / Offset 性能优化

  • 排序方案缓存到 query 实例(Lazy + 线程安全发布,首次解析后复用——翻页不再重复指纹拼接 / 结果类型反射校验)
  • 任意表达式排序应用器编译委托按表达式指纹缓存(KeysetRuntimeOptions.MaxExpressionAppliers,默认 1024,超限退化为每次编译)
  • 投影 DTO 值读取器缓存容量上限(KeysetRuntimeOptions.MaxDtoGetters,默认 1024,消除动态投影类型的内存膨胀)
  • 新增 KeysetRuntimeOptions 静态类统一承载引擎级缓存上限配置
  • Offset 排序复用 ApplyOrderKey(与 Keyset 同路径:表达式剥壳统一 + 按名 / 表达式指纹双缓存);IsEntityIdKey 提取为共享判断(Keyset / Offset 兜底逻辑收敛)

修复

  • 级联树递归模式:调用方 Condition 从"被覆盖丢弃"修正为每层生效(与注释语义及批量模式一致)
  • CA2208:KeysetQuery 两处 ArgumentExceptionparamName 由属性名改为不传(null)

(测试基线 227 → 241:新增 SortPlan 缓存 3 例、游标格式 3 例、Prev 空游标 1 例、ListQuery 集成 3 例)


8.9.0

2026年8月12日 星期三

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 8.9.0

新功能 — ResolveSort 排序键解析虚方法

  • WebApiBaseBLL 新增 protected virtual ResolveSort(orderBy):返回 (Expression<Func<TModel, object?>> Selector, string Name),默认按主键 Id 排序;子类覆写 switch 表达式声明"字段名 → (排序表达式, 排序键名)"(Name 支持点分路径),未知名 / null 回退 _ 兜底分支——消除子类 Keyset / Offset 查询重复的排序分支 switch
  • Demo TopicBLL 覆写(7 键),Offset GetListAsync 与 Keyset GetKeysetListAsync 共用 ResolveSort;Offset 排序表达式统一为 n.Stats == null ? 0 : ... 0 兜底(与 Keyset 及投影 null 兜底语义一致,避免 NULL 排序行为差异)
  • Selector 用 object?KeysetQuery.OrderBy 形参天然一致(Keyset 路径零转换);QueryOptions.OrderByobject 形参)赋值处用双重转换对齐 NRT 注解(运行时类型相同)
  • 测试:新增 SortSelectorTests 4 例(基线 227 → 231)

8.8.0

2026年8月11日 星期二

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 8.8.0

新功能 — Keyset 排序键支持点分路径(嵌套 DTO 提取排序值)

  • KeysetSortKey.GetValueFrom / ResolveSort 结果类型校验支持点分路径:排序键名可写为 "Stats.Heat",游标编码按点分路径逐段从投影 DTO 的嵌套对象内提取排序值(无需再把排序字段平铺到 DTO 顶层,响应可保留内嵌层级)
  • 用法:query.OrderBy(n => n.Stats == null ? 0 : n.Stats.Heat, name: "Stats.Heat").Select(n => new { ..., Stats = n.Stats == null ? new { Heat = 0, ... } : new { n.Stats.Heat, ... } })——排序分支与投影的 null 兜底语义须一致(SQL 与游标值不漂移);嵌套对象为 null 时游标编码抛带指引的 ArgumentException(不裸抛 NullReferenceException
  • Demo 已迁移:TopicBLL.GetKeysetListAsync 返回的 items 内嵌 stats 对象({ items: [{ ..., stats: { heat, likeCount, ... } }], pageInfo }

修复 — bool 排序键谓词构建崩溃

  • KeysetPredicateBuilder.BuildCompare 对 bool 与枚举对称处理:转 int 比较(false=0 < true=1),修复按 bool 字段排序时携带游标翻页(第 2 页起)抛 InvalidOperationExceptionGreaterThan 未定义于 bool)→ 500 的问题

重构 — OrderBy 全能化(合并并删除 OrderByExpr)

  • KeysetQuery / OffsetQueryOrderBy 升级为任意表达式排序:直接属性访问(n => n.CreatedAt)仍走按名缓存快路径(指纹格式不变,既有游标兼容);导航属性(n => n.Author.Name)/计算字段(n => n.Price * n.Quantity)走表达式重建路径(指纹附加结构签名 + 闭包捕获值)
  • OrderBy 新增可选 name 参数(与旧 OrderByExpr 一致):缺省依次取直接属性名 → 成员链最末成员名(n.Author.Name"Name")→ 表达式结构签名;投影 DTO 场景显式传名,排序字段位于嵌套对象内时用点分路径("Stats.Heat"
  • OrderByExpr删除(外部未使用,无兼容负担),调用处直接改名 OrderBy 即可;顺带移除 KeysetExpression.GetMemberName(旧 OrderBy 的直接属性强校验方法,升级后为死代码)
  • 工具类可见性收窄:KeysetSortKey / KeysetExpression / KeysetPredicateBuilder 降为 internal(外部零引用,缩小公共 API 面)

测试(基线 220 → 227)

  • 嵌套 DTO 点分路径翻页 + Prev、嵌套类型缺字段静态校验、嵌套对象 null 指引、TResult=object 匿名内嵌投影翻页、内嵌 + 顶层混合双键排序、bool 排序键翻页 + Prev、字符串排序键(string.CompareTo)多页翻页;原 19 例表达式排序集成测试(Keyset 15 + Offset 4)改调 OrderBy 后全部通过

8.7.0

2026年8月11日 星期二

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 8.7.0

破坏性变更 — 分页结果契约属性顺序调整与 required 强化

  • KeysetPage<T> / OffsetPage<T>PageInfo / Pagination 属性移至 Items 之前
  • 两个属性均改为 required init(移除默认值 = new()= []),调用方构造时必须显式提供所有属性
  • 迁移指南:JSON 反序列化顺序可能变化,手动构造对象时需调整属性顺序或使用命名参数

8.6.0

2026年8月10日 星期一

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 8.6.0

新功能 — 任意表达式排序(OrderByExpr)

  • KeysetQuery / OffsetQuery 新增 OrderByExpr(selector, ascending, name):支持按导航属性n => n.Author.Name)与计算字段n => n.Price * n.Quantity)排序,突破 OrderBy 的直接属性访问限制(OrderBy 约束不变)
  • 排序键名缺省时依次取:直接属性名 → 成员链最末成员名(n.Author.Name"Name")→ 表达式结构签名;投影 DTO 场景建议显式传 name(须与 DTO 属性一致,游标编码按名提取排序值)
  • 游标指纹对任意表达式附加表达式结构签名 + 闭包捕获值:排序表达式(含闭包值)变化后旧游标抛 ArgumentException(控制器边界映射 400),不静默错页;直接属性排序指纹格式不变,旧游标兼容
  • 主键 Id 唯一性兜底判断收窄为"实体自身主键"(修复 OrderByExpr(n => n.Author.Id) 派生名恰为 "Id" 时误判已兜底的隐患);KeysetSortPlan 排序值类型改从表达式体类型推导
  • 实体结果 + 导航属性排序需先 .Apply(q => q.Include(...)) 加载导航,或改用投影 DTO;未加载或排序值本身为 null 时,游标编码抛带指引的 ArgumentException(不再裸抛 NullReferenceException
  • 新增 KeysetExpression 表达式辅助:TryGetMemberName / ResolveSortKeyName / UnwrapBody / ReplaceParameter;直接属性排序仍走按名缓存快路径,仅任意表达式排序走表达式重建路径

破坏性变更 — 旧分页列表契约两段式(ListPage)

  • Common 新增 ListPage<T>(sealed、init-only):{ required Pagination Pagination, required List<T> Items }
  • GetListAsync(DAL / BLL / 接口)返回由 (Pagination, List<TResult>) 元组改为 ListPage<TResult>;BLL 版由返回 IActionResultPaginationOk 包装)改为直接返回 ListPage<TResult>
  • 分页版 GetRawListAsync(page, pageSize, options)(BLL / 接口)返回由 (Pagination, List<TResult>) 改为 ListPage<TResult>;不分页版 GetRawListAsync(options / condition, rows) 返回 List<T> 不变
  • 上述方法移除 [Obsolete](旧分页 API 以 ListPage 契约保留,不再是过时方法);同步版 GetList / GetRawList 仍标记 [Obsolete]
  • WebApiResponse 新增 PaginationOk<TResult>(ListPage<TResult>) 重载;BaseDAL.List.cs / WebApiBaseBLL.List.cs region 更名(旧分页列表分页列表旧不分页列表不分页列表
  • 破坏性:公开 API 返回类型变更,调用方需迁移(元组解构、PaginationOk(pagination, list) 包装改为直接消费 ListPage

8.5.0

2026年8月9日 星期日

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 8.5.0

重构(行为不变)

  • WebApiBaseBLL.Cascade 级联树构建方法 BuildCascadeTree / BuildChildrenFromDict 改 static(无实例状态依赖),NullableKeyWrapper 改主构造函数(readonly struct 保留)

代码整理(行为不变)

  • WebApiBaseBLL.List.cs / BaseDAL.List.cs region 重组:Keyset 分页 / Offset 分页拆为独立 region,旧分页列表与不分页列表 region 更名(旧分页列表 / 旧不分页列表
  • BaseDAL.Keyset.cs / BaseDAL.Offset.cs 分页结果对象初始化顺序调整(Items 赋值移至 PageInfo / Pagination 之后),无逻辑变化

缓存体系重构(破坏性 — useCache 三态化)

  • useCache 参数 bool → bool? 三态语义:null(默认)不缓存 / true 强制缓存 / false 强制回源;源码兼容(bool 隐式转 bool?),二进制需重编译
    • 门面方法(GetAsync / GetKvpsAsync(defaultName) / TotalAsync):useCache ?? CacheEnabled(端点开关)——端点开关控制端点路径,调用方显式传参可覆盖开关
    • 共享方法(GetByIdAsync 实体版/投影版、GetRawKvpsAsync 等):只响应调用方显式传入的三态参数,不再查询任何开关——自定义业务方法显式传 useCache: true/false 自由控制缓存,与端点开关互不干扰
    • ⚠️ 行为变化:直接调用共享方法(不传 useCache)从"默认缓存"变为"默认不缓存"
  • 新增端点级缓存开关 CacheEnable(Common):9 个开关默认全关(GetDetailsAsync / GetAsync / GetDetailsForAdminAsync / GetForEditorAsync / GetForAdminEditorAsync / GetSimpleAsync / GetListItemAsync / GetKvpsAsync / GetTotalAsync),按控制器类型 RegisterCacheEnable<TController>() 注册并由构造函数同步到 BLL;删除原方法级开关 GetByIdAsync / GetRawKvpsAsync(11 → 9)
  • 缓存默认关闭:注入 ICacheManager 后读方法默认也不缓存,需显式开端点开关(端点路径)或传 useCache: true(自定义方法)

缓存方法扩展

  • GetCascadeAsync(级联树)新增三态缓存:键含加载模式(batch/recursive)+ keySelector/Condition/OrderBy/Selector 表达式指纹 + 排序/标签;QueryFunc/SelectFunc 为委托无法指纹化,非 null 时强制回源(防串数据)
  • CountAsync / ExistAsync / AnyAsync 新增三态缓存:count:{表达式指纹} / exist:{表达式指纹} 键;ExistAsyncAnyAsync 语义等价共享缓存键互相命中

缓存健壮性增强

  • 故障降级:缓存/版本号存储的读写异常不影响主流程——读失败按未命中回源、写失败仅记录警告、值反序列化失败回源重写、版本号存储不可用无法拼键直接回源;仅 OperationCanceledException 向上传播(取消不降级)
  • ICacheManager.GetAsync<T> 新增 CachePolicy? policy 可选参数:命中时续期滑动过期(与 GetOrAddAsync 行为统一,修复纯读不续期导致高频键过期的问题)
  • 续期节流:命中续期窗口 = 滑动 TTL 的 1/2,高 QPS 热键减少 Redis 往返(默认滑动 4h 最多每 2h 续一次);CacheManager.MaxThrottleEntries(默认 50k)超过后过滤重建清理,误删无害(下次多续一次)
  • 删除 WebApiBaseBLL 中与 CachePolicy.Default 重复定义的 DetailCachePolicy(值完全相同),详情投影缓存统一使用 CachePolicy.Default

8.4.0

2026年8月8日 星期六

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 8.4.0

新功能 — TimeProvider 时间源注入(可测试)

  • WebApiBaseBLL / BaseDAL 主构造函数新增可选参数 TimeProvider? timeProvider = null(默认 TimeProvider.System,与 DateTimeOffset.Now 行为一致)
  • 新增 protected TimeProvider TimeProvider 属性,BLL / DAL 子类可直接使用统一时间源
  • 审计时间字段(CreatedAt / UpdatedAt / DeletedAt)与统计过滤(TotalAsync lastDays)共 10 处 DateTimeOffset.Now 全部改由注入的 TimeProvider 提供
  • 测试可注入 FakeTimeProviderMicrosoft.Extensions.TimeProvider.Testing 包)精确控制时间、断言审计字段;DI 注册 builder.Services.AddSingleton(TimeProvider.System) 后自动注入
  • 源码兼容:可选参数,现有构造调用(位置 / 命名 / DI)零改动,未注册 TimeProvider 时行为不变;升级需重新编译(构造签名变化,二进制不兼容)

8.3.0

2026年8月8日 星期六

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 8.3.0

新功能 — DatabaseExtensions

  • 新增 DatabaseExtensionsMigrate<TDb>() / EnsureCreated<TDb>(isDelete)Crping.EFCore.Controller 包的 Utils 类迁入(命名空间改为 Crping.EFCore),消除 Controller 包承载 EF 工具方法的不合理依赖

8.1.0

2026年8月8日 星期六

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 8.1.0

破坏性变更 — 分页结果契约两段式

  • GetOffsetPageAsync 返回 OffsetPage<T>{ items, pagination }GetKeysetPageAsync 返回 KeysetPage<T>{ items, pageInfo }(顶层扁平属性移除,调用方改用 page.Pagination.X / page.PageInfo.X
  • Demo BLL 6 处 PaginationOk(pageResult.ToPagination(), pageResult.Items) 手动转换删除,直接 Ok(pageResult) 返回两段式契约

8.0.0

2026年8月7日 星期五

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 8.0.0

破坏性变更 — ConnectionName 只读化

  • IMyDbContextFactory.ConnectionName 由可变属性改为 { get; init; }(构造后不可修改)
  • 一个工厂实例对应一个连接目标:运行时修改连接名的代码须改为构造传参(new MyDbContextFactory<TDb>(configuration, "连接名"))或每连接一个工厂实例
  • 目的:消除 Singleton 工厂跨请求竞态(多租户数据隔离);Config.Options 字典只读查询,不再有运行时切换路径

新功能 — 缓存方法级开关(useCache)

  • 所有自动走缓存的读方法新增 bool useCache = true 可选参数,传 false 强制回源查询,实现"全局缓存配置 + 个别方法不缓存":
    • GetByIdAsync(id) / FindAsync(id) / GetModelAsync(id) / GetAsync(id)(详情)
    • GetByIdAsync(id, options/selector)(投影详情系列)
    • TotalAsync(lastDays, policy)(统计)
    • GetRawKvpsAsync / GetKvpsAsync(键值对)
  • 默认 true,现有调用零改动(source 兼容);TotalAsync() 无参重载保持无参签名,避免与 TotalAsync(int=0,...) 形成无参调用歧义

修复

  • 共享 DbContext 守卫收窄:仅拦截"会 Dispose 共享实例的切换"(读库切换仍抛异常),共享后 SetWriteDb() 空操作路径放行——修复跨 BLL 事务编排(ShareDbContext + 多 BLL 写方法)被误拦截的问题,新增 DomainTransactionTests 集成测试复现验证

缓存扩展重命名

  • AddCrpingRedisCacheAddEFCoreRedisCacheAddCrpingMemoryCacheAddEFCoreMemoryCache:方法名前缀与库名对齐(Crping.EFCoreEFCore);AddEFCoreRedisCache 改为惰性连接(首次解析 IConnectionMultiplexer 时才 ConnectionMultiplexer.Connect,避免注册即急切连接导致应用启动失败),并补充空配置参数校验

7.7.0

2026年8月7日 星期五

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 7.7.0

读写分离健壮性(2026-08-07)

  • DAL 写操作强制走写库:Create / Update / Delete 全部写方法入口调用 SetWriteDb()(18 处),解决"先读后写"场景(_currentReadOnly=true)下写操作落到只读副本的隐患;共享实例(_currentReadOnly=false)下为空操作,不 Dispose
  • 共享 DbContext 切换守卫:共享模式下(_isSharingDbContext)切换到会 Dispose 共享实例的 DbContext 时抛明确异常,防止跨 BLL 事务中误释放共享实例
  • 只读副本故障摘除:新增 ReadOnlyFailureCooldown(默认 30 秒冷却)+ ValidateReadOnlyOnCreate(默认开启探活)——副本连接失败进入冷却期轮询跳过,到期自动重新尝试恢复;仅连接类异常(DbException)摘除,其他异常原样抛出;单副本走快速路径不探活
  • 多从库轮询计数器溢出修复Interlocked.Increment 结果用 uint 取模,避免计数器回绕到 int.MinValueMath.Abs 溢出为负数导致负索引
  • 写后读绑定(read-your-writes):新增 ReadWriteRouting.BindReadToWrite 静态开关(默认 false,不改变现有负载分布);开启后请求内发生写操作则后续读自动绑定主库,直到请求结束(AsyncLocal 隔离请求);⚠️ 开启后必须在请求管道注册 app.UseReadWriteRouting()ReadWriteRoutingMiddleware,请求开始/结束复位写标志),否则写标志在非 ASP.NET Core 场景(后台服务 / 长生命周期作用域)会跨请求残留

破坏性变更 — 统一缓存管理(全新设计,替换旧缓存实现)

  • 新增 ICacheManager 统一缓存门面(Common 层,无 EF 依赖):泛型 GetOrAddAsync<T> / GetAsync<T> / SetAsync<T> / RemoveAsync / InvalidateAsync,内置 JSON 序列化、单飞防击穿、空值防穿透
  • 模型版本号失效:每个模型一个版本号(ICacheVersionStore),缓存键携带版本号({FullName}:v{n}:detail:{id});写操作(Create / Update / Delete)成功后只递增版本号,该模型全部缓存键原子性失效,无需枚举键,旧键由 TTL 兜底清理
  • 版本号存储双实现:RedisCacheVersionStore(StackExchange.Redis INCR 原子递增,分布式全局生效)、MemoryCacheVersionStoreInterlocked,单机/测试)
  • 注入方式变更(破坏性)WebApiBaseBLL / BaseDomain / 所有 Controller 基类的构造参数由 IDistributedCache? 替换为 ICacheManager?IKit.Cache / ICacheBLL.CacheManager 类型同步更新;旧的 GetCacheValueAsync(int 专用)/ GetTotalCacheKey / InvalidateTotalCacheAsync 已移除
  • 自动缓存接入GetByIdAsync / FindAsync 详情(detail:{id})、GetRawKvpsAsync 键值对(kvp:{selector}:{condition}:{ascending})、TotalAsync 统计(total:{lastDays})三类读方法自动走缓存,未注入缓存管理器时直查数据库(零开销)
  • 缓存策略 CachePolicy:滑动/绝对过期、CacheNull 空值防护开关
  • DI 注册示例(Redis):AddSingleton<ICacheVersionStore, RedisCacheVersionStore>() + AddSingleton<ICacheManager, CacheManager>()
  • 新增测试:CacheManager 命中/未命中/滑动刷新/空值防穿透/版本号失效、写操作失效、单飞防击穿

DI 一键注册(2026-08-07)

  • AddCrpingRedisCache(configuration):一行注册 IConnectionMultiplexer(单例复用连接)+ RedisCacheVersionStore + CacheManager
  • AddCrpingMemoryCache():单机/测试场景,MemoryCacheVersionStore + CacheManager
  • ⚠️ 注意:AddStackExchangeRedisCache 只注册 IDistributedCache不注册 IConnectionMultiplexer——版本号 INCR 依赖后者,必须额外注册(AddCrpingRedisCache 已包含);业务数据存储仍需自行调用 AddStackExchangeRedisCache
  • 8.0.0 起两个方法重命名为 AddEFCoreRedisCache / AddEFCoreMemoryCache(前缀与库名对齐),AddEFCoreRedisCache 改为惰性连接 + 参数校验

缓存健壮性增强(2026-08-07)

  • 修复 Kvp 缓存键闭包冲突(正确性)CacheKeys.BuildExpressionKey 提取表达式结构签名 + 闭包捕获值,同一 lambda 位置不同捕获值时键不同,杜绝不同请求串数据(此前 Expression.ToString() 对闭包变量生成相同字符串)
  • 详情投影版接入缓存:GetByIdAsync(id, selector/options) 键含条件/投影指纹,GetDetailsAsync / GetForEditorAsync 等投影端点生效
  • 版本号本地短 TTL 缓存(CacheManager.VersionLocalCacheTtl,默认 5 秒):避免每次读请求访问 Redis 取版本号;写操作同步刷新本地值,写后立即读到新版本
  • 命中率统计:ICacheManager.Stats(按模型键聚合 Hits / Misses / HitRate),可观测性
  • 防雪崩:CachePolicy.ExpirationJitter 绝对过期时间叠加 [0, Jitter) 随机偏移,同一批键过期时间错开

7.6.0

2026年8月6日 星期四

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 7.6.0

Offset 偏移分页统一

  • 新增 GetOffsetPageAsync(query, page, ct):返回 OffsetPage<T> 契约(Items + Page + PageSize + RecordCount + PageCount + HasNext + HasPrev + OrderBy + Ascending),与 GetKeysetPageAsync / KeysetPage<T> 完全对称
  • 新增 OffsetQuery<TModel, TKey, TResult> 查询构建器:链式 OrderBy(未声明排序键默认 Id 降序,显式排序自动追加 Id 唯一性兜底)/ Where / Apply / Select / TagWith,与 KeysetQuery 同构
  • 旧偏移分页方法标记 [Obsolete](签名不变,完全兼容):GetListAsync(DAL/BLL)、GetRawListAsync(BLL 分页版),请迁移至 GetOffsetPageAsync
  • Controller 新增 GET /Page 端点(MethodsEnable.GetOffsetPageAsync 开关),直接返回 OffsetPage 契约
  • Demo 全部迁移:MemberController 新增 GET /OffsetPage 演示新契约直出;其余 BLL 内部切换至新 API 并保持旧响应形状
  • 适用场景提示:Offset 适合中小数据集与按页码跳转;大数据集、深分页、高频翻页请使用 Keyset 游标分页(GetKeysetPageAsync),避免大偏移(OFFSET)扫描
  • 页码越界宽容处理:page < 1 收敛为首页,page > PageCount 返回空页(不抛异常)

修复与增强(2026-08-06)

  • 分页健壮性:Offset 分页计算改用 long 中间量防 int 溢出(pageSize * (page - 1) / recordCount + pageSize),极端页码下 OFFSET 截断保护
  • 投影防护:未指定投影(Select)且结果类型与实体类型不一致时,GetOffsetPageAsync / GetKeysetPageAsync 提前抛出明确的 ArgumentException,替代运行时 InvalidCastException
  • 查询管道去重:新增 BaseDAL.Query.cs 共享辅助 BuildBaseQuery / ExecuteProjectionAsyncGetListAsync / GetOffsetPageAsync / GetKeysetPageAsync 三处统一复用(投影防护一处修改、三处生效)
  • 文件重命名:BaseDAL.OffsetPage.csBaseDAL.Offset.cs(与 BaseDAL.Keyset.cs 等功能名命名对齐)

Demo 修复(2026-08-06)

  • Topic 列表投影 N+1 修复:Images 改为一对多导航属性(Topic.Images + WithMany 配置),ListItemSelectorDb.Set<TopicImage>() 客户端评估(逐行查询)改为 EF 可翻译的相关子查询(单条 SQL);迁移快照已同步,无 schema 变更

修复与增强(2026-08-07)

  • 恢复写操作自动失效缓存:Create / Update / Delete 全部写路径在成功后自动调用 InvalidateTotalCacheAsync()(7.0.0 曾改为手动失效,现恢复自动),仅在实际写入成功(Created / NoContent / Ok / count > 0)时失效:
    • Create:CreateModelAsync、批量 CreateAsyncGetOrCreate* 系列
    • Update:UpdateModelAsync / UpdateSelectedPropsAsync / UpdateIgnoreAsync / UpdateModelListAsync / UpdateListAsync / ExecuteUpdateAsync / 三个 Toggle 状态切换
    • Delete:SoftDeleteAsync / DeleteListAsync / ExecuteDeleteAsync / ResetSoftDeleteAsync
  • 缓存键防冲突:GetTotalCacheKeytypeof(TModel).Name 改为 FullName(含命名空间),避免不同库/命名空间同名模型缓存键冲突
  • 缓存击穿防护(单飞):GetCacheValueAsync 每个缓存键一把 SemaphoreSlim_cacheLocks 并发字典),并发未命中合并为一次回源查询,写缓存后其余等待请求直接读取
  • 写失败清理跟踪状态:DbUpdateConcurrencyException / DbUpdateException 后调用 Db.ChangeTracker.Clear()(BaseDAL Create / Update / Delete),避免失败状态污染同一 DbContext 的后续操作
  • 软删除补全审计字段:单条软删除若实体实现 IDeletedAt 则自动补 DeletedAt ??= DateTimeOffset.Now;批量软删除(实现 IDeletedAtExecuteUpdateAsync 同时写入 IsDeleted + DeletedAt
  • BaseDAL 生命周期契约:文档注释明确 Scoped 生命周期(每个请求独立实例,禁止跨请求/线程共享);Db 属性与 SetDb 增加 ObjectDisposedException.ThrowIf 防护,释放后继续访问抛明确异常
  • ResultCode 新增 Unauthorized(401)WebApiResponse.Res 映射 ResultCode.UnauthorizedUnauthorized(),用于未登录/身份验证失败场景
  • 创建冲突防护:CreateModelAsync 捕获 DbUpdateException(唯一键/主键冲突)返回 Conflict 而非 500;TryCreateAsync(models, condition) 改为整体事务(BeginTransactionAsync),任一冲突整体回滚,避免部分成功;GetOrCreateAsync 提取 GetOrCreateCoreAsync 内核逻辑,事务边界由调用方决定
  • 新增单元测试:BatchCreateTransactionTests(批量创建事务回滚)、CacheKeyInvalidationTests(写操作后缓存失效)、FactoryRoundRobinTests(读副本轮询)、Controller/ExceptionMiddlewareTests(5xx 不泄露异常消息)

7.5.0

2026年8月6日 星期四

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 7.5.0

破坏性变更 — Keyset 游标分页重写

  • GetKeysetListAsync / GetKeysetRawListAsync 移除,替代为 GetKeysetPageAsync(query, cursor, direction),返回 KeysetPage<T>(Items + NextCursor / PrevCursor + HasNext / HasPrev)
  • 游标为不透明 Base64Url 令牌(KeysetCursor):携带排序指纹防错配;可配置 KeysetCursor.SigningKey 启用 HMAC 签名防伪造与篡改
  • 多字段排序生成全字段行值复合比较(k1 ▷ c1 OR (k1 = c1 AND k2 ▷ c2) ...),修复首键重复时翻页丢数据问题
  • 排序键自动追加主键 Id 唯一性兜底,保证排序全序;投影 DTO 必须包含排序字段
  • 使用说明详见 docs/Keyset游标分页.md

健壮性与性能

  • KeysetQuery 解析排序方案不再修改内部状态,构建后可并发复用
  • 游标取值改用编译表达式缓存:实体路径 AOT 安全,投影 DTO 走缓存成员访问,消除逐次反射
  • BLL 层对无效游标(过期缓存/篡改)记录警告日志
  • 新增 SQLite 集成测试(翻页正确性、同值首键、反向翻页、投影、签名、篡改防护)

修复与增强(2026-08-06)

  • 修复空页游标契约:游标指向的行被删除时,HasNext/HasPrev 与游标值保持一致,不再出现"有标志无游标"的矛盾
  • 恶意/篡改游标解码异常统一包装为 ArgumentException(签名校验、JSON 反序列化、值解析全流程),不再以 500 泄漏
  • 枚举排序键解码按排序方案值类型校验(消除对程序集版本脆弱的 Type.GetType),并修复枚举排序键翻页崩溃(比较表达式转底层数值类型)
  • RangeLimit 语义修正:<1 归 1、超过 MaxPageSize 封顶;GetRawList*rows<=0 表示不限制
  • KeysetPage 属性收紧为 init,响应构造后不可篡改
  • KeysetQuery.Where(bool when, ...) 新增条件式过滤重载:条件不成立时整体忽略,消除调用处分散的 if 分支
  • KeysetQuery 支持 object TResult:可直接复用 ListItemSelector(返回 object 的匿名投影)作为 Selector,游标按元素运行时类型自动取排序值(Demo 已演示 GetKeysetListAsync

7.4.0

2026年8月3日 星期一

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 7.4.0

破坏性变更 — 枚举重命名

  • DalResult 枚举重命名为 ResultCode,全项目 163 处引用更新
  • 文件从 DalResult.cs 改为 ResultCode.cs

迁移指南

  • 将代码中所有 DalResult 替换为 ResultCode

7.3.0

2026年7月30日 星期三

Crping.EFCore.Common / Crping.EFCore.Controller 同步升至 7.3.0

破坏性变更 — 时间类型从 DateTime 改为 DateTimeOffset

  • 所有 BLL 层的 DateTime.Now 改为 DateTimeOffset.NowWebApiBaseBLL.csWebApiBaseBLL.Update.csWebApiBaseBLL.Delete.cs
  • 统计查询 DateTime.Today 改为 DateTimeOffset.NowWebApiBaseBLL.Statistics.cs
  • EntityHelper.DefaultTime 类型从 DateTime 改为 DateTimeOffset
  • EntityHelper.GenerateId() 参数 fixedTimeDateTime? 改为 DateTimeOffset?
  • Utils.CustomTimestamp() 参数 fixedTimeDateTime? 改为 DateTimeOffset?
  • EFCoreExt.UpdateTo()DateTime.Now 改为 DateTimeOffset.Now

迁移指南

  • 将实体中的 DateTime 属性改为 DateTimeOffset
  • DateTime.Now 改为 DateTimeOffset.Now
  • 数据库列类型从 datetime2 改为 datetimeoffset

7.2.0

2026年7月28日 星期一

接口更新

  • MyDbContextFactory<TDb> 泛型约束从 IDbConfiguration 更新为 IAppConfiguration
  • IDbConfiguration 已删除,统一使用 IAppConfiguration

Demo 示例同步更新

  • EntityConfigureEntityHelper:种子数据中 16 处引用已更新
  • IMyConfigurationIAppConfiguration:TestDbContext 已更新
  • .Dom.Domain:TopicController 已更新
  • IEnabledIIsEnabled:StatusToggleTests 已更新

单元测试修复

  • CreateTests:修正 3 个 CreateAsync 测试断言(ObjectResult + 201 状态码替代 CreatedAtActionResult
  • CreateTests:修正空 ID 测试,验证 GenerateId 返回空字符串时的行为

7.1.0

2026年7月28日 星期一

SQLite 数据库支持

  • EntityHelper.UseDefaultTime() 新增 sql 参数,支持不同数据库的默认时间函数(SQLite: datetime('now'),PostgreSQL: NOW(),MySQL: CURRENT_TIMESTAMP
  • EntityHelper.UseStringKey() / UseStringBin() 新增 collation 参数,支持不同数据库的排序规则(SQLite: BINARY
  • 消费者只需在 DbContext.OnConfiguring() 中调用 UseSqlite(ConnectionString) 即可切换到 SQLite

新增类(向后兼容 — 旧类标记 [Obsolete]

  • EntityHelper:实体配置辅助类,替代 EntityConfigure(名称更清晰)
  • WebApiIntBLL:基于 int 类型用户主键的 BLL,替代 WebApiForIntBLL
  • WebApiLongBLL:基于 long 类型用户主键的 BLL,替代 WebApiForLongBLL

新增方法

  • EntityHelper.UseUrl():URL 路径配置方法(C# 命名规范:Url 而非 URL

重命名(向后兼容)

  • EntityConfigureEntityHelper:旧类保留静态成员转发,扩展方法仅在新类中
  • UseURLUseUrl:旧方法标记 [Obsolete] 并转发到新方法
  • WebApiForIntBLLWebApiIntBLL:旧类保留并继承新类
  • WebApiForLongBLLWebApiLongBLL:旧类保留并继承新类

代码质量

  • 修复 EntityHelper.cs region 注释拼写错误:DecimelDecimal
  • 重命名私有常量 parent_error_in_createParentErrorInCreate(PascalCase)
  • 重命名私有字段 _dom_domain(避免与 DOM 混淆)
  • 重命名文件 BaseDAL.Kvps.csBaseDAL.Kvp.csWebApiBaseBLL.Kvps.csWebApiBaseBLL.Kvp.cs

7.0.0

2026年7月26日 星期日

重大版本:含多项破坏性变更,下游升级请阅读完整说明

破坏性变更(必须关注)

  • DAL 层返回值由 HTTP code (int) 改为 ResultCode 枚举(200/201/204/400/403/404/409),解耦与 ASP.NET Core StatusCodes 的耦合;BaseDAL 公开方法签名变化
  • BaseDALWebApiBaseBLL 拆分为 partial class(按职责分离到独立文件:Create / Delete / Detail / Kvps / List / Persistence / Statistics / Update),部分内部 API 可见性变化(IDE 的"转到定义"层级会变)

安全性与并发

  • DAL 层捕获 DbUpdateConcurrencyException,写操作统一返回 Conflict(409);先检查实体是否存在,不存在则返回 NotFound(404)
  • 批量操作(obsolete 路径)启用事务保护(BeginTransactionAsync),updater 抛异常时回滚

性能

  • DeleteListAsync / UpdateListAsync 改用 ExecuteUpdateAsync / ExecuteDeleteAsync 服务端执行,不再加载全部数据到内存
  • Kvps 查询改用编译表达式,绕过 AsNoTracking 实体的 ChangeTracker
  • MyDbContextFactory 读副本轮询改 Interlocked.Increment 无锁化(替代 Lock + ++index
  • IgnoreSpecialProperties 字典映射替代 10 个 if 检查

可靠性与一致性

  • BaseDAL.Dispose 模式补全:_disposed 标志防重复释放;移除冗余 GC.SuppressFinalize
  • WebApiBaseBLL 移除 Dispose,由 DI 容器统一管理 DAL 生命周期
  • 缓存层:Create / Update / Delete 不再自动调用 InvalidateTotalCacheAsync();下游若依赖 CRUD 后缓存自动失效,需改为显式调用(注意:此变更为不对称 Revert 的结果,详见 git log 92a976e8fc943c

API 与代码组织

  • WebApiBaseBLL 同步方法标记 [Obsolete],与 BaseDAL 对齐
  • WebApiBaseBLL 提取 PrepareForCreate / ValidateUpdatePreCheck / ValidateUpdateCore / BuildSelfOnlyCondition,消除多处重复
  • 级联查询支持 batch 模式(batchMode 参数,一次性加载全部);引入 AsyncLocal 隔离 IsVerifyOwner / GetCurrentUserId;新增 NullableKeyWrapper 解决 nullable TKeyDictionary 中的约束
  • BaseDAL.SetWriteDb 恢复跟踪行为;Statistics 统一 AsNoTracking 并简化为表达式体
  • Update 错误处理提取为私有方法;Kvps 简化
  • IDomain 中未使用的 HTTP 类型依赖相关 refactor(commit 3eb4504)已被回退(65ae448),净状态未变
  • DateTime.Now → DateTime.UtcNow 改造 + 注释死代码清理(commit ff13f66)已被回退(bf06cb9),净状态未变

项目结构

  • SLN 嵌套重构:将 Crping.EFCore 项目从仓库根目录迁入 Crping.EFCore/ 子目录下(与 Common / Controller 一致),slnxProjectReference 路径相应更新

文档

  • 所有类与方法补全 XML 文档注释
  • 完整内部重构与注释微调(aa86f80 / 9c916de / 13d591c

6.11.0

2026年6月16日 星期二
  • 优化Create,Update,GetOrCreate相关方法
  • 移除接口IDomainCommonMethods
  • 移除BaseDomain中方法的virtual修饰符
  • 升级所有相关组件版本

6.10.0

2026年6月9日 星期二
  • 删除大部分方法的virtual修饰符
  • 新增方法FirstOrDefaultAsync<TOrderKey, TResult> (IQueryOptions<TModel, TOrderKey, TResult>? options),并添加相关接口方法
  • 新增方法FirstOrDefault<TOrderKey, TResult> (IQueryOptions<TModel, TOrderKey, TResult>? options),并添加相关接口方法

6.9.2

2026年3月20日 星期五
  • Create相关方法添加字符串类型Id的检查与赋值逻辑,并添加异常明确的消息提示

6.9.1

2026年3月18日 星期三
  • 优化创建相关方法的Id生成逻辑

6.9.0

2026年2月26日 星期三
  • 扩展接口支持管理员权限:修改 IDetailsSelectorIEditorSelectorIGetDetailsIGetForEditorIListItemSelector 等接口,添加管理员权限相关方法
  • 增强 WebApiBaseBLL 类:添加 DetailsSelectorForAdminEditorSelectorForAdminListItemSelectorForAdmin 等属性,支持管理员权限的数据选择

6.8.0

2026年2月6日 星期五
  • 新增方法ToggleDeleteStatusAsync

6.7.0

2026年2月6日 星期五
  • 优化软删除相关方法逻辑
  • 新增状态切换方法ToggleEnableStatusAsync, ToggleAuditStatusAsync

6.6.0

2026年1月20日 星期二
  • 新增属性Query
  • 新增一系列求和、平均值方法
  • 新增方法MaxPageSize并优化相关命名

6.5.0

2026年1月19日 星期一
  • 新增工具类Toolkit

6.4.0

2025年12月31日 星期三
  • 删除接口ICreateAndUpdateByDto
  • 优化所有命名空间

6.3.0

2025年12月28日 星期日
  • BLL添加并实现接口新增接口ICommonSelectors<TModel, TResult>, IListItemSelector<TModel, TResult>, IDetailsSelector<TModel, TResult>, IEditorSelector<TModel, TResult>, ISimpleSelector<TModel, TResult>,IGetKvps
  • 优化方法GetKvpsAsync
  • 新增接口ICreateAndUpdateByDto

6.2.0

2025年12月25日 星期四
  • 新增接口ICommonSelectors<TModel, TResult>, IListItemSelector<TModel, TResult>, IDetailsSelector<TModel, TResult>, IEditorSelector<TModel, TResult>, ISimpleSelector<TModel, TResult>,IGetKvps
  • 优化方法GetRawKvpsAsync, GetKvpsAsync

6.1.0

2025年12月2日 星期二
  • 新增接口IDomainCommonMethods<TModel, TKey>, ICreateAsync<TModel>, IUpdateAsync<TModel, TKey>, IGetDetails<TKey>

6.0.0

2025年11月27日 星期四
  • 升级为 .net10.0
  • 适配ExecuteUpdate相关方法

5.11.0

2025年11月13日 星期四
  • 新增接口:ICommonMethods<TKey>, IGetModelSelector

5.10.0

2025年11月2日 星期日
  • 将所有列表方法的返回值从接口改为具体类型
  • 新增Keyset分页方法GetKeysetListAsyncGetKeysetRawListAsync

5.9.0

2025年9月25日 星期四
  • BaseDAL添加对IAuditIDeletedAt的支持

5.8.0

2025年9月23日 星期二
  • 优化BLL, DAL中所有更新相关方法对特殊状态属性的更新保留,并重命名部分方法

5.7.0

2025年7月31日 星期四
  • BLL新增方法UpdateExcludeAsync
  • DAL方法UpdateAsync新增更多特殊状态属性的更新保留

5.6.0

2025年7月31日 星期四
  • Update相关方法的数组参数添加params

5.5.0

2025年6月18日 星期三
  • GetByOptionsAsyncGetBySelectorAsync 重命名为 GetByIdAsync

5.4.3

2025年5月23日 星期五
  • 修复方法 SoftDeleteAsync(...)

5.4.2

2025年5月23日 星期五
  • 修复方法 SoftDeleteAsync(...)

5.4.1

2025年5月23日 星期五
  • WebApiBaseBLL 优化方法 SoftDeleteAsync(...)

5.4.0

2025年5月22日 星期四
  • WebApiBaseBLL 新增方法SoftDeleteAsync, ResetSoftDeleteAsync

5.3.0

2025年5月21日 星期三
  • 修改BaseDAL的方法 RangeLimit(...)

5.2.0

2025年4月19日 星期六
  • WebApiBaseBLL 新增方法 GetByIdAsyncGetById

5.1.1

2025年4月9日 星期三
  • 优化方法:CustomTimestamp(...)

5.1.0

2025年3月25日 星期二
  • BaseDAL新增属性MaxList,新增方法SetMaxList(...)
  • WebApiBaseBLL新增方法SetMaxList(...)
  • 所有列表方法新增对列表最大数量的控制

5.0.2

2025年3月9日 星期日
  • 统一子组件传递依赖中的组件版本

5.0.1

2025年3月9日 星期日
  • 删除未使用的引用

5.0.0

2024年11月17日 星期日
  • 升级为 .net9.0
  • 更新引用组件,并清理替换弃用的组件

4.20.0

2024年11月16日 星期六
  • AuthenticationHeaderValue 修改为 AuthHeaderValue
  • GetAuthHeaderValue 修改为 GetAuthHeaderValue

4.19.0

2024年11月14日 星期四
  • BaseDomain 新增委托 GetAuthenticationHeaderValue 以及属性 AuthenticationHeaderValue

4.18.0

2024年11月14日 星期四
  • BaseDomain 新增委托 GetAuthorization 以及属性 Authorization

4.17.0

2024年11月12日 星期二
  • 修改EntityConfigure中方法UseURL的默认值
  • 新增方法SetTable

4.16.0

2024年10月22日 星期二
  • EntityConfigure 添加方法 ToStringId(...) 并修改 GenerateId(...)
  • DefaultDate 改为 DefaultTime

4.15.0

2024年10月20日 星期日
  • 新增方法 GenerateId(...), CustomTimestamp(...)
  • 修改方法 UseStringKey(...) 的参数顺序

4.14.0

2024年10月19日 星期六
  • 新增实体配置辅助类 EntityConfigure 以及一系列常用方法

4.13.1

2024年10月16日 星期三
  • BLL 方法 GetByOptionsAsync(...) 修改条件控制逻辑,优化使用外部传入控制条件

4.13.0

2024年10月12日 星期六
  • BLL 新增接口 IConfigBLL
  • BLL 修改类 BaseDomain 的构造方法参数与相关属性
  • BaseDomain 新增方法 ShareDbContext

4.12.0

2024年10月11日 星期五
  • BLL 新增接口 IGetSimple<TKey>

4.11.1

2024年10月11日 星期五
  • BaseDomain 优化方法 SyncConfig(),并移除构造方法对 SyncConfig() 的调用

4.11.0

2024年10月11日 星期五
  • BLL 修改 CurrentUserId 属性逻辑
  • BaseDomain 修改 CurrentUserId 属性逻辑,以及构造函数,自动同步 BLL 配置
  • 升级 EFCore 相关包引用

4.10.0

2024年10月6日 星期日
  • 修复 BLLIQueryOptions 的相关警告
  • BLL 新增方法 GetBySelectorAsync(...)
  • BLL 新增接口 IGetListItem
  • 方法 GetForEditorAsync(...) 修改为 GetByOptionsAsync(...)

4.9.0

2024年9月25日 星期三
  • BLL 新增接口 IGetDetails<TKey>IGetForEditor<TKey>
  • BLL 新增方法 GetForEditorAsync
  • IQueryOptions 添加泛型约束

4.8.0

2024年9月20日 星期五
  • 跳过有BUG的 4.7.0
  • BLL新增方法SetCurrentUser
  • BaseDomain修改构造方法,新增可选参数,并新增属性
  • BaseDomain新增方法SyncConfig

4.6.0

2024年4月19日 星期五
  • ILogger改为泛型
  • BaseDomain新增对接口IKit的实现,增加对logcache的支持

4.5.0

2024年4月9日 星期二
  • WebApiBaseBLL新增可选构造参数IToolkit,以方便注入外部工具
  • WebApiBaseBLL相关子类新增可选构造参数IToolkit,以方便注入外部工具
  • 方法CheckStringId(...)修改逻辑,以调用IToolkit中的方法

4.4.0

2024年4月7日 星期日
  • 清除不再使用的接口,如:IHostBLL
  • 添加新接口;IGenerateId
  • 接口IDomain添加委托;GenerateId

4.3.0

2024年3月28日 星期四
  • 新增接口:ICacheBLL
  • 修改接口:IWebApiBLL,并新增IWebApiForIntBLLIWebApiForLongBLL
  • 修改WebApiBLL,并新增WebApiForIntBLLWebApiForLongBLL
  • 新增委托:GenerateId
  • 所有创建相关方法添加字符串类型 Id 的检查与赋值

4.2.0

2024年3月15日 星期五
  • 新增方法:Any(...)AnyAsync(...)

4.1.0

2024年3月10日 星期日
  • 新增方法:Forbidden(...)MethodNotAllowed()

4.0.0

2024年1月27日 星期六
  • 升级目标框架为.net8.0
  • 升级EF相关组件为8.0以上最新版权
  • 降级两个官方弃用的包
  • 与系列相关组件对齐版本号

3.44.1

2023年11月13日 星期一
  • SetDb(...)方法添加相同Db判断,跳过不必要的替换

3.44.0

2023-08-30
  • 新增更简洁的获取键值对方法GetKvpsAsync

3.43.0

2023-07-27
  • 为方法GetCacheValueAsync添加默认值

3.42.0

2023-07-24
  • 修改IRelatedBLL
  • 修改Update相关方法对UpdatedAtUpdatedBy的赋值逻辑

3.41.0

2023-07-04
  • 新增IDomain<TKey>BaseDomain<TKey>

3.40.0

2023-06-29
  • 为所有泛型参数TKey添加约束notnull
  • 为用户操作自动绑定用户ID添加TUserKey,并添加notnull约束
  • 修改所有接口类因添加TUserKey所引发的所有问题
  • 修复所有创建功能因接口差异无法自动绑定创建者ID的问题
  • 修复所有更新功能因接口差异无法自动绑定创建者ID的问题
  • 修复所有删除功能因接口差异无法自动绑定创建者ID的问题

3.39.0

2023-06-11
  • DAL新增方法GetKeysetRawListAsync(...),及其接口
  • BLL新增方法GetKeysetRawListAsync(...),及其接口
  • BLL新增方法GetKeysetListAsync(...),及其接口
  • WebApiResponse新增方法PaginationOk(...)
  • 新增Keyset分页辅助类KeysetQueryOptions

3.38.0

2023-05-25
  • 修改领域接口为IDomain,并新增委托GetCurrentUserId()
  • 修改领域基类为BaseDomain,并新增委托GetCurrentUserId()

3.37.0

2023-05-18
  • 新增领域接口IDomain<TUserKey>
  • 新增领域基类BaseDomain<TUserKey>

3.36.1

2023-05-17
  • 将方法FirstAsync(...)First(...)的返回值改为非可空类型

3.36.0

2023-05-17
  • DAL新增方法:FirstAsync(...)First(...)
  • BLL新增方法:FirstAsync(...)First(...)
  • BLL新增方法:TryCreateAsync(...)TryCreate(...)

3.35.0

2023-05-17
  • 新增方法TryCreateAsync(...)TryCreate(...)

3.34.0

2023-05-12
  • 新增种子数据接口ISeedData<T>

3.33.0

2023-05-11
  • QueryOptions新增属性SelectFunc,并在所有列表相关方法中添加对其使用

3.32.0

2023-04-26
  • 修复方法GetOrCreateAsync(...)系列方法内部查询库与创建库不一致的问题
  • 新增方法:GetCascaderAsync(...)

3.31.0

2023-04-23
  • QueryOptions新增查询标签属性Tag
  • 为所有相关搜索列表方法添加对TagWith的调用

3.30.0

2023-04-04
  • 启用可空类型
  • 修复启用可空类型后产生的所有警告

3.22.0

2023-04-04
  • DAL新增方法SetReadDbIfNull(),属性ReadIfNull
  • DAL访问属性Db时,若为Null默认初始化为ReadDb
  • BLL将默认调用的方法WriteIfNull改为ReadIfNull

3.21.0

2023-03-28
  • 将接口IConnectionStringIMyConfiguration转移到Common

3.20.1

2023-03-13
  • GetKvpsAsync的零值项目名称请选择改为全部

3.20.0

2023-02-24
  • DAL新增LongCountAsyncLongCount
  • BLL新增LongCountAsyncLongCount
  • BLL修改TotalAsync相关方法,新增最后几天限定统计

3.19.0

2023-02-17
  • DAL新增方法SetNoTrackingSetTrackAll
  • DAL_db等重要对象私有化
  • DAL将所有对私有字段的访问改为公共属性,并去除所有多余的判空逻辑
  • DAL在切换为只读库里自动关闭查询跟踪
  • BLL新增属性Dal,用于统一验证
  • BLL将所有对_dal字段的引用改为属性Dal
  • BLL并去除所有多余的判空逻辑
  • BLL新增几个未添加的接口
  • BLL新增接口IKit,属性CacheLogger

3.18.0

2023-02-03
  • 将运行时修改为net7.0

3.17.1

2023-01-13
  • BLL修改方法Task<IActionResult> TotalAsync()中的默认缓存Key

3.17.0

2023-01-13
  • BLL新增方法Task<int> TotalAsync(string, TimeSpan?, TimeSpan?)
  • BLL新增方法Task<IActionResult> TotalAsync()

3.16.0

2023-01-09
  • DAL新增方法(int, TModel) CreateModel(TModel model)
  • BLL新增方法(int, TModel) CreateModel(TModel model)
  • BLL3.15版所有新增方法的返回值改为(int, TModel)

3.15.0

2023-01-09
  • DAL新增同步方法FirstOrDefault(condition)
  • BLL新增方法GetOrCreate(model)GetOrCreateAsync(model)
  • BLL新增方法GetOrCreate(model, condition)GetOrCreateAsync(model, condition)

3.14.0

2023-01-03
  • 更新对Crping.EFCore.Common的引用,EFCore相关组件的升级
  • 新增全局引用Global,并优化所有引用、命名空间、以及部分注释

3.13.0

2022-12-15
  • 新增接口IMyConfiguration,用于向DbContext相关类传递配置对象
  • MyDbContextFactory<TDb>类添加IConfiguration属性,并通过方法CreateDbContext将此属性赋值给DbContext相关类

3.12.0

2022-12-09
  • 添加批量删除方法ExecuteDeleteExecuteDeleteAsync

3.11.0

2022-12-09
  • 添加批量更新方法ExecuteUpdateAsyncExecuteUpdate

3.10.1

2022-11-28
  • 解决'pagination'添加返回值'orderBy'在不同.net版本中的返回值不同的问题

3.10.0

2022-11-28
  • DAL的列表分页返回类型'pagination'添加返回值'orderBy'

3.9.1

2022-11-26
  • 解决pagination添加返回值AscendingqueryOptions为空的Bug

3.9.0

2022-11-23
  • pagination添加返回值Ascending
  • 升级EFCore7.0.0

3.8.0

2022-09-17
  • GetValueFromCacheAsync重命令为GetCacheValueAsync,并删除参数isRefresh
  • 新增重载方法Task<int> GetCacheValueAsync(string key, Func<Task<int>> getNewValue, TimeSpan? slidingExpiration, TimeSpan? absoluteExpirationRelativeToNow)

3.7.1

2022-09-16
  • GetPageCountFromCacheAsync 重命名为 GetValueFromCacheAsync
  • 修改GetValueFromCacheAsync的委托参数的返回类型,以支持异步方法

3.7.0

2022-09-16
  • 添加内部工具类Utils
  • 添加分页缓存方法:GetPageCountFromCacheAsync

3.6.0

2022-06-05
  • 将所有DAL中业务相关逻辑转移到BLL,并将部分强制更新字段改为可选更新

2022-05-18
  • 修改所有InitDb相关方法
  • BLL层添加SetReadDb,SetWriteDb方法

2022-04-01
  1. 更新对Crping.EFCore.Common的引用,并对齐版本号

2021-07-15:

  1. IWebApiBLL接口与基础实现类添加Read,Write属性,以实现读写分离链接调用
  2. IBaseDAL接口与与基础实现类添加Read,Write属性,以实现读写分离链接调用
  3. IBaseDAL接口与与基础实现类添加ReadOnly方法,以实现以参数的方式读写分离链接调用
  4. IWebApiBLL接口与基础实现类去除所有readOnly参数,改为链式调用

历史记录
  • 3.6.0:将所有DAL中业务相关逻辑转移到BLL,并将部分强制更新字段改为可选更新
  • 3.5.1:更新引用组件版本
  • 3.5.0:修改所有InitDb相关方法,BLL层添加SetReadDb,SetWriteDb方法
  • 3.4.0:添加IHostBLL,删除IRelatedBLL,并修改相关方法
  • 3.3.0:更新对Crping.EFCore.Common的引用,并对齐版本号
  • 3.1.0:给所有方法添加判空逻辑,DbContextFactory类改为MyDbContextFactory
  • 3.0.0:目标框架改为.net6.0,并引用EFCore6.0
  • 2.4.0:新增方法:ShareCurrentUserIdFunc,重命名方法:ShareDbContext
  • 2.3.0:新增方法 GetRawKvpsAsync,并升级EFCore到5.0.10
  • 2.2.0:添加带条件的 Include 扩展方法
  • 2.1.0:修改默认排序BUG,新增IDisposable, IAsyncDisposable
  • 2.0.2:获取列表相关方法添加默认以ID为排序方式的升降序配置
  • 2.0.1:CreateDbContext ReadOnly时添加Lock,以防多线程处理时数组越界
  • 2.0.0:使用自定义DbContextFactory,延迟创建DbContext等(重大改版!!!)
  • 1.5.3:GetKvpsAsync添加参数,修改默认排序
  • 1.5.2:修复共享DbContext时的Bug
  • 1.5.1:添加DbContext共享
  • 1.5.0:重要!!,添加并实现IRelatedBLL接口,拆分DAL,BLL为CRUD等接口
  • 1.4.1:去除GetCurrentUserID未赋值时的默认值0,暴露BUG
  • 1.4.0:增加并修改DAL、BLL层相关列表方法
  • 1.3.1:简化Get类方法重载的参数
  • 1.3.0:BLL中的列表方法添加一系列重载
  • 1.2.0:修改DAL,BLL的实现基类名称
  • 1.1.0:添加Read、Write属性以链式调用方式取代传参方式。
  • 1.0.0:包含数据库常规操作的DAL基类,WebApi常规操作的BLL基类等
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 Crping.EFCore:

Package Downloads
Crping.EFCore.Controller

基于 Crping.EFCore 的 WebApi 控制器基类

Crping.Auth.BLL

Crping.Auth 的业务逻辑层

Crping.Auth.MySql.DAL

Crping.Auth.DAL 的 MySql 实现

Crping.AuthPolicy.BLL

Package Description

Crping.Auth.Domain

Package Description

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
10.2.2 101 9/15/2026
10.2.0 101 9/14/2026
10.1.0 92 9/13/2026
10.0.0 105 9/11/2026
9.13.0 134 9/3/2026
9.12.0 111 8/31/2026
9.11.0 113 8/31/2026
9.10.0 116 8/30/2026
9.9.0 110 8/29/2026
9.8.0 113 8/29/2026
9.7.0 121 8/26/2026
9.6.0 119 8/24/2026
9.5.0 143 8/22/2026
9.4.0 130 8/18/2026
9.3.0 120 8/18/2026
9.2.0 116 8/18/2026
9.1.2 132 8/16/2026
9.1.1 123 8/16/2026
9.1.0 117 8/15/2026
9.0.0 123 8/14/2026
Loading failed