DI服务具有适当的缓存和dbContext用法

J. *_*Doe 5 c# dependency-injection entity-framework-core asp.net-core-mvc .net-core

我真的想制作一个漂亮,干净和正确的代码,所以我几乎没有什么基本问题.一开始我有一个服务GetName._dbContext来自其他DI服务_cache

public Task<string> GetName(string title)
{
    var articleList = await _cache.GetOrCreate("CacheKey", entry =>
    {
        entry.SlidingExpiration = TimeSpan.FromSeconds(60 * 60);
        return _dbContext.Articles.ToListAsync;
    });

    var article = articleList.Where(c => c.Title == title).FirstOrDefault()

    if(article == null)
    {
        return "Non exist"
    }

    return article.Name();
}
Run Code Online (Sandbox Code Playgroud)
  1. 官方文档中我们可以读到EF上下文不是线程安全的,所以我应该await在返回后添加GetOrCreate
  2. 是否适合将其设置为Singleton服务并在操作和剃刀视图中使用它?
  3. 官方文档中,我们可以阅读:避免仅存在的"数据持有者"对象以允许访问 DI服务中的某些其他对象.当我看到我的服务时,我担心这条线.

Evk*_*Evk 5

EF 上下文不是线程安全的这一事实与 async\await 无关,因此您的第一个问题没有多大意义。无论如何,一般最佳实践是不要为多个逻辑操作重用单个 EF 上下文。在多线程应用程序中,违反此规则通常会立即导致灾难。如果您一次从单个线程使用相同的 EF 上下文 - 违规可能会导致某些意外行为,特别是如果您长时间对许多操作使用相同的上下文。

在 ASP.NET 中,每个请求都有专用的线程,每个请求不会持续很长时间,每个请求通常代表单个逻辑操作。出于这个原因,EF 上下文通常使用范围生命周期注册,这意味着每个请求都有单独的上下文,在请求结束时处理。对于大多数用途,这种模式工作得很好(尽管如果您在单个 http 请求中执行具有许多数据库请求的复杂逻辑,您仍然可能会出现意外行为)。我个人仍然更喜欢为每个操作使用 EF 上下文的新实例(所以在你的例子中,我会注入 EF 上下文工厂并在GetOrCreatebody内创建新实例,然后立即处理)。然而,这并不是一个非常流行的观点。

因此,每个请求都有单独的上下文(如果它注册了作用域生命周期),因此您不应该(通常,如果您自己不产生后台线程)担心对该实例的多线程访问。

您的方法仍然不正确,因为您现在保存在缓存中的不是List<Article>. 您Task<List<Acticle>>改为存储。使用内存缓存它可以工作,因为存储在内存缓存中不涉及任何序列化。但是一旦您更改缓存提供程序 - 事情就会中断,因为Task显然不可序列化。而是使用GetOrCreateAsync(注意仍然没有理由在await之后添加return):

var articleList = await _cache.GetOrCreateAsync("CacheKey", entry =>
{
    entry.SlidingExpiration = TimeSpan.FromSeconds(60 * 60);
    return _dbContext.Articles.ToListAsync();
});
Run Code Online (Sandbox Code Playgroud)

请注意,如果项目在缓存中已经存在-将不会有异步工作完成,整个操作将完全同步执行(不要让await欺骗你-如果你await的东西它不是一定意味着会有一些后台线程或异步操作涉及)。

将 EF 实体直接存储在缓存中通常是不好的。最好将它们转换为 DTO 实体并存储它们。但是,如果您存储 EF 实体 - 至少不要忘记为您的上下文禁用延迟加载和代理创建。

将此类服务注册为单例是不合适的,因为它取决于具有范围生命周期的 EF 上下文(默认情况下)。使用与 EF 上下文相同的生命周期注册它。

至于避免使用“数据持有者”对象- 我在这里看不到它是如何相关的。您的服务有一个逻辑(从缓存数据库中获取数据)——它的存在不仅仅是为了允许访问其他一些对象。


Win*_*Win 1

有些人认为 DbContext 本身是一个存储库,但有些人可能认为 DbContext 遵循工作单元模式。

对我来说,我个人喜欢使用一个小型存储库,它基本上是DbContext 的一个小包装。然后我从存储库公开IDbSet 。

请原谅我使用我的示例GitHub项目,因为用实际代码比用文字更容易解释。

存储库

