我真的想制作一个漂亮,干净和正确的代码,所以我几乎没有什么基本问题。一开始我有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();
}
await
返回后添加GetOrCreate
? Singleton
服务并在操作和剃刀视图中使用它是否合适?答案 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 。
async 需要在官方文档中,我们可以读到EF上下文不是线程安全的,所以我 应该在GetOrCreate返回后添加等待吗?
等待 关键字(不是因为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类中的文章列表中获取第一篇文章名称。但话说回来,这只是一个建议,不应该被视为一项艰难的规则。