Совместное использование Unity и ASP.NET MVC
В этой статье я опишу, как использовать библиотеку Microsoft Unity в приложениях ASP.NET MVC. А также, какие преимущества от этого можно получить, в частности для написания unit-тестов.
Как известно, Unity представляет собой реализацию паттерна Dependency Injection (DI). Кратко опишу, что собой представляет DI(“ внедрение зависимостей”). Представим, что некий объект использует в своей работе какой-то сервис. Ссылка на сервис хранится в приватном поле объекта. Для того, что бы объект не был жестко связан с сервисом (coupling), мы можем применить «внедрение зависимостей». DI представляет механизм, позволяющий в момент создания объекта извне инициализировать ссылку на сервис. Как правило, это делается через публичное свойство или параметризованный конструктор.
И так, для начала рассмотрим, как выглядит простое MVC приложение, без использования Unity. Предположим, что у нас есть контроллер, передающий во view список пользователей. Контроллер обращается к статическому методу класса DataHelper, для получения списка пользователей. DataHelper, в свою очередь, обращается к базе данных.
В данной реализации, мы видим сильную “связанность” классов UserController и DataHelper.
И как следствие этого, есть проблема с тестированием метода ShowAll. Мы не можем протестировать этот метод, без вызова метода GetAllUsers и соответственно обращения к базе данных.
Теперь, рассмотрим вариант с использованием Unity. Первое, что мы сделаем – избавимся от статического метода в DataHelper и выделим интерфейс. А заодно переименуем класс DataHelper в DataService.
Внесем изменения в реализацию контроллера: добавим публичное свойство DataService, в котором будет храниться ссылка на экземпляр IDataService, и пометим это свойство атрибутом Dependency.
Теперь осталось настроить собственно сам Unity. Мы сделаем это в модуле Global.ascx.
Обратите внимание, что в методе InitContainer мы регистрируем тип и его реализацию, который Unity будет внедрять, основываясь на атрибуте Dependency. При этом в качестве параметра метода RegisterType передается экземпляр ContainerControlledLifetimeManager. Это значит, что каждый раз, при вызове метода контейнера Resolve, будет возвращаться один и тот же экземпляр DataService.
И последнее, что нам осталось сделать, это реализовать свою собственную фабрику контроллеров UnityControllerFactory. Эта фабрика будет возвращать экземпляр котроллера UserController уже проинициализированный экземпляром DataService.
На этом этапе мы реализовали использование Unity в приложении MVC. Теперь можем написать unit test метода ShowAll класса UserController. Сначала создаем mock класс:
И собственно сам тест:
Теперь проверяется код только метода ShowAll, и он не зависит от класса DataService и тем более от базы данных.
В заключении хочу отметить два момента. В production мы можем настраивать контейнер Unity декларативно, с использованием конфигурационного файла. Т.о., для перехода на тестовый environment, нам не надо изменять код, достаточно поменять конфигурационный файл. И второе, «внедрение зависимостей» мы можем производить не только через публичное свойство, но и через параметризованный конструктор. И в том, и в другом случае такие члены должны помечаться атрибутом Dependency.
Как известно, Unity представляет собой реализацию паттерна Dependency Injection (DI). Кратко опишу, что собой представляет DI(“ внедрение зависимостей”). Представим, что некий объект использует в своей работе какой-то сервис. Ссылка на сервис хранится в приватном поле объекта. Для того, что бы объект не был жестко связан с сервисом (coupling), мы можем применить «внедрение зависимостей». DI представляет механизм, позволяющий в момент создания объекта извне инициализировать ссылку на сервис. Как правило, это делается через публичное свойство или параметризованный конструктор.
И так, для начала рассмотрим, как выглядит простое MVC приложение, без использования Unity. Предположим, что у нас есть контроллер, передающий во view список пользователей. Контроллер обращается к статическому методу класса DataHelper, для получения списка пользователей. DataHelper, в свою очередь, обращается к базе данных.
<code>public class DataHelper
{
public static List<User> GetAllUsers()
{
//connect to DB and fetch list of users
}
}
public class UserController
{
public ActionResult ShowAll()
{
return View(DataHelper.GetAllUsers().ToList());
}
}
* This source code was highlighted with Source Code Highlighter.
В данной реализации, мы видим сильную “связанность” классов UserController и DataHelper.
И как следствие этого, есть проблема с тестированием метода ShowAll. Мы не можем протестировать этот метод, без вызова метода GetAllUsers и соответственно обращения к базе данных.
Теперь, рассмотрим вариант с использованием Unity. Первое, что мы сделаем – избавимся от статического метода в DataHelper и выделим интерфейс. А заодно переименуем класс DataHelper в DataService.
public interface IDataService
{
List<User> GetAllUsers();
}
public class DataService : IDataService
{
public List<User> GetAllUsers()
{
//connect to DB and fetch list of users
}
}
* This source code was highlighted with Source Code Highlighter.
Внесем изменения в реализацию контроллера: добавим публичное свойство DataService, в котором будет храниться ссылка на экземпляр IDataService, и пометим это свойство атрибутом Dependency.
[Dependency]
public IDataService DataService { get; set; }
public class UserController
{
public ActionResult ShowAll()
{
return View(DataService.GetAllUsers().ToList());
}
}
* This source code was highlighted with Source Code Highlighter.
Теперь осталось настроить собственно сам Unity. Мы сделаем это в модуле Global.ascx.
protected void Application_Start()
{
…
InitContainer();
}
private static UnityContainer _container;
private static void InitContainer()
{
if (_container == null)
_container = new UnityContainer();
IControllerFactory controllerFactory = new UnityControllerFactory(_container);
ControllerBuilder.Current.SetControllerFactory(controllerFactory);
_container.RegisterType<IDataService, DataService>(new ContainerControlledLifetimeManager());
}
* This source code was highlighted with Source Code Highlighter.
Обратите внимание, что в методе InitContainer мы регистрируем тип и его реализацию, который Unity будет внедрять, основываясь на атрибуте Dependency. При этом в качестве параметра метода RegisterType передается экземпляр ContainerControlledLifetimeManager. Это значит, что каждый раз, при вызове метода контейнера Resolve, будет возвращаться один и тот же экземпляр DataService.
И последнее, что нам осталось сделать, это реализовать свою собственную фабрику контроллеров UnityControllerFactory. Эта фабрика будет возвращать экземпляр котроллера UserController уже проинициализированный экземпляром DataService.
public class UnityControllerFactory : DefaultControllerFactory
{
IUnityContainer _container;
public UnityControllerFactory(IUnityContainer container)
{
_container = container;
}
protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
{
if (controllerType == null)
throw new ArgumentNullException("controllerType");
if (!typeof(IController).IsAssignableFrom(controllerType))
throw new ArgumentException(string.Format(
"Type requested is not a controller: {0}", controllerType.Name),
"controllerType");
return _container.Resolve(controllerType) as IController;
}
}
* This source code was highlighted with Source Code Highlighter.
На этом этапе мы реализовали использование Unity в приложении MVC. Теперь можем написать unit test метода ShowAll класса UserController. Сначала создаем mock класс:
public class TestDataService : IDataService
{
public List<User> GetAllUsers()
{
return new List<User>
{
new User
{
FirstName = "firstname",
LastName = "lastname",
Age = "34"
},
new User
{
FirstName = "firstname1",
LastName = "lastname1",
Age = "31"
}
}
}
}
* This source code was highlighted with Source Code Highlighter.
И собственно сам тест:
[TestMethod]
public void ShowAllTest
{
var container = new UnityContainer();
container.RegistryType<IDataService, TestDataService>();
var userController = container.Resolve<UserController>();
ViewResult result = controller.ShowAll() as ViewResult;
var list = result.ViewData.Model as IEnumerable<User>
Assert.AreEqual(2, list.Count());
}
* This source code was highlighted with Source Code Highlighter.
Теперь проверяется код только метода ShowAll, и он не зависит от класса DataService и тем более от базы данных.
В заключении хочу отметить два момента. В production мы можем настраивать контейнер Unity декларативно, с использованием конфигурационного файла. Т.о., для перехода на тестовый environment, нам не надо изменять код, достаточно поменять конфигурационный файл. И второе, «внедрение зависимостей» мы можем производить не только через публичное свойство, но и через параметризованный конструктор. И в том, и в другом случае такие члены должны помечаться атрибутом Dependency.