Injektáž závislostí ve webovém rozhraní API ASP.NET 2
V tomto kurzu se dozvíte, jak do kontroleru webového rozhraní API ASP.NET vložit závislosti.
Verze softwaru používané v tomto kurzu
- Webové rozhraní API 2
- Blok aplikace Unity
- Entity Framework 6 (verze 5 také funguje)
Co je injektáž závislostí?
Závislost je libovolný objekt, který vyžaduje jiný objekt. Je například běžné definovat úložiště , které zpracovává přístup k datům. Pojďme si to ilustrovat příkladem. Nejprve definujeme doménový model:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
Tady je jednoduchá třída úložiště, která ukládá položky v databázi pomocí Entity Frameworku.
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);
}
}
Teď nadefinujme kontroler webového rozhraní API, který podporuje požadavky GET pro Product
entity. (Pro zjednodušení vynechám POST a další metody.) Tady je první pokus:
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);
}
}
Všimněte si, že třída kontroleru závisí na ProductRepository
a necháme kontroleru vytvořit ProductRepository
instanci. Je ale špatně zakódovat závislost tímto způsobem, a to z několika důvodů.
- Pokud chcete nahradit
ProductRepository
jinou implementací, musíte také upravit třídu kontroleru. - Pokud má
ProductRepository
závislosti, musíte je nakonfigurovat uvnitř kontroleru. U velkého projektu s více řadiči se váš konfigurační kód rozsadí v celém projektu. - Testování jednotek je obtížné, protože řadič je pevně zakódovaný pro dotazování databáze. Pro test jednotek byste měli použít napodobení nebo úložiště zástupných procedur, které není možné s aktuálním návrhem.
Tyto problémy můžeme vyřešit vložením úložiště do kontroleru. Nejprve refaktorujte ProductRepository
třídu do rozhraní:
public interface IProductRepository
{
IEnumerable<Product> GetAll();
Product GetById(int id);
void Add(Product product);
}
public class ProductRepository : IProductRepository
{
// Implementation not shown.
}
Pak zadejte parametr konstruktoru IProductRepository
:
public class ProductsController : ApiController
{
private IProductRepository _repository;
public ProductsController(IProductRepository repository)
{
_repository = repository;
}
// Other controller methods not shown.
}
Tento příklad používá injektáž konstruktoru. Můžete také použít injektáž setter, kde závislost nastavíte metodou setter nebo vlastností.
Ale teď je problém, protože vaše aplikace nevytvoří kontroler přímo. Webové rozhraní API vytvoří kontroler při směrování požadavku a webové rozhraní API o něm nic neví IProductRepository
. Tady přichází překladač závislostí webového rozhraní API.
Překladač závislostí webového rozhraní API
Webové rozhraní API definuje rozhraní IDependencyResolver pro překlad závislostí. Tady je definice rozhraní:
public interface IDependencyResolver : IDependencyScope, IDisposable
{
IDependencyScope BeginScope();
}
public interface IDependencyScope : IDisposable
{
object GetService(Type serviceType);
IEnumerable<object> GetServices(Type serviceType);
}
Rozhraní IDependencyScope má dvě metody:
- GetService vytvoří jednu instanci typu.
- GetServices vytvoří kolekci objektů zadaného typu.
IDependencyResolver metoda dědí IDependencyScope a přidá BeginScope metoda. Promluvím si o oborech později v tomto kurzu.
Když webové rozhraní API vytvoří instanci kontroleru, nejprve volá IDependencyResolver.GetService a předává typ kontroleru. Pomocí tohoto háku rozšiřitelnosti můžete vytvořit kontroler a přeložit všechny závislosti. Pokud GetService vrátí hodnotu null, webové rozhraní API hledá konstruktor bez parametrů ve třídě kontroleru.
Řešení závislostí s využitím kontejneru Unity
I když byste mohli napsat kompletní implementaci IDependencyResolver úplně od začátku, rozhraní je opravdu navržené tak, aby fungovalo jako most mezi webovým rozhraním API a existujícími kontejnery IoC.
Kontejner IoC je softwarová komponenta, která zodpovídá za správu závislostí. Zaregistrujete typy v kontejneru a pak pomocí kontejneru vytvoříte objekty. Kontejner automaticky zjistí vztahy závislostí. Mnoho kontejnerů IoC také umožňuje řídit věci, jako je životnost objektu a rozsah.
Poznámka:
IoC je zkratka pro "inversion of control", což je obecný vzor, kdy architektura volá do kódu aplikace. Kontejner IoC za vás vytvoří objekty, které "invertuje" obvyklý tok řízení.
Pro účely tohoto kurzu použijeme Unity z Microsoft Patterns &Practices. (Mezi další oblíbené knihovny patří:Castle Windsor, Spring.Net, Autofac, Ninject, Simple Injector a StructureMap.) K instalaci Unity můžete použít Správce balíčků NuGet. V nabídce Nástroje v sadě Visual Studio vyberte Správce balíčků NuGet a pak vyberte Správce balíčků Konzola. V okně konzoly Správce balíčků zadejte následující příkaz:
Install-Package Unity
Tady je implementace IDependencyResolver , která zabalí kontejner 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();
}
}
Konfigurace překladače závislostí
Nastavte překladač závislostí na DependencyResolver vlastnost globálního objektu HttpConfiguration .
Následující kód zaregistruje rozhraní v IProductRepository
Unity a pak vytvoří 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.
}
Rozsah závislostí a životnost kontroleru
Kontrolery se vytvářejí podle požadavku. Ke správě životností objektů používá IDependencyResolver koncept oboru.
Překladač závislostí připojený k objektu HttpConfiguration má globální obor. Když webové rozhraní API vytvoří kontroler, volá BeginScope. Tato metoda vrátí IDependencyScope , který představuje podřízený obor.
Webové rozhraní API pak volá GetService v podřízené oblasti, aby se vytvořil kontroler. Po dokončení požadavku volá webové rozhraní API v podřízeného oboru dispose . Pomocí metody Dispose odstraňte závislosti kontroleru.
Způsob implementace BeginScope závisí na kontejneru IoC. Pro Unity obor odpovídá podřízeného kontejneru:
public IDependencyScope BeginScope()
{
var child = container.CreateChildContainer();
return new UnityResolver(child);
}
Většina kontejnerů IoC má podobné ekvivalenty.