在 WebApi 项目上使用 Unity 依赖注入时 DbContext 被释放

DbContext is Disposed When Using Unity Dependency Injection on WebApi project

我对使用依赖注入还很陌生,我想我一定是忽略了一些非常简单的东西。

我有一个 Web API 项目,我正在其中注册通用存储库。存储库将 dbContext 作为其构造函数中的参数。

我发现奇怪的行为是我可以成功调用该服务,但任何后续调用都告诉我 dbcontext 已被释放。我确实有一个 using 语句,但这应该不是问题,因为 DI 应该为每个 Web 请求创建我的依赖项的新实例(尽管我可能是错的)。

这是我的通用存储库:

 public class GenericRepository<T> : IGenericRepository<T> where T : class
{
    internal DbContext _context;
    internal DbSet<T> _dbSet;
    private bool disposed;

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

    /// <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<T> Get()
    {
        return _dbSet;
    }

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

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

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

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

        _dbSet.Remove(entityToDelete);
    }

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

    public virtual void Save()
    {
        _context.SaveChanges();
    }
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposed)
            return;

        if (disposing)
        {
            //free managed objects here
            _context.Dispose();
        }

        //free any unmanaged objects here
        disposed = true;
    }

    ~GenericRepository()
    {
        Dispose(false);
    }
}

这是我的通用存储库界面:

 public interface IGenericRepository<T> : IDisposable where T : class
{
    void SetDatabase(string database);
    IQueryable<T> Get();       
    T GetById(object id);
    void Insert(T entity);
    void Delete(object id);
    void Delete(T entityToDelete);
    void Update(T entityToUpdate);
    void Save();
}

这是我的 WebApiConfig:

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>>(new HierarchicalLifetimeManager(), new InjectionConstructor(new AnimalEntities()));
        container.RegisterType<IGenericRepository<Dog>, GenericRepository<Dog>>(new HierarchicalLifetimeManager(), new InjectionConstructor(new AnimalEntities()));           

        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 }
        );
    }
}

这是我的 DependencyResolver(非常标准):

public class UnityResolver : IDependencyResolver
{
    protected IUnityContainer container;

    public UnityResolver(IUnityContainer container)
    {
        this.container = container ?? throw new ArgumentNullException(nameof(container));
    }

    public object GetService(Type serviceType)
    {
        try
        {
            return container.Resolve(serviceType);
        }
        catch (ResolutionFailedException)
        {
            return null;
        }
    }

    public IEnumerable<object> GetServices(Type serviceType)
    {
        try
        {
            return container.ResolveAll(serviceType);
        }
        catch (ResolutionFailedException)
        {
            return new List<object>();
        }
    }

    public IDependencyScope BeginScope()
    {
        var child = container.CreateChildContainer();
        return new UnityResolver(child);
    }

    public void Dispose()
    {
        Dispose(true);
    }

    protected virtual void Dispose(bool disposing)
    {
        container.Dispose();
    }
}

最后,这是给我带来麻烦的控制器的一部分:

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

    public AnimalController(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
        {
            //todo: fix DI so dbcontext is created on each call to the controller
            using (_catRepo)
            {
                return _catRepo.Get().Where(s => s.TagId == tagId.ToString()).SingleOrDefault();
            }
        }
        catch (Exception e)
        {
            //todo: add logging
            Console.WriteLine(e);
            throw;
        }
    }       
}

围绕 _catRepo 对象的 using 语句未按预期运行。在我进行第一次服务调用后,_catRepo 被处理掉。在随后的调用中,我希望实例化一个新的 _catRepo。但是,情况并非如此,因为我正在谈论正在处理的 dbcontext 的错误。

我试过将 LifeTimeManager 更改为其他一些可用的,但这没有帮助。

我也开始走另一条路,通用存储库将采用第二个通用 class 并从中实例化自己的 dbcontext。但是,当我这样做时,Unity 找不到我的控制器的单参数构造函数。

根据下面的评论,我想我真正需要的是一种在每个请求的基础上实例化 DbContext 的方法。不过我不知道该怎么做。

如有任何提示,我们将不胜感激。

让我们看看您的注册情况:

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

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

您在启动时创建了 AnimalEntities 的两个实例,但这些实例在整个应用程序运行期间重复使用。这是一个 terrible idea. You probably intend to have one DbContext per request,但被 InjectionConstructor 包装的实例是一个常量。

您应该将配置更改为以下内容:

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()));

这就简单多了,现在 AnimalEntities 也注册为 'scoped'。

这样做的好处是,一旦范围(网络请求)结束,Unity 现在将处理您的 AnimalEntities。这使您不必像 here and .

所解释的那样对 AnimalEntities 的消费者实施 IDisposable

我明白是怎么回事了。正如几个人指出的那样,我的存储库不需要从 IDisposable 继承,因为 Unity 容器会在适当的时候处理这​​些存储库。然而,这不是我问题的根源。

要克服的主要挑战是每个请求获得一个 dbContext。我的 IGenericRepository 界面保持不变,但我的 GenericRepository 实现现在看起来像这样:

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();
    }
}

默认构造函数现在负责创建实例化 class 时指定类型的新 DbContext(我的应用程序中实际上有不止一种类型的 DbContext ).这允许为每个 Web 请求创建一个新的 DbContext。我在我的原始存储库实现中使用 using 语句对此进行了测试。我能够验证我不再收到关于 DbContext 在后续请求中被处置的异常。

我的 WebApiConfig 现在看起来像这样:

 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 }
        );
    }
}

让我很痛苦的一件事是我没有意识到我仍然必须调用 InjectionConstructor 才能使用存储库 class 的无参数构造函数。不包括 InjectionConstructor 导致我收到有关未找到控制器构造函数的错误。

一旦我克服了这个障碍,我就能够改变我的控制器,如下所示。这里的主要区别是我不再使用 using 语句:

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;
    }
}       
}

我解决问题的方法与这些答案所建议的略有不同。

我在 MVC 应用程序中,但逻辑应该与此类似。

正如其他人所说,在内部创建一个对象的实例,InjectionContructor 本质上是创建该实例的静态副本,用于解析类型的所有未来实例。要解决此问题,我只需将上下文注册为一种类型,然后让 Unity 在解析服务时解析上下文。默认情况下,它每次都会创建一个新实例:

UnityConfig:

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

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

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

主要上下文:

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

定位服务:

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

我无法详细说明它的工作原理,但这似乎解决了我遇到的问题(我收到了相同的错误消息),而且非常简单。