Pull to refresh
0
True Engineering
Лаборатория технологических инноваций

О чем не пишут в документации, или тонкости рефакторинга на .Net Core

Reading time 6 min
Views 13K

Всем привет! Этим материалом мы открываем цикл из нескольких статей, посвященных длинной истории о том, как мы пришли с одной стороны к CD, а с другой — к high availability, основанной на избыточности.


Начнем по порядку. У нас есть API для мобильного приложения, которое находится в продуктовой среде, написанный на .NET.


И первым шагом мы переводим его на .NET Core и делимся с вами тонкостями, которые встретились нам на этом пути.



Несколько фактов о нашем web API:


  • 110 методов,
  • сервис push уведомлений,
  • сервис управлениями баннерам,
  • 35K запросов в день.

Задача очень простая и понятная — построить конвейер CD, так как наше приложение быстро и динамично развивается, и нам нужно руководствоваться принципом “done значит released”.


Как это сейчас развернуто на текущей продуктовой среде:


К чему идем:


Как мы будем это делать:


  • Разрабатываем методику автоматического тестирования, покрываем тестами и включаем в процесс сборки,
  • Переводим наш сервис на .NET Core,
  • Настраиваем сборку в Docker контейнер (под Linux — ну не зря же мы на Core замахнулись),
  • Разворачиваем кластер Kubernetes в продуктовой и тестовой средах,
  • Заезжаем в них.

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


Начинаем с перехода с ASP.NET Web Api 2 на ASP.NET Core 2 для core-части мобильного сервиса (пока без пушей и баннеров. С ними разберемся чуть позже, если будут подводные камни — расскажем в отдельной статье).


Задач много. Часть из них решается достаточно стандартными способами, следуя официальному мануалу, но есть и то, что не лежит на поверхности. И, вероятно, вызовет у вас вопросы при решении аналогичной задачи. Ими и хотим с вами поделиться.


Наш план рефакторинга мобильного сервиса выглядит следующим образом:


1. Создаем в Visual Studio 2017 новое решение


  • Проект ASP .NET Core Web Application с темплейтом Web Api для основного проекта.
  • Class Library (.NET Standard) для вспомогательных проектов.

2. Подключаем внешние зависимости


2.1 Подключаем WCF сервисы


Первым делом сверяемся с таблицей, есть ли в .Net Core 2.0 поддержка нужных фич WCF клиента.


То, что раньше называлось Service References, теперь именуется Connected Services. Добавляем в проект WCF Web Service Reference через меню Add Connected Service.


Поскольку в ASP.NET Core теперь нет Web.config-файлов, все настройки WCF клиента хранятся в сгенерированном коде Reference.cs.


В классе клиента предусмотрен метод для ввода дополнительных настроек клиента:


static partial void ConfigureEndpoint(ServiceEndpoint serviceEndpoint, ClientCredentials clientCredentials)


Пишем реализацию partial метода. В нашем случае в этом методе прописаны Credentials для авторизации в сервисе.


Помимо изменений в настройках клиента второе серьезное изменение — в клиенте больше нет синхронных методов. У нас async-вариант использовался сразу.


2.2. Подключаем nuget-пакеты TimeZoneConverter, Swashbuckle, Mime, XmlSerializer.Generator

С переносом пакетов TimeZoneConverter, Swashbuckle, Mime никаких проблем не возникло.


Поговорим о находках. При использовании стандартного Xml-сериализатора есть важный момент: кодогенератор запускается в рантайме при первом использовании. Соответственно, это увеличивает время холодного старта. Такое поведение можно обнаружить, если в студии у вас отключен Just My Code. Беглый поиск по интернету вывел нас на nuget-пакет XmlSerializer.Generator, который пришел на замену sgen и поддерживает минимальные Net Core 2 и Net Standard 2. Его предназначение — генерация кода xml сериализатора в compile-time.


С удовольствием добавляем его в проект. Пакет автоматически включается в последовательность сборки проекта.


Из ограничений:


а) Генератор не умеет резолвить названия классов с учетом namespace, поэтому придется избавиться от дублирования названий DTO-классов, если таковые имеются.


b) Ленивые люди используют xmltocsharp.azurewebsites.net для генерации DTO-классов из XML описания. Онлайн-сервис грешит повсеместной расстановкой XmlRoot-атрибутов. Генератор обижается на такое поведение. Огнем и мечом выкашиваем из ненужных мест XmlRoot.


3. Транслируем Global.asax в Startup.cs.


  • Используем стандартный IoC контейнер.
  • HttpHandler в .NET Core заменены на middleware, переписываем их. Фильтры остались фильтрами.
  • Поскольку мы отказались от IIS, настраиваем аутентификацию и Directory Browsing средствами ASP.NET.
  • Подключаем для логирования стандартный логгер через ILoggerFactory. Планируем выбросить NLog и использовать Serilog для ELK.

4. Настраиваем конфигурации


4.1. Настраиваем окружение через IHostingEnvironment.Environment


