首页 文章

在WebApi项目上使用Unity依赖注入时处理DbContext

提问于
浏览
3

我在使用依赖注入时相当新,我认为我必须忽略一些非常简单的东西 .

我有一个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无法找到我的控制器的单参数构造函数 .

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

任何提示将不胜感激 .

3 回答

  • 0

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

    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 也被注册为'scoped' .

    有什么好处的是,一旦范围(Web请求)结束, Unity 现在将处理您的 AnimalEntities . 这可以防止您必须在 AnimalEntities 的使用者上实现 IDisposable ,如herehere所述 .

  • 4

    我弄清楚发生了什么 . 有几个人指出,我的存储库不需要继承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 语句:

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

    我解决问题的方式与这些答案的建议略有不同 .

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

    LocationService:

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

    我无法详细说明它是如何工作的,但这似乎解决了我遇到的问题(我得到了相同的错误信息)而且它非常简单 .

相关问题