2

I have a website Angular frontend and WebAPI on the backend with all my controllers, I also have a service (C# class) that I call as a singleton as a long running task to listen for incoming Azure service bus messages.

FYI - I can't pass any scoped services (DbContext) to a singleton (ServiceBusConsumer), so I can't pass in my DB context to this service.

QUESTION - Once I receive an incoming service bus message, how do I call up my DB and use it?

Here is my service listening for and receiving messages.

Startup.cs

services.AddSingleton<IServiceBusConsumer, ServiceBusConsumer>();

Program.cs -> in Main() I start the service

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;
    }
}

Error

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'.

Here is 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>();
            });
}

Here is Startup.cs -> just the ConfigureServices method

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");
            });
        });
    }

Here is 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;
    }
chuckd
  • 13,460
  • 29
  • 152
  • 331

2 Answers2

3

It seems your problem is with DI. Your ServiceBusConsumer service is a singleton but you inject a DbContext as a constructor. This is generally the recommendation but in this case, it can't work.
You inject a DbContext in the constructor and "save" a "link" to it. But then it gets disposed, so that "link" won't work.

Instead, you should inject a DbContextFactory. With a factory, you can create DbContext instances on demand.

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())
    {
    }
}

Here's a link to a docs where they show how it can be used: https://learn.microsoft.com/en-us/ef/core/dbcontext-configuration/#using-a-dbcontext-factory-eg-for-blazor

You just need to register it:

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

You can't use the same DI as with controllers, since they're usually not singletons, therefore won't run into this problem. AFAIK the DbContextFactory was created exactly for this purpose (with Blazor in mind). If the service you needed was not a DbContext you would need to inject the service provider in the constructor and then request the service directly, although Microsoft doesn't recommend that.

mitiko
  • 751
  • 1
  • 8
  • 20
  • Hi Mitko. Just a quick follow up, can this solution also be used with a generic repository and a unit of work pattern? I have a similar post here - https://stackoverflow.com/questions/67579462/dispose-context-instance-error-when-trying-to-connect-to-my-db-after-azure-servi – chuckd May 21 '21 at 23:25
  • 1
    Hi Mitko. Just another quick question - Is .AddDbContextFactory or .AddDbContextFactory? Does your code have a typo? – chuckd May 22 '21 at 03:52
  • Oops, yeah, it should be the same in both places, I just copied the code from the docs in the first sample. I'll take a look at the other question, it looks like the same problem but you don't have a built-in written factory. – mitiko May 22 '21 at 19:17
-1

I solved same problem avoiding using statment, instead declare scope variable within Main task. You want to keep alive the scope created for queue message handlers so your Program.cs should be like this:

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>();
            });
}
Sergio M.
  • 88
  • 1
  • 7
  • Hi Sergio. This doesn't work, as I receive the error message on the line CreateHostBuilder(args).Build() that says: "An exception of type 'System.AggregateException' occurred in Microsoft.Extensions.DependencyInjection.dll but was not handled in user code: 'Some services are not able to be constructed' Inner exceptions found, see $exception in variables window for more details. Innermost exception System.InvalidOperationException : Cannot consume scoped service 'Infrastructure.Data.DataContext' from singleton 'Core.Interfaces.IServiceBusConsumer'." – chuckd Sep 25 '21 at 03:57
  • @user1186050 It seems like Infrastructure.Data.DataContext is not being registered in dependecy injection. ServiceBusConsumer needs DataContext and it is not being able to resolve its dependecy. – Sergio M. Sep 30 '21 at 06:54