public class EfRepository<T> : DbContext, IRepository<T> where T : BaseEntity
{
    private readonly DbContext _context;
    private IDbSet<T> _entities;

    public EfRepository(DbContext context)
    {
        _context = context;
    }

    public virtual IDbSet<T> Entities => _entities ?? (_entities = _context.Set<T>());

    public override async Task<int> SaveChangesAsync()
    {
        try
        {
            return await _context.SaveChangesAsync();
        }
        catch (DbEntityValidationException ex)
        {
            var sb = new StringBuilder();

            foreach (var validationErrors in ex.EntityValidationErrors)
                foreach (var validationError in validationErrors.ValidationErrors)
                    sb.AppendLine($"Property: {validationError.PropertyName} Error: {validationError.ErrorMessage}");

            throw new Exception(sb.ToString(), ex);
        }
    }
}

public class AppDbContext : DbContext
{
   public AppDbContext(string nameOrConnectionString)
      : base(nameOrConnectionString) {}

   public new IDbSet<TEntity> Set<TEntity>() where TEntity : BaseEntity
   {
      return base.Set<TEntity>();
   }
}
Run Code Online (Sandbox Code Playgroud)

服务中存储库的使用

然后我通过构造函数注入将存储库注入到服务类中。

public class UserService : IUserService
{
   private readonly IRepository<User> _repository;

   public UserService(IRepository<User> repository)
   {
      _repository = repository;
   }

   public async Task<User> GetUserByUserNameAsync(string userName)
   {
      var query = _repository.Entities
         .Where(x => x.UserName == userName);

   return await query.FirstOrDefaultAsync();
}
Run Code Online (Sandbox Code Playgroud)

单元测试助手

模拟异步方法以进行单元测试有点棘手。就我而言,我使用NSubstitute,因此我使用以下辅助方法NSubstituteHelper

如果您使用 moq 或其他单元测试框架,您可能需要阅读使用模拟框架进行实体框架测试(EF6 及以上)

public static IDbSet<T> CreateMockDbSet<T>(IQueryable<T> data = null) where T : class
{
    var mockSet = Substitute.For<MockableDbSetWithExtensions<T>, IQueryable<T>, IDbAsyncEnumerable<T>>();
    mockSet.AsNoTracking().Returns(mockSet);
    if (data != null)
    {
        ((IDbAsyncEnumerable<T>)mockSet).GetAsyncEnumerator().Returns(new TestDbAsyncEnumerator<T>(data.GetEnumerator()));
        ((IQueryable<T>)mockSet).Provider.Returns(new TestDbAsyncQueryProvider<T>(data.Provider));
        ((IQueryable<T>)mockSet).Expression.Returns(data.Expression);
        ((IQueryable<T>)mockSet).ElementType.Returns(data.ElementType);
        ((IQueryable<T>)mockSet).GetEnumerator().Returns(new TestDbEnumerator<T>(data.GetEnumerator()));
    }
    return mockSet;
}
Run Code Online (Sandbox Code Playgroud)

单元测试

// SetUp
var mockSet = NSubstituteHelper.CreateMockDbSet(_users);
var mockRepository = Substitute.For<IRepository<User>>();
mockRepository.Entities.Returns(mockSet);
_userRepository = mockRepository;

[Test]
public async Task GetUserByUserName_ValidUserName_Return1User()
{
    var sut = new UserService(_userRepository);
    var user = await sut.GetUserByUserNameAsync("123456789");
    Assert.AreEqual(_user3, user);
}
Run Code Online (Sandbox Code Playgroud)

回到原来的问题

为了从缓存中检索数据,我使用这种方法。当我们从内存中检索数据时,通常不会异步。

在官方文档中我们可以看到 EF 上下文不是线程安全的,所以我应该在 GetOrCreate 中返回后添加等待?

异步 需要await关键字(不是因为DbContext不是线程安全的)

将其设置为单例服务并在操作和剃刀视图中使用它是否合适?

这取决于你如何使用它。但是,我们不应该使用单例服务来实现每个请求的依赖。

例如,我使用这个单例对象来存储在应用程序生命周期中永远不会改变的设置和配置。

在官方文档中我们可以读到:避免仅存在以允许访问 DI 服务中的某些其他对象的“数据持有者”对象。当我看到我的服务时,我很担心这条线。

将整个 Article 表存储到缓存中并不常见。

如果在服务器内存中存储太多数据,服务器最终将抛出内存不足异常。在某些情况下,它甚至比从数据库中查询单个记录还要慢。