Версионность API в .NET MVC 4

в 18:52, , рубрики: .net, ASP, web api, Веб-разработка, метки: ,

Доброго времени суток.

С появлением ASP.NET Web API появился удобный и мощный инструмент для создания API для вашего сайта. Но, как известно, с течением времени, ваш API может меняться, дополняться или может быть вовсе переделан с нуля. Для совместимости со старыми клиентами необходимо реализовать версионность.

К сожалению, на данный момент Microsoft не предоставила удобного и простого способа для реализации версионности. В интернете можно найти некоторую информацию на эту тему, но, как правило, большинство найденных мной решений сводятся к добавлению параметра для версии в каждый запрос и его обработке. Мне же хотелось получить более гибкий метод для разделения на версии, который не будет засорять методы контроллера и избавит от множества блоков if else. И самым главным критерием для меня была возможность иметь контроллеры с одинаковыми именами для одних и тех же методов API, но разделенных на версии с помощью пространств имен.

В тоже время, в ASP.NET MVC Web API есть достаточно мощный механизм в виде интерфейса IHttpControllerSelector, с помощью которого можно реализовать версионность, оставив код чистым и понятным.

Давайте посмотрим, что из этого вышло.

В первую очередь нам необходимо правильно настроить маршрутизацию, что бы номер версии интерпретировался в качестве параметра (в контоллерах мы его будем просто игнорировать).

httpRoutes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/v{version}/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
                );

Таким образом все наши методы API будут иметь вид api/v{version}/{controller}/{id}, где {version} — номер версии API. На самом деле можно использовать не только цифры, а вообще что угодно. Главное, чтобы мы могли по этому параметру отличить реализации API.

Далее необходимо наладить правильную обработку запросов: выбор и создание контроллеров. Процесс этот выглядит очень простым. Фабрика контроллеров должна знать какой контроллер ей необходимо создать. Именно для этого и служит интерфейс IHttpControllerSelector.

В большенстве случаев, нас полностью устраивает стандартный DefaultHttpControllerSelector, поэтому для реализации версионности совсем необязательно полностью писать его с нуля.

Для этого мы отнаслудемся от DefaultHttpControllerSelector и переопределим его главный метод SelectController. Именно он отвечает за выбор контроллера и предоставляет фабрике дескриптор контроллера.

public class HttpControllerSelector : DefaultHttpControllerSelector
{
    private readonly HttpConfiguration configuration;

    public HttpControllerSelector(HttpConfiguration configuration) : base(configuration)
    {
        this.configuration = configuration;
    }

    public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
    {

    }
}

ControllerSelector требует текущий HttpConfiguration в качестве параметра. Поэтому регистрируем в IoC контейнере его с зависимостью. Чаще всего я использую Castle Windsor, поэтому в примере используется именно он.

container.Register(
                Component.For<IHttpControllerSelector>().ImplementedBy<HttpControllerSelector>().DependsOn(
                    Dependency.OnValue<HttpConfiguration>(GlobalConfiguration.Configuration)));

Теперь перейдем непосредственно к процессу выбора соответствующего контроллера в методе SelecController.
Фабрика контроллеров ожидает от нас дескиптор HttpControllerDescriptor, который состоит из имени контроллера и его типа.

HttpControllerDescriptor(HttpConfiguration, String, Type)

Для получения имени контроллера мы можем воспользоваться базовым функционалом класса DefaultHttpControllerSelector.

var controllerName = GetControllerName(request);

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

public class HttpControllerSelector : DefaultHttpControllerSelector
{
    private readonly HttpConfiguration configuration;
    private readonly Lazy<ConcurrentDictionary<string, Type>> controllerTypes;

    public HttpControllerSelector(HttpConfiguration configuration) : base(configuration)
    {
        this.configuration = configuration;
        controllerTypes = new Lazy<ConcurrentDictionary<string, Type>>(GetControllerTypes);
    }

    public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
    {
        var controllerName = GetControllerName(request);
    }

    private static ConcurrentDictionary<string, Type> GetControllerTypes()
        {
            var assemblies = AppDomain.CurrentDomain.GetAssemblies();

            var types = assemblies
                .SelectMany(a => a.GetTypes().Where(t => !t.IsAbstract &&
                                                         t.Name.EndsWith(ControllerSuffix, StringComparison.OrdinalIgnoreCase) &&
                                                         typeof (IHttpController).IsAssignableFrom(t)))
                .ToDictionary(t => t.FullName, t => t);

            return new ConcurrentDictionary<string, Type>(types);
        }
}

