简体   繁体   中英

Autofac registration and disposal issues

I'm using entity framework 6 and Autofac in my web application.

I inject unit of work with DbContext inside, both externally owned so I can dispose them myself.

DbContext registered PerLifetimeScope,

Unit of work is a factory, therefore registered as per dependency.

When Executing the first http Get action everthing works fine and I see the unit of work with the context are disposed after the response is coming from the db which is great.

My issue is that whenever I execute a second request, the context for some reason is disposed before I return an IQueryable. Therefore I get an execption saying:

The operation could not be executed because the DbContext is disposed.

For example - calling the GetFolders method works the first time, and afterwards fails..

I see the context is disposed too early, what I don't understand is what triggers it too soon in the second request..

public interface IUnitOfWork : IDisposable
{
    bool Commit();
}

public EFUnitOfWork : IUnitOfWork
{
    public IRepository<Folder> FoldersRepository {get; set;}

    public IRepository<Letter> LettersRepository {get; set;}

    private readonly DbContext _context;


    public EFUnitOfWork(DbContext context, IRepository<Folder> foldersRepo, IRepository<Letter> lettersRepo)
    {
        _context = context;
        _foldersRepo = foldersRepo;
        LettersRepository = lettersRepo;
    }

    private bool disposed = false;

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                _context.Dispose();
            }

            disposed = true;
        }
    }

    public bool Commit()
    {
        try
        {
            return SaveChanges() > 0;
        }
        catch (DbEntityValidationException exc)
        {
            // just to ease debugging
            foreach (var error in exc.EntityValidationErrors)
            {
                foreach (var errorMsg in error.ValidationErrors)
                {
                    logger.Log(LogLevel.Error, "Error trying to save EF changes - " + errorMsg.ErrorMessage);
                }
            }

            return false;
            throw exc;
        }
    }   
}



public class Repository<T> : IRepository<T>
{
    protected readonly DbContext Context;
    protected readonly DbSet<T> DbSet;

    public EFRepository(DbContext context)
    {
        Context = context;
    }

    public IQueryable<T> Get()
    {
        return DbSet;
    }

    public void Add(T item)
    {
        DbSet.Add(item);
    }

    public virtual Remove(T item)
    {
        DbSet.Remove(item);
    }

    public void Update(T item)
    {
        Context.Entry(item).State = EntityState.Modified;
    }

    public T FindById(int id)
    {
        return DbSet.Find(id); 
    }
}

public class DataService : IDataService
{
    private Func<IUnitOfWork> _unitOfWorkFactory;

    public (Func<IUnitOfWork> unitOfWorkFactory)
    {
        _unitOfWorkFactory = unitOfWorkFactory;             
    }

    public List<FolderPreview> GetFolders()
    {
        using(unitOfWork = _unitOfWorkFactory())
        {
            var foldersRepository = unitOfWork.FoldersRepository;

            var foldersData = foldersRepository.Get().Select(p => new FolderPreview
                                {
                                    Id = p.Id,
                                    Name = p.Name
                                }).ToList();

            return foldersData;
        }
    }
}

public class FolderPreview
{
    public int Id {get; set;}

    public string Name {get; set;}
}


Startup code:

{
    _container.RegisterGeneric<IRepository<>,Repository<>>().InstancePerLifetimeScope();
    _container.RegisterType<IDataService, DataService>().SingleInstance();
    _container.RegisterType<EFUnitOfWork, IUnitOfWork>().PerDepnendecny().ExternallyOwned();
    _container.RegisterType<DbContext, MyDbContext>().InstancePerLifetimeScope().ExternallyOwned(); 
}

Is this related to singletons some how? Almost all of my application is singletons, the DataService is also Singleton. Anyone?

Thanks!

The problem is that you are instancing only one Repository and one DbContext per request, but are instancing one new IUnitOfWork every time.

So when you call GetFolders you are creating a new IUnitOfWork and disposing it (which disposes the DbContext -on IUnitOfWork.Dispose() -): so when you call GetFolders again, when you create a second IUnitOfWork , since it's the same lifetime scope, it's injecting the already-created repository and the already-created DbContext , which is disposed (the container doesn't try to create a new instance since you are on the same lifetime scope)...

So on the second call, your Repository and IUnitOfWork are trying to use the disposed instance of DbContext , thus the error you are seeing.


As a solution, you can just not dispose the DbContext on IUnitOfWork , and dispose it only at the end of your request... or you could even not dispose it at all: this may sound strange, but check this post

I'm copying the important part in case the link goes dead, by Diego Vega:

The default behavior of DbContext is that the underlying connection is automatically opened any time is needed and closed when it is no longer needed. Eg when you execute a query and iterate over query results using “foreach”, the call to IEnumerable.GetEnumerator() will cause the connection to be opened, and when later there are no more results available, “foreach” will take care of calling Dispose on the enumerator, which will close the connection. In a similar way, a call to DbContext.SaveChanges() will open the connection before sending changes to the database and will close it before returning.

Given this default behavior, in many real-world cases it is harmless to leave the context without disposing it and just rely on garbage collection.

That said, there are two main reason our sample code tends to always use “using” or dispose the context in some other way:

  1. The default automatic open/close behavior is relatively easy to override: you can assume control of when the connection is opened and closed by manually opening the connection. Once you start doing this in some part of your code, then forgetting to dipose the context becomes harmful, because you might be leaking open connections.

  2. DbContext implements IDiposable following the recommended pattern, which includes exposing a virtual protected Dispose method that derived types can override if for example the need to aggregate other unmanaged resources into the lifetime of the context.

So basically, unless you are managing the connection, or have a specific need to dispose it, it's safe to not do it.

I'd still recommend disposing it, of course, but in case you don't see where it'd be a good time to do it, you may just not do it at all.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM