company_banner

ViennaNET: набор библиотек для backend’а

    Всем привет!


    Мы сообщество .NET-разработчиков Райффайзенбанка и мы хотим рассказать про набор инфраструктурных библиотек на .NET Core для быстрого создания микросервисов с единой экосистемой. Вывели его в Open Source!



    Немного истории


    Когда-то у нас был большой монолитный проект, который постепенно превращался в набор микросервисов (об особенностях данного процесса можно прочитать в этой статье). В процессе мы столкнулись с проблемой, что при создании новых микросервисов нам часто приходилось копировать различные инфраструктурные решения – вроде настройки логирования, работы с БД, WCF и т.п. Над данным проектом работала одна команда, и все уже привыкли к некоторому устоявшемуся подходу работы с инфраструктурой. Поэтому мы выделили общий код в отдельный репозиторий, собранные библиотеки завернули в Nuget-пакеты и поместили в наше внутреннее Nuget-хранилище.


    Время шло, проект понемногу дробился, появилось желание создавать новые модули клиентской части на современном Js-фреймворке и запускать их в браузере. Мы начали переходить с WCF/SOAP на REST/HTTP, поэтому нам потребовались новые библиотеки для быстрого запуска сервисов на базе AspNet WebApi. Первая версия на .Net Framework 4.5 была сделана нашим архитектором чуть ли не на коленке в свободное время, но она уже из коробки позволяла тремя строчками в Program.cs запустить сервис, который содержал авторизацию (NTLM), логирование, Swagger, IoC/DI на базе Castle Windsor, настроенных HTTP-клиентов, пробрасывающих различные заголовки для обеспечения сквозного логирования во всем проекте. И всё это дело можно было дополнительно сконфигурировать уже в непосредственно в файле конфигурации сервиса.


    Однако не всё было гладко: данная библиотека получилась крайне негибкой в плане внедрения новых модулей. Например, если требовалось добавить какое-нибудь особое middleware, то приходилось создавать новую сборку и наследоваться от базового класса, запускающего сервис, что было крайне неудобно. К счастью, таких случаев было не очень много.


    Эпоха Docker и Kubernetes


    Пришло время, когда и до нас докатилась волна c Docker и Kubernetes, за которой мы пристально наблюдали: ведь это был прекрасный шанс начать движение по технологиям дальше, в .Net Core. А значит, нам понадобится новая инфраструктура для запуска сервисов: часть библиотек перекочевала с .Net Framework на .Net Standard и .Net Core практически без изменений, часть с небольшими улучшениями. Но больше всего хотелось переработать функционал, связанный с запуском сервисов на AspNet Core.


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


    Главная цель – создать унифицированный подход, описывающий, как взаимодействовать с базами данных, шинами и другими сервисами. Мы постарались, чтобы интеграции были быстрыми и безболезненными, а разработчики могли сконцентрироваться на написании бизнес-логики, а не инфраструктуры – она уже готова. Общий репозиторий помогает улучшить опыт взаимодействия внутри команд: когда используются очень похожие внутренние инфраструктуры, то легче включиться в процесс разработки другой команды и обменяться экспертизой.


    И зачем нам Open Source?


    Мы хотим показать зрелость экспертизы и получить качественную обратную связь: человек, находящийся вне банка, сможет привнести что-то от себя. Также нам интересно развитие практик работы с микросервисами и DDD на .NET в индустрии, возможно, кто-то захочет забрать определенные части фреймворка к себе.


    Собственно, ViennaNET


    Теперь давайте рассмотрим всё подробнее. Полный исходный код положили сюда.


    ViennaNET.WebApi.*


    Данный набор библиотек состоит из «корня» ViennaNET.WebApi, содержащего класс-строитель для сервиса CompanyHostBuilder, и набора конфигураторов ViennaNET.WebApi.Configurators.*, каждый из которых позволяет добавить и сконфигурировать некоторый функционал в создаваемый сервис. Среди конфигураторов можно найти подключение логирования, диагностики, типа аутентификации и авторизации, swagger-а и т.д.


    Тут же ViennaNET.WebApi.Runners.* содержит предварительно настроенные строители сервисов. Эти пакеты позволяют не вспоминать всякий раз, создавая новый сервис, какие конфигураторы необходимо подключить. При этом они никак не ограничивают функциональность строителя сервисов.


    ViennaNET.Mediator.*


    Библиотеки, позволяющие создать внутреннюю шину-посредник для команд и запросов внутри сервиса. Такой подход позволяет сократить количество DI-инъекций до одной, например, в контроллерах. За счёт этого можно добавлять различные декораторы к запросам, что унифицирует их обработку и сокращает количество кода.


    ViennaNET.Validation


    Сборка, содержащая набор классов для создания валидационных правил и последовательностей из них. Очень удобна для реализации доменной валидации, так как позволяет описать каждое бизнес-условие в виде простого и отдельного правила.


    ViennaNET.Redis


    Библиотека с обертками для удобной работы с Redis в качестве in-memory cache.


    ViennaNET.Specifications


    Сборка, содержащая классы, реализующие паттерн «Спецификация».


    Это далеко не всё, что есть в нашем наборе. Остальное можно посмотреть в репозитории на GitHub. Скоро планируется выход в OpenSource наших библиотек для работы с базами данных.


    Спасибо за внимание, ждём ваших комментариев и pull request-ов.

    Райффайзенбанк
    Развеиваем мифы об IT в банках

    Похожие публикации

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

      +4

      А пример какой… чтобы в сорцы не лезть )))

        +1
        Да, пример, не выложили…
        Это по сути для «пустого» сервиса. Полноценный пример будет позже в репозитории.
        Program.cs:
        class Program
        { 
        	public static void Main(string[] args)
        	{
        		CompanyHostBuilder.Create()
                                          .UseServer((b, c) => { b.UseKestrel(); })
                                          .BuildWebHost(args)
                                          .Run()
        	}
        }

        Конфиг conf/appsettings.json:
        {
          "webApiConfiguration": {
        	"portNumber": 80
          }
        }
        +3
        ViennaNET.Mediator.*
        Такой подход позволяет сократить количество DI-инъекций до одной, например, в контроллерах.

        Это про то, что теперь вместо пяти разных сервисов, можно заинжектить какой-нибудь IMediator и юзать его?


        Если да, то есть мнение, что это тот же service locator.

          +1
          Это про то, что теперь вместо пяти разных сервисов, можно заинжектить какой-нибудь IMediator и юзать его?
          — Да

          Если да, то есть мнение, что это тот же service locator.
          — Нет. Медиатор — это внутренняя шина, по которой можно отправлять сообщения или команды, на которые будут реагировать обработчики. Таким образом проще соблюдать SRP, ведь каждый обработчик отвечает только за обработку одной конкретной команды.
            +5

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

              +1
              — Нет. Медиатор — это внутренняя шина, по которой можно отправлять сообщения или команды, на которые будут реагировать обработчики. Таким образом проще соблюдать SRP, ведь каждый обработчик отвечает только за обработку одной конкретной команды.


              Ох как мне эта идея не нравится… вместо того чтобы вызвать понятный метод у понятной зависимости мы теперь посылаем сообщения в надежде, что их кто-то, как-то обработает? А эти зависимости в транзакциях участвовать умеют? И зачем вообще прятать заморачиваться с IMediator?
                +1

                Плюсы медиатра в том, что очень легко решать cross cutting concerns, у marshinov хорошие статьи были на эту тему.

                +1

                Сейчас используем такой извращённый интересный подход:


                Какая-нибудь 'фича' или app service
                namespace Vendor.Features.Statistics
                {
                  public class GetAutomationStatisticsQuery : IRequest<GetAutomationStatisticsResult>
                  {
                    public DateTime? FromDate { get; set; }
                    public DateTime? ToDate { get; set; }
                    public string TaskArn { get; set; }
                  }
                
                  public class GetAutomationStatisticsResult
                  {
                    public long TotalAutomations { get; set; }
                    public long ErrorsReported { get; set; }
                    public long WarningsReported { get; set; }
                  }
                
                  internal class GetAutomationStatisticsHandler :
                    IRequestHandler<GetAutomationStatisticsQuery, GetAutomationStatisticsResult>
                  {
                    private readonly GetAutomationsLogTotalCount _getAutomationsLogTotalCount;
                    private readonly GetAutomationsTotalCount _getAutomationsTotalCount;
                
                    public GetAutomationStatisticsHandler(
                      // Это делегаты на другие фичи (этакая инкапсуляция над медиатром)
                      GetAutomationsLogTotalCount getAutomationsLogTotalCount,
                      GetAutomationsTotalCount getAutomationsTotalCount)
                    {
                      _getAutomationsLogTotalCount = getAutomationsLogTotalCount;
                      _getAutomationsTotalCount = getAutomationsTotalCount;
                    }
                
                    public async Task<GetAutomationStatisticsResult> Handle(GetAutomationStatisticsQuery query, CancellationToken ct)
                    {
                      // some logic
                    }
                  }
                
                  public delegate Task<GetAutomationStatisticsResult> GetAutomationStatistics(
                    GetAutomationStatisticsQuery query, CancellationToken ct = default);
                }

                В каждом файле "фичи" лежат классы для реквеста, респонса, хендлера и делегат (GetAutomationStatistics в примере).


                Затем этот делегат через рефлексию регистрируется в IoC с помощью такого класса:


                MediatorForwarderForDelegates
                    private sealed class MediatorForwarderForDelegates<TRequest, TResponse> where TRequest : IRequest<TResponse>
                    {
                      private readonly IMediator _mediator;
                
                      public MediatorForwarderForDelegates(IMediator mediator)
                      {
                        _mediator = mediator;
                      }
                
                      public Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken)
                      {
                        return _mediator.Send(request, cancellationToken);
                      }
                    }
                

                Ну и эти делегаты потом уже можно инжектировать в другие хендлеры/сервисы.

                  +1
                  Кстати, как впечатления от работы, в сравнении с расположением /Controllers
                  /…
                  /Models
                  /…
                  /Views
                  /…
                  ?
                    +1

                    В целом положительные.
                    Но у нас не как у Джими Богарда с его vertical features.


                    У нас "почти clean" — сборка с апп сервисами и в ней свои фичи, сборка презентейшн — либо напрямую юзает классы (дто) из апп сервисов, либо добавляет свои фичи (например, для graphql).


                    Кроме размещения фич в одном файле, не стесняемся пользоваться nested классами — меньше проблем с придумыванием имён и меньше нейм-конфликтов. Если класс получается большим из-за nested-классов, то делаем класс partial и выносим в отдельный файл.


                    Раньше вообще делали так, тоже неплохо было, но не прижилось почему-то:


                    Заголовок спойлера
                      public class CreateAccount
                      {
                        public class Command : MediatR.IRequest<Result>
                        {
                          public string Name { get; }
                        }
                    
                        public class Result
                        {
                          public Account Account { get; set; }
                        }
                    
                        // dto
                        public class Account
                        {
                        }
                    
                        public class Mappings : Profile
                        {
                        }
                    
                        public class Validation : AbstractValidator<Command>
                        {
                        }
                    
                        public class Handler : IRequestHandler<Command, Result>
                        {
                        }
                      }
                    
              +8
              Думаю, похожий набор библиотек есть в каждой крупной компании. Минус у них общий — кастомность.
              Хочешь заинжектить вторую реализацию интерфейса — хрен тебе, наш DI контейнер это не поддерживает, так что страдай. Хочешь два http клиента с разной авторизацией — хрен тебе, страдай. Хочешь… ну вы поняли.
              Разумеется все эти хотелки рано или поздно попадут в либу… Но они нужны сейчас, а не поздно или даже рано. И в итоге ты стоишь перед выбором — или отказаться от либы (т.е. ВСЕЙ инфраструктуры) или страдать и костылять.
                +3
                При построении нашего набора библиотек мы учитывали расширяемость. Есть и клиенты с разной авторизацией (тип авторизации указывается в конфиге), для одного интерфейса мессаджинга есть разные реализации (IBM, rabbit, кафка, опять же, тип реализации указывается в конфиге), для общего набора интерфейсов ORM есть поддержка 5 СУБД.

                Выбор взять всё как есть или отказаться — не стоит, можно начать с нуля и набрать нужный набор реализаций. Например если моё приложение использует ViennaNET.ORM и его реализацию для MsSQL, то чтобы поменять базу на PostgresQL, мне нужно просто зареференсить другую реализацию и в конфиге поменять тип базы. Более того, можно зареференсить обе реализации, указать два подключения и на уровне регистрации Entity указать, с какого коннекта его доставать — так делали при миграции данных из одной БД в другую.

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

                  Допустим, я решил использовать ViennaNET.Logging. Везде, во всем проекте, у меня ILog. И тут проекту потребовался EF Core. Без ViennaNET.ORM (которой пока нет и не уверен, что там этой проблемы не будет) и… и получаю факап, потому потому что EF Core для подключения логгирования запросов хочет классический ILogger. И теперь мне нужно пилить свой ILoggerFactory и пытаться как-то натянуть сову на глобус, т.е. ILog на ILogger.
                  +1
                  Совсем не обязательно ждать, когда что-то появится непосредственно в библиотеке. Можно сделать свою реализацию конфигуратора, которая использует абстракции корневого билдера, завернуть в свой пакет, и подключать во все необходимые сервисы.

                  Немного дополню по поводу Http-клиентов. Вот, пример реализации для JWT-клиента: github.com/Raiffeisen-DGTL/ViennaNET/blob/master/ViennaNET.WebApi.Configurators.HttpClients.Jwt/JwtHttpClientsConfigurator.cs. По сути здесь мы просто добавляем хэндлер для переотправки входящего токена.

                  В подобных конфигураторах можно сделать более сложную логику, добавить другие хэндлеры, считывать дополнительные параметры из файла конфигурации. Ну и затем, через свой метод расширения, подключить:
                  Скрытый текст
                  CompanyHostBuilder.Create()
                                    ...
                                    .UseCustomJwtClient()
                                    ...
                                    .BuildWebHost(args)
                                    .Run()



                    +1
                    Да, HttpClient у вас весьма гибкий) А если, например, мне нужно сказать сваггеру игнорировать поля запросов, помеченные определенным атрибутом — я смогу это сделать?
                +1
                Не пробовали вместо сервисов использовать Hangfire и его backgroundJobs? Возможно не пришлось бы лепить свои велосипеды. У нас тоже в банке есть какой-то адский фреймворк для запуска сервисов, сейчас мы от него отказываемся и переводим все на Hangfire.
                p.s. Ну а вообще не удивлен что вы выпсутили свой велосипед, это так по банковски. Чем вас не устроил с 10ок других решений на рынке, неужели все не подходило?
                  +1
                  Вместо сервисов — нет. Да и разве можно это полноценно реализовать через него? Он не для этого. Использовали только по прямому назначению — для отложенного запуска задач.
                  Наш фреймворк делался для запуска рестовых сервисов, а не для бэкграунд воркеров.
                    +2
                    Раз вы опубликовали статью и выложили это все, вы видимо очень гордитесь этим.
                    Но мнение со стороны, вы просто потратили время зря, зачем было писать свой фреймворк когда есть компании производители софта, которые занимаются этим нон стоп, а вы вроде как должны заниматься прикладными задачми в банке.
                    Но успехов, может пару лет проработает, пока всем не надоест у вас его поддерживать или не уволиться затейник всего этого)
                      +3

                      Кажется вы немного загоняетесь по поводу причин написания. В любой мало-мальски большой конторе со временем возникает желание унифицировать какие-то практики/методики, чтоб не заниматься копипастой и не втаскивать в каждый проект зависимости от конкретных реализаций инфраструктурных компонентов. То же логирование, метрики, валидация — они во всех проектах одинаковые и если со временем надо что-то поменять в способе их доставки, то что — ползать по всем проектам и править тонны разношерстных велосипедов или всё же поправить одну либу и пнуть всем командам "обновитесь"? Т.е. причина появления этого


                      Да, по сути это boilerplate. Фреймворком особо не назвать и применимость его за пределами компании-разработчика — нулевая. Ну реально никто это не будет себе тащить в проект, потому что подобных фреймворков тьма. Каждый второй погромист в желании избавить себя от ctrl+c/v пишет нечто подобное. И каждому первому именно эта реализация не подойдёт по причине недостаточной гибкости.
                      Суть-то в чём: есть фреймворк, ну тот же aspnet core. Он набит достаточно базовыми компонентами, чтоб их можно было комбинировать и управлять ими, но при этом имеет достаточно много готовых "полных" реализаций, цель которых именно в уменьшении ручной работы. Т.е. есть и готовые однострочники с кучей дефолтов и есть возможность всё настроить руками.


                      А приведённый в статье вариант — это ещё один однострочник но с другими дефолтами. И если тебе нужто что-то, отличное от дефолтов — ты опять собираешь руками. И смысл этого фреймворка испаряется.
                      Т.е. всё это хорошо, пока ты внутри компании пишешь кодик по внутренним гайдлайнам, завязанный на внутреннюю инфраструктуру. За пределами компании это труп. Ну т.е. это должен быть либо прям вот неимоверно исчерпывающий набор, представляющий из себя реально другую и более удобную модель, чем уже существует (и тогда у него могут найтись какие-то поклонники), либо это просто ещё один набор дефолт-обвязок, который тащить в паблик бессмысленно. Даже с точки зрения самопиара — посмотрев на такой код фиг кто загорится "Хочу кодить в райфе!"


                      зы: умилил ребрендинг-коммит
                      ззы: блин, да вы даже валидацию собственную навелосипедили, НОЗАЧЕМ? логирование запилили на собственном куцем интерфейсе, запиленном под конкретного мастодонта log4net. дефолтовые абстракции ILogger вам чем не угодили?


                      в общем смысла выкладки вот этого в гит, а соответственно и поста не вижу вообще.

                        0
                        Разве станет хуже, если будет много фреймворков хороших и разных, чтобы можно выбрать или что-то позаимствовать… Интересно было бы увидеть подобные инфраструктурные наработки других банков, что если в них будет больше общего чем разного? Да и по нынешним меркам, настолько ли внутренняя инфраструктура будет отличаться от внешней, те же докеры, куберы, кэши и т.п.
                  –1
                  На будущее совет — свои ошибки надо наследовать от ApplicationException а не от глобального Exception и уж тем более не кидать в своем коде стандартные ошибки .net фреймворка а бросать свои унаследованные от ApplicationException.
                    +2
                    Мне кажется это не совсем так
                    Заголовок спойлера
                    User applications, not the common language runtime, throw custom exceptions derived from the ApplicationException class. The ApplicationException class differentiates between exceptions defined by applications versus exceptions defined by the system.

                    If you are designing an application that needs to create its own exceptions, you are advised to derive custom exceptions from the Exception class. It was originally thought that custom exceptions should derive from the ApplicationException class; however in practice this has not been found to add significant value. For more information, see Best Practices for Handling Exceptions

                    ApplicationException Class

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

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