Внедрение зависимостей в веб-API ASP.NET 2

Скачивание завершенного проекта

В этом руководстве показано, как внедрить зависимости в контроллер веб-API ASP.NET.

Версии программного обеспечения, используемые в этом руководстве

Что такое внедрение зависимостей?

Зависимость — это любой объект, который требуется другому объекту. Например, обычно определяется репозиторий , который обрабатывает доступ к данным. Давайте проиллюстрируем пример. Сначала мы определим модель предметной области:

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

Ниже приведен простой класс репозитория, который хранит элементы в базе данных с помощью 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);
    }
}

Теперь определим контроллер веб-API, который поддерживает запросы GET для Product сущностей. (Для простоты я оставлю POST и другие методы.) Вот первая попытка:

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);
    }
}

Обратите внимание, что класс контроллера зависит от ProductRepository, и мы позволяем контроллеру ProductRepository создать экземпляр . Однако не рекомендуется жестко кодировать зависимость таким образом по ряду причин.

  • Если вы хотите заменить ProductRepository на другую реализацию, необходимо также изменить класс контроллера.
  • ProductRepository Если имеет зависимости, необходимо настроить их внутри контроллера. Для большого проекта с несколькими контроллерами код конфигурации разбросан по всему проекту.
  • Модульный тест сложно выполнить, так как контроллер жестко запрограммирован для запроса базы данных. Для модульного теста следует использовать макет или репозиторий заглушки, что невозможно в текущей архитектуре.

Мы можем решить эти проблемы, внедрив репозиторий в контроллер. Сначала выполните рефакторинг ProductRepository класса в интерфейс :

public interface IProductRepository
{
    IEnumerable<Product> GetAll();
    Product GetById(int id);
    void Add(Product product);
}

public class ProductRepository : IProductRepository
{
    // Implementation not shown.
}

Затем укажите в IProductRepository качестве параметра конструктора:

public class ProductsController : ApiController
{
    private IProductRepository _repository;

    public ProductsController(IProductRepository repository)  
    {
        _repository = repository;
    }

    // Other controller methods not shown.
}

В этом примере используется внедрение конструктора. Можно также использовать внедрение метода задания, где зависимость задается с помощью метода задания или свойства.

Но теперь возникла проблема, так как приложение не создает контроллер напрямую. Веб-API создает контроллер при маршрутизации запроса, а веб-API ничего не знает о IProductRepository. Именно здесь появляется сопоставитель зависимостей веб-API.

Сопоставитель зависимостей веб-API

Веб-API определяет интерфейс IDependencyResolver для разрешения зависимостей. Ниже приведено определение интерфейса.

public interface IDependencyResolver : IDependencyScope, IDisposable
{
    IDependencyScope BeginScope();
}

public interface IDependencyScope : IDisposable
{
    object GetService(Type serviceType);
    IEnumerable<object> GetServices(Type serviceType);
}

Интерфейс IDependencyScope имеет два метода:

  • GetService создает один экземпляр типа .
  • GetServices создает коллекцию объектов указанного типа.

Метод IDependencyResolver наследует IDependencyScope и добавляет метод BeginScope . Я поговорим об областях далее в этом руководстве.

Когда веб-API создает экземпляр контроллера, он сначала вызывает IDependencyResolver.GetService, передавая тип контроллера. Этот обработчик расширяемости можно использовать для создания контроллера и разрешения любых зависимостей. Если GetService возвращает значение NULL, веб-API ищет конструктор без параметров в классе контроллера.

Разрешение зависимостей с помощью контейнера Unity

Хотя вы можете написать полную реализацию IDependencyResolver с нуля, интерфейс действительно предназначен для работы в качестве моста между веб-API и существующими контейнерами IoC.

Контейнер IoC — это программный компонент, отвечающий за управление зависимостями. Вы регистрируете типы в контейнере, а затем используете контейнер для создания объектов. Контейнер автоматически определяет отношения зависимостей. Многие контейнеры IoC также позволяют управлять временем существования объектов и область.

Примечание

IoC означает инверсию управления, что является общим шаблоном, в котором платформа вызывает код приложения. Контейнер IoC создает объекты за вас, что "инвертирует" обычный поток управления.

В этом руководстве мы будем использовать Unity из microsoft Patterns & Practices. (Другие популярные библиотеки включают Castle Windsor, Spring.Net, Autofac, Ninject и StructureMap.) Для установки Unity можно использовать диспетчер пакетов NuGet. В меню Сервис в Visual Studio выберите Диспетчер пакетов NuGet, а затем — Консоль диспетчера пакетов. В окне Консоль диспетчера пакетов введите следующую команду:

Install-Package Unity

Ниже приведена реализация IDependencyResolver , которая служит оболочкой для контейнера 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();
    }
}

Настройка сопоставителя зависимостей

Задайте сопоставитель зависимостей для свойства DependencyResolver глобального объекта HttpConfiguration .

Следующий код регистрирует интерфейс в IProductRepository Unity, а затем создает 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.
}

Область зависимостей и время существования контроллера

Контроллеры создаются для каждого запроса. Для управления временем существования объектов IDependencyResolver использует концепцию область.

Сопоставитель зависимостей, подключенный к объекту HttpConfiguration, имеет глобальные область. Когда веб-API создает контроллер, он вызывает BeginScope. Этот метод возвращает IDependencyScope, представляющий дочерний область.

Затем веб-API вызывает GetService на дочернем область для создания контроллера. По завершении запроса веб-API вызывает Dispose на дочернем область. Используйте метод Dispose для удаления зависимостей контроллера.

Способ реализации BeginScope зависит от контейнера IoC. Для Unity область соответствует дочернему контейнеру:

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

Большинство контейнеров IoC имеют аналогичные эквиваленты.