Мы планируем на выходе билда получать единственный докер-образ и пропускать его в неизменном виде через все среды тестирования. Настройка окружения должна полностью управляться через переменную окружения ASPNETCORE_ENVIRONMENT. Поэтому по максимуму избавляемся от условной компиляции в коде и смотрим на значение IHostingEnvironment.Environment.


4.2. Переносим блок AppSettings из в Web.{configuration}.config в AppSettings.{environment}.json.


4.3. Прописываем для WCF сервисов конфигурацию для окружений Dev, Test, Stage, Release.


5. Убираем зависимость от HttpContext


HttpContext синглтон сыграл в ящик, на смену ему пришел IHttpContextAccessor.


Вот 3 вещи, которые были затронуты таким изменением:


  • HttpContext.Current.AddErrors использовался для сквозного сбора различных ошибок в ходе обработки конкретного реквеста на тестовом сервере. Все собранные ошибки затем в специальном HttpHandler записывались в warning_message в теле респонса, что позволяло быстрее диагностировать проблемы.

Вместо HttpContext.Current.AddErrors будем пользоваться IHttpContextAccessor.HttpContext.Items


  • HttpContext.Current.HttpContext.Timestamp использовался для
    a. замера времени реквеста,
    b. тегирования операций, связанных с определенным реквестом, в логах.

С увеличением количества запросов стали часто сталкиваться с перекрытием запросов по Timestamp, т.е. один и тот же тег использовался для разных запросов. В NET Core доступно свойство IHttpContextAccessor.HttpContext.TraceIdentifier — действительно уникальный идентификатор.


  • Теперь для получения IP не нужно использовать никакой магии, все находится в одном месте — IHttpContextAccessor.HttpContext.Connection.RemoteIpAddress.

6. Переносим код контроллеров

6.1. ASP NET WebApi был поглощён в ASP NET MVC.


Затронуты маршрутизация, биндинг, negotiation, исчезли многие классы — начиная с ApiController и далее.


Для того, чтобы обойтись минимальной кровью при переносе кода контроллеров, есть workaround в виде nuget-пакета WebApiCompatShim, который эмулирует концепции Web Api на базе MVC.


Мы же решили сразу отказаться от прослойки и пользоваться чистым MVC со своими костылями, чтобы почувствовать боль и явно понимать, какой объем работы предстоит сделать, для того, чтобы в скором светлом будущем привести все к надлежащему виду по последнему слову ASP NET Core. Как оказалось, все совсем не грустно.


  • Меняем ApiController на Controller.
  • Для expires и cache-control респонс-хедеров используем ResponseCache-атрибут из коробки.
  • Для кастомных реквест-хедеров ASP NET Core может нас порадовать уже реализованным FromHeader-атрибутом.
  • Биндинг FromUri заменяем на FromQuery.

6.2. Пишем свой HttpResponseException


В проекте по старинке в action возвращается результирующий объект вместо IActionResult. В .NET Core, о горе, убрали HttpResponseException, объясняя тем, что разработчики платформы заботятся о правильном использовании их детища и подсказывают нам не использовать исключения для логики запросов — bad request, unauthorized и т.д…


Договорившись с совестью, откладываем рутину на потом и пилим свой HttpResponseException и ActionFilter для него, ибо в рамках быстрого перехода переписывать все на IActionResult слишком долго. Да и к тому же, придется в каждом методе указать ProducesResponseType атрибут, по которому сваггер будет понимать класс результата для action и генерировать документацию.


    public class HttpResponseException : Exception
    {
        public int StatusCode { get; private set; }
        public string ContentType { get; private set; } = "text/plain";

        public HttpResponseException(int statusCode)
        {
            StatusCode = statusCode;
        }

        public HttpResponseException(int statusCode, string message) : base(message)
        {
            StatusCode = statusCode;
        }        
    }

    public class HttpResponseExceptionFilter : IActionFilter
    {
        public void OnActionExecuting(ActionExecutingContext context)
        {
        }

        public void OnActionExecuted(ActionExecutedContext context)
        {
            if (context.Exception is HttpResponseException)
            {
                var ex = (HttpResponseException)context.Exception;
                context.Result = new ContentResult() {
                    StatusCode = ex.StatusCode,
                    Content = ex.Message,
                    ContentType = ex.ContentType
                };
                context.ExceptionHandled = true;
            }   
        }
    }

Для методов загрузки и отправки файлов сделали исключение: здесь по-честному переписали с HttpRequestMessage и HttpContent на FileContentResult и IFormFile, иначе никак нельзя.


7. Документация


  • Включаем сборку XML документации.
  • Обновляем фильтры и атрибуты сваггера Swashbuckle.

P.S. Не скажем, что рефакторинг был долгим по времени, но все же потребовал немало усилий. Этот опыт теперь с нами (и с вами) и в следующий раз мы c вами сможем пройти этот путь быстрее.


В следующей серии поделимся находками по настройке сборки в Docker-контейнер. Как говорится, «не переключайтесь».

Tags:
Hubs:
+25
Comments 17
Comments Comments 17

Articles

Information

Website
www.trueengineering.ru
Registered
Founded
Employees
101–200 employees
Location
Россия