ASP.NET Core: Dependency Injection trong ASP.NET Web API
Tải xuống dự án đã hoàn thành.
Hướng dẫn này cho thấy cách đưa các phần phụ thuộc vào controller API Web ASP.NET của bạn.
Các phiên bản phần mềm được sử dụng trong hướng dẫn
- API Web 2
- Unity Application Block
- Entity Framework 6 (phiên bản 5 cũng hoạt động)
Dependency Injection là gì?
Phụ thuộc (Dependency) là bất kỳ đối tượng nào mà đối tượng khác yêu cầu. Ví dụ: việc định nghĩa một repository xử lý việc truy cập dữ liệu là điều phổ biến. Hãy minh họa bằng một ví dụ. Đầu tiên, ta sẽ định nghĩa một model miền:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
Đây là một lớp kho lưu trữ đơn giản lưu trữ các mục trong cơ sở dữ liệu, sử dụng 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);
}
}
Bây giờ, hãy định nghĩa controller API Web hỗ trợ các yêu cầu GET cho các thực thể Product
. (Ta sẽ bỏ qua POST và các phương thức khác để đơn giản hóa.) Đây là lần thử đầu tiên:
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);
}
}
Lưu ý rằng lớp controller phụ thuộc vào ProductRepository
và ta đang cho phép controller tạo thể hiện của ProductRepository
. Tuy nhiên, việc mã hóa cứng phần phụ thuộc theo cách này là một ý tưởng tồi vì một số lý do:
- Nếu bạn muốn thay thế
ProductRepository
bằng cách triển khai khác, thì bạn cũng cần sửa đổi lớp controller. - Nếu
ProductRepository
có phần phụ thuộc, bạn phải định cấu hình những phần này bên trong controller. Đối với một dự án lớn có nhiều controller, mã cấu hình của bạn sẽ nằm rải rác trong dự án. - Thật khó để kiểm tra đơn vị vì controller được mã hóa cứng để truy vấn cơ sở dữ liệu. Đối với thử nghiệm đơn vị, bạn nên sử dụng repository mô phỏng hoặc sơ khai, điều này là không thể với thiết kế hiện tại.
Ta có thể giải quyết những vấn đề này bằng cách đưa (injecting) repository vào controller. Đầu tiên, cấu trúc lại lớp ProductRepository
thành một interface:
public interface IProductRepository
{
IEnumerable<Product> GetAll();
Product GetById(int id);
void Add(Product product);
}
public class ProductRepository : IProductRepository
{
// Implementation not shown.
}
Sau đó cung cấp tham số IProductRepository
dưới dạng hàm tạo:
public class ProductsController : ApiController
{
private IProductRepository _repository;
public ProductsController(IProductRepository repository)
{
_repository = repository;
}
// Other controller methods not shown.
}
Ví dụ này sử dụng hàm tạo. Bạn cũng có thể sử dụng setter injection, trong đó bạn đặt phần phụ thuộc thông qua một phương thức hoặc property setter.
Nhưng bây giờ có một vấn đề vì ứng dụng của bạn không trực tiếp tạo controller. API Web tạo controller khi route yêu cầu và API Web không biết gì về IProductRepository
. Đây là lúc trình giải quyết phụ thuộc API Web xuất hiện.
Trình giải quyết phụ thuộc API Web
API Web định nghĩa interface IDependencyResolver để giải quyết các phần phụ thuộc. Đây là định nghĩa của interface:
public interface IDependencyResolver : IDependencyScope, IDisposable
{
IDependencyScope BeginScope();
}
public interface IDependencyScope : IDisposable
{
object GetService(Type serviceType);
IEnumerable<object> GetServices(Type serviceType);
}
Interface IDependencyScope có hai phương thức:
- GetService tạo một thể hiện của một kiểu.
- GetServices tạo một tập hợp các đối tượng thuộc một kiểu được chỉ định.
Phương thức IDependencyResolver kế thừa IDependencyScope và thêm phương thức BeginScope. Ta sẽ nói về phạm vi sau trong bài hướng dẫn này.
Khi API Web tạo một thể hiện của controller, thì trước tiên nó sẽ gọi IDependencyResolver.GetService, truyền vào kiểu controller. Bạn có thể sử dụng hook mở rộng này để tạo controller, giải quyết mọi phụ thuộc. Nếu GetService trả về null, API Web sẽ tìm kiếm hàm tạo không tham số trên lớp controller.
Giải pháp phụ thuộc với Unity Container
Mặc dù bạn có thể viết triển khai IDependencyResolver hoàn chỉnh từ đầu, interface thực sự được thiết kế để đóng vai trò là cầu nối giữa API Web và các bộ chứa IoC hiện có.
Bộ chứa IoC là một thành phần phần mềm chịu trách nhiệm quản lý các phần phụ thuộc. Bạn đăng ký các kiểu với vùng chứa, sau đó sử dụng vùng chứa để tạo đối tượng. Vùng chứa tự động tìm ra các mối quan hệ phụ thuộc. Nhiều bộ chứa IoC cũng cho phép bạn kiểm soát những thứ như phạm vi và thời gian tồn tại của đối tượng.
Ghi chú
"IoC" là viết tắt của "Inversion of Control - Đảo ngược điều khiển", là mẫu chung trong đó framework gọi vào code ứng dụng. Bộ chứa IoC xây dựng các đối tượng cho bạn, điều này "đảo ngược" luồng điều khiển thông thường.
Đối với hướng dẫn này, ta sẽ sử dụng Unity từ Microsoft Patterns & Practices. (Các thư viện phổ biến khác bao gồm Castle Windsor, Spring.Net, Autofac, Ninject và StructureMap.) Bạn có thể sử dụng NuGet Package Manager để cài đặt Unity. Từ menu Tools trong Visual Studio, chọn NuGet Package Manager, sau đó chọn Package Manager Console. Trong cửa sổ Bảng điều khiển quản lý gói, gõ lệnh sau:
Install-Package Unity
Đây là cách triển khai IDependencyResolver bao bọc (wrap) vùng chứa Unity.
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();
}
}
Định cấu hình Trình giải quyết phụ thuộc
Đặt trình giải quyết phụ thuộc trên property DependencyResolver của đối tượng HttpConfiguration global.
Đoạn code sau đăng ký interface IProductRepository
với Unity và sau đó tạo tệp 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.
}
Phạm vi phụ thuộc và vòng đời của controller
Controller được tạo theo yêu cầu. Để quản lý vòng đời của đối tượng, IDependencyResolver sử dụng khái niệm phạm vi (scope).
Trình phân giải phụ thuộc được gắn vào đối tượng HttpConfiguration có phạm vi global. Khi API Web tạo controller, nó sẽ gọi BeginScope. Phương thức này trả về IDependencyScope đại diện cho phạm vi con.
API Web sau đó gọi GetService trên phạm vi con để tạo controller. Khi yêu cầu hoàn tất, API Web sẽ gọi Dispose trên phạm vi con. Sử dụng phương thức Dispose để loại bỏ các phần phụ thuộc của controller.
Cách bạn triển khai BeginScope tùy thuộc vào vùng chứa IoC. Đối với Unity, phạm vi tương ứng với vùng chứa con:
public IDependencyScope BeginScope()
{
var child = container.CreateChildContainer();
return new UnityResolver(child);
}
Hầu hết các container IoC đều có các tính năng tương đương tương tự.