ASP.NET MVC, WebApi, SignalR и UnityContainer

в 14:11, , рубрики: .net, ASP, asp.net mvc, dependency injection, signalr, web api, метки: , , , , ,

Известно, что все хорошие джедаи используют внедрение зависимости (перевод) в своих проектах, это увеличивает концентрацию мидихлориан в крови и тестируемость кода в приложении. В данной статье я хочу рассмотреть некоторые аспекты использования UnityContainer в ASP.NET приложении, а именно, использование инжекции зависимостей через конструкторы контроллеров в ASP.NET MVC и WebApi и хабов в SignalR. Пример приложения присутствует.

Dependency Injection - Golf analogy

Теория

Допустим, что у нас есть открытый универсальный интерфейс репозитория:

    public interface IRepository<T> : IDisposable {
        IEnumerable<string> GetData();
    }

И мы очень хотим получить в контроллере экземпляр данного репозитория, но уже с конкретным типом. Я выбрал такой пример специально, так как подобный принцип часто используется при работе с EntityFramework. Кроме этого, он позволяет продемонстрировать те трудности, с которыми можно столкнуться при попытках получить такой типа в контроллере. Рассмотрим возможные варианты.

Использование Service Locator

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

    public static class Global {
         public static readonly IUnityContainer ServiceLocator = new UnityContainer();
    }

Регистрируем конкретный класс где-нибудь в Global.asax при загрузке приложения.

Global.ServiceLocator.RegisterType(typeof(IRepository<>), typeof(Repository<>));

Получаем репозиторий в контроллере.

        public ActionResult Index() {
            var repository = Global.ServiceLocator.Resolve<IRepository<int>>();
            var data = repository.GetData();
            return View(data);
        }

Всё работает просто отлично, а количество кода минимально. Проблема в том, что теперь наш контроллер зависит от Global.ServiceLocator. Это, конечно, не так страшно, как зависимость от конкретного класса, но тоже может прилично отравить жизнь при тестировании, особенно, если тесты запускаются параллельно в нескольких потоках, и каждый тест хочет использовать свою реализацию репозитория.

Использование Dependency Resolver

ASP.NET MVC, WebApi и SignalR предоставляют отличные возможности для расширения, в частности, возможность замены стандартных DependencyResolvers, используемых инфраструктурой для разрешения зависимостей. О том как это сделать, можно почитать следующие статьи:

Во всех случаях нам необходимо создать свой класс, реализующий необходимый IDependencyResolver и зарегистрировать его в соответствующей инфраструктуре. Хочу заметить, что в каждом случае данные интерфейсы имеют одно имя, но находятся в разных пространствах имён и имеют немного отличающиеся методы. Но они имеют одну общую черту: если запрашиваемый тип не может быть разрешён, то они возвращают null. Или пытаются найти данный тип в DefaultDependencyResolver, в случае с SignalR, так как именно в нём происходит регистрация всех компонент его инфраструктуры. Очевидно, что данное поведение кардинально отличается от того, что делает метод IUnitiContainer.Resolve(), который в данном случает просто вбросит исключение.

Казалось бы, в чём проблема? Используй метод-расширение IUnityContainer.IsRegistered() и, если тип не зарегистрирован, возвращай null.

        public static T TryResolve<T>(this IUnityContainer container) {
            var isRegistered = true;
            var typeToCheck = typeof (T);
            if (typeToCheck.IsInterface || typeToCheck.IsAbstract) {
                isRegistered = container.IsRegistered(typeToCheck);

                if (!isRegistered && typeToCheck.IsGenericType) {
                    var openGenericType = typeToCheck.GetGenericTypeDefinition();
                    isRegistered = container.IsRegistered(openGenericType);
                }
            }
            return isRegistered ? container.Resolve<T>() : default(T);
        }

Приведённый выше пример взят из реализации Unity.MVC3. Единственная его проблема в том, что в нашем примере он работать не будет.

System.ArgumentException: The supplied type IRepository`1 does not have the same number of generic arguments as the target type Repository`1.

