Многие компании, использующие C# для разработки, стремятся к микросервисной архитектуре. Этот подход обеспечивает гибкость, отказоустойчивость и масштабируемость.
В этой статье описанны основные принципы построения микросервисов, популярные библиотеки и подходы. Расчитана на новичков.
Шаблон:
Если хочешь быстро запустить свой первый микросервис на .NET без лишней возни — загляни в Template.Net.Microservice после прочтения статьи.
Готовая структура, поддержка описываемых в статье паттернов — просто ставь шаблон и сразу кодь свой сервис. Минимум настройки, максимум пользы 🔥
Что такое микросервисы?
Микросервисы — это подход к архитектуре системы, при котором приложение разбивается на набор небольших, автономных сервисов, каждый из которых реализует отдельную бизнес-функциональность. Каждый сервис обладает собственной базой данных, своим циклом разработки, развёртывания и масштабирования. Такой подход позволяет:
-
Независимо развивать отдельные части системы.
-
Улучшать отказоустойчивость: сбой одного сервиса не приводит к остановке всей системы.
-
Гибко масштабировать компоненты в зависимости от нагрузки.
-
Легкое внедрение новых технологий и обновление версий библиотек без ущерба основной системы инструментов.
Разделение по слоям
Чтобы обеспечить масштабируемость и простоту сопровождения, рекомендуется разделять приложение на отдельные слои:
-
Domain (Домен): Содержит основные сущности, агрегаты, бизнес-правила и доменные события. Здесь происходит моделирование предметной области с помощью DDD.
-
Application (Приложение): Реализует бизнес-логику, команды, запросы и сервисы, объединяя функциональность домена и инфраструктуры. Здесь часто применяется паттерн CQRS.
-
Infrastructure (Инфраструктура): Отвечает за взаимодействие с внешними ресурсами: базы данных, очереди сообщений, сторонние API, файловые системы.
-
UI/Presentation (Пользовательский интерфейс): Реализует интерфейсы взаимодействия (например, REST API, Minimal API, GraphQL).
Polyglot Persistence
Каждый микросервис должен иметь свою базу данных, подобранную с учётом его требований. Возможны следующие варианты:
-
Реляционные базы данных: PostgreSQL, SQL Server — для хранения структурированных данных с транзакционной целостностью.
-
NoSQL решения: MongoDB, Cassandra — для масштабируемости и работы с неструктурированными данными.
-
Кэширование и быстрый доступ: Redis — для кэширования, хранения сессий и временных данных.
Такой подход, называемый «Database per Service», позволяет избежать жёстких связей между сервисами и оптимизировать каждую часть системы под её конкретную задачу.
CQRS и MediatR: Разделение обязанностей чтения и записи
Command Query Responsibility Segregation (CQRS) разделяет операции записи (команды) и чтения (запросы) в приложении. Это позволяет оптимизировать каждую часть системы отдельно, снижая сложность и повышая производительность.
Преимущества CQRS:
-
Масштабируемость: Можно оптимизировать производительность операций чтения и записи независимо.
-
Упрощение логики: Каждая команда или запрос отвечает только за свою часть функциональности.
-
Лёгкость тестирования: Изолированное тестирование команд и запросов.
-
Единое место обработки сообщения: У вас могут быть разные виды API, но все они будут идти в общий обработчик.
Использование MediatR
Библиотека MediatR позволяет реализовать паттерн CQRS, объеденяя команды и запросы в общее понятие request и создавая обработчики к request.
Добавление в DI:
var assemblies = AppDomain.CurrentDomain.GetAssemblies(); services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblies(assemblies));
Команды или запросы должны реализовывать интерфейс IRequest<out TResponse>. Где T это тип, который вернет этот request при обработке
// Определение команды создания заказа public record OrderCreateCommand(decimal Price) : IRequest<bool>; //Обычно команды возвращают флаг статуса выполнения команды // Определение запроса для получения заказа по идентификатору public record OrderGetByIdRequest(Guid OrderId) : IRequest<OrderDto>;
Обработчики к request`ам должны реализовывать интерфейс IRequestHandler<in TRequest, out TResponse>
// Обработчик команды public class OrderCreateCommandHandler(IOrderRepository orderRepository) : IRequestHandler<OrderCreateCommand, bool> { public Task<bool> Handle(OrderCreateCommand request, CancellationToken cancellationToken) { var order = new Order(request.Price); await orderRepository.InsertAsync(order); return true; } } // Обработчик запроса public class OrderGetByIdRequestHandler(IOrderRepository orderRepository, IMapper mapper) : IRequestHandler<OrderGetByIdRequest, OrderDto> { public async Task<OrderDto> Handle(OrderGetByIdRequest request, CancellationToken cancellationToken) { var order = await orderRepository.GetByIdAsync(request.OrderId); return mapper.Map<OrderDto>(order); } }
Очереди сообщений: RabbitMQ и MassTransit
RabbitMQ — популярный брокер сообщений, который позволяет организовать очередь сообщений. MassTransit — библиотека для упрощения работы с брокерами (поддерживает RabbitMQ, Azure Service Bus и др.).
Добавление MassTransit в DI:
services.AddMassTransit(x => { //Добавление всех слушателей Message Consumer по Assembly var assembly = typeof(Application).Assembly; x.AddConsumers(assembly); //При использовании RabbitMq необходимо скачать дополнительный пакет MassTransit.RabbitMQ x.UsingRabbitMq((context, cfg) => { cfg.Host("rabbitmq://localhost", h => { h.Username("user"); h.Password("password"); }); cfg.ConfigureEndpoints(context); //Автоматическая конфигурация endpoint }); });
Message Consumer совместно с CQRS
Представим, что в сервисе заказов есть модель запроса (OrderGetByIdRequest) и её обработчик (OrderGetByIdRequestHandler). Чтобы другие сервисы могли ходить в обработчик запроса нужно описать слушателя для очереди сообщения:
public class OrderGetByIdRequestConsumer(IMediator mediator) : IConsumer<OrderGetByIdRequest> { //Метод вызовется, когда в очередь придет сообщение OrderGetByIdRequest public async Task Consume(ConsumeContext<OrderGetByIdRequest> context) { //Перенаправление сообщение в обработчик var result = await mediator.Send(context.Message, context.CancellationToken); await context.RespondAsync(result); //Ответ от обработчка вернуть в очередь } }
В другом сервисе можно обратиться к этому слушателю с помощью клиента:
private readonly IRequestClient<OrderGetByIdRequest> _orderGetByIdRequestClient; try { var response = await _profilePaymentOrderCreateClient.GetResponse<OrderDto>( new OrderGetByIdRequest(Guid.Empty), cancellationToken, RequestTimeout.Default); //В response.Message находится ответ из очереди сообщений } catch (RequestException e) { //Обработка исключения при ошибке в очереди сообщений }
В случае, если вы используете несколько очередей в одном проекте, то необходимо явно указать RequestClient к нужной очереди
services.AddMassTransit(x => { x.AddRequestClient<OrderGetByIdRequest>(); });
Отправка и обработка событий (Publisher/Subscriber)
Отправка события из сервиса заказов:
private readonly IPublishEndpoint _publishEndpoint await _publishEndpoint.Publish(new OrderCreatedEvent(order.Id, order.ProductName, order.Price));
Потребитель, обрабатывающий событие (например, сервис уведомлений):
public class OrderCreatedConsumer : IConsumer<OrderCreatedEvent> { public async Task Consume(ConsumeContext<OrderCreatedEvent> context) { // Логика обработки события: отправка уведомления, обновление статистики и т.д. Console.WriteLine($"Получено событие о создании заказа: {context.Message.OrderId}"); } }
При Publisher/Subscriber подходе необходимо чтобы каждый подписчик имел свою очередь иначе ивент будет приходить не всем подписчикам сразу, а по очереди:
services.AddMassTransit(x => { x.UsingRabbitMq((context, cfg) => { //Обязательно указывать разное имя очереди в разных сервисах при Publisher/Subscriber cfg.ReceiveEndpoint("order-created-queue", e => { e.ConfigureConsumer<OrderCreatedConsumer>(context); }); }); });
Контракт-First подход
Contract First — это подход к разработке микросервисов, при котором API определяется заранее, а затем на его основе пишется сервис. Для определения контрактов можно использовать как спецификации (например, Avro, Protobuf, AsyncAPI), так и общий пакет с моделями. Рассмотрим последний вариант.
Общий пакет (проект) контрактов может храниться в solution, если вы используете Mono-Repo, или же храниться в отдельном nuget-пакете при Multi-Repo подходе.
Что хранить в пакете контракта?
На самом деле хранить можно все что может помочь при интеграции с сервисов. В рабочих проектах я использую подобную струткуру:
-
Model — Внешние модели сервиса, например, viewmodel, dto и т.п.
-
Enum — Enum, которые нужны для моделей контракта.
-
Validator — Правила валидации для моделей контрактов.
-
Event — Ивенты для паттерна Publisher/Subscriber.
-
Request или Query/Command — CQRS модели для Message Consumer паттерна.
-
SagaEvent — Ивенты для паттерна SagaStateMashine.
Дабы в других сервисах не было потаницы из какого контракта та или иная модель, в имени модели можно добавлять префикс с именем сервиса.
Например, есть сервис интернет магазина (Shop), в нем есть модель корзины (Cart) и команда по записи корзины в базу (c). Если в каком-то другом сервисе будут модели с такими же названиями в контрактах, то это вызовет проблемы при разработке. В таком случае можно добавить префикс перед названиями моделей: ShopCart, ShopCartInsertCommand.
Minimal API
Minimal API позволяет создать легковесное и быстрое HTTP API без необходимости писать лишний шаблонный код. Это особенно актуально для микросервисов, где важно быстрое прототипирование и простота.
Пример Minimal API с CQRS
var builder = WebApplication.CreateBuilder(args); builder.Services.AddMediatR(typeof(Program)); builder.Services.AddEndpointsApiExplorer(); var app = builder.Build(); //Если EndPoint`ов в сервисе много, то стоит разнести их по файлам app.MapPost("/orders", async ([FromBody] OrderCreateCommand command, [FromServices] IMediator mediator, HttpContext context) => { var result = await mediator.Send(command, context.RequestAborted) return result ? Results.Ok() : Results.BadRequest(); }); app.Run();
Запрос идет в тот же обработчик что используется в очереди сообщений.
Документирование API
Интеграция Swagger через пакет Swashbuckle.AspNetCore позволяет автоматически генерировать документацию для вашего API, что упрощает разработку и тестирование.
builder.Services.AddSwaggerGen(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); }
Свагер автоматически сгенерирует ui со всеми http методами. Если необходимо дополнительно описать то нужно можно воспользоваться подобным синтаксисом:
app.MapPost("/orders", CreateOrder) .WithTags("Orders") .WithSummary("Краткое описание") .WithDescription("Основное описание") .WithOpenApi(); [ProducesResponseType(200, Type = typeof(bool))] [ProducesResponseType(400)] private async Task<IResult> CreateOrder( [FromBody] OrderCreateCommand command, [FromServices] IMediator mediator, HttpContext context) { var result = await mediator.Send(command, context.RequestAborted); return (bool)result ? Results.Ok(true) : Results.BadRequest(); }
Лучше http методы всегда выносить отдельно, указывать все возвращаемые значения и описание. Вот как это будет выглядеть в свагере:

Безопасность: аутентификация и авторизация
Безопасность является критически важным аспектом любой системы. Рекомендуется использовать централизованный Identity-сервис, который может базироваться на таких решениях, как:
-
OpenIddict(IdentityServer4) / Duende IdentityServer: Обеспечивают поддержку протоколов OpenID Connect и OAuth 2.0.
-
ASP.NET Core Identity с JWT: Легковесное решение для аутентификации через JSON Web Token.
Или же вы можите использовать уже готовые opensource приложения по типу KeyСloak или ознакомиться с мои решением на основе OpenIddict.
Централизованный Identity-сервис позволяет:
-
Управлять пользователями, ролями и разрешениями.
-
Обеспечить единую точку аутентификации для всех микросервисов.
-
Гарантировать безопасность и соответствие стандартам.
Контейнеризация и структура
Структура директорий проекта
Всегда стоит использовать одну струтуру директорий проекта, чтобы упростить работу DevOps инженера и понимание другими разработчиками, особенно если вы используете Multi-Repo. Пример простой структуры:
-
.gitignore: Файл описывающий ограничение по сканированию git
-
.gitlab-ci.yml или .github/workflows/main.yml: CI/CD файлы
-
LICENSE: Лецензия по которой распространяется проект
-
README.md: Описание проета
-
src: Все файлы, которые непосредственно связанны с кодом проекта
-
docker-compose.yml: Помогает собрать образы, если есть несколько запускаемых приложений
-
.nuget/NuGet/NuGet.Config: Если используются закрытые nuget резитории
-
Проект/
-
Контейнеризация с Docker
Упаковка микросервисов в Docker-контейнеры позволяет обеспечить воспроизводимость среды и легкость развёртывания. Пример Dockerfile для микросервиса:
#Определение окружения контейнера FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base USER $APP_UID WORKDIR /app #Рестор пакетов и билд проекта FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build ARG BUILD_CONFIGURATION=Release WORKDIR /src COPY ["MyService/MyService.PL.Api/MyService.PL.Api.csproj", "MyService/MyService.PL.Api/"] COPY ["MyService/MyService.Application/MyService.Application.csproj", "MyService/MyService.Application/"] COPY ["MyService/MyService.Domain/MyService.Domain.csproj", "MyService/MyService.Domain/"] COPY ["MyService/MyService.Infrastructure/MyService.Infrastructure.csproj", "MyService/MyService.Infrastructure/"] RUN dotnet restore "MyService/MyService.PL.Api/MyService.PL.Api.csproj" COPY . . WORKDIR "/src/MyService/MyService.PL.Api" RUN dotnet build "MyService.PL.Api.csproj" -c $BUILD_CONFIGURATION -o /app/build #Публикация FROM build AS publish ARG BUILD_CONFIGURATION=Release RUN dotnet publish "MyService.PL.Api.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false #Финальная конфигурация запуска FROM base AS final ENV ASPNETCORE_URLS=https://+:10001;http://+:10000 #Можно отдельно указать http порты WORKDIR /app COPY --from=publish /app/publish . ENTRYPOINT ["dotnet", "MyService.PL.Api.dll"]
Этот Dockerfile должен запускаться из контекста выше проекта т.е. над директорией проекта должна быть общая директория:
docker build -t MyService:latest -f src/MyService/MyService.UI.Api/Dockerfile src/
🚀 Поддержать автора
Если статья оказалась полезной, можно поддержать меня:
-
USDT (TRC-20):
TDDXvViV857dZuGTNeUTDBzAZW4iADqnwv -
BTC:
bc1qrl8rfe0qxw7q4mpnaw5tccu3xmusn42v0pgza9 -
ETH:
0xd20312EC530D859B5Bf527FbBB1ff127Fa05417F -
TON:
UQAWH4nlcIZbTWdhud3wOTpIa0XdColpF6WnCyDYaIIgZdgI
💡 Даже небольшая поддержка мотивирует писать больше полезных статей и гайдов!
ссылка на оригинал статьи https://habr.com/ru/articles/891382/
Добавить комментарий