[英]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();
}
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.