chuckd
chuckd

Reputation: 14520

Dispose context instance error when trying to connect to my DB after Azure service bus message is consumed

I'm listening for an incoming Azure service bus message. Following the documentation and receiving the message, I parse the message body and then I want to connect to my DB to edit an entry and then save. But I'm getting this error below when trying to make the call

var ybEvent = await _unitOfWork.Repository<YogabandEvent>().GetEntityWithSpec(spec);

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 the full service with the method that listens for and picks up incoming Azure messages. Error is on the last line of MessageHandler()

FYI - If I remove the 'await' on the DB call, I still get the same error for a disposed context.

QUESTION - how do I fix this?

public class ServiceBusConsumer : IServiceBusConsumer
{
    private readonly IConfiguration _config;
    private readonly ServiceBusClient _queueClient;
    private readonly ServiceBusProcessor _processor;
    private readonly IUnitOfWork _unitOfWork;
    private readonly IEventConsumer _eventConsumer;

    public ServiceBusConsumer(IConfiguration config, IEventConsumer eventConsumer, IUnitOfWork unitOfWork)
    {
        _config = config;
        _unitOfWork = unitOfWork;
        _eventConsumer = eventConsumer;
        _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);
        
        var spec = new YogabandEventWithMessageIdSpecification(args.Message.SequenceNumber);
        // error here...
        var ybEvent =  await _unitOfWork.Repository<YogabandEvent>().GetEntityWithSpec(spec);

        // do something then save
    }

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

Here is my unit of work

public IGenericRepository<TEntity> Repository<TEntity>() where TEntity : class // : BaseEntity
    {
        if(_repositories == null) 
            _repositories = new Hashtable();

        var type = typeof(TEntity).Name;

        if (!_repositories.ContainsKey(type))
        {
            var repositoryType = typeof(GenericRepository<>);
            var repositoryInstance = Activator.CreateInstance(repositoryType.MakeGenericType(typeof(TEntity)), _context);

            _repositories.Add(type, repositoryInstance);
        }

        return (IGenericRepository<TEntity>) _repositories[type];
    }

I tried to call my generic repo directly inside the handler but that still fails with the dispose error.

Here is the call I changed in the handler, now I call the gen repo instead of the unit of work

var ybEvent = await _eventsRepo.GetEntityWithSpec(spec);

Here is GetEntityWIthSpec() from my generic repo

public async Task<T> GetEntityWithSpec(ISpecification<T> spec)
{
    return await ApplySpecification(spec).FirstOrDefaultAsync();
}
private IQueryable<T> ApplySpecification(ISpecification<T> spec)
    {
        return SpecificationEvaluator<T>.GetQuery(_context.Set<T>().AsQueryable(), spec);
    }

FYI - here is how I init my repo call

private readonly IGenericRepository<YogabandEvent> _eventsRepo;

then I inject it into the constructor

public ServiceBusConsumer(IConfiguration config, IEventConsumer eventConsumer, IUnitOfWork unitOfWork, IGenericRepository<YogabandEvent> eventsRepo)
    {
        _config = config;
        _unitOfWork = unitOfWork;
        _eventConsumer = eventConsumer;
        _eventsRepo = eventsRepo;
        _queueClient = new ServiceBusClient(_config["ServiceBus:Connection"]);
        _processor = _queueClient.CreateProcessor(_config["ServiceBus:Queue"], new ServiceBusProcessorOptions());
    }

Code that starts the ServiceBusConsumer it's in Main()

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 
            {

                // do some work here


                // https://stackoverflow.com/questions/48590579/cannot-resolve-scoped-service-from-root-provider-net-core-2
                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();
    }

Here is my unit of work

public class UnitOfWork : IUnitOfWork
{
    private readonly DataContext _context;
    private Hashtable _repositories;

    public UnitOfWork(DataContext context)
    {
        _context = context;
    }

    public async Task<int> Complete()
    {
        return await _context.SaveChangesAsync();
    }

    public void Dispose()
    {
        _context.Dispose();
    }

    public IGenericRepository<TEntity> Repository<TEntity>() where TEntity : class // : BaseEntity
    {
        if(_repositories == null) 
            _repositories = new Hashtable();

        var type = typeof(TEntity).Name;

        if (!_repositories.ContainsKey(type))
        {
            var repositoryType = typeof(GenericRepository<>);
            var repositoryInstance = Activator.CreateInstance(repositoryType.MakeGenericType(typeof(TEntity)), _context);

            _repositories.Add(type, repositoryInstance);
        }

        return (IGenericRepository<TEntity>) _repositories[type];
    }
}

Upvotes: 1

Views: 682

Answers (2)

Svyatoslav Danyliv
Svyatoslav Danyliv

Reputation: 27282

Remove this dispose from UnitOfWork:

    public void Dispose()
    {
        _context.Dispose();
    }

Simple rule: if you have not created object - do not dispose. It will be disposed automatically when scope diposed.

Also consider to remove this boilerplate. DbContext is already UoW, DbSet is already repository.

Upvotes: 2

Modar Na
Modar Na

Reputation: 918

I cannot verify this but I had a similar situation where creating a response call to a service actually disposed my UoW and the datacontext and I faced the same error

I'm suspecting that the this call await args.CompleteMessageAsync(args.Message); is doing the disposing somewhere between the lines, (you can continue to trace here CompleteMessageAsync calls this and a lot of disposing is going on)

to verify that, you can try to postpone that call till after you use the repository to save the changes.

    // await args.CompleteMessageAsync(args.Message); <-- comment this line
    
    var spec = new YogabandEventWithMessageIdSpecification(args.Message.SequenceNumber);
    // error here...
    var ybEvent =  await _unitOfWork.Repository<YogabandEvent>().GetEntityWithSpec(spec);

    // do something then save
    await args.CompleteMessageAsync(args.Message); // <-- add it here

Upvotes: 0

Related Questions