Опыт портирования legacy enterprise проекта c Net Framework на Net Core

    Опыт портирования legacy enterprise проекта c Net Framework на Net Core


    net framework to net core


    Вводная часть


    Постараюсь дать информацию о том, как легко портировать существующее Enterprise-решение C .Net Framework на .Net Core. Пройдусь по всем важным разделам и не буду углубляться, чтобы не увеличивать размер статьи, ниже будет множество ссылок на разделы Microsoft, но в первую очередь идея заключается в том, чтобы дать вам представление о том, как переносить конкретную часть вашей системы и чтобы можно было обсудить в комментариях. В общем, эту статью можно считать руководством на коленке.


    Что имеем


    Дано: Enterprise система, которая написана с использованием следующих технологий (всё написано на C# под Net Framework):


    • Множество ASMX web-служб
    • Множество WCF служб
    • Бэкграунд задачи на Workflow Foundation
    • Web-приложения на WebForms и частично на ASP.NET MVC 4
    • Отчёты SQL Server Reporting Services
    • Утилиты на Windows Forms и консольные приложения

    Зачем переходим на Net Core:


    • Обновляем стек технологий
    • Избавляемся от старых технологий (разработчики счастливы, а новые нанимаемые разработчики не пугаются)
    • Используем все преимущества NetCore: мультиплатформенность, масштабируемость, контейнеризация

    Есть статья Выбор между .NET Core и .NET Framework для серверных приложений, которая вам поможет что выбрать.


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


    Для того, чтобы добиться переносимости кода между различными средами исполнения (Framework и Core), нам на помощью приходит NetStandard (а конкретнее — netstandard2.0).


    Ещё нужно быть готовым к тому, что часть технологий частично или полностью отсутствует в NetCore, а конкретно:


    • Windows Communication Foundation. Нет API для служб, только API для клиентской стороны
    • Workflow Foundation
    • ASP.NET Web Forms (отсутствует System.Web)
    • Отсутствует API работы с очередями MSMQ

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


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


    Анализируем переносимость с помощью утилиты Portatibility Analyzer


    Portatibility Analyzer позволяет выбрать несколько target'ов (NetCore, NetFramework, NetStandard, Mono, ...) и их версии, а затем выдаёт очень удобный и подробный отчёт о том, какие API можно перенести и какие невозможно с указанием ссылок на код. Утилита является плагином для Visual Studio.


    Portatibility Analyzer


    Директивы препроцессора и условная компиляция


    Директивы препроцессора (условная компиляция #if) помогает в тех случаях, если нет необходимых API и часть методов или целиком классы нельзя портировать с .Net Framewrk на .Net Core.


    Когда приходилось использовать #if:


    • Часть API переехало в другие namespace'ы и приходилось их импортировать для .Net Core реализации
    • Частично переписывать реализацию методов в связи с отсутствующем API
    • Полностью отказываться от некоторых классов в связи с невозможностью их портирования на .Net Core

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


    Часто используемая проверка: реализация кода для Net Framework иначе реализуем для NetStandard или NetCore:


    #if NETFRAMEWORK
    #elif NETSTANDARD || NETCOREAPP
    #endif

    В очень редких случаях если нужна была реализация для NetCore или NetFramework (связано с тем, что использовались API, которых нет в .NetStandard2.0):


    #if NETFRAMEWORK
    #elif NETCOREAPP
    #endif

    Как вы заметили, специально не применялись проверки на конкретные версии runtime'ов для упрощения кода.


    Target'ы в *.csproj проектах выглядят так:


        <TargetFrameworks>netstandard2.0;net471;netcoreapp3.1</TargetFrameworks>

    → Больше про Кроссплатформенное нацеливание


    Реализация заглушек API


    В редких случаях вам придётся скопировать часть интрерфейсного API, которая отсутствует в NetStandard и NetCore реализации. Например, чтобы избежать большого числа условных директив в коде, пришлось скопировать часть атрибутов WCF, конфигурации, а также некоторые классы в виде заглушек.


    Например, в наших контрактах часто используется WCFный атрибут TransactionFlowAttribute, но он будет использоваться, но интерфейсы, на которые он навешивается используется повсеместно, поэтому делаем так:


    #if NETFRAMEWORK
    #elif NETSTANDARD || NETCOREAPP
    namespace System.ServiceModel
    {
        [System.AttributeUsage(System.AttributeTargets.Method)]
        public sealed class TransactionFlowAttribute : Attribute, System.ServiceModel.Description.IOperationBehavior
        {
            public TransactionFlowAttribute(TransactionFlowOption transactions)
            {
                Transactions = transactions;
            }
    
            public TransactionFlowOption Transactions { get; }
    
            public void AddBindingParameters(OperationDescription operationDescription,
                BindingParameterCollection bindingParameters)
            {
            }
    
            public void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation)
            {
            }
    
            public void ApplyDispatchBehavior(OperationDescription operationDescription,
                DispatchOperation dispatchOperation)
            {
            }
    
            public void Validate(OperationDescription operationDescription)
            {
            }
        }
    
        public enum TransactionFlowOption
        {
            NotAllowed,
            Allowed,
            Mandatory,
        }
    }
    
    #endif

    Заворачиваем всё в NuGet пакеты


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


    https://docs.microsoft.com/ru-ru/dotnet/standard/library-guidance/media/cross-platform-targeting/nuget-package-multiple-assemblies.png


    Инфраструктурная часть


    К инфраструктурной части приложения относится:


    • Хостинг приложения (Реализация служб)
    • Логирование
    • Обработка ошибок
    • Конфигурация
    • Загрузочная часть (Bootstrapper)
    • Мониторинг

    Конфигурация приложений


    Конфигурация классических приложений Net Framework основывается на файлах app.config, и web.config, которые представляют из себя XML файлы и API работы с ними: System.Configuration и класс System.Configuration.ConfigurationManager. Например, часто приходится считывать данные из AppSettings и гораздо реже делать свои классы конфигурации в ConfigurationSection.


    В NetCore появилось новое API, которое позволяет работать с конфигурацией в различных форматах (JSON, INI, XML) и использовать различные источники (Файлы, Командная строка, Переменные окружения и т. д.)


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


    Кстати, в StackOverflow имеется куча вопросов как организовать конфигурацию


    Если всё-таки нужно использовать старую реализацию, то имеется NuGet пакет System.Configuration.ConfigurationManager.


    NetCore реализация конфигурации более интуитивная и простая. Здесь вы работаете с конфигурацией по конкретному пути в конфигурационном файле, либо как с объектом (и не нужно описывать сложных ConfigurationSection)


    Логирование


    В старом проекте применялось логирование в EventLog с использованием API из System.Diagnostics, но а так же была абстракция в виде интерфейса ILogger, которая позволяла логировать с различными уровнями messageLevel (Debug, Info, Warning, Error) и указанием категорий. В более новых проектах уже применялся NLog c той же самой абстракцией ILogger.


    В NetCore появилось новое универсальное API: Microsoft.Extensions.Logging, которое предоставляет интерфейс ILogger<T>.


    Мы же продолжили использовать нашу абстракцию ILogger, потому что она везде, но конкретная реализация уже использует Microsoft.Extensions.Logging.ILogger<T>, а также она легко позволяет подключить и сконфигурировать кучу существующих логеров, например: NLog, log4Net, Serilog и т.д.


    Внедрение зависимостей и инверсия управления


    В наших проектах использовались IoC-контейнеры Unity, а в более новых — AutoFac, либо вовсе отсутствовали.


    В NetCore добавлена абстракция Microsoft.Extensions.DependencyInjection с использованием класса ServiceCollection, которая позволяет регистрировать типы с уровнями:


    • Singleton — создаст инстанс один раз и будет всегда переиспользовать его
    • Scoped — в рамках некого контекста, например на время Http-запроса
    • Transient — будет создавать инстанс каждый раз

    Также имеется класс IServiceProvider, который обеспечивающий получение нужной регистрации.


    Более подробнее читаем тут: Внедрение зависимостей в ASP.NET Core.


    Советы:


    • Всегда используйте внедрение зависимостей через конструктор, затем методы или свойства. Если необходимо что-то конструировать в конкретном методе, то можете внедрить какую-нибудь фабрику и создавать что-либо вызывая её методы, саму фабрику регистрируем отдельно и поближе к конфигурации
    • Не пробрасывать контекст IoC-контейнера в код (это заставило нас попотеть, чтобы вынести код из классов)
    • Сосредоточьте регистрацию в одном месте

    Избавляемся от Global Assembly Cache


    Очень давно на проекте было принято решение использовать Global Assembly Cache, чтобы приложения не искали сборки и было централизованное место где они лежат.


    Net Core не умеет в GAC, поэтому было принято решение написать кастомный AssemblyResolver, который искал бы в заданной директории используя конфигурацию.


    Модели приложений


    Консольные приложения


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


    Достаточно поменять *.cproj файл в формат SDK: <Project Sdk="Microsoft.NET.Sdk"> ну и соответственно использовать <TargetFramework>netcoreapp3.1</TargetFramework>


    Windows Forms


    Начиная с версии NetCore 3.0 появилась возможность запускать приложения Windows Forms, но после перехода на версию Net Core 3.1 часть legacy контроллов было удалено, поэтому
    придётся немного переписать приложения.


    Вот список контроллов, которые были "выпилены":


    • DataGrid и связанные с ним типы. Можно заменить на DataGridView;
    • ToolBar. Заменяем на ToolStrip;
    • MainMenu. Заменяем на MenuStrip;
    • ContextMenu. Заменяем на ContextMenuStrip.

    Более подробно про изменения можно почитать Критические изменения в Windows Forms.


    Позднее Microsoft выпустили руководство Процесс переноса классического приложения Windows Forms в .NET Core,
    которое сильно помогло нам.


    На первых этапах существования Windows Forms для NetCore 3.0 отсутствовал дизайнер форм для Visual Studio 2019, поэтому приходилось рисовать GUI в Net Framework, а потом переключаться на NetCore 3.0, но более поздних редакция появилась такая возможность.


    Переносим ASP.NET MVC приложения в Asp Net Core MVC


    Перенос будет чуть сложнее чем Windows Form приложения, но всё-равно всё происходит достаточно безболезненно.


    Первое большое изменение — убран бутрстраппер Global.asax и заменён на класс Startup.


    Второе изменение — отсутствует сборка System.Web и все типы HttpSession, Cookies, HttpRequest и т. д. соответственно.


    Для общего представления читаем тут: Запуск приложения в ASP.NET Core


    Третье большое изменение — изменена модель аутентификации, авторизации, обработка ошибок, фильтры, отсутствуют HTTP модули.


    Подробнее можно прочитать здесь: Миграция обработчиков и модулей HTTP в ASP.NET Core по промежуточного слоя. Для этого всего используется новая модель Middleware. Теперь можно организовать конвейер запроса и вклиниваться на каждом этапе прохождения запроса:


    Конвейер ПО промежуточного слоя ASP.NET Core


    Конвейер фильтра ASP.NET Core


    Пример простейшего Middleware:


    public class Startup
    {
        public void Configure(IApplicationBuilder app)
        {
            app.Use(async (context, next) =>
            {
                // Do work that doesn't write to the Response.
                await next.Invoke();
                // Do logging or other work that doesn't write to the Response.
            });
    
            app.Run(async context =>
            {
                await context.Response.WriteAsync("Hello");
            });
        }
    }

    API контроллеров практически не поменялось, разметка представлений Razor также практически не поменялась, смотрим тут: Обработка запросов с помощью контроллеров в ASP.NET Core MVC и тут: Представления в ASP.NET Core MVC.


    Следующее изменение — отсутствие Bundler'а для объединения и минификации js и css файлов, поэтому читаем тут: Объединение и минификация статических ресурсов в ASP.NET Core


    ASPNET ASMX переносим на AspNetCore WebAPI


    В данном случае всё-таки придётся отказаться от использования SOAP и использовать HTTP с JSON или XML.


    На каждый asmx сервис создаём контроллер WebAPI и на каждый WebMethod создаём
    Action POST метод и переносим соответствующий код с реализацией из asmx сервиса. Недостаток заключается в том, что мы полностью отходим от SOAP модели и вам также придётся переписывать клиентов. Если хотите, то можете оформить в виде Rest-служб.


    Другой вариант — придётся использовать сторонние библиотеки, которые могут в SOAP, например: SoapCore.


    Ещё один вариант — JSON-RPC, имеется куча различных библиотек под NetCore, все они хорошо внедряются в AspNet Core через Middleware.


    Также у Microsoft имеется guide как написать с помощью AspNetCore Middleware свой SOAP обработчик: Custom ASP.NET Core Middleware Example.


    ASPNET WebApi переносим на AspNetCore WebAPI


    Достаточно простая задача, потому что идеология простоя: есть контроллеры и экшны.


    Следуем этому guide'у: Переход с веб-API ASP.NET на ASP.NET Core


    шаги почти аналогичные с переносом MVC:


    • Global.asax и заменяем на класс Startup
    • Настраиваем авторизация и аутентификацию
    • Настраиваем Logger, Exception Handler
    • Портируем фильтры и т.д.

    ASPNET Web Forms переносим на Blazor


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


    Почему было решено портировать Web Forms на Blazor:


    • Компонентная структура кода (контроллы)
    • Code-behind стиль: есть разметка и есть логика и они друг с другом сильно связаны
    • Минимум Java-Script разработки (для кастомизации заказчики и разработчики предпочитают кодить на C#)

    Для переноса было написано 2 утилиты:


    • Первая парсила aspx страницы, конвертировала в XML код (так удобнее работать на C#) и описывала иерархию контроллов в виде дерева в достаточно абстрактном виде (тут кнопка, grid и т. д.), также генерировала html-разметку отделённую от aspx
    • Вторая утилита делала обратную работу: генерировала Blazor-страницы и code-behind мы дописывали сами

    В связи с тем, что у нас интерфейсы достаточно однотипные и простые, нам достаточно легко удалось портировать ASPNET Web Forms приложения.


    Избавляемся от WCF


    В NetCore есть частичная реализация WCF Client API:


    • Имеются только простые BasicHttpBinding, NetTcpBinding
    • Нет security на Message уровне, только на Transport
    • Нет поддержки распределённых транзакций

    Так как серверная сторона WCF полностью отсутствует в Net Core, то есть несколько вариантов:


    • Портируем как AspNetCore WebAPI
    • Портируем как AspNetCore gRPC
    • Используем стороннюю библиотеку CoreWCF с многими ограничениям
    • Портируем как AspNetCore + JSON-RPC

    Перенос WCF служб на AspNetCore WebAPI аналогичен с asmx службами: на каждый сервис — свой контроллер и свой action-метод на каждый метод WCF. Способ реализации полностью ложится на Вас: каждый метод может принимать Post-запрос, где URL будет иметь название соответствующего метода как https://you-service.com/MethodName, ну или выбираем JSON-RPC.


    Другой вариант — AspNetCore gRPC: Перенос службы WCF "запрос — ответ" в gRPC унарный RPC


    Пример WCF службы:


    [ServiceContract]
    public interface IItemService
    {
        [OperationContract]
        Task<Item> GetItem(int id);
    }
    
    [DataContract]
    public class Item
    {
        [DataMember]
        public int Id { get; set; }
    
        [DataMember]
        public string Name { get; set; }
    }

    Пример gRPC контракта в protobuf формате:


    message GetItemRequest {
        int32 id = 1;
    }
    
    message Item {
        int32 id = 1;
        string name = 2;
    }
    
    service ItemService {
        rpc GetItem(GetItemRequest) returns (Item);
    }

    Какой вариант реализации — решать вам, но я предпочитаю следующее:


    • Все внешние службы реализовать в виде WebAPI в стиле REST, либо JSON-RPC
    • Внутренние службы взаимодействуют по gRPC

    Избавляемся от Workflow Foundation


    Службы на Workflow Foundation полностью отсутствуют (в Microsoft, видимо, поняли что графическое представление службы никому не удобно и проще всё писать кодом), поэтому у вас есть такие варианты:



    Лично мы решили просто избавиться от Workflow Foundation, он нам всегда доставлял неудобства и сделали старым добрым кодом. А что же может быть лучше старого доброго кода?


    Подводим итоги


    В итоге к чему приходим? А приходим к тому, что часть подсистем переносятся в лоб, часть с небольшими доработками, но часть придётся глубоко перерабатывать,
    что может потребовать много усилий. Но у нас уже выработался стиль реализации подсистем: для внешних систем службы всегда делать HTTP Rest like службы, если нужна производительность
    (а для бизнес задач стандартных средств хватает с головой) — gRPC лучший подход. И да… Побыстрее избавляйтесь (по возможности) от Web Forms.


    UPD: Добавлена информация по Portatibility Analyzer.

    ICL Services
    Цифровые технологии для бизнеса

    Комментарии 12

      +2
      Лучше переезжать на OpenApi, понимаю wcf, но мс убила соап! И не собирается тащить старый стек в новую эпоху.
        –1
        Кстати, была идея, но разрабы хотели видеть что-то похожее на WCF методы. В сердцах было Json-RPC натянуть.
        +5
        странно что не упомянут .net portability analyzer, в свое время сэкономил мне кучу времени при переезде с framework на core
        docs.microsoft.com/en-us/dotnet/standard/analyzers/portability-analyzer

        docs.microsoft.com/en-us/dotnet/core/porting
          0
          Кстати, да, спасибо что напомнили! Забыл что-то про него! Он сильно помог в начале, видимо из-за того, что использовался чуть-чуть. Добавлю про него в статью.
          +1

          С удовольствием прочитал про переезд wcf, тоже уже пора задумываться, что с этим делать.


          Кстати, а никто не сталкивался с переносом очередей сообщений msmq в netcore?

            +2
            Отказаться от MSMQ и переходить на RabbitMQ/ZeroMQ/Apache/***MQ, либо Azure EventHub.
              +1
              Мы сохранили SOAP, для этого пришлось написать библиотеку, которую упомянул автор, github.com/DigDes/SoapCore
              Майкрософт неспешно делают WCF на .NET Core, но никаких обещаний не дают по поводу релиза и что туда войдет.
                0
                А что скажете по поводу этого: github.com/CoreWCF/CoreWCF Смотрели ли её?
                  +1
                  Я про нее и писал выше. Сами беты не смотрели. По внешним признакам дела идут у ребят не быстро: команда маленькая, роадмапа нет и пока реализован самый минимум. При этом утверждают, что какие-то команды Майкрософт используют у себя внутри эту библиотеку.
                  Здесь текущий статус: github.com/CoreWCF/CoreWCF/issues/159
                    0
                    Что уже есть — это круто! Нет распределённых транзакций, кому-то может быть критично.
              +1
              «ASPNET Web Forms переносим на Blazor»
              Почему блейзер?
              Почему не MVC?
                –1
                Просто: заказчики не хотят JS СОВСЕМ. Для кастомизации им нужно, что веб приложение было на чистом C# и напоминало Web Forms.

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

              Самое читаемое