【问题标题】:DbContext is Disposed When Using Unity Dependency Injection on WebApi projectWeb Api项目中使用Unity依赖注入时DbContext被丢弃
【发布时间】:2017-10-04 20:15:37
【问题描述】:

我在使用依赖注入方面还很陌生,我想我一定忽略了一些非常简单的事情。

我有一个 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 更改为其他可用的,但没有帮助。

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

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

任何提示将不胜感激。

【问题讨论】:

  • 当您执行 using (_catRepo) 时,在调用结束时会释放 repo,从而释放 db 上下文。不要在那里放置using 块。您的 DI 容器应该能够将 db 上下文配置为按请求进行(不确定这在 Unity 中如何工作),但如果设置正确,则无需调用 repo.Dispose()
  • 不应该对 Web 服务的后续调用实例化一个新的 _catRepo 吗?我想我一定是在设置中遗漏了一些东西......
  • DI 容器应该为您处理资源。您不必手动操作。
  • 您仅在应用程序启动时创建上下文,因此如果第一个请求发生并且您处置了数据库,则下一个请求没有更多的数据库可供使用。尝试更改它以根据请求创建和处理数据库。
  • 另外,我之前也遇到过这个兔子洞:我不推荐通用存储库。使用特定的存储库。稍后当您需要非 CRUD 的特定于域的操作/事务等时,您会感谢我

标签: c# dependency-injection asp.net-web-api2 unity-container


【解决方案1】:

让我们看看你的注册:

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。您可能打算拥有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 也被注册为“作用域”。

这样做的好处是,一旦范围(网络请求)结束,Unity 现在将处置您的AnimalEntities。如herehere 解释的那样,这使您不必对AnimalEntities 的消费者实施IDisposable

【讨论】:

  • 感谢您提供如此详细的答案。你是对的,我的意图是每个请求都有一个 DbContext 。不幸的是,你的建议对我不起作用。看起来我的控制器的构造函数找不到,因为我收到此错误消息:“尝试创建“AnimalController”类型的控制器时发生错误。确保控制器具有无参数的公共构造函数。”。我应该更改控制器的签名吗?
  • 无参数 ctor 是因为您的注册未正确绑定。您应该能够深入了解注册容器以查看 Unity 容器中的错误消息。我建议在尝试在多参数控制器中解析之前将此新注册码放入 TEST 中。
【解决方案2】:

我知道发生了什么。正如一些人所指出的,我的存储库不需要从 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();
    }
}

默认构造函数现在负责创建一个新的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 才能使用存储库类的无参数构造函数。不包括 InjectionConstructor 导致我收到关于找不到控制器构造函数的错误。

一旦我克服了这个障碍,我就可以改变我的控制器了。这里的主要区别是我不再使用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;
    }
}       
}

【讨论】:

    【解决方案3】:

    我解决问题的方式与这些答案所暗示的略有不同。

    我在一个 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));
    }
    

    PrimaryContext:

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-03-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多