Хабрахабр

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

Всем привет! Этим материалом мы открываем цикл из нескольких статей, посвященных длинной истории о том, как мы пришли с одной стороны к 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-контейнер. Как говорится, «не переключайтесь».

Теги
Показать больше

Похожие статьи

Кнопка «Наверх»
Закрыть