DbContext has been disposed and autofac

2019-01-17 08:53发布

问题:

I have a controller:

private readonly ILogger _logger;    
private readonly IRepository _repository;

public HomeController(ILogger logger, IRepository repository)
{
   _logger = logger;
   _repository = repository;
}

This is the repository:

public class EfRepository : IRepository
{
    // ...methods for add, delete, update entities
    // ....

    public void Dispose()
    {
         if (this._context != null)
         {
             this._context.SaveChanges();
             (this._context as IDisposable).Dispose();
             this._context = null;
         }
    }
}

Finally, registration types in IoC:

_builder.RegisterType<Logger>().As<ILogger>();
_builder.RegisterType<EfRepository>().As<IRepository>().WithParameter("context", new PcpContext());

When I run the application I get this error:

The operation cannot be completed because the DbContext has been disposed.

I tried to change registration EfRepository like this:

_builder.RegisterType<EfRepository>()
   .As<IRepository>()
   .WithParameter("context", new PcpContext()).InstancePerLifetimeScope();

In this case the first request finish, but when trying to open other pages, I get the error again. Where is the problem?

回答1:

When using the WithParameter method, the parameter instance will be the same for every resolved object. So with .WithParameter("context", new PcpContext()) you are effectively using the same instance of the PcpContext class for any resolved instance of IRepository.

With your current code, when an IRepository instance is disposed, it will also dispose that PcpContext instance. Then any subsequent attempt to resolve an IRepository will receive the PcpContext instance that was disposed. You need a way to receive a fresh new instance of the EF DbContext on each Http Request that is disposed of at the end of the request.

One option could be to register a code block for the IRepository so that code block is executed every time an IRepository needs to be resolved:

_builder.Register<IRepository>(c => new EfRepository(new PcpContext()))

A better option would be to create a new IDatabaseContext abstraction, updating EfRepository so it depends on the new IDatabaseContext abstraction instead of the PcpContextclass (Which may already be the case :) ).

The implementation class for IDatabaseContext will be your PcpContext class, which must inherit from the EF DbContext and probably receive the connection string as a parameter.

public class EfRepository : IRepository
{
    private readonly IDatabaseContext _context;

    public EfRepository(IDatabaseContext context)
    {
        _context = context;
    }

    ...methods for add, delete, update entities

    //There is no longer need for this to be disposable.
    //The disaposable object is the database context, and Autofac will take care of it
    //public void Dispose()
}

public interface IDatabaseContext : IDisposable 
{
    ... declare methods for add, delete, update entities
}

public class PcpContext: DbContext, IDatabaseContext 
{
    public EntityFrameworkContext(string connectionString)
        : base(connectionString)
    {
    }

    ...methods exposing EF for add, delete, update entities

    //No need to implement IDisposable as we inherit from DbContext 
    //that already implements it and we don´t introduce new resources that should be disposed of
}

This gets better with the idea of using an IoC container and leaving the burden of lifetime management to them. Now your Repository class does not need to be disposable nor needs to manage and dispose of its IDatabaseContext dependency. It is Autofac who will keep track of the context instance and dispose of it when appropriate.

For the same reason, you probably want to use InstancePerLifetimeScope with the database context dependency. That would mean the same EF context is shared for every repository instance on the same Http request and is disposed at the end of the request.

_builder.RegisterType<EfRepository>()
   .As<IRepository>();

_builder.RegisterType<PcpContext>()
   .As<IDatabaseContext>()
   .WithParameter("connectionString", "NameOfConnStringInWebConfig")
   .InstancePerLifetimeScope();


回答2:

I went with the simple solution of a 'code block' as @Daniel J.G suggested (a lambda).

Below a code example of this in Autofac. Daniels' example is for Unity as he also mentions himself. Because the OP added Autofac as a tag this seemed relevant to me:

_builder.Register(c => new AppDbContext()).As(typeof(AppDbContext));

That code solved the DbContext has been disposed issue I was having with Entity Framework. Note that compared to most other DI containers - including Unity - Autofac switches around the thing registered and the the thing it resolves to.

For the code example given by the OP the fix would be something like this:

_builder.Register(c => new EfRepository(new PcpContext())).As(IRepository);

Note that this last bit is untested code. But you should refer to Daniels answer for more info anyway, because I think he is right with the 'better option'. But you might use my solution option if you don't have time right now (like me). Just add a TODO so you can make good on the technical debt you're incurring :).

When I do I'll see if I can update this answer with working code for Autofac that follows his 'better option'. First I want to carefully read this article. On quick reading it seems to me the Autofac people are promoting using the 'Service Locator' for handling lifetime scope. But according to Mark Seemann that is an anti-pattern, so I have some stuff to figure out... Any DI expert with an opinion?