четверг, 2 декабря 2010 г.

ASP.NET MVC 3: Поддержка Dependency Injection

Материал основан на опыте использования ASP.NET MVC 3 RC.
Одно из нововведений ASP.NET MVC 3 - это поддержка инъекций кода "из коробки". Уже в preview 1 была добавлена поддержка инъекций для:
  • инстанцирование фабрики контроллера и самих контроллеров;
  • инстанцирование движков представлений и самих страниц представлений;
  • инстанцирование фильтров действий.
И тогда же разработчики пообещали в будущем добавить поддержку инъекций кода и для:
  • для механизма Model Binder;
  • для провайдеров значений (Value Providers);
  • для провайдеров валидации;
  • для провайдеров метаданных моделей.
Что и было сделано уже в Beta версии. Также при переходе к Beta несколько изменился механизм применения этих нововведений. Как раз о применении инъекций кода в ASP.NET MVC 3 я и хочу рассказать далее.

Введение

Я уже не мало времени занимаюсь разработкой на .NET (C#), при этом имею опыт создания приложений как типа WinForms, так и типа WebForms. Последнее время постоянно работаю и зарабатываю на ASP.NET WebForms, в свете чего накопилось достаточно много всяких своих заморочек как надо и как не надо писать приложения такого типа. При всём этом пока не предоставлялось возможности плотно поработать с тестированием, инъекциями кода. Читал обзоры, пробовал, но всё по мелочи...

К ASP.NET MVC начал присматриваться ещё со времен его первых превью, читал обзоры и сравнения с WebForms, но применить на толковом проекте всё руки никак не доходили (то времени на проект нет, то темы для проекта нет, то прочитаю какой-нибудь отзыв, где говорится, что оно ничем и не лучше, а только хуже ещё...) Прочитав по подробнее про паттерн MVC с удовольствием для себя отметил, что к схожей модели я устремлял многие свои наработки и не безуспешно, и это был ещё один задерживающий фактор (если можно использовать эти преимущества и так, зачем что-то менять?). Кроме того несколько пугала смесь разметки и кода в представлениях ASP.NET MVC, в WebForms можно было всё нарисовать тегами и это выглядело "красивее" (imho).

С анонсом 3 версии платформы ASP.NET MVC и обзором нововведений решил, что хватит откладывать - пора научиться работать и с ней. Не малую роль в этом решении сыграли новый движок представлений Razor и желание плотнее заняться тестированием кода. Надо отметить, что кстати и проектик один подвернулся, что и подтолкнуло процесс. И вот я нашёл книгу, подходящую под мои пожелания, и сразу через огонь, воду и медные трубы. Захотел сразу сделать "правильно", как в книжке умной учат: есть модель, нужно её грузить из БД? - отлично - для этого нужен объект типа репозиторий; модель для представления загружает кто? - контроллер, а правильно сделать как? - через инъекции кода. Это всё хорошо и для поддержки и для тестирования и т.д. и т.п. И вот тут, когда начал исследовать инъекции кода внутри ASP.NET MVC и всплыло это нововведение третьего выпуска.

Собственно, по теме...

А по теме начну описание с того, что было ещё в ASP.NET MVC 1 и 2, вернее, как там это надо делать. В предыдущих версиях ASP.NET MVC инъекции кода встраивались с помощью реализации собственной фабрики контроллеров, нужно было отнаследоваться от фабрики контроллеров по умолчанию и перегрузить один метод, который, собственно, и создавал экземпляры контроллеров. Выглядело это, примерно, так:

public class UnityControllerFactory : DefaultControllerFactory
{
    protected override IController GetControllerInstance(RequestContext reqContext, Type controllerType)
    {
        IController controller;
        // ... всякие проверки и начальная обработка [1]
        try
        {
            controller = MvcUnityContainer.Container.Resolve(controllerType)
                            as IController;
        }
        catch (Exception ex)
        {
            // обработка исключения [2]
        }
        return controller;
    }
}

Здесь MvcUnityContainer.Container - проинициализированный и сконфигурированный DI-контейнер. Здесь и далее в примерах используется библиотека Unity. И собственно подключение реализованной фабрики контроллеров происходит при старте приложения:

void Application_Start()
{
    // всё что нужно при старте +
    ControllerBuilder.Current.SetControllerFactory(
                        typeof(UnityControllerFactory));
}

Таким образом получалось, что при создании контроллеров будет использован указанный DI-контейнер. При этом я встречал разные реализации опущенных кусков кода [1] и [2], на почве чего возникали и дискуссии.

В ASP.NET MVC 3 разработчики решили упростить жизнь программистам и взяли на себя реализацию спорных кусков кода. А программистам дали возможность указать как именно создавать объекты (например, объект репозитория), которые нужны для инициализации других объектов (например, контроллеров). Т.е. часть инъекций кода стала встроенной в ASP.NET MVC 3 - это часть, которая касается создания внутренних объектов, таких как контроллеры. Но это совсем не означает, что больше не нужен внешний DI-контейнер, - он нужен, просто его подключение значительно упростилось и стало единичным для всех внутренних объектов.

Для подключения контейнера есть статический класс с тремя статическими методами:

public class DependencyResolver
{
    public static IDependencyResolver Current { get; }
 
    public static void SetResolver(IDependencyResolver resolver);
    public static void SetResolver(object commonServiceLocator);
    public static void SetResolver(Func<Type, object> getService,
                                   Func<Type, IEnumerable<object>> getServices);
}

Первый метод принимает в качестве аргумента объект, реализующий интерфейс IDependencyResolver (добавлен в Beta версии - это и есть упомянутое выше изменение относительно Preview 1):

public interface IDependencyResolver {
    object GetService(Type serviceType);
    IEnumerable<object> GetServices(Type serviceType);
}

Реализация этого интерфейса подразумевает всего два метода: "получить объект по типу" и "пол множество объектов по типу". С использованием Unity реализуется всё достаточно просто:

public class UnityDependencyResolver : IDependencyResolver
{
    IUnityContainer container;

    public UnityDependencyResolver(IUnityContainer c)
    {
        container = c;
    }

    public object GetService(Type serviceType)
    {
        try { return container.Resolve(serviceType); }
        catch { return null; }
    }

    public IEnumerable<object> GetServices(Type serviceType)
    {
        try { return container.ResolveAll(serviceType); }
        catch { return new List<object>(); }
    }
}

И подключение при старте приложения:

void Application_Start()
{
    // всё что нужно при старте +
    IUnityContainer container = InitContainer();
    DependencyResolver.SetResolver(new UnityDependencyResolver(container));
}

А так как интерфейс и его реализация очень просты, то можно пойти дальше и воспользоваться статическим методом, который принимает два делегата функций:

void Application_Start()
{
    // всё что нужно при старте +
    IUnityContainer container = InitContainer();
    DependencyResolver.SetResolver(
        t => {
            try { return container.ResolveAll(t); }
            catch { return new List<object>(); }
        },
        t => {
            try { return container.ResolveAll(t); }
            catch { return new List<object>(); }
        }
    );
}

И всё. Стало проще, не правда ли?

Вместо заключения

При написании текста были использованы материалы: ASP.NET MVC 3: подробный обзор нововведенийASP.NET MVC 3 Service Location, Part 5: IDependencyResolver, Dependency Injection in MVC 3 Was Made Easier

Есть мнение, что этот подход реализует паттерн ServiceLocator. Кстати, именно такое название носил статический класс в версии ASP.NET MVC 3 preview 1 - MvcServiceLocator. В этом же мнении говорится, что этот паттерн на самом деле является анти-паттерном. Мне эта тема довольна интересна и об этом я хочу написать в следующем своём сообщении (Такой вот мини-анонс ;-) ).