Также нам потребуется номер версии из запроса

object version;
            request.GetRouteData().Values.TryGetValue("version", out version);

Добавим метод для получения типа контроллера по версии

var type = GetControllerType((string)version, controllerName);

Сам метод предельно прост: из списка типов контроллеров нам нужно выбрать контроллер, лежащий в неймспейсе соответствующей версии API

private Type GetControllerType(string version, string controllerName)
        {
            var query = controllerTypes.Value.AsEnumerable();

            return query.ByVersion(version)
                .ByControllerName(controllerName)
                .Select(x => x.Value)
                .Single();
        }

Здесь используется два кастомных экстешнена для фильтрации контроллеров

public static IEnumerable<KeyValuePair<string, Type>> ByVersion(this IEnumerable<KeyValuePair<string, Type>> query, string version)
        {
            var versionNamespace = string.Format(CultureInfo.InvariantCulture, ".V{0}.", version);

            return query.Where(x => x.Key.IndexOf(versionNamespace, StringComparison.OrdinalIgnoreCase) != -1);
        }

        public static IEnumerable<KeyValuePair<string, Type>> ByControllerName(this IEnumerable<KeyValuePair<string, Type>> query, string controllerName)
        {
            var controllerNameToFind = string.Format(CultureInfo.InvariantCulture, ".{0}{1}", controllerName, DefaultHttpControllerSelector.ControllerSuffix);

            return query.Where(x => x.Key.EndsWith(controllerNameToFind, StringComparison.OrdinalIgnoreCase));
        }

Теперь у нас имеется все необходимое для создания дескриптора контроллера. Результирующий класс:

public class HttpControllerSelector : DefaultHttpControllerSelector
    {
        private readonly HttpConfiguration configuration;
        private readonly Lazy<ConcurrentDictionary<string, Type>> controllerTypes;

        public HttpControllerSelector(HttpConfiguration configuration) : base(configuration)
        {
            this.configuration = configuration;
            controllerTypes = new Lazy<ConcurrentDictionary<string, Type>>(GetControllerTypes);
        }

        public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
        {
            object version;
            request.GetRouteData().Values.TryGetValue("version", out version);

            var controllerName = GetControllerName(request);
            var type = GetControllerType((string)version, controllerName);

            return new HttpControllerDescriptor(configuration, controllerName, type);
        }

        private static ConcurrentDictionary<string, Type> GetControllerTypes()
        {
            var assemblies = AppDomain.CurrentDomain.GetAssemblies();

            var types = assemblies
                .SelectMany(a => a.GetTypes().Where(t => !t.IsAbstract &&
                                                         t.Name.EndsWith(ControllerSuffix, StringComparison.OrdinalIgnoreCase) &&
                                                         typeof (IHttpController).IsAssignableFrom(t)))
                .ToDictionary(t => t.FullName, t => t);

            return new ConcurrentDictionary<string, Type>(types);
        }

        private Type GetControllerType(string version, string controllerName)
        {
            var query = controllerTypes.Value.AsEnumerable();

            return query.ByVersion(version)
                .ByControllerName(controllerName)
                .Select(x => x.Value)
                .Single();
        }
    }

    public static class ControllerTypeSpecifications
    {
        public static IEnumerable<KeyValuePair<string, Type>> ByVersion(this IEnumerable<KeyValuePair<string, Type>> query, string version)
        {
            var versionNamespace = string.Format(CultureInfo.InvariantCulture, ".V{0}.", version);

            return query.Where(x => x.Key.IndexOf(versionNamespace, StringComparison.OrdinalIgnoreCase) != -1);
        }

        public static IEnumerable<KeyValuePair<string, Type>> ByControllerName(this IEnumerable<KeyValuePair<string, Type>> query, string controllerName)
        {
            var controllerNameToFind = string.Format(CultureInfo.InvariantCulture, ".{0}{1}", controllerName, DefaultHttpControllerSelector.ControllerSuffix);

            return query.Where(x => x.Key.EndsWith(controllerNameToFind, StringComparison.OrdinalIgnoreCase));
        }
    }

В результате мы имеем простой механизм версионности API с возможностью иметь контроллеры вида

Controllers.Api.V1.UserController
Controllers.Api.V2.UserController

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

Спасибо за внимание.

Автор: quozd

Источник

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


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