Entity Framework 查询在多次请求后抛出 'async error'
Entity Framework query throws 'async error' after many requests
在我使用 .NET Framework 4.6.1、EF 6.1.4 和 IdentityServer3 的项目中,我设置了以下 DbContext:
public class ValueContext : DbContext
{
public IValueContext(bool lazyLoadingEnabled = false) : base("MyConnectionString")
{
Database.SetInitializer<IValueContext>(null);
Configuration.LazyLoadingEnabled = lazyLoadingEnabled;
}
public DbSet<NetworkUser> NetworkUser { get; set; }
public DbSet<User> User { get; set; }
[...]
还有我的实体模型User
:
[Table("shared.tb_usuarios")]
public class NetworkUser
{
[Column("id")]
[Key()]
public int Id { get; set; }
[Required]
[StringLength(255)]
[Column("email")]
public string Email { get; set; }
[...]
public virtual Office Office { get; set; }
[...]
到目前为止,我认为一切都很好。
然后我在 UserRepository
(使用 DI)
中设置以下查询
protected readonly ValueContext Db;
public RepositoryBase(ValueContext db)
{
Db = db;
}
public async Task<ImobUser> GetUser(string email)
{
//sometimes I get some error here
return await Db.User.AsNoTracking()
.Include(im => im.Office)
.Include(off => off.Office.Agency)
.Where(u => u.Email == email &&
u.Office.Agency.Active)
.FirstOrDefaultAsync();
}
一切 运行 都很好,直到它开始收到 许多顺序请求 ,然后我开始收到这些类型的错误,随机出现在任何使用我的 ValueContext
作为数据源:
System.NotSupportedException: 'A second operation started on this context before a previous asynchronous operation completed. Use 'await' to ensure that any asynchronous operations have completed before calling another method on this context. Any instance members are not guaranteed to be thread safe.'
这是我最后的希望,因为我尝试了很多不同的东西。其中有些有效,有些无效,例如:
- 将 dbContext 转换为使用 DI:没有区别。
- 使用 context lifetime 来 运行 查询:有效,但不是我想要的解决方案。
- 从请求中删除异步:有效,但我觉得这不是正确的方法。
我做错了什么?
编辑 1
这就是我在 Startup.cs
中设置 DI 的方式:
private void AddAuth()
{
Builder.Map("/identity", app =>
{
var factory = new IdentityServerServiceFactory()
{
//here I implemented the IdentityServer services to work
ClientStore = new Registration<IClientStore>(typeof(ClientStore)),
[...]
};
AddDependencyInjector(factory);
}
[...]
}
private void AddDependencyInjector(IdentityServerServiceFactory factory)
{
//here I inject all the services I need, as my DbContext
factory.Register(new Registration<ValueContext>(typeof(ValueContext)));
[...]
}
这就是我的 UserService
的工作方式:
public class UserService : IUserService
{
[Service injection goes here]
//this is a identityServer method using my dbContext implementation on UserRepository
public async Task AuthenticateLocalAsync(LocalAuthenticationContext context)
{
SystemType clientId;
Enum.TryParse(context.SignInMessage.ClientId, true, out clientId);
switch (clientId)
{
case 2:
result = await _userService.GetUser(context.UserName);
break;
case 3:
//also using async/await correctly
result = await _userService.Authenticate(context.UserName, context.Password);
break;
default:
result = false;
break;
}
if (result)
context.AuthenticateResult = new AuthenticateResult(context.UserName, context.UserName);
}
更新 - 代码发布后
当一起使用 ASP.Net DI 和 IdentityServer DI 时,我们必须小心确保 IdentityServer 和底层 DbContext 都在 OWIN 请求上下文范围内,我们通过将 DbContext 注入到IdentityServer 上下文。这个答案有一些有用的背景:
我怀疑您需要做的就是解析 DbContext,而不是显式实例化它:
private void AddDependencyInjector(IdentityServerServiceFactory factory)
{
//here I inject all the services I need, as my DbContext
factory.Register(new Registration<ValueContext>(resolver => new ValueContext()));
[...]
}
支持讨论,现在基本上无关紧要...
对于 EF,重要的是要确保没有同时对同一个 DbContext
实例的并发查询。即使您为此端点指定了 AsNoTracking()
,也没有迹象表明该端点实际上是罪魁祸首。 同步的原因是上下文可以管理原始状态,有许多内部结构根本不是为多个并发查询设计的,包括数据库连接和事务的管理方式。
(在幕后,DbContext 将汇集并重新使用到数据库的连接(如果它们可用),但是 ADO.Net 为我们做了这件事,它发生在较低级别,所以是 NOT 维护单例 DbContext 的参数)
As a safety precaution, the context will actively block any attempts to re-query while an existing query is still pending.
EF 实现了 Unit-Of-Work 模式,您只需要为当前操作维护相同的上下文,并在完成后处理它。为单个方法实例化一个 DbContext
作用域是完全可以接受的,如果你需要它们,你可以实例化多个上下文。
There is some anecdotal advice floating around the web based on previous versions of EF that suggest there is a heavy initialization sequence when you create the context and so they encourage the singleton use of the EF context. This advice worked in non-async environments like WinForms apps, but it was never good advice for entity framework.
在基于 HTTP 的服务架构中使用 EF 时,正确的模式是为每个 HTTP 请求创建一个新的上下文,而不是尝试在请求之间维护上下文或状态。如果你愿意,你可以在每个方法中手动执行此操作,但是 DI 可以帮助最小化管道代码,只需确保 HTTP 请求获得一个 new 实例,而不是共享或回收了一个。
Because most client-side programming can create multiple concurrent HTTP requests (this of a web site, how many concurrent requests might go to the same server for a single page load) it is a frivolous exercise to synchronise the incoming requests, or introduce a blocking pattern to ensure that the requests to the DbContext
are synchronous or queued.
创建新上下文实例的开销预计最小,并且 DbContext 预计以这种方式使用,尤其是用于 HTTP 服务实现,所以不要试图对抗 EF 运行时,使用它。
存储库和 EF
当您在 EF 之上使用存储库模式时...(IMO 本身就是一个反模式)重要的是,存储库的每个新实例都有自己唯一的 DbContext
实例。如果您改为在 Repo init 逻辑中从头开始创建 DbContext 实例,则您的 repo 应该具有相同的功能。传入上下文的唯一原因是让 DI 或其他通用例程为您预先创建 DbContext 实例。
Once the DbContext instance is passed into the Repo, we lose the ability to maintain synchronicity of the queries against it, this is an easy pain point that should be avoided.
No amount of await
or using synchronous methods on the DbContext will help you if multiple repos are trying to service requests at the same time against the same DbContext
.
在我使用 .NET Framework 4.6.1、EF 6.1.4 和 IdentityServer3 的项目中,我设置了以下 DbContext:
public class ValueContext : DbContext
{
public IValueContext(bool lazyLoadingEnabled = false) : base("MyConnectionString")
{
Database.SetInitializer<IValueContext>(null);
Configuration.LazyLoadingEnabled = lazyLoadingEnabled;
}
public DbSet<NetworkUser> NetworkUser { get; set; }
public DbSet<User> User { get; set; }
[...]
还有我的实体模型User
:
[Table("shared.tb_usuarios")]
public class NetworkUser
{
[Column("id")]
[Key()]
public int Id { get; set; }
[Required]
[StringLength(255)]
[Column("email")]
public string Email { get; set; }
[...]
public virtual Office Office { get; set; }
[...]
到目前为止,我认为一切都很好。
然后我在 UserRepository
(使用 DI)
protected readonly ValueContext Db;
public RepositoryBase(ValueContext db)
{
Db = db;
}
public async Task<ImobUser> GetUser(string email)
{
//sometimes I get some error here
return await Db.User.AsNoTracking()
.Include(im => im.Office)
.Include(off => off.Office.Agency)
.Where(u => u.Email == email &&
u.Office.Agency.Active)
.FirstOrDefaultAsync();
}
一切 运行 都很好,直到它开始收到 许多顺序请求 ,然后我开始收到这些类型的错误,随机出现在任何使用我的 ValueContext
作为数据源:
System.NotSupportedException: 'A second operation started on this context before a previous asynchronous operation completed. Use 'await' to ensure that any asynchronous operations have completed before calling another method on this context. Any instance members are not guaranteed to be thread safe.'
这是我最后的希望,因为我尝试了很多不同的东西。其中有些有效,有些无效,例如:
- 将 dbContext 转换为使用 DI:没有区别。
- 使用 context lifetime 来 运行 查询:有效,但不是我想要的解决方案。
- 从请求中删除异步:有效,但我觉得这不是正确的方法。
我做错了什么?
编辑 1
这就是我在 Startup.cs
中设置 DI 的方式:
private void AddAuth()
{
Builder.Map("/identity", app =>
{
var factory = new IdentityServerServiceFactory()
{
//here I implemented the IdentityServer services to work
ClientStore = new Registration<IClientStore>(typeof(ClientStore)),
[...]
};
AddDependencyInjector(factory);
}
[...]
}
private void AddDependencyInjector(IdentityServerServiceFactory factory)
{
//here I inject all the services I need, as my DbContext
factory.Register(new Registration<ValueContext>(typeof(ValueContext)));
[...]
}
这就是我的 UserService
的工作方式:
public class UserService : IUserService
{
[Service injection goes here]
//this is a identityServer method using my dbContext implementation on UserRepository
public async Task AuthenticateLocalAsync(LocalAuthenticationContext context)
{
SystemType clientId;
Enum.TryParse(context.SignInMessage.ClientId, true, out clientId);
switch (clientId)
{
case 2:
result = await _userService.GetUser(context.UserName);
break;
case 3:
//also using async/await correctly
result = await _userService.Authenticate(context.UserName, context.Password);
break;
default:
result = false;
break;
}
if (result)
context.AuthenticateResult = new AuthenticateResult(context.UserName, context.UserName);
}
更新 - 代码发布后
当一起使用 ASP.Net DI 和 IdentityServer DI 时,我们必须小心确保 IdentityServer 和底层 DbContext 都在 OWIN 请求上下文范围内,我们通过将 DbContext 注入到IdentityServer 上下文。这个答案有一些有用的背景:
我怀疑您需要做的就是解析 DbContext,而不是显式实例化它:
private void AddDependencyInjector(IdentityServerServiceFactory factory)
{
//here I inject all the services I need, as my DbContext
factory.Register(new Registration<ValueContext>(resolver => new ValueContext()));
[...]
}
支持讨论,现在基本上无关紧要...
对于 EF,重要的是要确保没有同时对同一个 DbContext
实例的并发查询。即使您为此端点指定了 AsNoTracking()
,也没有迹象表明该端点实际上是罪魁祸首。 同步的原因是上下文可以管理原始状态,有许多内部结构根本不是为多个并发查询设计的,包括数据库连接和事务的管理方式。
(在幕后,DbContext 将汇集并重新使用到数据库的连接(如果它们可用),但是 ADO.Net 为我们做了这件事,它发生在较低级别,所以是 NOT 维护单例 DbContext 的参数)
As a safety precaution, the context will actively block any attempts to re-query while an existing query is still pending.
EF 实现了 Unit-Of-Work 模式,您只需要为当前操作维护相同的上下文,并在完成后处理它。为单个方法实例化一个 DbContext
作用域是完全可以接受的,如果你需要它们,你可以实例化多个上下文。
There is some anecdotal advice floating around the web based on previous versions of EF that suggest there is a heavy initialization sequence when you create the context and so they encourage the singleton use of the EF context. This advice worked in non-async environments like WinForms apps, but it was never good advice for entity framework.
在基于 HTTP 的服务架构中使用 EF 时,正确的模式是为每个 HTTP 请求创建一个新的上下文,而不是尝试在请求之间维护上下文或状态。如果你愿意,你可以在每个方法中手动执行此操作,但是 DI 可以帮助最小化管道代码,只需确保 HTTP 请求获得一个 new 实例,而不是共享或回收了一个。
Because most client-side programming can create multiple concurrent HTTP requests (this of a web site, how many concurrent requests might go to the same server for a single page load) it is a frivolous exercise to synchronise the incoming requests, or introduce a blocking pattern to ensure that the requests to the
DbContext
are synchronous or queued.
创建新上下文实例的开销预计最小,并且 DbContext 预计以这种方式使用,尤其是用于 HTTP 服务实现,所以不要试图对抗 EF 运行时,使用它。
存储库和 EF
当您在 EF 之上使用存储库模式时...(IMO 本身就是一个反模式)重要的是,存储库的每个新实例都有自己唯一的 DbContext
实例。如果您改为在 Repo init 逻辑中从头开始创建 DbContext 实例,则您的 repo 应该具有相同的功能。传入上下文的唯一原因是让 DI 或其他通用例程为您预先创建 DbContext 实例。
Once the DbContext instance is passed into the Repo, we lose the ability to maintain synchronicity of the queries against it, this is an easy pain point that should be avoided.
No amount ofawait
or using synchronous methods on the DbContext will help you if multiple repos are trying to service requests at the same time against the sameDbContext
.