在我们的代码库中,我们有一个具有以下继承模式的存储库类:
public interface IReadOnlyRepository
{
public IEnumerable<TEntity> Get<TEntity>() where TEntity : BaseEntity
}
public interface IReadWriteRepository : IReadOnlyRepository{}
{
public void Add<TEntity>(TEntity entity) where TEntity : BaseEntity
public void Update<TEntity>(TEntity entity) where TEntity : BaseEntity
// more CRUD methods etc
}
public class Repository(IReadWriteRepository repo) : IReadWriteRepository
{
public IEnumerable<TEntity> Get<TEntity>() where TEntity : BaseEntity
{
// implementation
}
public void Add<TEntity>() where TEntity : BaseEntity
{
// implementation
}
// rest of CRUD method implementations
}
然后在注册服务时,我们注册
Repository
类的两个实例,但使用工厂模式具有不同的接口定义:
// Read write repo
services.AddScoped(typeof(IReadWriteRepository, p => new Repository(p.GetRequiredService<DbContext>));
// Read only repo
services.AddScoped(typeof(IReadOnlyRepository, p => new Repository(p.GetRequiredService<DbContext>));
问题是,即使我们将其注册为具有所有 CRUD 方法的方法定义的基本类型,第二次注册仍然只能访问
IReadOnlyRepository
的 Get() 方法吗?如果是这样,在这种情况下 CRUD 方法会发生什么情况?它们只是不包含在 Repository
的只读版本中吗?
我在注释中解释了为什么这是一个反模式 - 这段代码用低级 CRUD 类包装了高级工作单元,而不是存储库。
Repository
类本身仍然是可写的,有人可以将IReadOnlyRepository
转换为IReadWriteRepository
,甚至直接转换为Repository
并开始删除。就这么简单:
(_myReadOnlyRepo as Repository).Delete(something);
更糟糕的是,
IReadOnlyRepository<TEntity>.Get
返回的对象将被完全跟踪,这意味着对它们所做的任何更改都可以被持久化,即使没有强制转换。如果控制器同时具有 IReadOnlyRepository
和 IReadWriteRepository
依赖项,那么它们的 both 将使用相同的 DbContext 实例。例如,有人可以从“只读”存储库加载博客文章,对其进行修改,然后更新其他存储库中的投票。该更新还将保留对博客文章的更改。
DbContext
是一个工作单元,它检测它跟踪的所有对象的更改,因此使其只读的方法是禁用更改跟踪。这对于使用 AsNoTracking() 的单个查询或在配置期间调用 DbContextOptionsBuilder.UseQueryTrackingBehavior() 的整个 DbContext 都是可能的。
在适当的域存储库中,有人可以仅在查询级别禁用,例如:
public Blog GetBlogPostsForRendering(int blogId,DateOnly since)
{
var blogAndPosts = _context.Blog.AsNoTracking()
.Include(b=>b.Posts.Where(p=>p.Created>=since))
.ToList();
return blogAndPosts;
}
在问题的情况下,整个 DbContext 实例必须设置为只读。这可以通过
UseQueryTrackingBehavior
来完成,例如:
services.AddScoped<IReadOnlyRepository>(p => {
var contextOptions = new DbContextOptionsBuilder<MyContext>()
.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Test")
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking)
.Options;
var context=new MyContext(options);
return new Repository(context);
});
为了避免重复,可以将通用配置选项提取到单独的方法中:
DbContextOptionsBuilder<MyContext> ContextConfiguration(
IServiceProvider services,
bool readonly)
{
var config=services.GetRequiredService<IConfiguration>();
var cns=config.GetConnectionString("mydb");
var builder=new DbContextOptionsBuilder<MyContext>()
.UseSqlServer(cns)
...;
if(readonly)
{
builder=builder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
}
return builder;
}
...
services.AddScoped<IReadOnlyRepository>(p => {
var contextOptions = ContextConfiguration(p,true).Options;
var context=new MyContext(options);
return new Repository(context);
});