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

时间:2017-03-22 13:32:00

标签: c# dependency-injection asp.net-core-mvc .net-core entity-framework-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();
}
  1. official docs中,我们可以读到EF上下文不是线程安全的,所以我应该在await返回后添加GetOrCreate
  2. 将其设置为Singleton服务并在操作和剃刀视图中使用它是否合适?
  3. official docs中,我们可以阅读:避免&#34;数据持有者&#34;仅存在以允许访问DI服务中的某些其他对象的对象。当我看到我的服务时,我担心这条线。

3 个答案:

答案 0 :(得分:3)

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

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

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

您的方法仍然不正确,因为您现在保存在缓存中的内容不是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();
});

请注意,如果项目已存在于缓存中 - 将不会执行异步工作,整个操作将完全同步执行(不要让await欺骗您 - 如果您await某事必要意味着会涉及一些后台线程或异步操作。

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

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

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

答案 1 :(得分:2)

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

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

请原谅我使用我的样本GitHub项目,因为它很容易用实际代码解释而不是用文字解释。

<强> Repository

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

<强> Usage of Repository in Service

然后我通过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();
}

<强> Unit Test Helper

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

如果您使用moq或其他单元测试框架,您可能需要阅读Entity Framework Testing with a Mocking Framework (EF6 onwards)

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

<强> Unit Test

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

返回原始问题

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

  

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

async 需要

等待 关键字(不是因为DbContext不是线程安全的)< / em>的

  

将它设置为Singleton服务并使用它是否合适   行动和剃刀观点?

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

例如,我使用this singleton object来存储应用程序生命周期中永远不会改变的设置和配置。

  

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

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

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

答案 2 :(得分:2)

  

在官方文档中,我们可以读到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 core docs的警告:&#34;要警惕的主要危险是解决单身人士的Scoped服务问题。在处理后续请求时,服务可能会出现错误状态。&#34;

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

    services.AddTransient<MyService>();

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

    // ...
}
  

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

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