DbContext is Disposed When Using Unity Dependency Injection on WebApi project

隐身守侯 提交于 2019-12-06 00:12:39
Steven

Let's take a look at your registration:

container.RegisterType<IGenericRepository<Cat>, GenericRepository<Cat>>(
    new HierarchicalLifetimeManager(), 
    new InjectionConstructor(new AnimalEntities()));

container.RegisterType<IGenericRepository<Dog>, GenericRepository<Dog>>(
    new HierarchicalLifetimeManager(), 
    new InjectionConstructor(new AnimalEntities()));

You are creating two instances of AnimalEntities at startup, but those instances are reused for the duration of the whole application. This is a terrible idea. You probably intend to have one DbContext per request, but the instance wrapped by the InjectionConstructor is a constant.

You should change your configuration to the following:

container.RegisterType<IGenericRepository<Cat>, GenericRepository<Cat>>(
    new HierarchicalLifetimeManager());

container.RegisterType<IGenericRepository<Dog>, GenericRepository<Dog>>(
    new HierarchicalLifetimeManager());

// Separate 'scoped' registration for AnimalEntities.
container.Register<AnimalEntities>(
    new HierarchicalLifetimeManager()
    new InjectionFactory(c => new AnimalEntities()));

This is much simpler and now the AnimalEntities is registered as 'scoped' as well.

What's nice about this is that Unity will now dispose your AnimalEntities once the scope (the web request) ends. This prevents you from having to implement IDisposable on consumers of AnimalEntities, as explained here and here.

I figured out what was going on. As several people have indicated, my repository doesn't need to inherit from IDisposable since the Unity container will dispose of these repositories when the time is right. However, that wasn't the root of my problems.

The main challenge to overcome was getting one dbContext per request. My IGenericRepository interface has stayed the same but my GenericRepository implemenation now looks like this:

public class GenericRepository<TDbSet, TDbContext> : 
    IGenericRepository<TDbSet> where TDbSet : class
    where TDbContext : DbContext, new()
{
    internal DbContext _context;
    internal DbSet<TDbSet> _dbSet;

    public GenericRepository(DbContext context)
    {
        _context = context;
        _dbSet = _context.Set<TDbSet>();
    }

    public GenericRepository() : this(new TDbContext())
    {
    }

    /// <summary>
    /// This constructor will set the database of the repository 
    /// to the one indicated by the "database" parameter
    /// </summary>
    /// <param name="context"></param>
    /// <param name="database"></param>       
    public GenericRepository(string database = null)
    {
        SetDatabase(database);
    }

    public void SetDatabase(string database)
    {
        var dbConnection = _context.Database.Connection;
        if (string.IsNullOrEmpty(database) || dbConnection.Database == database)
            return;

        if (dbConnection.State == ConnectionState.Closed)
            dbConnection.Open();

        _context.Database.Connection.ChangeDatabase(database);
    }

    public virtual IQueryable<TDbSet> Get()
    {
        return _dbSet;
    }

    public virtual TDbSet GetById(object id)
    {
        return _dbSet.Find(id);
    }

    public virtual void Insert(TDbSet entity)
    {
        _dbSet.Add(entity);
    }

    public virtual void Delete(object id)
    {
        TDbSet entityToDelete = _dbSet.Find(id);
        Delete(entityToDelete);
    }

    public virtual void Delete(TDbSet entityToDelete)
    {
        if (_context.Entry(entityToDelete).State == EntityState.Detached)
        {
            _dbSet.Attach(entityToDelete);
        }

        _dbSet.Remove(entityToDelete);
    }

    public virtual void Update(TDbSet entityToUpdate)
    {
        _dbSet.Attach(entityToUpdate);
        _context.Entry(entityToUpdate).State = EntityState.Modified;
    }

    public virtual void Save()
    {
        _context.SaveChanges();
    }
}

The default constructor is now responsible for creating a new DbContext of the type specified when the class is instantiated(I actually have more than one type of DbContext in my application). This allows for a new DbContext to be created for every web request. I tested this by using the using statement in my original repository implementation. I was able to verify that I no longer get the exception about the DbContext being disposed on subsequent requests.

My WebApiConfig now looks like this:

 public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        // Web API configuration and services
        var container = new UnityContainer();

        container.RegisterType<IGenericRepository<Cat>, GenericRepository<Cat, AnimalEntities>>(new HierarchicalLifetimeManager(), new InjectionConstructor());
        container.RegisterType<IGenericRepository<Dog>, GenericRepository<Dog, AnimalEntities>>(new HierarchicalLifetimeManager(), new InjectionConstructor());                                  

        config.DependencyResolver = new UnityResolver(container);

        config.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/html"));

        // Web API routes
        config.MapHttpAttributeRoutes();

        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
    }
}

One thing that was causing me a lot of pain here is that I didn't realize I still had to call the InjectionConstructor in order to use the parameterless constructor of the repository class. Not including the InjectionConstructor was causing me to get the error about my controller's constructor not being found.

Once I got over that hurdle I was able to change my controller what I have below. The main difference here is that I'm no longer using using statments:

public class IntegrationController : ApiController
{
    private readonly IGenericRepository<Cat> _catRepo;
    private readonly IGenericRepository<Dog> _dogPackRepo;

    public IntegrationController(IGenericRepository<Cat> catRepository,
        IGenericRepository<Dog> dogRepository)
    {
        _catRepo = catRepository;
        _dogRepo = dogRepository;
    }

[HttpGet]
public AnimalDetails GetAnimalDetails(int tagId)
{
    var animalDetails = new animalDetails();

    try
    {
        var dbName = getAnimalName(tagId);

        if (dbName == null)
        {
            animalDetails.ErrorMessage = $"Could not find animal name for tag Id {tagId}";
            return animalDetails;
        }
    }
    catch (Exception ex)
    {
        //todo: add logging
        Console.WriteLine(ex.Message);
        animalDetails.ErrorMessage = ex.Message;
        return animalDetails;
    }

    return animalDetails;
}

private string getAnimalName(int tagId)
{
    try
    {            
         return _catRepo.Get().Where(s => s.TagId == 
           tagId.ToString()).SingleOrDefault();            
    }
    catch (Exception e)
    {
        //todo: add logging
        Console.WriteLine(e);
        throw;
    }
}       
}
Tyler Sells

The way I solved my problem is a little different than what these answers suggest.

I'm in an MVC app but the logic should be similar for this.

As others have stated, creating an instance of an object inside and InjectionContructor essentially creates a static copy of that instance that is used for all future instances of the resolving type. To fix this, I simply register the context as a type and then let Unity resolve the context when it resolves the service. By default, it creates a new instance each time:

UnityConfig:

public static void RegisterComponents()
{
    var container = new UnityContainer();

    container.RegisterType<PrimaryContext>(new InjectionConstructor());
    container.RegisterType<LocationService>();

    DependencyResolver.SetResolver(new UnityDependencyResolver(container));
}

PrimaryContext:

//Allows for a default value if none is passed
public PrimaryContext() : base(Settings.Default.db) { }
public PrimaryContext(string connection) : base(connection)
{
}

LocationService:

PrimaryContext _context;
public LocationService(PrimaryContext context)
{
    _context = context;
}

I can't give a ton of specifics about exactly how it works, but this appears to have fixed the problem I was having (I was getting the same error message) and it's super simple.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!