Внедрение зависимостей в веб-API ASP.NET 2
Скачивание завершенного проекта
В этом руководстве показано, как внедрить зависимости в контроллер веб-API ASP.NET.
Версии программного обеспечения, используемые в руководстве
- Веб-API 2
- Блок приложения Unity
- Entity Framework 6 (версия 5 также работает)
Что такое внедрение зависимостей?
Зависимость — это любой объект, который требуется другому объекту. Например, обычно определяет репозиторий, который обрабатывает доступ к данным. Рассмотрим пример. Сначала мы определим модель домена:
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, Simple Injector и 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 имеют аналогичные эквиваленты.