Asp.Net 核心健康检查并发 Entity Framework DbContext 错误

Asp.Net Core HealthChecks Concurrent Entity Framework DbContext Errors

我读到这个:https://github.com/dotnet/aspnetcore/issues/14453 看来我也遇到了同样的问题。

我的解决方案中有大约 50 个项目,每个项目都有自己的 RegisterServices.cs,它定义了数据库上下文

 services.AddDbContextPool<Db.ACME.Context>(
          options => options.UseSqlServer(configuration.GetConnectionString("ACME"))); 

在那里我还添加了健康检查,但是一旦我添加了超过 1 个,例如在项目“PROJECTS”中,一个用于检查重复的项目名称,一个用于检查重复的任务名称。

services.AddHealthChecks()
           .AddCheck<DuplicateTaskNamesHealthCheck>("Duplicate Task Names", HealthStatus.Unhealthy,
               tags: new[] { "org_project" });

        services.AddHealthChecks()
           .AddCheck<DuplicateProjectNamesHealthCheck>("Duplicate Project Names", HealthStatus.Unhealthy,
               tags: new[] { "org_project" });

Healthcheck 只是调用服务“projectservice”和另一个“taskservice”(都在同一个 vs 解决方案项目中)

它会因已知的原因而失败

A second operation was started on this context before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.

Healthchecks 本身不包含任何逻辑,只是调用逻辑(EF linq 查询)所在的特定服务。当我评论其中一个时,它们会起作用。

似乎唯一的解决方案是从健康检查内部的服务中复制功能,但令人不安的是这并不是很方便。健康检查越多,越多的方法将被复制过来,DRY 超出了 window.

还有其他已知的解决方法吗?


在项目服务中调用失败的方法示例

public async Task<List<string>> GetDuplicateProjectNamesAsync()
    {
        IQueryable<IGrouping<string, PROJECT>> DuplicateProjectNames = _context
            .PROJECT
            .AsNoTracking()
            .GroupBy(x => x.PROJECTNAME)
            .Where(x => x.Count() > 1);
        
        /* next line fails */
        var hasItems = await DuplicateProjectNames.AnyAsync();

        if (hasItems)
        {
            return await DuplicateProjectNames.Select(x=>x.Key).ToListAsync();
        }
        else 
        { 
            return new List<string>();
        }
    }

健康检查示例

 namespace ACME.Org.Project.Healthchecks
 { 
public class DuplicateProjectNamesHealthCheck : IHealthCheck
{
    public IACME6_PROJECT _ACME6_PROJECT;
    public Settings _settings;

    public DuplicateProjectNamesHealthCheck(IACME6_PROJECT PROJECT, IOptions<Settings> settings)
    {
        _ACME6_PROJECT = PROJECT;
        _settings = settings.Value;
    }

    public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
    {
        List<string> duplicateProjectNames = await _ACME6_PROJECT.GetDuplicateProjectNamesAsync();

        if (duplicateProjectNames.Count > 0)
        {
            if (_settings.AutoFixDuplicateProjectNames)
            {                    
                await _ACME6_PROJECT.FixDuplicateProjectNamesAsync();                    
            }
            else
            {
                string errorMessage = "Duplicate Project Names found: ";
                foreach (string projectName in duplicateProjectNames)
                {
                    errorMessage += projectName + ") |";
                }
                errorMessage += "To autofix this set AutoFixDuplicateProjectNames to true in settings.";
                return new HealthCheckResult(status: context.Registration.FailureStatus, errorMessage);
            }
        }
        return HealthCheckResult.Healthy("OK");
    }
}

}


和带注释的行(见上文)236 完整的错误消息要求:

System.InvalidOperationException: A second operation was started on this context before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
   at Microsoft.EntityFrameworkCore.Internal.ConcurrencyDetector.EnterCriticalSection()
   at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.MoveNextAsync()
   at Microsoft.EntityFrameworkCore.Query.ShapedQueryCompilingExpressionVisitor.SingleAsync[TSource](IAsyncEnumerable`1 asyncEnumerable, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Query.ShapedQueryCompilingExpressionVisitor.SingleAsync[TSource](IAsyncEnumerable`1 asyncEnumerable, CancellationToken cancellationToken)
   at ACME.Org.Project.Services.ACME6.ACME6_PROJECT.GetDuplicateProjectNamesAsync() in E:\ACME\repo\acme-7-api\ACME.Org.Project\Services\ACME6\ACME6_PROJECT\ACME6_PROJECT.cs:line 236
   at ACME.Org.Project.Healthchecks.DuplicateProjectNamesHealthCheck.CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken) in E:\ACME\repo\acme-7-api\ACME.Org.Project\Healthchecks\DuplicateProjectNamesHealthCheck.cs:line 23
   at Microsoft.Extensions.Diagnostics.HealthChecks.DefaultHealthCheckService.RunCheckAsync(IServiceScope scope, HealthCheckRegistration registration, CancellationToken cancellationToken)
Microsoft.Extensions.Diagnostics.HealthChecks.DefaultHealthCheckService: Error: Health check "Duplicate Project Names" threw an unhandled exception after 562.888ms

原因是AddCheckwill create and use a single instance of the healthcheck type

return builder.Add(new HealthCheckRegistration(name,
     s => ActivatorUtilities.GetServiceOrCreateInstance<T>(s), 
     failureStatus, tags, timeout));

ActivatorUtilities.GetServiceOrCreateInstance<T>(s) 将尝试从 DI 中检索该类型的实例或创建一个新实例。在任何一种情况下,这都意味着 DuplicateTaskNamesHealthCheck 表现为单例,任何注入其中的 DbContext 实例将保持活动状态直到关闭。

为了解决这个问题,健康检查的构造函数应该接受 IServiceProviderIDbContextFactory 并在需要时创建上下文实例。

例如:

public class ExampleHealthCheck : IHealthCheck
{
   private readonly IDbContextFactory<ApplicationDbContext> _contextFactory;

   public ExampleHealthCheck(IDbContextFactory<ApplicationDbContext> contextFactory)
   {
       _contextFactory = contextFactory;
   }

   public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context,
        CancellationToken cancellationToken = default(CancellationToken))
   {
       using (var context = _contextFactory.CreateDbContext())
       {
           // ...
       }
   }
}

IServiceProvider 应该用于使用像 IACME6_PROJECT 这样的作用域或瞬态服务。 IServiceProvider 可用于直接检索瞬态服务或创建范围以使用范围服务:

public class DuplicateProjectNamesHealthCheck : IHealthCheck
{
    public IServiceProvider _serviceProvider;
    public Settings _settings;

    public DuplicateProjectNamesHealthCheck(IServiceProvider serviceProvider, IOptions<Settings> settings)
    {
        _serviceProvider=serviceProvider;
        _settings = settings.Value;
    }

    public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context,
        CancellationToken cancellationToken = default(CancellationToken))
    {
        using (var scope = _serviceProvider.CreateScope())
        {
            var project = scope.ServiceProvider.GetRequiredService< IACME6_PROJECT>();

            var duplicateProjectNames = await _ACME6_PROJECT.GetDuplicateProjectNamesAsync();
            ...
        }
    }