Wstrzykiwanie zależności w interfejsie API sieci Web ASP.NET 2
Pobieranie ukończonego projektu
W tym samouczku pokazano, jak wstrzyknąć zależności do kontrolera internetowego interfejsu API ASP.NET.
Wersje oprogramowania używane w samouczku
- Internetowy interfejs API 2
- Blok aplikacji aparatu Unity
- Entity Framework 6 (działa również wersja 5)
Co to jest wstrzykiwanie zależności?
Zależność to dowolny obiekt, którego wymaga inny obiekt. Na przykład często definiuje się repozytorium obsługujące dostęp do danych. Zilustrujmy przykład. Najpierw zdefiniujemy model domeny:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
Oto prosta klasa repozytorium, która przechowuje elementy w bazie danych przy użyciu programu 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);
}
}
Teraz zdefiniujmy kontroler internetowego interfejsu API, który obsługuje żądania GET dla Product
jednostek. (Pomijam post i inne metody dla uproszczenia). Oto pierwsza próba:
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);
}
}
Zwróć uwagę, że klasa kontrolera zależy ProductRepository
od klasy i umożliwiamy kontrolerowi utworzenie ProductRepository
wystąpienia. Jednak jest to zły pomysł, aby zakodować zależność w ten sposób, z kilku powodów.
- Jeśli chcesz zastąpić
ProductRepository
inną implementacją, należy również zmodyfikować klasę kontrolera. - Jeśli element
ProductRepository
ma zależności, należy je skonfigurować wewnątrz kontrolera. W przypadku dużego projektu z wieloma kontrolerami kod konfiguracji staje się rozproszony w całym projekcie. - Trudno jest przeprowadzić test jednostkowy, ponieważ kontroler jest zakodowany w celu wykonywania zapytań względem bazy danych. W przypadku testu jednostkowego należy użyć makiety lub repozytorium wycinków, które nie jest możliwe w przypadku bieżącego projektu.
Możemy rozwiązać te problemy, wstrzykiwając repozytorium do kontrolera. Najpierw refaktoryzuj klasę ProductRepository
do interfejsu:
public interface IProductRepository
{
IEnumerable<Product> GetAll();
Product GetById(int id);
void Add(Product product);
}
public class ProductRepository : IProductRepository
{
// Implementation not shown.
}
Następnie podaj IProductRepository
jako parametr konstruktora:
public class ProductsController : ApiController
{
private IProductRepository _repository;
public ProductsController(IProductRepository repository)
{
_repository = repository;
}
// Other controller methods not shown.
}
W tym przykładzie użyto iniekcji konstruktora. Można również użyć iniekcji ustawiającej, gdzie można ustawić zależność za pomocą metody lub właściwości setter.
Ale teraz występuje problem, ponieważ aplikacja nie tworzy kontrolera bezpośrednio. Internetowy interfejs API tworzy kontroler podczas kierowania żądania, a internetowy interfejs API nie wie nic o IProductRepository
. W tym miejscu jest dostępny program rozpoznawania zależności interfejsu API sieci Web.
Program rozpoznawania zależności interfejsu API sieci Web
Internetowy interfejs API definiuje interfejs IDependencyResolver na potrzeby rozpoznawania zależności. Oto definicja interfejsu:
public interface IDependencyResolver : IDependencyScope, IDisposable
{
IDependencyScope BeginScope();
}
public interface IDependencyScope : IDisposable
{
object GetService(Type serviceType);
IEnumerable<object> GetServices(Type serviceType);
}
Interfejs IDependencyScope ma dwie metody:
- Polecenie GetService tworzy jedno wystąpienie typu.
- GetServices tworzy kolekcję obiektów określonego typu.
Metoda IDependencyResolver dziedziczy metodę IDependencyScope i dodaje metodę BeginScope . W dalszej części tego samouczka omówię zakresy.
Gdy internetowy interfejs API tworzy wystąpienie kontrolera, najpierw wywołuje element IDependencyResolver.GetService, przekazując typ kontrolera. Możesz użyć tego haka rozszerzalności, aby utworzyć kontroler, rozwiązując wszelkie zależności. Jeśli funkcja GetService zwraca wartość null, internetowy interfejs API szuka konstruktora bez parametrów w klasie kontrolera.
Rozwiązywanie zależności za pomocą kontenera aparatu Unity
Chociaż można napisać kompletną implementację IDependencyResolver od podstaw, interfejs jest naprawdę zaprojektowany tak, aby działał jako most między internetowym interfejsem API i istniejącymi kontenerami IoC.
Kontener IoC to składnik oprogramowania, który jest odpowiedzialny za zarządzanie zależnościami. Rejestrujesz typy w kontenerze, a następnie używasz kontenera do tworzenia obiektów. Kontener automatycznie określa relacje zależności. Wiele kontenerów IoC umożliwia również kontrolowanie elementów, takich jak okres istnienia obiektu i zakres.
Uwaga
"IoC" oznacza "inwersję kontroli", czyli ogólny wzorzec, w którym struktura wywołuje kod aplikacji. Kontener IoC konstruuje obiekty dla Ciebie, co "odwraca" zwykły przepływ sterowania.
Na potrzeby tego samouczka użyjemy aparatu Unity z witryny Microsoft Patterns & Practices. (Inne popularne biblioteki to m.in.Zamek Windsor, Spring.Net, Autofac, Ninject, Simple Injector i StructureMap.) Do zainstalowania aparatu Unity można użyć Menedżer pakietów NuGet. W menu Narzędzia w programie Visual Studio wybierz pozycję NuGet Menedżer pakietów, a następnie wybierz pozycję Menedżer pakietów Konsola. W oknie Menedżer pakietów Console (Konsola Menedżer pakietów) wpisz następujące polecenie:
Install-Package Unity
Oto implementacja interfejsu IDependencyResolver , która opakowuje kontener 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();
}
}
Konfigurowanie narzędzia rozpoznawania zależności
Ustaw program rozpoznawania zależności we właściwości DependencyResolver globalnego obiektu HttpConfiguration .
Poniższy kod rejestruje IProductRepository
interfejs za pomocą aparatu Unity, a następnie tworzy element 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.
}
Zakres zależności i okres istnienia kontrolera
Kontrolery są tworzone na żądanie. Aby zarządzać okresami istnienia obiektów, IDependencyResolver używa koncepcji zakresu.
Program rozpoznawania zależności dołączony do obiektu HttpConfiguration ma zakres globalny. Gdy internetowy interfejs API tworzy kontroler, wywołuje metodę BeginScope. Ta metoda zwraca element IDependencyScope , który reprezentuje zakres podrzędny.
Następnie internetowy interfejs API wywołuje metodę GetService w zakresie podrzędnym, aby utworzyć kontroler. Po zakończeniu żądania internetowy interfejs API wywołuje metodę Dispose w zakresie podrzędnym. Użyj metody Dispose, aby usunąć zależności kontrolera.
Sposób implementowania aplikacji BeginScope zależy od kontenera IoC. W przypadku aparatu Unity zakres odpowiada kontenerowi podrzędnym:
public IDependencyScope BeginScope()
{
var child = container.CreateChildContainer();
return new UnityResolver(child);
}
Większość kontenerów IoC ma podobne odpowiedniki.