如果我的上下文已处理,在收到来自 Azure 服务总线 listener/consumer 的消息后如何使用 EF Core?

How to use EF Core after I've received a message from my Azure Service bus listener/consumer, if my context is disposed?

我有一个网站 Angular 前端和后端的 WebAPI 以及我所有的控制器,我还有一个服务 (C# class),我将其作为单例调用为 long 运行 侦听传入的 Azure 服务总线消息的任务。

仅供参考 - 我无法将任何作用域服务 (DbContext) 传递给单例 (ServiceBusConsumer),因此我无法将我的数据库上下文传递给此服务。

问题 - 一旦我收到传入的服务总线消息,我该如何调用我的数据库并使用它?

这是我的服务监听和接收消息。

Startup.cs

services.AddSingleton<IServiceBusConsumer, ServiceBusConsumer>();

Program.cs -> 在 Main() 中启动服务

var bus = services.GetRequiredService<IServiceBusConsumer>();
bus.RegisterOnMessageHandlerAndReceiveMessages();

ServiceBusConsumer.cs

public class ServiceBusConsumer : IServiceBusConsumer
{
    private readonly IConfiguration _config;
    private readonly ServiceBusClient _queueClient;
    private readonly ServiceBusProcessor _processor;

    // private readonly DataContext _context;

    public ServiceBusConsumer(IConfiguration config,
    // DataContext context)
    {
        _config = config;
        // _context = context;
        _queueClient = new ServiceBusClient(_config["ServiceBus:Connection"]);
        _processor = _queueClient.CreateProcessor(_config["ServiceBus:Queue"], new ServiceBusProcessorOptions());
    }

    public void RegisterOnMessageHandlerAndReceiveMessages() {
        _processor.ProcessMessageAsync += MessageHandler;
        _processor.ProcessErrorAsync += ErrorHandler;
        _processor.StartProcessingAsync();
    }

    private async Task MessageHandler(ProcessMessageEventArgs args)
    {
        string body = args.Message.Body.ToString();
        JObject jsonObject = JObject.Parse(body);
        var eventStatus = (string)jsonObject["EventStatus"];

        await args.CompleteMessageAsync(args.Message);

        // _context is disposed 
        // want to connect to DB here but don't know how!
        // var ybEvent = _context.YogabandEvents.Where(p => p.ServiceBusSequenceNumber == args.Message.SequenceNumber).FirstOrDefault();

    }

    private Task ErrorHandler(ProcessErrorEventArgs args)
    {
        var error = args.Exception.ToString();
        return Task.CompletedTask;
    }
}

错误

Cannot access a disposed context instance. A common cause of this error is disposing a context instance that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling 'Dispose' on the context instance, or wrapping it in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances.\nObject name: 'DataContext'.

这里是Program.cs

public class Program
{
    public static async Task Main(string[] args)
    {
        var host = CreateHostBuilder(args).Build();
        using (var scope = host.Services.CreateScope())
        {
            var services = scope.ServiceProvider;
            var loggerFactory = services.GetRequiredService<ILoggerFactory>();
            try 
            {
                var context = services.GetRequiredService<DataContext>();

                
                var userManager = services.GetRequiredService<UserManager<User>>();
                var roleManager = services.GetRequiredService<RoleManager<Role>>();


                var bus = services.GetRequiredService<IServiceBusConsumer>();
                bus.RegisterOnMessageHandlerAndReceiveMessages();
                
            }
            catch (Exception ex)
            {
                var logger = loggerFactory.CreateLogger<Program>();
                logger.LogError(ex, "An error occured during migration");
            }
        }

        host.Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

这里是 Startup.cs -> 只是 ConfigureServices 方法

public void ConfigureServices(IServiceCollection services)
    {
        services.AddAutoMapper(typeof(MappingEvents));
        services.AddAutoMapper(typeof(MappingMembers));
        services.AddAutoMapper(typeof(MappingUsers));
        services.AddAutoMapper(typeof(MappingYogabands));
        services.AddAutoMapper(typeof(MappingReviews));

        // objects being passed back to the UI. Before I was passing User/Photo/etc and they 
        // had loops/refrences back to the user objects
        services.AddControllers().AddNewtonsoftJson(opt => 
        {
            opt.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Error;
        });


        services.AddDbContext<DataContext>(x =>
            // x.UseSqlite(_config.GetConnectionString("DefaultConnection"), y => y.UseNetTopologySuite()));
            x.UseSqlServer(_config.GetConnectionString("SqlServerConnection"), y => y.UseNetTopologySuite()));


        services.Configure<AuthMessageSenderOptions>(_config.GetSection("SendGrid"));
        services.Configure<AuthMessageSenderOptionsNew>(_config.GetSection("SendGrid"));
        services.Configure<ConfirmationOptions>(_config.GetSection("Confirmation"));

        services.Configure<CloudinarySettings>(_config.GetSection("CloudinarySettings"));
        
        services.AddApplicationServices();
        services.AddIdentityServices(_config);
        services.AddSwaggerDocumentation();
        
        services.AddCors(opt => 
        {
            opt.AddPolicy("CorsPolicy", policy => 
            {
                policy.AllowAnyHeader().AllowAnyMethod().WithOrigins("https://localhost:4200");
            });
        });
    }

这里是 AddApplicationServices()

public static IServiceCollection AddApplicationServices(this IServiceCollection services)
    {
        // scoped - better option when you want to maintain state within a request
        // services.AddScoped<IEventConsumer, EventConsumer>();
        services.AddScoped<IServiceBusProducer, ServiceBusProducer>();
        services.AddSingleton<IServiceBusConsumer, ServiceBusConsumer>();
        services.AddScoped<IEmailSender, EmailSender>();
        services.AddScoped<IEmailSender, EmailSenderNew>();

        services.AddScoped<IEmailService, EmailService>();
        services.AddScoped<ITokenService, TokenService>();
        services.AddScoped<IUnitOfWork, UnitOfWork>();
        services.AddScoped(typeof(IGenericRepository<>), (typeof(GenericRepository<>)));
        services.AddScoped<LogUserActivity>();

        services.Configure<ApiBehaviorOptions>(options => 
        {
            options.InvalidModelStateResponseFactory = actionContext => 
            {
                var errors = actionContext.ModelState
                .Where(e => e.Value.Errors.Count > 0)
                .SelectMany(x => x.Value.Errors)
                .Select(x => x.ErrorMessage).ToArray();
                
                var errorResponse = new ApiValidationErrorResponse 
                {
                    Errors = errors
                };

                return new BadRequestObjectResult(errorResponse);
            };
        });

        return services;
    }

看来你的问题出在 DI 上。 您的 ServiceBusConsumer 服务是单例,但您注入了一个 DbContext 作为构造函数。这通常是建议,但在这种情况下,它不起作用。
您在构造函数中注入一个 DbContext 并向其“保存”一个“link”。但随后它被处理掉,因此“link”将不起作用。

相反,您应该注入 DbContextFactory。使用工厂,您可以按需创建 DbContext 实例。

private readonly IDbContextFactory<DataContext> _contextFactory;

public ServiceBusConsumer(IConfiguration config, IDbContextFactory<DataContext> contextFactory)
{
     // Add this line
     _contextFactory = contextFactory;
}

private async Task MessageHandler(ProcessMessageEventArgs args)
{
    // With the new C# 8 syntax you can do
    using var db = _contextFactory.CreateDbContext();
    // Otherwise, wrap it up
    using (var db = _contextFactory.CreateDbContext())
    {
    }
}

这是一个 link 文档,其中显示了如何使用它:https://docs.microsoft.com/en-us/ef/core/dbcontext-configuration/#using-a-dbcontext-factory-eg-for-blazor

您只需注册即可:

public void ConfigureServices(IServiceCollection services)
{
    // Add this line to register a context factory
    services.AddDbContextFactory<DataContext>(
        options =>
            .UseSqlServer(_config.GetConnectionString("SqlServerConnection"), y => y.UseNetTopologySuite()));
}

您不能使用与控制器相同的 DI,因为它们通常不是单例,因此不会 运行 进入这个问题。据我所知,DbContextFactory 正是为此目的而创建的(考虑到 Blazor)。如果您需要的服务不是 DbContext,您需要在构造函数中注入服务提供者,然后直接请求服务,尽管 Microsoft 不建议这样做。

我解决了同样的问题,避免使用 using 语句,而是在 Main 任务中声明范围变量。你想保持为队列消息处理程序创建的范围,所以你的 Program.cs 应该是这样的:

public class Program
{
    public static async Task Main(string[] args)
    {
        var host = CreateHostBuilder(args).Build();
        
        // This variable is working within ServiceBus threads so it need to bee keept alive until Main ends
        var scope = host.Services.CreateScope();
                
        var services = scope.ServiceProvider;
        var loggerFactory = services.GetRequiredService<ILoggerFactory>();
        try 
        {
            var context = services.GetRequiredService<DataContext>();
            
            var userManager = services.GetRequiredService<UserManager<User>>();
            var roleManager = services.GetRequiredService<RoleManager<Role>>();

            var bus = services.GetRequiredService<IServiceBusConsumer>();
            bus.RegisterOnMessageHandlerAndReceiveMessages();
            
        }
        catch (Exception ex)
        {
            var logger = loggerFactory.CreateLogger<Program>();
            logger.LogError(ex, "An error occured during migration");
        }

        host.Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}