Injektáž závislostí ve webovém rozhraní API ASP.NET 2

Stažení dokončeného projektu

V tomto kurzu se dozvíte, jak vložit závislosti do kontroleru ASP.NET webového rozhraní API.

Verze softwaru použité v kurzu

Co je injektáž závislostí?

Závislost je libovolný objekt, který vyžaduje jiný objekt. Například je běžné definovat úložiště , které zpracovává přístup k datům. Pojďme si to ukázat na příkladu. Nejprve nadefinujeme doménový model:

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

Tady je třída jednoduchého ú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 metodu 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 ProductRepositorya necháme kontroler vytvořit ProductRepository instanci. Není ale vhodné závislost tímto způsobem pevně kódovat, a to z několika důvodů.

  • Pokud chcete nahradit ProductRepository jinou implementací, musíte také upravit třídu kontroleru.
  • ProductRepository Pokud má závislosti, musíte je nakonfigurovat v kontroleru. U velkého projektu s více kontrolery se konfigurační kód rozsadí po 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 úložiště napodobenou nebo zástupným kódem, což v aktuálním návrhu není možné.

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 jako parametr konstruktoru IProductRepository zadejte :

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 nastavíte závislost prostřednictvím metody setter nebo vlastnosti.

Teď je ale 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 neví nic o IProductRepository. To je místo, kde je k dispozici 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. O oborech budu mluvit později v tomto kurzu.

Když webové rozhraní API vytvoří instanci kontroleru, nejprve zavolá IDependencyResolver.GetService a předá typ kontroleru. Pomocí tohoto háku rozšiřitelnosti můžete vytvořit kontroler a vyřešit případné závislosti. Pokud GetService vrátí hodnotu null, webové rozhraní API vyhledá konstruktor bez parametrů ve třídě kontroleru.

Řešení závislostí s využitím kontejneru Unity

I když byste mohli napsat úplnou implementaci IDependencyResolver od začátku, rozhraní je ve skutečnosti 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í. V kontejneru zaregistrujete typy a pak kontejner použijete k vytváření objektů. Kontejner automaticky zjistí vztahy závislostí. Mnoho kontejnerů IoC také umožňuje řídit věci, jako je životnost objektu a rozsah.

Poznámka

"IoC" znamená "inverze řízení", což je obecný vzor, kdy architektura volá kód aplikace. Kontejner IoC vytvoří objekty za vás, což "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 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 Konzola Správce balíčků. V okně Konzola 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 vlastnosti DependencyResolver 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í pro jednotlivé požadavky. Ke správě životností objektů IDependencyResolver používá 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 zavolá metodu GetService pro podřízený obor a vytvoří kontroler. Po dokončení požadavku zavolá webové rozhraní API dispose v podřízeného oboru. 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.