ASP.NET Web API 2 中的依赖项注入

下载已完成的项目

本教程演示如何将依赖项注入 ASP.NET Web API 控制器。

本教程中使用的软件版本

什么是依赖关系注入?

依赖项是指另一个对象所需的任何对象。 例如,通常定义处理 数据访问的存储库 。 让我们用示例来说明一下。 首先,我们将定义域模型:

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

下面是一个简单的存储库类,它使用 Entity Framework 将项存储在数据库中。

public class ProductsContext : DbContext
{
    public ProductsContext()
        : base("name=ProductsContext")
    {
    }
    public DbSet<Product> Products { get; set; }
}

public class ProductRepository : IDisposable
{
    private ProductsContext db = new ProductsContext();

    public IEnumerable<Product> GetAll()
    {
        return db.Products;
    }
    public Product GetByID(int id)
    {
        return db.Products.FirstOrDefault(p => p.Id == id);
    }
    public void Add(Product product)
    {
        db.Products.Add(product);
        db.SaveChanges();
    }

    protected void Dispose(bool disposing)
    {
        if (disposing)
        {
            if (db != null)
            {
                db.Dispose();
                db = null;
            }
        }
    }

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

现在,让我们定义一个 Web API 控制器,该控制器支持对实体的 Product GET 请求。 (为了简单起见,我正在退出 POST 和其他方法。下面是首次尝试:

public class ProductsController : ApiController
{
    // This line of code is a problem!
    ProductRepository _repository = new ProductRepository();

    public IEnumerable<Product> Get()
    {
        return _repository.GetAll();
    }

    public IHttpActionResult Get(int id)
    {
        var product = _repository.GetByID(id);
        if (product == null)
        {
            return NotFound();
        }
        return Ok(product);
    }
}

请注意,控制器类依赖于 ProductRepository该类,我们正在让控制器创建 ProductRepository 实例。 但是,出于多种原因,以这种方式硬编码依赖项是个坏主意。

  • 如果要替换为 ProductRepository 其他实现,则还需要修改控制器类。
  • ProductRepository如果具有依赖项,则必须在控制器中配置这些依赖项。 对于具有多个控制器的大型项目,配置代码分散在项目中。
  • 很难进行单元测试,因为控制器对数据库进行硬编码。 对于单元测试,应使用模拟或存根存储库,这是当前设计无法实现的。

我们可以通过将 存储库注入 控制器来解决这些问题。 首先,将 ProductRepository 类重构为接口:

public interface IProductRepository
{
    IEnumerable<Product> GetAll();
    Product GetById(int id);
    void Add(Product product);
}

public class ProductRepository : IProductRepository
{
    // Implementation not shown.
}

然后提供 IProductRepository 构造函数参数:

public class ProductsController : ApiController
{
    private IProductRepository _repository;

    public ProductsController(IProductRepository repository)  
    {
        _repository = repository;
    }

    // Other controller methods not shown.
}

此示例使用 构造函数注入。 还可以使用 setter 注入,可在其中通过 setter 方法或属性设置依赖项。

但现在出现问题,因为应用程序不会直接创建控制器。 Web API 在路由请求时创建控制器,而 Web API 不知道任何相关信息 IProductRepository。 这是 Web API 依赖项解析程序所在的位置。

Web API 依赖项解析程序

Web API 定义用于解析依赖项的 IDependencyResolver 接口。 下面是接口的定义:

public interface IDependencyResolver : IDependencyScope, IDisposable
{
    IDependencyScope BeginScope();
}

public interface IDependencyScope : IDisposable
{
    object GetService(Type serviceType);
    IEnumerable<object> GetServices(Type serviceType);
}

IDependencyScope 接口有两种方法:

  • GetService 创建一个类型的实例。
  • GetServices 创建指定类型的对象的集合。

IDependencyResolver 方法继承 IDependencyScope 并添加 BeginScope 方法。 稍后将在本教程中讨论范围。

当 Web API 创建控制器实例时,它首先调用 IDependencyResolver.GetService,传入控制器类型。 可以使用此扩展性挂钩来创建控制器,解析任何依赖项。 如果 GetService 返回 null,Web API 在控制器类上查找无参数构造函数。

Unity 容器的依赖项解析

尽管可以从头开始编写完整的 IDependencyResolver 实现,但接口实际上旨在充当 Web API 和现有 IoC 容器之间的桥梁。

IoC 容器是负责管理依赖项的软件组件。 向容器注册类型,然后使用容器创建对象。 容器会自动确定依赖关系。 许多 IoC 容器还允许你控制对象生存期和范围等内容。

注意

“IoC”代表“控制反转”,这是一种常规模式,即框架调用应用程序代码。 IoC 容器为你构造对象,该对象“反转”了通常的控制流。

在本教程中,我们将使用 来自 Microsoft 模式和做法的 Unity 。 (其他常用库包括城堡温莎, Spring.Net, AutofacNinject简单注入器结构地图可以使用 NuGet 程序包管理器安装 Unity。 在 Visual Studio 中的“工具”菜单中,选择“NuGet 程序包管理器”,然后选择“程序包管理器控制台”。 在“程序包管理器控制台”窗口中,键入以下命令:

Install-Package Unity

下面是包装 Unity 容器的 IDependencyResolver 的实现。

using Microsoft.Practices.Unity;
using System;
using System.Collections.Generic;
using System.Web.Http.Dependencies;

public class UnityResolver : IDependencyResolver
{
    protected IUnityContainer container;

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

    public object GetService(Type serviceType)
    {
        try
        {
            return container.Resolve(serviceType);
        }
        catch (ResolutionFailedException exception)
        {
            throw new InvalidOperationException(
                $"Unable to resolve service for type {serviceType}.",
                exception)
        }
    }

    public IEnumerable<object> GetServices(Type serviceType)
    {
        try
        {
            return container.ResolveAll(serviceType);
        }
        catch (ResolutionFailedException exception)
        {
            throw new InvalidOperationException(
                $"Unable to resolve service for type {serviceType}.",
                exception)
        }
    }

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

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

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

配置依赖项解析程序

设置全局 HttpConfiguration 对象的 DependencyResolver 属性上的依赖项解析程序

以下代码向 IProductRepository Unity 注册接口,然后创建一个 UnityResolver

public static void Register(HttpConfiguration config)
{
    var container = new UnityContainer();
    container.RegisterType<IProductRepository, ProductRepository>(new HierarchicalLifetimeManager());
    config.DependencyResolver = new UnityResolver(container);

    // Other Web API configuration not shown.
}

依赖项范围和控制器生存期

控制器是按请求创建的。 为了管理对象生存期,IDependencyResolver 使用范围的概念

附加到 HttpConfiguration 对象的依赖项解析程序具有全局范围。 当 Web API 创建控制器时,它将调用 BeginScope。 此方法返回表示 子范围的 IDependencyScope

然后,Web API 在子范围上调用 GetService 以创建控制器。 请求完成后,Web API 会在子范围调用 Dispose使用 Dispose 方法释放控制器的依赖项。

如何实现 BeginScope 取决于 IoC 容器。 对于 Unity,范围对应于子容器:

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

大多数 IoC 容器具有类似的等效项。