ASP.NET Web API 2 中的依賴注入
本教學課程示範如何將相依性注入 ASP.NET Web API 控制器。
教學課程中使用的軟體版本
- Web API 2
- Unity 應用程式區塊
- Entity Framework 6 (版本 5 也適用)
什麼是依賴注入?
「相依性」是另一個物件所需的任何物件。 例如,通常會定義一個存放庫來處理資料存取。 我們用一個例子來做說明。 首先,我們定義一個領域模型:
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);
}
}
現在讓我們定義一個支援 Product
實體 GET 要求的 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。 (其他熱門的程式庫包括 Castle Windsor、Spring.Net、Autofac、Ninject、Simple Injector 和 StructureMap。) 您可以使用 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 容器都有類似的對應功能。