繁体   English   中英

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

[英]DI Service with proper cache and dbContext usage

我真的想制作一个漂亮,干净和正确的代码,所以我几乎没有什么基本问题。 一开始我有一个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();
}
  1. 官方文档中,我们可以读到EF上下文不是线程安全的,所以我应该在GetOrCreate返回后添加await
  2. 是否适合将其设置为Singleton服务并在操作和剃刀视图中使用它?
  3. 官方文档中,我们可以阅读: 避免仅存在的“数据持有者”对象以允许访问 DI服务中的某些其他对象 当我看到我的服务时,我担心这条线。

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

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

因此,每个请求都有单独的上下文(如果它是使用作用域生存期注册的),所以你不应该(通常,如果你不自己生成后台线程),那么就不必考虑对该实例的多线程访问了。

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

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

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

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

将此类服务注册为singleton是不合适的,因为它依赖于具有作用域生存期的EF上下文(默认情况下)。 使用与EF上下文相同的生命周期注册它。

至于避免“数据持有者”对象 - 我不知道它在这里是如何相关的。 您的服务具有逻辑(从缓存数据库获取数据) - 它不存在允许访问其他一些对象。

有人认为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>();
   }
}

在服务中使用存储库

然后我通过Constructor Injection将存储库注入服务类。

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();
}

单元测试助手

模拟单元测试的异步方法有点棘手。 在我的情况下,我使用NSubstitute ,所以我使用以下帮助方法NSubstituteHelper

如果您使用moq或其他单元测试框架,您可能希望使用Mocking Framework(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;
}

单元测试

// 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);
}

回到原始问题

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

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

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

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

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

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

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

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

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

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

这不是EF上下文不是线程安全的问题。 当您调用异步方法时,您应该添加await

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

    var article = await articleList.Where(c => c.Title == title).FirstOrDefaultAsync();

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

    return article.Name();
}

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

我不认为将您的服务设置为Singleton是合适的,因为您的服务依赖于具有Scoped服务生命周期的_dbContext 如果您将服务注册为Singleton,那么您应该只注入一个Singleton(并且_dbContext显然不是Singleton ...它的Scoped)。 使用Transient服务生命周期设置服务可能更好。 请注意来自asp.net核心文档的警告: “要警惕的主要危险是从单例解析Scoped服务。在这种情况下,服务处理后续请求时可能会出现错误的状态。”

public void ConfigureServices(IServiceCollection services)
{
    // ...

    services.AddTransient<MyService>();

    services.AddDbContext<MyDbContext>(ServiceLifetime.Scoped);

    // ...
}

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

我能理解你的担忧。 当我看到GetName ,我不明白为什么你不能从Controller类中的文章列表中获取第一篇文章名称。 但话说回来,这只是一个建议,不应该被视为一个硬性规则。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM