Всем привет, я разработчик из компании Bimeister и я хочу рассказать о шаблонном C# сервисе, который мы создали, чтобы унифицировать наши приложения и сократить время работы разработчиков над базовой настройкой приложения.

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

Для решения этой задачи Microsoft создала возможность делать свои шаблоны приложений для .net. Он представляет из себя обычный проект на C#, который можно упаковать в nuget пакет.

Перво-наперво необходимо определиться с архитектурой приложения, какой подход, какие паттерны будем использовать. Для вдохновения мы смотрели на пример от Microsoft https://github.com/dotnet/eShop.

Структура

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

Microservice.Api → Microservice.Infrastacture → Microservice.Application → Microservice.Domain

Microservice.Domain

Ядро нашего приложения с бизнес-логикой. Должен содержать минимум nuget пакетов и не зависеть ни от одной другой библиотеки из нашего списка. Там должны содержаться агрегаты, сущности, объекты-значения или другие модели из предметной области, в зависимости от того, как вы организуете логику. Интерфейсы, такие как доменные репозитории, будут находиться тоже тут. Их будут реализовывать другие слои. Для создания этих доменных сущностей нужны базовые классы, для этого подойдет пример  из ShopOnContainers. https://github.com/dotnet/eShop/tree/main/src/Ordering.Domain/SeedWork. Если вы ведете разработку используя агрегаты, сущности и объекты-значения, то вы получите необходимое.

Microservice.Application

Слой приложения, который будет подготавливать данные для доменного слоя и организовывать процесс обработки команды и запросов. Мы уже 5 лет успешно используем пакет Mediatr, как основу этого слоя. https://github.com/jbogard/MediatR. Старайтесь этот слой тоже сделать без лишних зависимостей; признаком хорошо написанного слоя приложения служит то, что его можно запустить и в консольном приложении и в webapi, а также он может работать с любым видом хранилища.

Microservice.Infrastacture

Этот слой нужен для работы с базой данных, в нашем случае это был Postgres. Подключаем к проекту необходимые библиотеки EntityFramework и создаем реализации интерфейсов репозиториев, которые находится на внутренних слоях.

Microservice.Api

Webapi, содержащая контроллеры и запускающая все приложение. Из контроллеров мы вызываем команды и запросы слоя приложения. А при старте он добавляет в DI контейнер все необходимые сущности, для этого у каждого слоя приложения было расширение для IServiceCollection. Также в этом слое мы подключили свои библиотеки для логирования, трейсинга и проверка работоспособности(health checks).

В итоге наш сервис выглядит так:

Описанный тут сервис проще, чем наш настоящий, потому что я хочу в этой статье лишь описать структуру в целом.

Рекомендации

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

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

Так же не стоит пытаться запихнуть туда все, включая настройки для CI/CD и Git. Их часто настраивает команда DevOps, и искать эти настройки внутри шаблонного сервиса будет неудобно. Ваш шаблон служит для создания .net сборки и только.

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

Формирование пакета

Теперь у нас есть общая структура для приложения. Пора его положить в nuget пакет и опубликовать как шаблон приложения. Информация о шаблоне хранится в отдельном файле, создаем папку template и кладем туда солюшен и проекты. На уровне папки templates создаем папку .template.config и кладем туда файл template.json со следующим содержанием

{
  "$schema": "<https://json.schemastore.org/template>",
  "author": "Marat Kaloev",
  "classifications": ["Web", "Template", "Api", ".NET 8"],
  "identity": "Microservice",
  "name": "Bimeister WebApi",
  "shortName": "bimeister-webapi",
  "tags": { "language": "C#" },
  "sourceName": "Microservice"
}

Подробно о содержимом этого файла можно почитать тут. Отмечу, что при генерации проекта из вашего шаблона, название будет вставлено в название проекта, заменяя значение свойства "sourceName". Я создал пример с префиксом Microservice, поэтому туда его и вписал.

Теперь нужно создать из этой сборки nuget пакет.

В корневой папке создаем файл nuget.csproj для упаковки нашего приложения в пакет. Основное отличие от обычного csroj для nuget в том, что атрибут PackageType имеет значение Template, так же надо указать, что мы исключаем солюшен-файл (*.sln) из шаблона.

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <PackageType>Template</PackageType>
    <PackageVersion>1.0</PackageVersion>
    <PackageId>Bimeister.Microservice.Template</PackageId>
    <Title>Bimeister Microservice Template</Title>
    <Authors>Marat Kaloev</Authors>
    <Description>Template for bimeister applications.</Description>
    <PackageTags>C#;webapi;.net8</PackageTags>
    <TargetFramework>net8.0</TargetFramework>
    <IncludeContentInPack>true</IncludeContentInPack>
    <IncludeBuildOutput>false</IncludeBuildOutput>
    <ContentTargetFolders>content</ContentTargetFolders>
  </PropertyGroup>
  <ItemGroup>
    <Content Include="template\\**\\*" Exclude="template\\**\\bin\\**;template\\**\\obj\\**"/>
    <Compile Remove="**\\*"/>
  </ItemGroup>
</Project>

Теперь с помощью команды dotnet pack можно собрать пакет и опубликовать его в репозитории. Для того чтобы его добавить в список доступных шаблонов, надо его установить, указав PackageId пакета - dotnet new install Bimeister.Microservice.Template (не забудьте подключить ваш репозиторий).

Использование

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

Итоги

Наш шаблонный сервис неплохо сэкономил время и стандартизировал наши приложения. Так что теперь нет особых проблем при переключении между разработкой различных сервисов. Так же он сильно упрощает вход новичков в команду – можно изучить примеры приложения и быстрее начать разработку. Конечно, приходится этот шаблон поддерживать, иначе со временем он станет не актуальным, поэтому не забывайте получать обратную связь и обновлять зависимости.

Ссылки для более глубокого изучения

Комментарии (6)


  1. arwyl
    27.03.2024 12:22

    так же надо указать, что мы исключаем солюшен-файл (*.sln) из шаблона

    А зачем? Чтобы разработчик руками его создавал каждый раз?


    1. MKaloev Автор
      27.03.2024 12:22

      солюшен-файл будет создан самим dotnet, и если не исключить .sln из шаблона, то будет создан лишний.


      1. arwyl
        27.03.2024 12:22

        в какой момент?

        если бы при использование шаблона дотнет создавал бы солюшен, то, наверное, шаблона для создания солюшена бы не было =)

        UPD: возможно, это делает visual studio или rider, но точно не дотнет, насколько мне известно


  1. navferty
    27.03.2024 12:22
    +1

    Надо заметить, что шаблоны dotnet также содержат механизмы для параметров (как задаваемых пользователем, так и вычисляемых). Это могут быть как текстовые параметры (например, имя проекта, которое может подставляться в cs файлы как часть namespace), так и булевые параметры, в зависимости от которых можно формировать различное содержимое файлов, например так:

    namespace MyProject.Con
    {
      class Program
      {
        static void Main(string[] args)
        {
          Console.WriteLine("Hello World!");
    #if( addMethod )
          HelloWordAgain();
    #endif    
      }
    #if( addMethod )
    
        static void HelloWordAgain() {
          Console.WriteLine("Hello World Again!");
        }
    #endif    
      }
    }

    Подробнее в вики dotnet/templating на гитхабе.


    1. arwyl
      27.03.2024 12:22
      +1

      тоже хотел скинуть эту ссылку. А еще есть замена в именах файлов, conditional exclude файлов и многое другое


  1. AleksejMsk
    27.03.2024 12:22

    Мы пошли дальше.

    Сделали шаблон сервера с полным фаршем что нужен серверам.

    Логирование, телеметрия, DDD, тесты и прочее…

    https://github.com/KlestovAlexej/Wattle3.DemoServer