1

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];
    }
}
chuckd
  • 13,460
  • 29
  • 152
  • 331
  • can you add your method await _unitOfWork.Repository() i think problem is inside of it – Vova Bilyachat May 18 '21 at 06:04
  • I'll add it, but that can't be changed as it's my generic repo. – chuckd May 18 '21 at 06:07
  • Your implementation is wrong. If you are using unit of work it should not be cached since each time you shouldve dispose it. – Vova Bilyachat May 18 '21 at 06:12
  • try to search in code _unitOfWork.Repository if you are using with "using( ..... _unitOfWork.Repository" – Vova Bilyachat May 18 '21 at 06:13
  • can you also add code for GetEntityWithSpec – Vova Bilyachat May 18 '21 at 06:18
  • I think I'll just call my generic repo. Let me see if that works. – chuckd May 18 '21 at 06:20
  • Ok but how do you initialize _eventsRepo ? – Vova Bilyachat May 18 '21 at 06:30
  • through dependency injection. I'll post that code. – chuckd May 18 '21 at 06:30
  • @user1186050 the error is clear - you're trying to use a disposed object. Perhaps you tried to use a scoped or transient object in a Singleton. Perhaps you stored the instance in a field and tried to use it *after* it was already disposed. – Panagiotis Kanavos May 18 '21 at 06:34
  • What is `DataContext`? That's what the error complains about. If it's an EF Core DbContext, the bug is the "generic repository" itself. A DbSet is already a repository, a DbContext is already a unit of work. A "generic repository" in this case is an ugly antipattern, one that *causes* errors and prevents EF (or any other ORM) from working properly – Panagiotis Kanavos May 18 '21 at 06:36
  • It is an EF Core DbContext. – chuckd May 18 '21 at 06:37
  • BTW post the actual, *full* exception text that includes the call stack, *and* the code that contains `DataContext`. Azure Service Bus has nothing to do with this problem – Panagiotis Kanavos May 18 '21 at 06:38
  • `It is an EF Core DbContext.` there's your problem then. By adding that "generic repository" and "unit of work" you broke them – Panagiotis Kanavos May 18 '21 at 06:38
  • ok give me a min or two to run through it and generate another exception to post – chuckd May 18 '21 at 06:38
  • 2
    THere's not much point - the problem is the "generic repository" antipattern. And Azure is irrelevant. The exception message explains what happened too - you tried to use a scoped service from a singleton – Panagiotis Kanavos May 18 '21 at 06:39
  • Well, this problem only occurs in this Azure web service bus handler, not in any of my controllers where I'm using both the unit of work and generic repo pattern to call and access my DB. So what is my solution here? Do I bypass the unit of work and generic repo and access a context directly to make my call to edit the db? – chuckd May 18 '21 at 06:41
  • You have two serious bugs - the antipattern and the scope problem. Gunnar Peipman's [No need for repositories and unit of work with Entity Framework Core](https://gunnarpeipman.com/ef-core-repository-unit-of-work/) explains why the "generic repo" is logically wrong, not just bad for performance. As for the scope issue, `ServiceBusConsumer` was probably declared as a singleton in DI or worse, it's a static instance created in your code. You didn't post how `ServiceBusConsumer` is registered, or how it's created. The solution in this case is described in the docs: create a scope inside singleton – Panagiotis Kanavos May 18 '21 at 06:45
  • No singleton from my understanding I used "services.AddScoped();" and then I inject it into the constructor for the class where I want to use it – chuckd May 18 '21 at 06:47
  • @user1186050 the problem has nothing to do with Azure. It's actually documented: [Consuming a scoped service in a background task](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-5.0&tabs=visual-studio#consuming-a-scoped-service-in-a-background-task). An event listener by definition is a long-running service, while a DbContext is a short-lived UoW. `AddDbContext` registers it as a scoped service. For it to work, you must inject `IServiceProvider` in your singleton, create a scope with `CreateScope` and get the context from there – Panagiotis Kanavos May 18 '21 at 06:49
  • FYI - I've followed this guys Udemy tutorials for unitofwork and gen repo. He has posted some projects that use both here in case your interested in what I've tried to follow and imitate. https://github.com/TryCatchLearn – chuckd May 18 '21 at 06:49
  • @user1186050 post the relevant code, don't make people guess. What's the scope you used though? An event listener has to be active for a long time. A DbContext is created and disposes as soon as possible – Panagiotis Kanavos May 18 '21 at 06:50
  • I apologize if this question is stupid, but what do you mean by "what's the scope" and what can I post in terms of code to help you understand better? – chuckd May 18 '21 at 06:52
  • @user1186050 `I've followed this guys Udemy tutorials ...` now you know why that was a bad idea. That `this guy` now has your contact details. While there are a few good courses in Udemy, a *lot* of them are stolen from other sites, and many "authors" have no contact information. Besides, *everyone* can create a Udemy course. That doesn't mean they know what they're talking about. And even "good" authors will keep pushing an idea that still sells. [Repository is the new Singleton](https://ayende.com/blog/3955/repository-is-the-new-singleton) was written in 2009 – Panagiotis Kanavos May 18 '21 at 06:54
  • @user1186050 `what do you mean by "what's the scope"` post your DI configuration code, and the code that creates or uses `ServiceBusConsumer`. Using `AddScoped` or `AddDbContext` only tells the DI container to dispose the objects when a "scope" exits. It doesn't specify one. In an ASP.NET Core application, the middleware explicitly creates a scope with `CreateScope` when a request starts and closes it when it finishes – Panagiotis Kanavos May 18 '21 at 06:58
  • @user1186050 even if `ServiceBusConsumer` and `DbContext` are created by middleware and have the same scope, what happens with the repo and UoW classes? You're using two extra classes on top of the single DbContext instance. If *either* of them calls `DbContext.Dispose` the other one will have a disposed DbContext in its hands. You'd see which one threw in the exception's call stack. That's why you need to post the full exception text – Panagiotis Kanavos May 18 '21 at 07:00
  • Sorry but appologies again as you're throwing a lot out there. I'm posting the code that starts the "ServiceBusConsumer", it's located in the program.cs file main(). And I'm not exactly sure where the DI config code is located. I thinks it's in the startup.cs file – chuckd May 18 '21 at 07:03
  • @user1186050 or it could be the `_repositories` HashTable that keeps repos alive even when the DbContexts they point to are already disposed. – Panagiotis Kanavos May 18 '21 at 07:03
  • I think I've posted what you asked, but probably more lost now in terms of trying to understand what to do. My web app that uses .Net Core as the backend seems to be working ok with all the unit of work and gen repo calls in the controllers. Now that I have a service bus continually listening and consuming messages I need to edit my DB when I receive a message. So I'm not sure what direction I need to take. – chuckd May 18 '21 at 07:07
  • @user1186050 in that case, why did you use some "repository" code without knowing whether it's needed or not? It makes debugging a **lot** harder. The definition of a pattern is `A solution to a problem in a context`. Not `Always do this`. It means that the same problem will require different solutions in different situations (contexts). In this case, *remove the generic repo and UoW completely*. Get your code to work with just an injected DbContext. Once you get that working, think about whether you really need to abstract the ORM – Panagiotis Kanavos May 18 '21 at 07:08
  • @user1186050 in the code you posted the `bus` is created and *abandoned* before the application starts running. None of the classes is defined in a `using` block, especially the `context`, which means they may still be active *after* the scope is disposed. This is some very convoluted code. And the DI code is still missing – Panagiotis Kanavos May 18 '21 at 07:11
  • I think maybe you mis understood me, the generic repo and unit of work are definitely helping me. Removing both right now would probably not benefit me as there are many controllers using them and it would take a while (a few days of work) – chuckd May 18 '21 at 07:12
  • What do you mean by "abandoned" before the app starts? – chuckd May 18 '21 at 07:12
  • I really can't find any examples anywhere, other than the Microsoft site, where I can see examples of running and listening to a service bus for messages. If I had some examples to look at it would be beneficial. – chuckd May 18 '21 at 07:14
  • `are definitely helping me.` to do what? You don't need the UoW, a DbContext is already a UoW. And `Repository` is used to abstract data access. That's what ORMs do. Again, post the full exception text. This will show you *which* chain of calls lead to the exception. Is the UoW trying to dispose an already disposed DbContext? Worse, is it trying to save changes that have already been lost? Or is some other class trying to access the disposed object? – Panagiotis Kanavos May 18 '21 at 07:14
  • Unit of work - helps to eliminate any code that would roll back DB edits or saves as some controllers I hit need to make multiple edits and calls to get different entities and then multiple entities need to be modified in one controller call. The generic repo helps eliminate redundancy. I remember the author of Udemy showing examples of how it helped, just don't remember the details now as it's late here in SF CA – chuckd May 18 '21 at 07:17
  • Just in the code you posted there are two paths that could lead to this exception: ServiceBus-> IGenericRepository->DbContext and `ServiceBus-> IUnitOfWork->DbContext`. There may be others. You need to simplify your code so you can understand it first. When you get there, you can add more – Panagiotis Kanavos May 18 '21 at 07:18
  • ok give me a sec to post the full exception. – chuckd May 18 '21 at 07:19
  • @user1186050 again, and again, and again, DbContext **is already a UoW**. It doesn't need extra code and definitely no explicit transactions. It caches *all* changes made to it and only persists them in a single transaction when you call `SaveChanges`. If you dispose it, the changes are discarded. All you need to rollback changes is to not call `SaveChanges` – Panagiotis Kanavos May 18 '21 at 07:20
  • `The generic repo helps eliminate redundancy.` no, that's not what a Repository is for. It's meant to abstract CRUD operations for specific entities. What's what DbSet does. Creating your own abstraction on top of the abstraction is redundant – Panagiotis Kanavos May 18 '21 at 07:22
  • You're right about the repo, it's just really late here and I'm tired. I posted the stack trace above – chuckd May 18 '21 at 07:25
  • The call stack says that the repository code tried to access a disposed DbContext in `GenericRepository.ApplySpecification`, at `GenericRepository.cs:line 51`. That's not the full call stack though, it's interrupted before showing which method in `ServiceBusConsumer` started that call. You can either log the entire exception in your code (a good idea anyway) or click on `Copy Details` in Visual Studio's exception popup – Panagiotis Kanavos May 18 '21 at 07:31
  • I'm using Visual Studio Code. Does that have an equivalent of "Copy Details"? – chuckd May 18 '21 at 07:33
  • @user1186050 I'm not sure. You *have* to configure logging in a service application though, as errors, especially transient errors, are guaranteed. Logging (and tracing) is the only way to understand what happened, which, btw, is why Azure and .NET Core have made some pretty big investments in logging, App Insights and OpenTelemetry. Add an `ILogger< ServiceBusConsumer>` parameter to the SB constructor and use `ILogger.LogError(Exception,string...)` in `catch` blocks to log the actual exception along with a message. – Panagiotis Kanavos May 18 '21 at 08:06
  • And after all these comments we still don't see what happens to the context that's initialized in `Main` and enters some unknown method `SeedAsync`. – Gert Arnold May 22 '21 at 09:50
  • Hi Gert. It's irrelevant to this question, so I removed it. FYI - all it does is seed data in the db on startup if it doesn't exist. – chuckd May 22 '21 at 18:37
  • Hi Gert. I've posted my unit of work to show exactly what is being used in the ServiceBusConsumer. I know I can call my DB with a IDbContextFactory, but don't know if there is a way to do it while using a generic repo or unit of work? – chuckd May 22 '21 at 18:42
  • 1
    I think host.Run(); should be inside the using or your scope is disposed before you actually run your services. Not sure if I am correct though, just seems this way when I look at the code. Might want to try it – Stilgar May 22 '21 at 23:10

2 Answers2

1

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.

Svyatoslav Danyliv
  • 21,911
  • 3
  • 16
  • 32
0

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
Modar Na
  • 873
  • 7
  • 18
  • Hi Modar. I actually tried that early on with no luck. I posted a different, but similar question here - https://stackoverflow.com/questions/67596293/how-to-use-ef-core-after-ive-received-a-message-from-my-azure-service-bus-liste - Somehow I think I need to use a contextFactory with UnitOfWork or GenericRepo. But not sure how I would do it? – chuckd May 22 '21 at 23:52
  • oh well that answers it... I think you should inject the `contextFactory` in your `UnitOfWork` then create a context when calling the IGenericRepository , – Modar Na May 23 '21 at 00:12
  • note that if you bind the repository with a context and then dispose the context and leave the repository in the array you will always have disposed contexts – Modar Na May 23 '21 at 00:13
  • Hi Modar. If you can show me a pretty good example that I can use, I'll give you the points. – chuckd May 23 '21 at 00:35