repository接口必须使用泛型约束irepository,其中tentity : class, new(),以支持new tentity();需避免暴露iqueryable;dbcontext须通过di注入并注册为scoped;软删除应分getall()与getallwithdeleted(),后者须调用ignorequeryfilters();异步方法须真正await且命名带async后缀。

Repository 接口设计必须包含泛型约束 TEntity,否则无法统一操作
很多初学者直接写 IRepository 而不加泛型,结果发现增删改查都要为每个实体重复定义一套方法。正确做法是让接口承载实体类型信息:IRepository<tentity></tentity>,其中 TEntity 必须是 class 且有无参构造函数(where TEntity : class, new())。否则 new TEntity() 在通用添加逻辑里会编译失败。
常见错误现象:Cannot create an instance of the variable type 'TEntity' because it does not have the new() constraint。
-
TEntity必须是引用类型(避免值类型误传) - 若实体用 EF Core 的
[Key]或 Fluent API 配置主键,GetById才能正确映射,否则可能返回 null - 不要在接口里暴露
IQueryable<tentity></tentity>——这会让调用方意外触发延迟执行或跨层拼接 SQL,破坏仓储边界
EF Core 中实现 GenericRepository 时,DbContext 实例不能共享生命周期
一个 GenericRepository<user></user> 和 GenericRepository<order></order> 如果共用同一个 DbContext 实例(比如从单例容器解析),会在并发请求下出现 System.InvalidOperationException: A second operation started on this context before a previous operation completed。
根本原因是 EF Core 的 DbContext 不是线程安全的,且默认注册为 Scoped(每次 HTTP 请求新建一次),但手动 new 出来的 Repository 容易绕过 DI 生命周期管理。
- 必须通过构造函数注入
DbContext,而不是在 Repository 内部 new - 注册方式应为:
services.AddScoped<irepository>, GenericRepository<user>>()</user></irepository>,并确保GenericRepository构造函数接收AppDbContext - 如果要用内存数据库做测试,记得每次测试都新建独立的
DbContextOptions,否则上下文状态会污染
软删除场景下,GetAll() 默认不应返回已标记删除的数据
多数业务要求 IsDeleted == false 的数据才参与查询,但直接在 GetAll() 方法里硬编码过滤条件,会导致无法获取真实全量(比如后台管理需要查看所有记录)。更合理的方式是分层暴露:
-
GetAll():默认带软删除过滤,面向业务主流程 -
GetAllWithDeleted():显式命名,只在必要场景(如审核、迁移)中调用 - 在 EF Core 中,可用全局查询过滤器(
modelBuilder.Entity<user>().HasQueryFilter(x => !x.IsDeleted)</user>)自动生效,但注意它无法被IgnoreQueryFilters()之外的方式绕过——所以GetAllWithDeleted()内部必须调用.IgnoreQueryFilters()
漏掉 IgnoreQueryFilters() 是最常踩的坑:表面看方法名写了 “with deleted”,实际返回的还是过滤后的结果。
异步方法命名必须带 Async 后缀,且内部必须真正 await
EF Core 的 SaveChangesAsync()、FirstOrDefaultAsync() 等方法返回 Task,但有人写成 public async Task<user> GetById(int id) { return await _context.Users.FindAsync(id); }</user> —— 这里 FindAsync 是同步查找主键缓存,若未命中才发异步查询,行为不稳定。
- 查主键优先用
FindAsync()(支持缓存),但非主键查询一律用FirstOrDefaultAsync()或ToListAsync() - 不要用
Task.Run(() => ...).Result模拟异步,这会阻塞线程池,高并发下迅速拖垮性能 - 仓储接口方法签名必须与实现一致:接口声明
Task<tentity> GetByIdAsync(int id)</tentity>,实现就不能只写GetById()同步版本
最容易被忽略的是:单元测试里用 GetAwaiter().GetResult() 看似能跑通,但掩盖了死锁风险(尤其在 ASP.NET Framework 旧项目中)。