Ну ладно, попробуем использовать блок try/catch.

        public static T TryResolve<T>(this IUnityContainer container) {
            try {
                return container.Resolve<T>();
            }
            catch (Exception) {
                return default(T);
            }
        }

Даже если учесть, что управление потоком выполнения через исключения является плохой практикой, данный метод имеет небольшие проблемы с производительностью.

Time comparsion with 100000 iterations.

  1. Elapsed (Resolve when registered): 00.38940s
  2. Elapsed (TryResolve when registered): 00.37679s
  3. Elapsed (TryResolve when does't registered): 00.00393s
  4. Elapsed (Resolve with IsRegistered when registered): 01.00460s
  5. Elapsed (Resolve with IsRegistered when does't registered): 00.72297s
  6. Elapsed (Resolve with Exception when registered): 00.32938s
  7. Elapsed (Resolve with Exception when does't registered): 05.60332s

В первой строке показан результат теста при использовании простого вызова метода Resolve без каких-либо проверок. Во второй и третьей — метод TryResolve, о котором пойдёт разговор ниже. В четвёртой и пятой — проверка через IsRegistered(). В последних двух — с использование блока try/catch. Видно, что время выполнения в третьей и седьмой строках различается в 1500 раз. Это, конечно, может быть незаметно в реальном приложении, но, всё равно, не приятно.

В недрах UnityContainer метод TryResolve обращается к полю UnityContainer.registeredNames.registeredKeys типа Dictionary<Type, List<string>> в котором хранятся зарегистрированные типы. Причём, если тип зарегистрирован без имени, то список будет содержать null элемент. Проблема в том, что TryResolve проверяет наличие регистрации на напрямую, а через кучу посредников, которые несколько раз преобразуют словарь в список, список в словарь, всё это в хитрый Linq итератор и только после этого проверяют наличие регистрации типа. Вбрасывая, как я уже упоминал, исключение при проверке регистрации универсального типа, видимо для того, чтобы разработчики сильно не скучали. Для ускорения процесса можно обратиться напрямую к этому полю, что невозможно, так как оно private и находится в internal классе. Но на помощь всегда могут прийти деревья выражений и отражение.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.Practices.Unity;

namespace CommonInfrastructure {

    public static class UnityExtensions {
        private static readonly Func<UnityContainer, Dictionary<Type, List<string>>> GetRegisteredKeys;

        private static readonly ResolverOverride[] EmptyResolverOverrides = new ResolverOverride[0];

        static UnityExtensions() {
            // Get information about fileds.
            var unityType = typeof (UnityContainer);
            var registryType = Type.GetType("Microsoft.Practices.Unity.NamedTypesRegistry, " + unityType.Assembly.FullName);

            const BindingFlags flags = BindingFlags.Instance | BindingFlags.NonPublic;
            var registeredNamesInfo = unityType.GetField("registeredNames", flags);
            var registeredKeysInfo = registryType.GetField("registeredKeys", flags);

            // Create and compile expression to accessing the field
            // UnityContainer.registeredNames.registeredKeys of type Dictionary<Type, List<string>>
            var unityParam = Expression.Parameter(unityType);
            GetRegisteredKeys = Expression
                .Lambda<Func<UnityContainer, Dictionary<Type, List<string>>>>(
                    Expression.Field(
                        Expression.Field(
                            unityParam,
                            registeredNamesInfo),
                        registeredKeysInfo),
                    unityParam)
                .Compile();
        }
    ....

Получаем типы контейнера и internal класса NamedTypesRegistry, объявленного в той же сборке. Получаем FieldInfo для необходимых приватных полей. Собираем и компилируем выражение вида Func<UnityContainer, Dictionary<Type, List<string>>> GetRegisteredKeys = container => container.registeredNames.registeredKeys;.

    .... 
        /// <summary>
        ///     Try to resolve an instance of requested type <typeparamref name="T" /> without name.
        ///     If type is interface or abstract class and it is't registered then return null.
        /// </summary>
        public static T TryResolve<T>(this IUnityContainer container) where T : class {
            return (T) TryResolve(container, typeof (T));
        }

        /// <summary>
        ///     Try to resolve an instance of requested <paramref name="type" /> without name.
        ///     If type is interface or abstract class and it is't registered then return null.
        /// </summary>
        public static object TryResolve(this IUnityContainer container, Type type) {
            bool resolve;
            if (type.IsInterface || type.IsAbstract) {
                // Get the dictionary with registered types and names.
                var keys = GetRegisteredKeys((UnityContainer) container);

                // Check if interface or abstract type is registered in the container.
                resolve = IsRegistered(type, keys);

                // If type still is't registered and it's generic type then check if generic type definition is registered.
                if (!resolve && type.IsGenericType) {
                    var openGenericType = type.GetGenericTypeDefinition();
                    resolve = IsRegistered(openGenericType, keys);
                }
            }
            else {
                // If type can be created then resolve it immediately.
                resolve = true;
            }

            // If type is registered then resolve it or return null if nothing was found.
            return resolve ? container.Resolve(type, null, EmptyResolverOverrides) : null;
        }

        private static bool IsRegistered(Type type, Dictionary<Type, List<string>> keys) {
            List<string> names;
            if (keys.TryGetValue(type, out names)) {
                // By default type without name is registered with null string.
                if (names.Exists(name => ReferenceEquals(name, null))) {
                    return true;
                }
            }
            return false;
        }
    }
}

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

Можно, конечно, написать расширение для UnityContainer и проверять регистрацию через него как это сделано, к примеру, в этой статье: Test if classes are registered in unity. Но, в нашем случае, можно вообще отказаться от этой проверки.

Использование активаторов

ASP.NET MVC, WebApi и SignalR для создания контроллеров и хабов используют реализации интерфейсов I*Activator. В ASP.NET MVC это System.Web.Mvc.IControllerActivator, в WebApi это System.Web.Http.Dispatcher.IHttpControllerActivator, в SignalR это Microsoft.AspNet.SignalR.Hubs.IHubActivator. Все они содержат единственный метод Create, параметрами у которого являются тип создаваемого класса и некоторая информация о контексте. Остаётся только написать базовые реализации этих интерфейсов и зарегистрировать их в необходимых инфраструктурах.

ASP.NET MVC

using System;
using System.Diagnostics;
using System.Web.Mvc;
using System.Web.Routing;
using Microsoft.Practices.Unity;

namespace WebApp.Infrastructure {
    public sealed class UnityControllerActivator : IControllerActivator {
        private readonly IUnityContainer _container;

        public UnityControllerActivator(IUnityContainer container) {
            _container = container;
        }

        public IController Create(RequestContext requestContext, Type controllerType) {
            return (IController) _container.Resolve(controllerType);
        }
    }
}

WebApi

using System;
using System.Diagnostics;
using System.Net.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Dispatcher;
using Microsoft.Practices.Unity;

namespace WebApp.Infrastructure {
    public sealed class UnityHttpControllerActivator : IHttpControllerActivator {
        private readonly IUnityContainer _container;

        public UnityHttpControllerActivator(IUnityContainer container) {
            _container = container;
        }

        public IHttpController Create(HttpRequestMessage request, HttpControllerDescriptor controllerDescriptor, Type controllerType) {
            return (IHttpController) _container.Resolve(controllerType);
        }
    }
}

SignalR

using System.Diagnostics;
using Microsoft.AspNet.SignalR.Hubs;
using Microsoft.Practices.Unity;

namespace WebApp.Infrastructure {
    public sealed class UnityHubActivator : IHubActivator {
        private readonly IUnityContainer _container;

        public UnityHubActivator(IUnityContainer container) {
            Debug.Assert(container != null, "container == null");

            _container = container;
        }

        public IHub Create(HubDescriptor descriptor) {
            var hubType = descriptor.HubType;
            return hubType != null ? _container.Resolve(hubType) as IHub : null;
        }
    }
}

Зарегистрируем активаторы в инфраструктуре.

using System.Web.Http;
using System.Web.Http.Dispatcher;
using System.Web.Mvc;
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;
using Microsoft.Practices.Unity;
using WebApp.Infrastructure;
using WebApp.Models;

namespace WebApp.App_Start {
    public static class ContainerConfig {
        public static void Config() {
            var container = new UnityContainer();
            MapTypes(container);

            // Set resolver to MVC.
            var controllerActivator = new UnityControllerActivator(container);
            ControllerBuilder.Current.SetControllerFactory(new DefaultControllerFactory(controllerActivator));

            // Set resolver to WebApi.
            var httpControllerActivator = new UnityHttpControllerActivator(container);
            GlobalConfiguration.Configuration.Services.Replace(typeof (IHttpControllerActivator), httpControllerActivator);

            // Set resolver to SignalR.
            var hubActivator = new UnityHubActivator(container);
            GlobalHost.DependencyResolver.Register(typeof (IHubActivator), () => hubActivator);
        }

        private static void MapTypes(IUnityContainer container) {
            container.RegisterType(typeof(IRepository<>), typeof(Repository<>));
        }
    }
}

И вызовем всё это во время запуска приложения, к примеру, в Global.asax. Не забудем, что SignalR должен быть сконфигурирован перед всем остальным.

using System.Web;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
using WebApp.App_Start;

namespace WebApp {
    public class MvcApplication : HttpApplication {
        protected void Application_Start() {
            ContainerConfig.Config();

            SignalRConfig.Config(RouteTable.Routes);

            AreaRegistration.RegisterAllAreas();
            WebApiConfig.Config(GlobalConfiguration.Configuration);
            FilterConfig.Config(GlobalFilters.Filters);
            RouteConfig.Config(RouteTable.Routes);
            BundleConfig.Config(BundleTable.Bundles);
            AuthConfig.Config();
        }
    }
}

Теперь можно переходить к реализации контроллеров и хабов.

Практика

Для примера создадим небольшое одностраничное приложение, единственной задачей которого будет вывод информации из репозитория различными путями. Реализация репозитория будет очень простой, при вызове GetData() будем получать несколько строк данных, по которым можно опознать, из какого репозитория они были получены. Также, он должен быть корректно уничтожен после окончания запроса.

    public sealed class Repository<T> : IRepository<T> {
        public IEnumerable<string> GetData() {
            for (int i = 0; i < 10; i++) {
                yield return string.Format("Data {0} of type {1}", i, typeof (T));
            }
        }

        public void Dispose() {
            Debug.WriteLine("Repository<{0}>.Dispose", typeof (T));
        }
    }

Создадим контроллеры для ASP.NET MVC и WebApi и поместим их в папку Controllers, чтобы не нарушать конвенции. Хочу заметить, что к классам контроллеров применены атрибуты System.Web.Mvc.AuthorizeAttribute и System.Web.Http.AuthorizeAttribute для предотвращения доступа неавторизованным пользователям. Приложение создавалось на базе шаблона ASP.NET MVC 4 Web Application — Single Page Application, так что вся необходимая инфраструктура, необходимая для регистрации на основе ролей там уже присутствует.

ASP.NET MVC

using System.Web.Mvc;
using WebApp.Models;

namespace WebApp.Controllers {

    [Authorize]
    public sealed class DataController : Controller {
        private readonly IRepository<int> _repository;

        public DataController(IRepository<int> repository) {
            _repository = repository;
        }

        // GET: /Data/
        public ActionResult Index() {
            ViewBag.Title = "Data from DataController";
            var data = _repository.GetData();
            return View(data);
        }

        protected override void Dispose(bool disposing) {
            _repository.Dispose();
            base.Dispose(disposing);
        }
    }
}

WebApi

using System.Collections.Generic;
using System.Web.Http;
using WebApp.Filters;
using WebApp.Models;

namespace WebApp.Controllers {

    [Authorize]
    [ValidateHttpAntiForgeryToken]
    public sealed class DataWebApiController : ApiController {
        private readonly IRepository<string> _repository;

        public DataWebApiController(IRepository<string> repository) {
            _repository = repository;
        }

        // GET /api/DataWebApi/
        public IEnumerable<string> GetData() {
            return _repository.GetData();
        }

        protected override void Dispose(bool disposing) {
            _repository.Dispose();
            base.Dispose(disposing);
        }
    }
}

В папке Hubs создадим хаб с аналогичной функциональностью. Его тоже пометим атрибутом Microsoft.AspNet.SignalR.AuthorizeAttribute.

SignalR

using System.Collections.Generic;
using Microsoft.AspNet.SignalR;
using WebApp.Models;

namespace WebApp.Hubs {

    [Authorize]
    public sealed class DataHub : Hub {
        private readonly IRepository<double> _repository;

        public DataHub(IRepository<double> repository) {
            _repository = repository;
        }

        public IEnumerable<string> GetData() {
            return _repository.GetData();
        }

        protected override void Dispose(bool disposing) {
            _repository.Dispose();
            base.Dispose(disposing);
        }
    }
}

Во всех случаях мы получаем экземпляр репозитория через конструктор. Так как мы зарегистрировали необходимые активаторы, то при создании хаба или контроллера мы получим необходимый закрытый тип обобщённого репозитория.

Приложение я решил написать на TypeScript. Для его корректной работы необходим плагин TypeScript for Visual Studio 2012, а также Web Essentials 2012. Все скрипты, относящиеся к приложению, будут находиться в папке Scripts/app. Также понадобятся следующие NuGet пакеты: Twitter.Bootstrap, RequireJs, KnockoutJs, jQuery, SignalR, а также TypeScript Definitions для них. Последние являются описаниями интерфейсов библиотек для TypeScript. Если у вас стоит ReSharper, то он может подло скрыть пункты меню с рефакторингами для TypeScript, для предотвращения этого, снимем галку с пункта «Hide overridden Visual Studio menu items» в его настройках. Создадим модель представления для приложения и экспортируем её в AMD формате.

/scripts/app/app.ts

/// <reference path="../typings/knockout/knockout.d.ts" />
/// <reference path="../typings/jquery/jquery.d.ts" />
/// <reference path="../typings/signalr/signalr.d.ts" />

/** 
* Helper method to creating ajax requests with anti firgery token.
* @param type Request type: get, post, delete, put.
* @param url A string containing the URL to which the request is sent.
* @param data Data to be sent to the server. Before sending serializes to Json format.
* @param dataType The type of data that you're expecting back from the server. Default is Json.
*/
function ajaxRequest(type: string, url: string, data?: any, dataType: string = "json") {
    var headers = {};
    var antiForgeryToken = $("#antiForgeryToken").val();
    if (antiForgeryToken) {
        headers = {
            'RequestVerificationToken': antiForgeryToken
        }
    }
    return $.ajax(url, {
        headers: headers,
        dataType: dataType,
        contentType: "application/json",
        cache: false,
        type: type,
        data: data ? data.toJson() : null
    });
}

interface IDataHub {
    client: {};
    server: {
        getData(): JQueryDeferred;
    };
}

export class AppViewModel {
    private dataHub: IDataHub;

    /** If error occurred this field should contain error message. Otherwise it's empty string. */
    errorMessage = ko.observable("");

    /** Displays optional text. */
    text = ko.observable("");

    /** Data received from server. */
    data = ko.observableArray([]);

    /** Create ViewModel and connect to hubs. */
    constructor() {
        this.dataHub = $.connection["dataHub"];
        $.connection.hub
            .start()
            .fail(e => this.onFail(e));
    }

    /** Receive data from SignalR hub. */
    signalrBtnClick() {
        this.text("SignalR");
        this.dataHub.server
            .getData()
            .done(d => this.showData(d))
            .fail(e => this.onFail(e));
    }

    /** Receive data from WebApi controller. */
    apiBtnClick() {
        this.text("WebApi");
        ajaxRequest("get", "/api/dataWebApi")
            .done(d => this.showData(d))
            .fail(e => this.onFail(e));
    }

    /** Receive data from static page controller. */
    staticBtnClick() {
        ajaxRequest("get", "/Data", null, "html")
            .done(data => {
                this.data([]);
                this.errorMessage("");
                this.text(data);
            })
            .fail(e => this.onFail(e));
    }

    private onFail(error: string) {
        this.text("Error!");
        this.data([]);
        this.errorMessage(error);
    }

    private showData(data: string[]) {
        this.data(data);
        this.errorMessage("");
    }
}

В данной модели представления, в момент её создания, устанавливается соединение с хабом DataHub. Далее, в ответ на вызов методов *BtnClick, мы получаем данные с сервера и отображаем в представлении с помощью привязок.

<div class="row">
    <div class="span2">
        <div class="btn-group btn-group-vertical" data-toggle="buttons-radio">
            <button class="btn span2" data-bind="click: signalrBtnClick">SignalR</button>
            <button class="btn span2" data-bind="click: apiBtnClick">WebApi</button>
            <button class="btn span2" data-bind="click: staticBtnClick">Static page</button>
        </div>
    </div>

    <div class="span5">
        <div  data-bind="visible: errorMessage() != ''">
            <span class="text-error" data-bind="text: errorMessage"></span>
        </div>

        <div  class="well" data-bind="visible: text() != ''">
            <p data-bind="html: text"></p>
            <ol data-bind="foreach: data">
                <li><span data-bind="text: $data"></span></li>
            </ol>
        </div>
    </div>
</div>

Для работы всего этого добра, подключим где-нибудь вышеуказанные скрипты, кроме require.js и файла модели представления. Для доступа к хабам на сервере, необходимо подключить генерируемый во время выполнения скрипт ~/signalr/hubs. Инициализация биндингов будет происходить в файле init.ts, который будет загружен RequireJs автоматически. Для предотвращения кэширования скриптов на время разработки, сконфигурируем RequireJs перед его загрузкой.

<script src="~/signalr/hubs"> </script>       
<script type="text/javascript">
var require = {
    waitSeconds: 15,
    urlArgs: "bust=" + new Date().getTime()
};
</script>
<script data-main="/scripts/app/init" type="text/javascript" src="@Scripts.Url("~/Scripts/require.js")"> </script>

В init.ts импортируем модуль с моделью представления и применим необходимые привязки. Для импорта и экспорта модулей в TypeScript необходимо выставить «Use the AMD module» в true в настройках «Tools»-«Options»-«Web Essentials»-«TypeScript»-«Compiler flags».

/scripts/app/init.ts

import app = module("app");
$(() => {
    ko.applyBindings(new app.AppViewModel());
});

Ключевое слово export помечает класс, функцию, или переменную как экспортируемую. Импорт производится с помощью ключевых слов import и module , после чего можно получить доступ к экспортированным членам в импортированном модуле. В итоге получим нечто вроде этого.

ASP.NET MVC, WebApi, SignalR и UnityContainer

Заключение

Как было показано выше, единообразное применение контейнера внедрения зависимостей в ASP.NET приложении возможно и не представляет особых трудностей. Это особенно актуально в одностраничных приложениях, где иногда бывает необходимо для разных целей использовать разные технологии, наиболее для них подходящие. TypeScript отлично себя показывает при разработке подобных приложений, особенно в свете недавнего появления в NuGet пакетов с TypeScript Definitions. Каталог рефакторингов, правда, заканчивается на функции переименования, но JetBrains обещают поддержку TypeScript уже в ReSharper уже в ближайшем будущем. SignalR является очень крутой штукой. Учитывая то, что WebSockets отказывается работать под Windows7, использование технологии, которая работает везде и сама выберет наиболее оптимальный способ передачи данных, может очень сильно помочь при разработке.

Автор: Unrul

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js