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

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 od ProductRepositoryelementu i pozwalamy kontrolerowi ProductRepository utworzyć wystąpienie. Jednak jest to zły pomysł, aby ciężko kodować 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 wycinkowego, które nie jest możliwe w przypadku bieżącego projektu.

Możemy rozwiązać te problemy, wstrzykiwając repozytorium do kontrolera. Najpierw refaktoryzacja ProductRepository klasy w interfejsie:

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 settera, gdzie ustawia się 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. Jest to miejsce, w którym jest dostępny program rozpoznawania zależności interfejsu API sieci Web.

Rozpoznawanie zależności interfejsu API sieci Web

Internetowy interfejs API definiuje interfejs IDependencyResolver do 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.
  • Usługa 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 metodę IDependencyResolver.GetService, przekazując typ kontrolera. Możesz użyć tego zaczepienia rozszerzalności, aby utworzyć kontroler, rozpoznają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 unity

Mimo że 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 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 "inversion of control", czyli ogólny wzorzec, w którym struktura wywołuje kod aplikacji. Kontener IoC tworzy obiekty dla Ciebie, co "odwraca" zwykły przepływ sterowania.

Na potrzeby tego samouczka użyjemy aparatu Unity z usługi Microsoft Patterns & Practices. (Inne popularne biblioteki to Castle Windsor, Spring.Net, Autofac, Ninject i StructureMap. Do zainstalowania aparatu Unity można użyć Menedżera pakietów NuGet. W menu Narzędzia w programie Visual Studio wybierz pozycję Menedżer pakietów NuGet, a następnie wybierz pozycję Konsola menedżera pakietów. W oknie Konsola menedżera pakietów wpisz następujące polecenie:

Install-Package Unity

Oto implementacja protokołu 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 do rozpoznawania zależności

Ustaw program rozpoznawania zależności we właściwości DependencyResolver globalnego obiektu HttpConfiguration .

Poniższy kod rejestruje interfejs za IProductRepository 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, protokół IDependencyResolver używa koncepcji zakresu.

Rozpoznawanie zależności dołączone 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 w celu utworzenia kontrolera. 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 implementacji aplikacji BeginScope zależy od kontenera IoC. W przypadku aparatu Unity zakres odpowiada kontenerowi podrzędnego:

public IDependencyScope BeginScope()
{
    var child = container.CreateChildContainer();
    return new UnityResolver(child);
}

Większość kontenerów IoC ma podobne odpowiedniki.