实体框架 - 使用不同的接口注册存储库实现

问题描述 投票:0回答:1

在我们的代码库中,我们有一个具有以下继承模式的存储库类:

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
的只读版本中吗?

c# asp.net .net entity-framework
1个回答
0
投票

我在注释中解释了为什么这是一个反模式 - 这段代码用低级 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);
});
© www.soinside.com 2019 - 2024. All rights reserved.