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

現在讓我們定義支持實體 GET 要求的 Product Web API 控制器。 (我為了簡單起見而離開 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 Patterns & Practices 的 Unity 。 (其他熱門連結庫包括 Spring.NetAutofacNinjectStructureMap.) 您可以使用 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容器都有類似的對等專案。