Форум программистов, компьютерный форум, киберфорум
bytestream
Войти
Регистрация
Восстановить пароль
Рейтинг: 4.67. Голосов: 3.

Как написать микросервис с нуля на C# с RabbitMQ, CQRS, Swagger и CI/CD

Запись от bytestream размещена 14.01.2025 в 21:04. Обновил(-а) bytestream 15.01.2025 в 15:40
Показов 2626 Комментарии 0

Нажмите на изображение для увеличения
Название: 5a4e99ea-6422-4726-8d2a-22a7c567c408.png
Просмотров: 109
Размер:	947.9 Кб
ID:	9203
В современном мире разработки программного обеспечения микросервисная архитектура стала стандартом де-факто для создания масштабируемых и гибких приложений. Этот архитектурный подход предполагает разделение большого монолитного приложения на набор небольших, независимых сервисов, каждый из которых отвечает за конкретную бизнес-функцию.

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

Основные преимущества микросервисной архитектуры включают:

- Масштабируемость: каждый сервис может масштабироваться независимо, что позволяет оптимально распределять ресурсы
- Отказоустойчивость: сбой одного сервиса не приводит к падению всей системы
- Технологическая гибкость: разные сервисы могут быть написаны на разных языках программирования
- Простота разработки: небольшие команды могут работать над отдельными сервисами независимо

При разработке микросервиса на C# мы получаем ряд существенных преимуществ. Платформа .NET предоставляет богатую экосистему инструментов и библиотек, которые значительно упрощают создание надёжных и производительных микросервисов. Фреймворк ASP.NET Core обеспечивает отличную поддержку для создания REST API, а встроенные механизмы внедрения зависимостей и конфигурации делают разработку более структурированной.

Для создания полноценного микросервиса необходимо учитывать несколько ключевых аспектов:

1. REST API как основной интерфейс взаимодействия
2. Персистентность данных через подключение к базе данных
3. Асинхронная коммуникация через очереди сообщений
4. Контейнеризация для упрощения развертывания
5. Непрерывная интеграция и доставка (CI/CD)

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

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

- ASP.NET Core как основной фреймворк для создания веб-приложений
- Entity Framework Core для работы с базой данных
- PostgreSQL в качестве системы управления базами данных
- RabbitMQ для организации асинхронного взаимодействия
- Docker для контейнеризации приложения
- GitHub Actions для настройки процессов CI/CD

Этот набор технологий обеспечивает оптимальный баланс между производительностью, надежностью и простотой разработки. ASP.NET Core предоставляет отличную производительность и кроссплатформенность, что делает его идеальным выбором для микросервисов. Entity Framework Core значительно упрощает работу с базой данных благодаря мощному ORM и поддержке миграций.

При разработке микросервиса важно следовать определенным принципам проектирования:

1. Принцип единой ответственности: каждый сервис должен выполнять только одну бизнес-функцию
2. Независимость данных: каждый микросервис должен иметь свою собственную базу данных
3. Слабая связанность: минимизация зависимостей между сервисами
4. Отказоустойчивость: реализация паттернов для обработки отказов
5. Мониторинг: внедрение средств наблюдения за работой сервиса

В процессе разработки мы будем использовать современные практики программирования, включая:

- Чистую архитектуру для организации кода
- Dependency Injection для управления зависимостями
- CQRS для разделения операций чтения и записи
- REST принципы для проектирования API
- Автоматическое тестирование для обеспечения качества кода

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

Подготовка окружения разработки



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

Начнем с установки основных компонентов:

1. .NET SDK: Установите последнюю версию .NET SDK (не ниже версии 6.0). SDK включает в себя все необходимые инструменты для разработки, компиляции и запуска приложений на платформе .NET.

2. Visual Studio: Рекомендуется использовать последнюю версию Visual Studio (Community Edition подойдет для большинства разработчиков). При установке убедитесь, что выбраны следующие рабочие нагрузки:
- ASP.NET и веб-разработка
- Разработка для .NET
- Кроссплатформенная разработка .NET

3. Docker Desktop: Установите Docker Desktop для работы с контейнерами. Убедитесь, что система соответствует требованиям для запуска Docker:
- Включена виртуализация в BIOS
- Windows 10/11 Pro или Enterprise для Windows-систем
- WSL 2 для Windows-систем

4. Git: Установите систему контроля версий Git для управления исходным кодом.

После установки основных компонентов создадим новый проект в Visual Studio:

1. Запустите Visual Studio и выберите "Создать новый проект"
2. В поиске введите "ASP.NET Core Web API"
3. Выберите шаблон "API веб-приложения ASP.NET Core"
4. Укажите следующие параметры проекта:
- Имя проекта: `OrderService`
- Расположение: выберите удобную директорию
- Решение: создать новое
- Имя решения: `Microservices`

При настройке проекта выберите следующие опции:

- Платформа: .NET 6.0 или выше
- Аутентификация: Нет
- HTTPS: Включено
- Поддержка Docker: Включено
- Поддержка OpenAPI: Включено

Структура решения должна включать несколько проектов:

Код
Microservices/
├── src/
│   ├── OrderService.API/
│   ├── OrderService.Core/
│   ├── OrderService.Infrastructure/
│   └── OrderService.Application/
└── tests/
    ├── OrderService.UnitTests/
    └── OrderService.IntegrationTests/
Создайте каждый проект с соответствующим типом:

1. OrderService.API: ASP.NET Core Web API
2. OrderService.Core: .NET Class Library
3. OrderService.Infrastructure: .NET Class Library
4. OrderService.Application: .NET Class Library
5. OrderService.UnitTests: xUnit Test Project
6. OrderService.IntegrationTests: xUnit Test Project

После создания проектов настройте зависимости между ними:

- API зависит от Application
- Application зависит от Core
- Infrastructure зависит от Core
- API зависит от Infrastructure

Установите основные NuGet-пакеты для каждого проекта:

OrderService.API:
XML
1
2
3
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="5.1.0" />
<PackageReference Include="Serilog.AspNetCore" Version="7.0.0" />
OrderService.Infrastructure:
XML
1
2
3
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.0" />
<PackageReference Include="RabbitMQ.Client" Version="6.5.0" />
OrderService.Application:
XML
1
2
3
<PackageReference Include="MediatR" Version="12.1.1" />
<PackageReference Include="FluentValidation" Version="11.7.1" />
<PackageReference Include="AutoMapper" Version="12.0.1" />
Настройте файл .gitignore для исключения ненужных файлов из системы контроля версий. Добавьте следующие директории:

Код
bin/
obj/
.vs/
*.user
*.suo
appsettings.Development.json
Теперь настроим конфигурацию проекта и добавим базовые классы, необходимые для работы микросервиса. В файле `appsettings.json` определим основные параметры конфигурации:

JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
  "ConnectionStrings": {
    "DefaultConnection": "Host=localhost;Port=5432;Database=OrdersDb;Username=postgres;Password=password"
  },
  "RabbitMQ": {
    "HostName": "localhost",
    "UserName": "guest",
    "Password": "guest",
    "VirtualHost": "/"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning"
    }
  }
}
В проекте OrderService.Core создадим базовые сущности и интерфейсы. Начнем с определения базовой сущности:

C#
1
2
3
4
5
6
public abstract class Entity
{
    public Guid Id { get; protected set; }
    public DateTime CreatedDate { get; protected set; }
    public DateTime? ModifiedDate { get; protected set; }
}
Создадим интерфейс репозитория для работы с данными:

C#
1
2
3
4
5
6
7
8
public interface IRepository<T> where T : Entity
{
    Task<T> GetByIdAsync(Guid id);
    Task<IEnumerable<T>> GetAllAsync();
    Task AddAsync(T entity);
    Task UpdateAsync(T entity);
    Task DeleteAsync(Guid id);
}
В проекте OrderService.Application настроим MediatR для реализации паттерна CQRS. Создадим базовые классы для команд и запросов:

C#
1
2
3
4
5
6
7
8
9
public interface ICommand<TResponse>
    : IRequest<TResponse>
{
}
 
public interface IQuery<TResponse>
    : IRequest<TResponse>
{
}
Добавим базовый обработчик ошибок:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class ErrorDetails
{
    public int StatusCode { get; set; }
    public string Message { get; set; }
    public string StackTrace { get; set; }
}
 
public class GlobalExceptionHandler : IExceptionHandler
{
    private readonly ILogger<GlobalExceptionHandler> _logger;
 
    public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
    {
        _logger = logger;
    }
 
    public async Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        _logger.LogError(exception, "An unexpected error occurred");
 
        var response = context.Response;
        response.ContentType = "application/json";
        
        var errorDetails = new ErrorDetails
        {
            StatusCode = (int)HttpStatusCode.InternalServerError,
            Message = exception.Message,
            StackTrace = exception.StackTrace
        };
 
        await response.WriteAsync(JsonSerializer.Serialize(errorDetails));
    }
}
В проекте OrderService.API настроим Swagger для документирования API:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static class SwaggerConfiguration
{
    public static IServiceCollection AddSwaggerConfiguration(this IServiceCollection services)
    {
        services.AddSwaggerGen(c =>
        {
            c.SwaggerDoc("v1", new OpenApiInfo
            {
                Title = "Order Service API",
                Version = "v1",
                Description = "API для управления заказами"
            });
            
            c.EnableAnnotations();
            c.DescribeAllParametersInCamelCase();
        });
 
        return services;
    }
}
Настроим Serilog для логирования:

C#
1
2
3
4
5
6
7
8
9
10
11
12
public static class LoggingConfiguration
{
    public static IHostBuilder ConfigureLogging(this IHostBuilder hostBuilder)
    {
        return hostBuilder.UseSerilog((context, services, configuration) => configuration
            .ReadFrom.Configuration(context.Configuration)
            .ReadFrom.Services(services)
            .Enrich.FromLogContext()
            .WriteTo.Console()
            .WriteTo.File("logs/log-.txt", rollingInterval: RollingInterval.Day));
    }
}
В файле `Program.cs` добавим необходимые сервисы и middleware:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var builder = WebApplication.CreateBuilder(args);
 
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerConfiguration();
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));
builder.Services.AddAutoMapper(typeof(Program).Assembly);
 
var app = builder.Build();
 
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}
 
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.UseMiddleware<GlobalExceptionHandler>();
 
app.Run();
Эта конфигурация обеспечивает основу для создания надежного и масштабируемого микросервиса. Мы настроили логирование, обработку ошибок, документацию API и базовую инфраструктуру для работы с данными. Теперь можно переходить к реализации конкретной бизнес-логики сервиса.

Rest и другие виды архитектуры
Неподскажете где рассказано понятно про rest ,или раскажите что такое rest своими словами.И подскажите пожалуста какие другие виды архитектуры бывают.

Когда подключаю Rest Controller невозможно получить доступ к статическим файлам
Здравствуйте, вообщем сабж. В веб-проекте, присутствует html файл и несколько конфигурационных файлов: Эти два сгенерировались IDE ...

Silverlight + Java + REST как быть?
всем привет есть задача: сервер на Java, а клиент на Silverlight, необходимо подружить. информауия на клиенте должна обновляться после изменения...

Rest google Drive
Добрый день! Пытаюсь разобраться с REST библиотекой Delphi XE7 для работы с Google Drive. Использую OAuth2Authenticator1, RESTClient1,...


Разработка базового REST API



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

Начнем с определения основных сущностей нашего домена. Создадим класс Order в проекте OrderService.Core:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Order : Entity
{
    public string OrderNumber { get; private set; }
    public OrderStatus Status { get; private set; }
    public decimal TotalAmount { get; private set; }
    public List<OrderItem> Items { get; private set; }
    
    public Order(string orderNumber)
    {
        OrderNumber = orderNumber;
        Status = OrderStatus.Created;
        Items = new List<OrderItem>();
        CreatedDate = DateTime.UtcNow;
    }
 
    public void AddItem(string productId, int quantity, decimal price)
    {
        var item = new OrderItem(productId, quantity, price);
        Items.Add(item);
        RecalculateTotal();
    }
 
    private void RecalculateTotal()
    {
        TotalAmount = Items.Sum(item => item.Quantity * item.Price);
    }
}
 
public enum OrderStatus
{
    Created,
    Processing,
    Completed,
    Cancelled
}
Создадим DTO-классы в проекте OrderService.Application для передачи данных между слоями:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class CreateOrderDto
{
    public string OrderNumber { get; set; }
    public List<OrderItemDto> Items { get; set; }
}
 
public class OrderItemDto
{
    public string ProductId { get; set; }
    public int Quantity { get; set; }
    public decimal Price { get; set; }
}
 
public class OrderResponseDto
{
    public Guid Id { get; set; }
    public string OrderNumber { get; set; }
    public OrderStatus Status { get; set; }
    public decimal TotalAmount { get; set; }
    public List<OrderItemDto> Items { get; set; }
    public DateTime CreatedDate { get; set; }
}
Реализуем команды и обработчики для операций с заказами:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class CreateOrderCommand : ICommand<OrderResponseDto>
{
    public CreateOrderDto OrderDto { get; set; }
}
 
public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, OrderResponseDto>
{
    private readonly IOrderRepository _orderRepository;
    private readonly IMapper _mapper;
 
    public CreateOrderCommandHandler(IOrderRepository orderRepository, IMapper mapper)
    {
        _orderRepository = orderRepository;
        _mapper = mapper;
    }
 
    public async Task<OrderResponseDto> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
    {
        var order = new Order(request.OrderDto.OrderNumber);
        
        foreach (var item in request.OrderDto.Items)
        {
            order.AddItem(item.ProductId, item.Quantity, item.Price);
        }
 
        await _orderRepository.AddAsync(order);
        return _mapper.Map<OrderResponseDto>(order);
    }
}
Создадим контроллер для обработки HTTP-запросов:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
    private readonly IMediator _mediator;
 
    public OrdersController(IMediator mediator)
    {
        _mediator = mediator;
    }
 
    [HttpPost]
    public async Task<ActionResult<OrderResponseDto>> CreateOrder([FromBody] CreateOrderDto orderDto)
    {
        var command = new CreateOrderCommand { OrderDto = orderDto };
        var result = await _mediator.Send(command);
        return CreatedAtAction(nameof(GetOrder), new { id = result.Id }, result);
    }
 
    [HttpGet("{id}")]
    public async Task<ActionResult<OrderResponseDto>> GetOrder(Guid id)
    {
        var query = new GetOrderByIdQuery { Id = id };
        var result = await _mediator.Send(query);
        
        if (result == null)
            return NotFound();
            
        return Ok(result);
    }
 
    [HttpGet]
    public async Task<ActionResult<List<OrderResponseDto>>> GetOrders()
    {
        var query = new GetOrdersQuery();
        var result = await _mediator.Send(query);
        return Ok(result);
    }
}
Добавим валидацию входящих данных с помощью FluentValidation:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class CreateOrderDtoValidator : AbstractValidator<CreateOrderDto>
{
    public CreateOrderDtoValidator()
    {
        RuleFor(x => x.OrderNumber)
            .NotEmpty()
            .MaximumLength(50);
 
        RuleFor(x => x.Items)
            .NotEmpty()
            .WithMessage("Заказ должен содержать хотя бы один товар");
 
        RuleForEach(x => x.Items).SetValidator(new OrderItemDtoValidator());
    }
}
 
public class OrderItemDtoValidator : AbstractValidator<OrderItemDto>
{
    public OrderItemDtoValidator()
    {
        RuleFor(x => x.ProductId)
            .NotEmpty();
 
        RuleFor(x => x.Quantity)
            .GreaterThan(0);
 
        RuleFor(x => x.Price)
            .GreaterThan(0);
    }
}
Настроим Swagger для документирования API endpoints:

C#
1
2
3
4
5
6
7
8
9
[SwaggerOperation(
    Summary = "Создание нового заказа",
    Description = "Создает новый заказ с указанными товарами")]
[ProducesResponseType(typeof(OrderResponseDto), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<OrderResponseDto>> CreateOrder([FromBody] CreateOrderDto orderDto)
{
    // ... существующий код ...
}
Добавим обработку ошибок на уровне контроллера:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
    private readonly ILogger<OrdersController> _logger;
    
    public OrdersController(IMediator mediator, ILogger<OrdersController> logger)
    {
        _mediator = mediator;
        _logger = logger;
    }
 
    [HttpPut("{id}/status")]
    public async Task<IActionResult> UpdateOrderStatus(Guid id, [FromBody] UpdateOrderStatusDto statusDto)
    {
        try
        {
            var command = new UpdateOrderStatusCommand 
            { 
                Id = id, 
                Status = statusDto.Status 
            };
            
            await _mediator.Send(command);
            return NoContent();
        }
        catch (NotFoundException ex)
        {
            _logger.LogWarning(ex, "Заказ не найден");
            return NotFound();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Ошибка при обновлении статуса заказа");
            return StatusCode(500, "Произошла внутренняя ошибка сервера");
        }
    }
}
Для обеспечения надежной работы API добавим промежуточное программное обеспечение для мониторинга и метрик:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class RequestLoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger _logger;
 
    public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }
 
    public async Task InvokeAsync(HttpContext context)
    {
        var startTime = DateTime.UtcNow;
        try
        {
            await _next(context);
        }
        finally
        {
            var duration = DateTime.UtcNow - startTime;
            _logger.LogInformation(
                "Request {Method} {Path} completed in {Duration}ms with status {Status}",
                context.Request.Method,
                context.Request.Path,
                duration.TotalMilliseconds,
                context.Response.StatusCode);
        }
    }
}
Реализуем поддержку версионирования API:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[controller]")]
public class OrdersController : ControllerBase
{
    // ... существующий код ...
}
 
public static class ApiVersioningConfiguration
{
    public static IServiceCollection AddApiVersioningConfiguration(this IServiceCollection services)
    {
        services.AddApiVersioning(options =>
        {
            options.DefaultApiVersion = new ApiVersion(1, 0);
            options.AssumeDefaultVersionWhenUnspecified = true;
            options.ReportApiVersions = true;
        });
 
        return services;
    }
}
Добавим поддержку пагинации для получения списка заказов:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class PaginationParameters
{
    private const int MaxPageSize = 50;
    private int _pageSize = 10;
    
    public int PageNumber { get; set; } = 1;
    
    public int PageSize
    {
        get => _pageSize;
        set => _pageSize = value > MaxPageSize ? MaxPageSize : value;
    }
}
 
public class PagedResponse<T>
{
    public IEnumerable<T> Items { get; set; }
    public int PageNumber { get; set; }
    public int PageSize { get; set; }
    public int TotalCount { get; set; }
    public int TotalPages { get; set; }
    public bool HasPrevious => PageNumber > 1;
    public bool HasNext => PageNumber < TotalPages;
}
 
[HttpGet]
public async Task<ActionResult<PagedResponse<OrderResponseDto>>> GetOrders([FromQuery] PaginationParameters parameters)
{
    var query = new GetOrdersQuery { 
        PageNumber = parameters.PageNumber,
        PageSize = parameters.PageSize 
    };
    
    var result = await _mediator.Send(query);
    return Ok(result);
}
Реализуем фильтрацию и сортировку заказов:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class OrderFilterParameters
{
    public DateTime? StartDate { get; set; }
    public DateTime? EndDate { get; set; }
    public OrderStatus? Status { get; set; }
    public decimal? MinAmount { get; set; }
    public decimal? MaxAmount { get; set; }
    public string SortBy { get; set; }
    public bool SortDescending { get; set; }
}
 
public class GetFilteredOrdersQuery : IQuery<PagedResponse<OrderResponseDto>>
{
    public PaginationParameters Pagination { get; set; }
    public OrderFilterParameters Filters { get; set; }
}
Добавим кэширование для часто запрашиваемых данных:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[HttpGet("{id}")]
[ResponseCache(Duration = 60)]
public async Task<ActionResult<OrderResponseDto>> GetOrder(Guid id)
{
    var cacheKey = $"order-{id}";
    var cachedOrder = await _cache.GetAsync<OrderResponseDto>(cacheKey);
    
    if (cachedOrder != null)
        return Ok(cachedOrder);
        
    var query = new GetOrderByIdQuery { Id = id };
    var result = await _mediator.Send(query);
    
    if (result == null)
        return NotFound();
        
    await _cache.SetAsync(cacheKey, result, TimeSpan.FromMinutes(5));
    return Ok(result);
}
Реализуем поддержку условных GET-запросов с помощью ETag:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[HttpGet("{id}")]
public async Task<ActionResult<OrderResponseDto>> GetOrder(Guid id)
{
    var order = await _mediator.Send(new GetOrderByIdQuery { Id = id });
    if (order == null)
        return NotFound();
        
    var etag = $"\"{order.ModifiedDate:yyyy-MM-dd HH:mm:ss}\"";
    
    if (Request.Headers.TryGetValue("If-None-Match", out var etags) && 
        etags.Contains(etag))
    {
        return StatusCode(StatusCodes.Status304NotModified);
    }
    
    Response.Headers.ETag = etag;
    return Ok(order);
}
Для обеспечения безопасности API добавим rate limiting:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class RateLimitingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IMemoryCache _cache;
    private readonly int _maxRequests;
    private readonly TimeSpan _interval;
 
    public RateLimitingMiddleware(
        RequestDelegate next,
        IMemoryCache cache,
        int maxRequests = 100,
        int intervalInSeconds = 60)
    {
        _next = next;
        _cache = cache;
        _maxRequests = maxRequests;
        _interval = TimeSpan.FromSeconds(intervalInSeconds);
    }
 
    public async Task InvokeAsync(HttpContext context)
    {
        var clientIp = context.Connection.RemoteIpAddress.ToString();
        var cacheKey = $"rate-limit-{clientIp}";
        
        var requests = _cache.GetOrCreate(cacheKey, entry =>
        {
            entry.AbsoluteExpirationRelativeToNow = _interval;
            return new List<DateTime>();
        });
 
        requests.RemoveAll(x => x < DateTime.UtcNow - _interval);
        
        if (requests.Count >= _maxRequests)
        {
            context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
            await context.Response.WriteAsync("Too many requests");
            return;
        }
        
        requests.Add(DateTime.UtcNow);
        await _next(context);
    }
}
Добавим компрессию ответов для уменьшения объема передаваемых данных:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseResponseCompression();
    
    // ... остальная конфигурация
}
 
public void ConfigureServices(IServiceCollection services)
{
    services.AddResponseCompression(options =>
    {
        options.EnableForHttps = true;
        options.Providers.Add<GzipCompressionProvider>();
        options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
            new[] { "application/json" });
    });
    
    // ... остальные сервисы
}
Эти дополнительные функции значительно улучшают производительность, безопасность и удобство использования нашего API. Все реализованные механизмы следуют лучшим практикам разработки REST API и обеспечивают надежную основу для дальнейшего расширения функциональности микросервиса.

<востadequanlParagthresh="susceptible"likвъ

В классах репозитория реализуем методы для работы с данными:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class OrderRepository : IOrderRepository
{
    private readonly OrderDbContext _context;
 
    public OrderRepository(OrderDbContext context)
    {
        _context = context;
    }
 
    public async Task<Order> GetByIdAsync(Guid id)
    {
        return await _context.Orders
            .Include(o => o.Items)
            .FirstOrDefaultAsync(o => o.Id == id);
    }
 
    public async Task<IEnumerable<Order>> GetAllAsync()
    {
        return await _context.Orders
            .Include(o => o.Items)
            .ToListAsync();
    }
 
    public async Task AddAsync(Order order)
    {
        await _context.Orders.AddAsync(order);
        await _context.SaveChangesAsync();
    }
 
    public async Task UpdateAsync(Order order)
    {
        _context.Entry(order).State = EntityState.Modified;
        await _context.SaveChangesAsync();
    }
 
    public async Task DeleteAsync(Guid id)
    {
        var order = await GetByIdAsync(id);
        if (order != null)
        {
            _context.Orders.Remove(order);
            await _context.SaveChangesAsync();
        }
    }
}
Для обеспечения целостности данных добавим реализацию Unit of Work:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public interface IUnitOfWork : IDisposable
{
    IOrderRepository Orders { get; }
    Task<bool> SaveChangesAsync();
}
 
public class UnitOfWork : IUnitOfWork
{
    private readonly OrderDbContext _context;
    private IOrderRepository _orderRepository;
 
    public UnitOfWork(OrderDbContext context)
    {
        _context = context;
    }
 
    public IOrderRepository Orders
    {
        get
        {
            return _orderRepository ??= new OrderRepository(_context);
        }
    }
 
    public async Task<bool> SaveChangesAsync()
    {
        using var transaction = await _context.Database.BeginTransactionAsync();
        try
        {
            await _context.SaveChangesAsync();
            await transaction.CommitAsync();
            return true;
        }
        catch
        {
            await transaction.RollbackAsync();
            throw;
        }
    }
 
    public void Dispose()
    {
        _context.Dispose();
    }
}
Реализуем механизм миграций для управления схемой базы данных:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class InitialMigration : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.CreateTable(
            name: "Orders",
            columns: table => new
            {
                Id = table.Column<Guid>(nullable: false),
                OrderNumber = table.Column<string>(maxLength: 50, nullable: false),
                Status = table.Column<int>(nullable: false),
                TotalAmount = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
                CreatedDate = table.Column<DateTime>(nullable: false),
                ModifiedDate = table.Column<DateTime>(nullable: true)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_Orders", x => x.Id);
            });
 
        migrationBuilder.CreateTable(
            name: "OrderItems",
            columns: table => new
            {
                Id = table.Column<Guid>(nullable: false),
                OrderId = table.Column<Guid>(nullable: false),
                ProductId = table.Column<string>(nullable: false),
                Quantity = table.Column<int>(nullable: false),
                Price = table.Column<decimal>(type: "decimal(18,2)", nullable: false)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_OrderItems", x => x.Id);
                table.ForeignKey(
                    name: "FK_OrderItems_Orders_OrderId",
                    column: x => x.OrderId,
                    principalTable: "Orders",
                    principalColumn: "Id",
                    onDelete: ReferentialAction.Cascade);
            });
    }
 
    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropTable(name: "OrderItems");
        migrationBuilder.DropTable(name: "Orders");
    }
}
Добавим индексы для оптимизации запросов:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class AddOrderIndexesMigration : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.CreateIndex(
            name: "IX_Orders_OrderNumber",
            table: "Orders",
            column: "OrderNumber",
            unique: true);
 
        migrationBuilder.CreateIndex(
            name: "IX_Orders_Status",
            table: "Orders",
            column: "Status");
 
        migrationBuilder.CreateIndex(
            name: "IX_Orders_CreatedDate",
            table: "Orders",
            column: "CreatedDate");
    }
 
    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropIndex(name: "IX_Orders_OrderNumber");
        migrationBuilder.DropIndex(name: "IX_Orders_Status");
        migrationBuilder.DropIndex(name: "IX_Orders_CreatedDate");
    }
}
Реализуем механизм аудита для отслеживания изменений в базе данных:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
public class AuditEntry
{
    public Guid Id { get; set; }
    public string EntityName { get; set; }
    public string ActionType { get; set; }
    public string EntityId { get; set; }
    public string Changes { get; set; }
    public DateTime Timestamp { get; set; }
    public string Username { get; set; }
}
 
public class AuditableDbContext : DbContext
{
    private readonly ICurrentUserService _currentUserService;
    
    public DbSet<AuditEntry> AuditLogs { get; set; }
 
    public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
    {
        var auditEntries = OnBeforeSaveChanges();
        var result = await base.SaveChangesAsync(cancellationToken);
        await OnAfterSaveChanges(auditEntries);
        return result;
    }
 
    private List<AuditEntry> OnBeforeSaveChanges()
    {
        ChangeTracker.DetectChanges();
        var auditEntries = new List<AuditEntry>();
        
        foreach (var entry in ChangeTracker.Entries())
        {
            if (entry.Entity is AuditEntry || entry.State == EntityState.Detached || entry.State == EntityState.Unchanged)
                continue;
 
            var auditEntry = new AuditEntry
            {
                EntityName = entry.Entity.GetType().Name,
                ActionType = entry.State.ToString(),
                EntityId = entry.Properties.First(p => p.Metadata.IsPrimaryKey()).CurrentValue?.ToString(),
                Username = _currentUserService.Username,
                Timestamp = DateTime.UtcNow,
                Changes = JsonSerializer.Serialize(GetChanges(entry))
            };
            
            auditEntries.Add(auditEntry);
        }
 
        return auditEntries;
    }
 
    private async Task OnAfterSaveChanges(List<AuditEntry> auditEntries)
    {
        if (auditEntries == null || auditEntries.Count == 0)
            return;
 
        await AuditLogs.AddRangeAsync(auditEntries);
        await SaveChangesAsync();
    }
 
    private Dictionary<string, object> GetChanges(EntityEntry entry)
    {
        var changes = new Dictionary<string, object>();
        
        foreach (var property in entry.Properties)
        {
            if (property.IsTemporary)
                continue;
 
            switch (entry.State)
            {
                case EntityState.Added:
                    changes[property.Metadata.Name] = property.CurrentValue;
                    break;
                case EntityState.Modified:
                    if (property.IsModified)
                    {
                        changes[$"{property.Metadata.Name}_Old"] = property.OriginalValue;
                        changes[$"{property.Metadata.Name}_New"] = property.CurrentValue;
                    }
                    break;
                case EntityState.Deleted:
                    changes[property.Metadata.Name] = property.OriginalValue;
                    break;
            }
        }
 
        return changes;
    }
}
Эта реализация обеспечивает надежное хранение и управление данными в нашем микросервисе, включая поддержку транзакций, аудит изменений и оптимизацию производительности через индексы. Использование паттерна Repository и Unit of Work позволяет абстрагировать бизнес-логику от деталей хранения данных и обеспечивает гибкость при возможной смене технологии хранения в будущем.

Реализация очередей сообщений



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

Начнем с установки необходимых пакетов NuGet:

C#
1
2
3
4
5
6
7
public class MessagingConfiguration
{
    public string HostName { get; set; }
    public string UserName { get; set; }
    public string Password { get; set; }
    public string VirtualHost { get; set; }
}
Создадим базовый интерфейс для работы с сообщениями:

C#
1
2
3
4
5
public interface IMessageBus
{
    Task PublishAsync<T>(T message, string routingKey) where T : class;
    Task SubscribeAsync<T>(string queueName, Func<T, Task> handler) where T : class;
}
Реализуем сервис для работы с RabbitMQ:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
public class RabbitMQMessageBus : IMessageBus, IDisposable
{
    private readonly IConnection _connection;
    private readonly IModel _channel;
    private readonly ILogger<RabbitMQMessageBus> _logger;
 
    public RabbitMQMessageBus(MessagingConfiguration config, ILogger<RabbitMQMessageBus> logger)
    {
        _logger = logger;
        
        var factory = new ConnectionFactory
        {
            HostName = config.HostName,
            UserName = config.UserName,
            Password = config.Password,
            VirtualHost = config.VirtualHost
        };
 
        _connection = factory.CreateConnection();
        _channel = _connection.CreateModel();
    }
 
    public async Task PublishAsync<T>(T message, string routingKey) where T : class
    {
        try
        {
            var exchangeName = typeof(T).Name.ToLower();
            
            _channel.ExchangeDeclare(
                exchange: exchangeName,
                type: ExchangeType.Direct,
                durable: true);
 
            var messageBytes = JsonSerializer.SerializeToUtf8Bytes(message);
            
            var properties = _channel.CreateBasicProperties();
            properties.Persistent = true;
            properties.MessageId = Guid.NewGuid().ToString();
 
            _channel.BasicPublish(
                exchange: exchangeName,
                routingKey: routingKey,
                basicProperties: properties,
                body: messageBytes);
 
            _logger.LogInformation($"Message {properties.MessageId} published to {exchangeName}");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error publishing message");
            throw;
        }
    }
 
    public async Task SubscribeAsync<T>(string queueName, Func<T, Task> handler) where T : class
    {
        var exchangeName = typeof(T).Name.ToLower();
        
        _channel.ExchangeDeclare(
            exchange: exchangeName,
            type: ExchangeType.Direct,
            durable: true);
 
        _channel.QueueDeclare(
            queue: queueName,
            durable: true,
            exclusive: false,
            autoDelete: false);
 
        _channel.QueueBind(
            queue: queueName,
            exchange: exchangeName,
            routingKey: queueName);
 
        var consumer = new AsyncEventingBasicConsumer(_channel);
        
        consumer.Received += async (model, ea) =>
        {
            try
            {
                var body = ea.Body.ToArray();
                var message = JsonSerializer.Deserialize<T>(body);
                
                await handler(message);
                
                _channel.BasicAck(ea.DeliveryTag, false);
                _logger.LogInformation($"Message {ea.BasicProperties.MessageId} processed");
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error processing message");
                _channel.BasicNack(ea.DeliveryTag, false, true);
            }
        };
 
        _channel.BasicConsume(
            queue: queueName,
            autoAck: false,
            consumer: consumer);
    }
 
    public void Dispose()
    {
        _channel?.Dispose();
        _connection?.Dispose();
    }
}
Определим классы сообщений для нашего микросервиса:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class OrderCreatedMessage
{
    public Guid OrderId { get; set; }
    public string OrderNumber { get; set; }
    public decimal TotalAmount { get; set; }
    public DateTime CreatedDate { get; set; }
}
 
public class OrderStatusChangedMessage
{
    public Guid OrderId { get; set; }
    public OrderStatus NewStatus { get; set; }
    public OrderStatus PreviousStatus { get; set; }
    public DateTime ChangedDate { get; set; }
}
Создадим обработчики сообщений:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class OrderMessageHandler
{
    private readonly ILogger<OrderMessageHandler> _logger;
    private readonly IOrderRepository _orderRepository;
 
    public OrderMessageHandler(
        ILogger<OrderMessageHandler> logger,
        IOrderRepository orderRepository)
    {
        _logger = logger;
        _orderRepository = orderRepository;
    }
 
    public async Task HandleOrderStatusChanged(OrderStatusChangedMessage message)
    {
        _logger.LogInformation($"Processing status change for order {message.OrderId}");
        
        var order = await _orderRepository.GetByIdAsync(message.OrderId);
        if (order == null)
        {
            _logger.LogWarning($"Order {message.OrderId} not found");
            return;
        }
 
        // Обработка изменения статуса
        await ProcessOrderStatusChange(order, message.NewStatus);
    }
 
    private async Task ProcessOrderStatusChange(Order order, OrderStatus newStatus)
    {
        // Реализация бизнес-логики при изменении статуса
        // Например, отправка уведомлений, обновление связанных данных и т.д.
    }
}
Добавим механизм повторных попыток для обработки сообщений:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class RetryPolicy
{
    private readonly int _maxRetries;
    private readonly TimeSpan _delay;
 
    public RetryPolicy(int maxRetries = 3, int delayMilliseconds = 1000)
    {
        _maxRetries = maxRetries;
        _delay = TimeSpan.FromMilliseconds(delayMilliseconds);
    }
 
    public async Task ExecuteAsync(Func<Task> action)
    {
        var attempts = 0;
        while (true)
        {
            try
            {
                attempts++;
                await action();
                return;
            }
            catch (Exception) when (attempts < _maxRetries)
            {
                await Task.Delay(_delay * attempts);
            }
        }
    }
}
Зарегистрируем все необходимые сервисы в DI-контейнере:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static class MessagingExtensions
{
    public static IServiceCollection AddMessaging(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        services.Configure<MessagingConfiguration>(
            configuration.GetSection("RabbitMQ"));
            
        services.AddSingleton<IMessageBus, RabbitMQMessageBus>();
        services.AddScoped<OrderMessageHandler>();
        services.AddHostedService<MessageBusSubscriber>();
        
        return services;
    }
}
Создадим фоновый сервис для подписки на сообщения:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class MessageBusSubscriber : BackgroundService
{
    private readonly IMessageBus _messageBus;
    private readonly IServiceProvider _serviceProvider;
    private readonly ILogger<MessageBusSubscriber> _logger;
 
    public MessageBusSubscriber(
        IMessageBus messageBus,
        IServiceProvider serviceProvider,
        ILogger<MessageBusSubscriber> logger)
    {
        _messageBus = messageBus;
        _serviceProvider = serviceProvider;
        _logger = logger;
    }
 
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await SubscribeToMessages(stoppingToken);
    }
 
    private async Task SubscribeToMessages(CancellationToken stoppingToken)
    {
        await _messageBus.SubscribeAsync<OrderStatusChangedMessage>(
            "order-status-changes",
            async message =>
            {
                using var scope = _serviceProvider.CreateScope();
                var handler = scope.ServiceProvider.GetRequiredService<OrderMessageHandler>();
                await handler.HandleOrderStatusChanged(message);
            });
    }
}
Для обработки ошибок и восстановления после сбоев реализуем механизм Dead Letter Queue (DLQ):

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class DeadLetterQueueHandler
{
    private readonly IModel _channel;
    private readonly ILogger<DeadLetterQueueHandler> _logger;
 
    public DeadLetterQueueHandler(IModel channel, ILogger<DeadLetterQueueHandler> logger)
    {
        _channel = channel;
        _logger = logger;
    }
 
    public void ConfigureDeadLetterQueue(string queueName)
    {
        var deadLetterQueue = $"{queueName}-dlq";
        var arguments = new Dictionary<string, object>
        {
            {"x-dead-letter-exchange", ""},
            {"x-dead-letter-routing-key", deadLetterQueue}
        };
 
        _channel.QueueDeclare(
            queue: deadLetterQueue,
            durable: true,
            exclusive: false,
            autoDelete: false);
 
        _channel.QueueDeclare(
            queue: queueName,
            durable: true,
            exclusive: false,
            autoDelete: false,
            arguments: arguments);
    }
 
    public async Task ProcessDeadLetterQueue(string queueName, Func<byte[], Task> messageHandler)
    {
        var deadLetterQueue = $"{queueName}-dlq";
        var consumer = new AsyncEventingBasicConsumer(_channel);
 
        consumer.Received += async (model, ea) =>
        {
            try
            {
                await messageHandler(ea.Body.ToArray());
                _channel.BasicAck(ea.DeliveryTag, false);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error processing message from DLQ");
                _channel.BasicNack(ea.DeliveryTag, false, true);
            }
        };
 
        _channel.BasicConsume(
            queue: deadLetterQueue,
            autoAck: false,
            consumer: consumer);
    }
}
Добавим мониторинг состояния очередей:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class QueueMonitor
{
    private readonly IModel _channel;
    private readonly ILogger<QueueMonitor> _logger;
    private readonly Dictionary<string, QueueMetrics> _queueMetrics;
 
    public QueueMonitor(IModel channel, ILogger<QueueMonitor> logger)
    {
        _channel = channel;
        _logger = logger;
        _queueMetrics = new Dictionary<string, QueueMetrics>();
    }
 
    public QueueMetrics GetQueueMetrics(string queueName)
    {
        var response = _channel.QueueDeclarePassive(queueName);
        return new QueueMetrics
        {
            MessageCount = response.MessageCount,
            ConsumerCount = response.ConsumerCount
        };
    }
 
    public void StartMonitoring(string queueName, TimeSpan interval)
    {
        Timer timer = new Timer(state =>
        {
            var metrics = GetQueueMetrics(queueName);
            _queueMetrics[queueName] = metrics;
 
            _logger.LogInformation(
                $"Queue {queueName}: Messages={metrics.MessageCount}, Consumers={metrics.ConsumerCount}");
        }, null, TimeSpan.Zero, interval);
    }
}
 
public class QueueMetrics
{
    public uint MessageCount { get; set; }
    public uint ConsumerCount { get; set; }
}
Реализуем механизм сериализации сообщений с поддержкой версионирования:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class MessageSerializer
{
    public byte[] Serialize<T>(T message) where T : class
    {
        var envelope = new MessageEnvelope<T>
        {
            Version = 1,
            Content = message,
            Timestamp = DateTime.UtcNow
        };
 
        return JsonSerializer.SerializeToUtf8Bytes(envelope);
    }
 
    public T Deserialize<T>(byte[] data) where T : class
    {
        var envelope = JsonSerializer.Deserialize<MessageEnvelope<T>>(data);
        return envelope.Content;
    }
}
 
public class MessageEnvelope<T>
{
    public int Version { get; set; }
    public T Content { get; set; }
    public DateTime Timestamp { get; set; }
}
Добавим поддержку корреляции сообщений для отслеживания бизнес-процессов:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class CorrelationContext
{
    public Guid CorrelationId { get; set; }
    public string UserId { get; set; }
    public Dictionary<string, string> Metadata { get; set; }
}
 
public static class MessageBusExtensions
{
    public static async Task PublishWithContextAsync<T>(
        this IMessageBus messageBus,
        T message,
        CorrelationContext context,
        string routingKey) where T : class
    {
        var properties = new BasicProperties
        {
            Headers = new Dictionary<string, object>
            {
                ["CorrelationId"] = context.CorrelationId.ToString(),
                ["UserId"] = context.UserId
            }
        };
 
        foreach (var meta in context.Metadata)
        {
            properties.Headers[meta.Key] = meta.Value;
        }
 
        await messageBus.PublishAsync(message, routingKey, properties);
    }
}
Эта реализация обеспечивает надежную обработку сообщений, мониторинг очередей и поддержку распределенных транзакций в нашем микросервисе. Использование DLQ и механизмов повторных попыток гарантирует, что ни одно сообщение не будет потеряно даже при возникновении ошибок обработки.

Контейнеризация микросервиса



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

Начнем с создания Dockerfile для нашего приложения:

Windows Batch file
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
 
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["src/OrderService.API/OrderService.API.csproj", "src/OrderService.API/"]
COPY ["src/OrderService.Core/OrderService.Core.csproj", "src/OrderService.Core/"]
COPY ["src/OrderService.Infrastructure/OrderService.Infrastructure.csproj", "src/OrderService.Infrastructure/"]
RUN dotnet restore "src/OrderService.API/OrderService.API.csproj"
COPY . .
WORKDIR "/src/src/OrderService.API"
RUN dotnet build "OrderService.API.csproj" -c Release -o /app/build
 
FROM build AS publish
RUN dotnet publish "OrderService.API.csproj" -c Release -o /app/publish
 
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "OrderService.API.dll"]
Для организации многоконтейнерного окружения создадим файл docker-compose.yml:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
version: '3.8'
 
services:
  orderservice:
    build:
      context: .
      dockerfile: src/OrderService.API/Dockerfile
    ports:
      - "5000:80"
      - "5001:443"
    environment:
      - ASPNETCORE_ENVIRONMENT=Production
      - ConnectionStrings__DefaultConnection=Host=postgres;Database=OrdersDb;Username=postgres;Password=password
      - RabbitMQ__HostName=rabbitmq
    depends_on:
      - postgres
      - rabbitmq
 
  postgres:
    image: postgres:latest
    ports:
      - "5432:5432"
    environment:
      - POSTGRES_DB=OrdersDb
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=password
    volumes:
      - postgres-data:/var/lib/postgresql/data
 
  rabbitmq:
    image: rabbitmq:3-management
    ports:
      - "5672:5672"
      - "15672:15672"
    environment:
      - RABBITMQ_DEFAULT_USER=guest
      - RABBITMQ_DEFAULT_PASS=guest
 
volumes:
  postgres-data:
Для оптимизации размера образа добавим .dockerignore файл:

Код
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/bin
**/charts
**/docker-compose*
**/compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
Для обеспечения безопасности контейнеров создадим пользователя с ограниченными правами:

Windows Batch file
1
2
RUN adduser --disabled-password --gecos "" appuser
USER appuser
Реализуем [/B]healthcheck** для мониторинга состояния контейнера:

YAML
1
2
3
4
5
6
healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost:80/health"]
  interval: 30s
  timeout: 10s
  retries: 3
  start_period: 40s
Для управления секретами и конфигурацией добавим поддержку Docker secrets:

YAML
1
2
3
4
5
6
7
8
9
10
11
secrets:
  db_password:
    file: db_password.txt
  rabbitmq_password:
    file: rabbitmq_password.txt
 
services:
  orderservice:
    secrets:
      - db_password
      - rabbitmq_password
Настроим сетевое взаимодействие между контейнерами:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
networks:
  backend:
    driver: bridge
  frontend:
    driver: bridge
 
services:
  orderservice:
    networks:
      - frontend
      - backend
  
  postgres:
    networks:
      - backend
  
  rabbitmq:
    networks:
      - backend
Добавим поддержку многоэтапной сборки для оптимизации размера образа:

Windows Batch file
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["OrderService.sln", "./"]
COPY ["src/*/", "./src/"]
RUN dotnet restore
COPY . .
RUN dotnet build -c Release -o /app
 
FROM build AS publish
RUN dotnet publish -c Release -o /app
 
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS final
WORKDIR /app
COPY --from=publish /app .
ENTRYPOINT ["dotnet", "OrderService.API.dll"]
Для развертывания в Kubernetes создадим базовые манифесты:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: apps/v1
kind: Deployment
metadata:
  name: orderservice
spec:
  replicas: 3
  selector:
    matchLabels:
      app: orderservice
  template:
    metadata:
      labels:
        app: orderservice
    spec:
      containers:
      - name: orderservice
        image: orderservice:latest
        ports:
        - containerPort: 80
        env:
        - name: ASPNETCORE_ENVIRONMENT
          value: Production
Настроим постоянное хранилище для базы данных:

YAML
1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
Для автоматического масштабирования добавим конфигурацию HorizontalPodAutoscaler:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: orderservice-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: orderservice
  minReplicas: 3
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
Для обеспечения безопасности развертывания добавим конфигурацию NetworkPolicy:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: orderservice-policy
spec:
  podSelector:
    matchLabels:
      app: orderservice
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: frontend
    ports:
    - protocol: TCP
      port: 80
  egress:
  - to:
    - podSelector:
        matchLabels:
          app: postgres
    ports:
    - protocol: TCP
      port: 5432
Для мониторинга контейнеров добавим конфигурацию Prometheus и Grafana:

YAML
1
2
3
4
5
6
7
8
9
10
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: orderservice-monitor
spec:
  selector:
    matchLabels:
      app: orderservice
  endpoints:
  - port: metrics
Настроим сбор логов с помощью FluentD:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: v1
kind: ConfigMap
metadata:
  name: fluentd-config
data:
  fluent.conf: |
    <source>
      @type tail
      path /var/log/containers/orderservice-*.log
      pos_file /var/log/fluentd-containers.log.pos
      tag kubernetes.*
      read_from_head true
      <parse>
        @type json
      </parse>
    </source>
Создадим скрипт для автоматизации процесса сборки и публикации образов:

Bash
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/bin/bash
 
VERSION=$(date +%Y%m%d%H%M%S)
REGISTRY="your-registry.azurecr.io"
 
#Сборка образа
docker build -t $REGISTRY/orderservice:$VERSION .
 
#Сканирование на уязвимости[/H2]
trivy image $REGISTRY/orderservice:$VERSION
 
#Публикация образа
docker push $REGISTRY/orderservice:$VERSION
 
#Обновление тегов
docker tag $REGISTRY/orderservice:$VERSION $REGISTRY/orderservice:latest
docker push $REGISTRY/orderservice:latest
Для оптимизации производительности настроим кэширование слоев Docker:

Windows Batch file
1
2
3
4
5
6
7
8
9
10
#Кэширование пакетов NuGet
COPY ["*.csproj", "./"]
RUN dotnet restore
 
#Кэширование зависимостей npm (если используются)
COPY ["package.json", "package-lock.json", "./"]
RUN npm ci
 
#Копирование остального кода
COPY . .
Добавим конфигурацию для распределенного кэширования:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
apiVersion: v1
kind: Service
metadata:
  name: redis-cache
spec:
  selector:
    app: redis
  ports:
  - port: 6379
    targetPort: 6379
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: redis
spec:
  serviceName: redis-cache
  replicas: 3
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      containers:
      - name: redis
        image: redis:6.2-alpine
        ports:
        - containerPort: 6379
Настроим политики резервного копирования для постоянных хранилищ:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: velero.io/v1
kind: Schedule
metadata:
  name: daily-backup
spec:
  schedule: "0 1 * * *"
  template:
    includedNamespaces:
    - orderservice
    includedResources:
    - persistentvolumeclaims
    - persistentvolumes
Для обеспечения отказоустойчивости добавим конфигурацию распределенного хранилища:

YAML
1
2
3
4
5
6
7
8
9
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: distributed-storage
provisioner: kubernetes.io/aws-ebs
parameters:
  type: gp2
reclaimPolicy: Retain
allowVolumeExpansion: true
Эти конфигурации обеспечивают надежное, безопасное и масштабируемое развертывание микросервиса в контейнерной среде. Использование многоуровневой защиты, мониторинга и автоматического масштабирования позволяет создать отказоустойчивую систему, готовую к промышленной эксплуатации.

Настройка CI/CD пайплайна



Непрерывная интеграция и непрерывная доставка (CI/CD) являются важнейшими практиками современной разработки программного обеспечения. Для нашего микросервиса мы настроим автоматизированный процесс сборки, тестирования и развертывания с использованием GitHub Actions.

Начнем с создания базового workflow файла `.github/workflows/main.yml`:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
name: Build and Deploy
 
on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]
 
jobs:
  build:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup .NET
      uses: actions/setup-dotnet@v3
      with:
        dotnet-version: '6.0.x'
    
    - name: Restore dependencies
      run: dotnet restore
    
    - name: Build
      run: dotnet build --no-restore --configuration Release
    
    - name: Test
      run: dotnet test --no-build --verbosity normal
Добавим этап статического анализа кода:

YAML
1
2
3
4
5
    - name: Code Analysis
      uses: github/codeql-action/analyze@v2
      with:
        languages: csharp
        queries: security-and-quality
Настроим сборку и публикацию Docker-образа:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    - name: Login to Container Registry
      uses: docker/login-action@v2
      with:
        registry: ghcr.io
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}
    
    - name: Build and Push Docker Image
      uses: docker/build-push-action@v4
      with:
        context: .
        push: true
        tags: |
          ghcr.io/${{ github.repository }}/orderservice:latest
          ghcr.io/${{ github.repository }}/orderservice:${{ github.sha }}
Реализуем автоматическое развертывание в Kubernetes:

YAML
1
2
3
4
5
6
7
8
9
    - name: Deploy to Kubernetes
      uses: azure/k8s-deploy@v1
      with:
        manifests: |
          k8s/deployment.yaml
          k8s/service.yaml
        images: |
          ghcr.io/${{ github.repository }}/orderservice:${{ github.sha }}
        kubectl-version: 'latest'
Добавим этап проверки качества кода и покрытия тестами:

YAML
1
2
3
4
5
6
7
8
    - name: Code Coverage
      run: |
        dotnet test --no-build --collect:"XPlat Code Coverage"
        
    - name: Upload Coverage Report
      uses: codecov/codecov-action@v3
      with:
        files: ./TestResults/*/coverage.cobertura.xml
Настроим автоматическое управление версиями:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
    - name: Bump version
      id: tag_version
      uses: mathieudutour/github-tag-action@v6.1
      with:
        github_token: ${{ secrets.GITHUB_TOKEN }}
        
    - name: Create Release
      uses: actions/create-release@v1
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      with:
        tag_name: ${{ steps.tag_version.outputs.new_tag }}
        release_name: Release ${{ steps.tag_version.outputs.new_tag }}
        body: ${{ steps.tag_version.outputs.changelog }}
Реализуем автоматическое обновление зависимостей:

YAML
1
2
3
4
5
6
7
8
9
10
11
    - name: Check Dependencies
      uses: dependabot/fetch-metadata@v1
      with:
        github-token: ${{ secrets.GITHUB_TOKEN }}
        
    - name: Auto-merge Dependencies
      if: ${{ steps.metadata.outputs.update-type == 'version-update:semver-patch' }}
      run: gh pr merge --auto --merge "$PR_URL"
      env:
        PR_URL: ${{ github.event.pull_request.html_url }}
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Добавим этап сканирования безопасности:

YAML
1
2
3
4
5
6
7
8
9
10
11
    - name: Security Scan
      uses: aquasecurity/trivy-action@master
      with:
        image-ref: ghcr.io/${{ github.repository }}/orderservice:${{ github.sha }}
        format: 'sarif'
        output: 'trivy-results.sarif'
        
    - name: Upload Security Results
      uses: github/codeql-action/upload-sarif@v2
      with:
        sarif_file: 'trivy-results.sarif'
Настроим матрицу тестирования для различных окружений:

YAML
1
2
3
4
5
6
    strategy:
      matrix:
        configuration: [Debug, Release]
        dotnet-version: ['6.0.x', '7.0.x']
        os: [ubuntu-latest, windows-latest]
      fail-fast: false
Добавим автоматическое развертывание в разные окружения:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    - name: Deploy to Development
      if: github.ref == 'refs/heads/develop'
      uses: azure/k8s-deploy@v1
      with:
        namespace: development
        manifests: k8s/dev/*.yaml
        
    - name: Deploy to Staging
      if: github.ref == 'refs/heads/main'
      uses: azure/k8s-deploy@v1
      with:
        namespace: staging
        manifests: k8s/staging/*.yaml
        
    - name: Deploy to Production
      if: startsWith(github.ref, 'refs/tags/v')
      uses: azure/k8s-deploy@v1
      with:
        namespace: production
        manifests: k8s/prod/*.yaml
Настроим уведомления о результатах сборки:

YAML
1
2
3
4
5
6
    - name: Notify Teams
      uses: aliencube/microsoft-teams-actions@v0.8.0
      with:
        webhook_url: ${{ secrets.TEAMS_WEBHOOK_URL }}
        title: Build Status
        summary: Build ${{ job.status }}
Реализуем кэширование зависимостей для ускорения сборки:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
    - name: Cache NuGet packages
      uses: actions/cache@v3
      with:
        path: ~/.nuget/packages
        key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
        restore-keys: |
          ${{ runner.os }}-nuget-
 
    - name: Cache SonarCloud packages
      uses: actions/cache@v3
      with:
        path: ~\sonar\cache
        key: ${{ runner.os }}-sonar
        restore-keys: ${{ runner.os }}-sonar
Добавим интеграцию с системой анализа качества кода SonarCloud:

YAML
1
2
3
4
5
6
7
8
9
10
    - name: SonarCloud Scan
      uses: SonarSource/sonarcloud-github-action@master
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
      with:
        args: >
          -Dsonar.organization=${{ github.repository_owner }}
          -Dsonar.projectKey=${{ github.repository_owner }}_${{ github.event.repository.name }}
          -Dsonar.cs.opencover.reportsPaths=**/coverage.opencover.xml
Реализуем автоматическую генерацию документации:

YAML
1
2
3
4
5
6
7
8
9
10
    - name: Generate API Documentation
      run: |
        dotnet tool install -g Swashbuckle.AspNetCore.Cli
        swagger tofile --output api-docs.json bin/Release/net6.0/OrderService.API.dll v1
        
    - name: Publish Documentation
      uses: peaceiris/actions-gh-pages@v3
      with:
        github_token: ${{ secrets.GITHUB_TOKEN }}
        publish_dir: ./docs
Добавим проверку производительности с помощью k6:

YAML
1
2
3
4
    - name: Performance Testing
      run: |
        npm install -g k6
        k6 run performance-tests/load-test.js
Настроим мониторинг процесса развертывания:

YAML
1
2
3
4
5
6
7
8
9
    - name: Monitor Deployment
      uses: azure/k8s-set-context@v1
      with:
        kubeconfig: ${{ secrets.KUBE_CONFIG }}
        
    - name: Check Deployment Status
      run: |
        kubectl rollout status deployment/orderservice
        kubectl get pods -l app=orderservice
Реализуем автоматическое развертывание инфраструктуры с помощью Terraform:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
    - name: Setup Terraform
      uses: hashicorp/setup-terraform@v2
      
    - name: Terraform Init
      run: terraform init
      
    - name: Terraform Plan
      run: terraform plan
      
    - name: Terraform Apply
      if: github.ref == 'refs/heads/main'
      run: terraform apply -auto-approve
Добавим автоматическое создание и обновление миграций базы данных:

YAML
1
2
3
4
5
6
    - name: Database Migration
      run: |
        dotnet tool install --global dotnet-ef
        dotnet ef database update
      env:
        ConnectionStrings__DefaultConnection: ${{ secrets.DB_CONNECTION }}
Настроим автоматическое тегирование релизов по семантическому версионированию:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    - name: Extract Version
      id: extract_version
      run: echo "::set-output name=version::$(grep '<Version>' OrderService.API/OrderService.API.csproj | awk -F'>' '{print $2}' | awk -F'<' '{print $1}')"
      
    - name: Create Tag
      uses: actions/github-script@v6
      with:
        script: |
          github.rest.git.createRef({
            owner: context.repo.owner,
            repo: context.repo.repo,
            ref: 'refs/tags/v' + process.env.VERSION,
            sha: context.sha
          })
      env:
        VERSION: ${{ steps.extract_version.outputs.version }}
Добавим автоматическое обновление changelog:

YAML
1
2
3
4
5
6
7
8
9
10
11
    - name: Generate Changelog
      id: changelog
      uses: metcalfc/changelog-generator@v4.0.1
      with:
        myToken: ${{ secrets.GITHUB_TOKEN }}
        
    - name: Update Changelog
      uses: stefanzweifel/git-auto-commit-action@v4
      with:
        commit_message: Update CHANGELOG
        file_pattern: CHANGELOG.md
Реализуем автоматическое создание и обновление пулл-реквестов:

YAML
1
2
3
4
5
6
7
8
9
10
11
    - name: Create Pull Request
      uses: peter-evans/create-pull-request@v5
      with:
        token: ${{ secrets.GITHUB_TOKEN }}
        commit-message: Update dependencies
        title: 'chore: update dependencies'
        body: |
          Automated dependency updates
          - Dependencies updated
          - Tests passed
        branch: dependency-updates
Настроим автоматическую проверку и форматирование кода:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
    - name: Code Format Check
      run: |
        dotnet format --verify-no-changes
        
    - name: Apply Code Format
      if: failure()
      run: |
        dotnet format
        git config --global user.name 'github-actions'
        git config --global user.email 'github-actions@github.com'
        git commit -am "style: format code"
        git push
Добавим интеграционное тестирование с использованием тестовых контейнеров:

YAML
1
2
3
4
5
    - name: Integration Tests
      run: |
        docker-compose -f docker-compose.test.yml up -d
        dotnet test --filter Category=Integration
        docker-compose -f docker-compose.test.yml down
Эти дополнительные конфигурации расширяют возможности нашего CI/CD пайплайна, обеспечивая автоматизацию всех аспектов разработки, тестирования и развертывания микросервиса. Использование различных инструментов и практик позволяет создать надежный и эффективный процесс доставки программного обеспечения.

Полный пример рабочего микросервиса



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

Структура проекта на GitHub включает следующие компоненты:

Код
OrderService/
├── src/
│   ├── OrderService.API/
│   ├── OrderService.Core/
│   ├── OrderService.Infrastructure/
│   └── OrderService.Application/
├── tests/
│   ├── OrderService.UnitTests/
│   └── OrderService.IntegrationTests/
├── docker/
│   ├── Dockerfile
│   └── docker-compose.yml
├── k8s/
│   ├── deployment.yaml
│   └── service.yaml
└── .github/
    └── workflows/
        └── main.yml
Для запуска микросервиса необходимо выполнить следующие шаги:

1. Клонировать репозиторий
2. Перейти в директорию проекта
3. Запустить Docker Compose:
Bash
1
docker-compose up --build
После запуска микросервис будет доступен по адресу http://localhost:5000, а Swagger документация - по адресу http://localhost:5000/swagger.

Основные эндпоинты API:
- POST /api/orders - создание нового заказа
- GET /api/orders/{id} - получение информации о заказе
- PUT /api/orders/{id}/status - обновление статуса заказа
- GET /api/orders - получение списка заказов

Микросервис поддерживает асинхронную обработку через RabbitMQ, имеет настроенный мониторинг и систему логирования, а также готов к развертыванию в Kubernetes-кластере.

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

Для практического примера использования микросервиса рассмотрим базовые сценарии:

1. Создание нового заказа:
JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
POST /api/orders
Content-Type: application/json
 
{
  "orderNumber": "ORD-2023-001",
  "items": [
    {
      "productId": "PROD-001",
      "quantity": 2,
      "price": 29.99
    },
    {
      "productId": "PROD-002",
      "quantity": 1,
      "price": 49.99
    }
  ]
}
2. Получение информации о заказе:
JSON
1
GET /api/orders/5f7b1d8f-e35d-4c6d-a821-c7e5a25c5e9c
3. Обновление статуса заказа:
JSON
1
2
3
4
5
6
PUT /api/orders/5f7b1d8f-e35d-4c6d-a821-c7e5a25c5e9c/status
Content-Type: application/json
 
{
  "status": "Processing"
}
Для локальной разработки можно использовать предоставленный docker-compose файл, который поднимет все необходимые сервисы:

Bash
1
2
3
git clone https://github.com/your-username/orderservice
cd orderservice
docker-compose up -d
Для мониторинга работы сервиса доступны следующие инструменты:
- Grafana: http://localhost:3000
- Prometheus: http://localhost:9090
- RabbitMQ Management: http://localhost:15672

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

Микросервис готов к промышленной эксплуатации и может быть легко интегрирован в существующую инфраструктуру через REST API или систему очередей сообщений.

REST vs SOAP
В заре развития и планирования архитектуры нового проекта прошу помощи в решении - какую архитектуру избрать для оболочки предоставления сервисов. ...

Работа с WCF3.0 в духе REST
Здравствуйте! Встала такая задача: требуется написать сервис, работающий в .NET 3.0 и кросс-доменно доступный из Сильверлайта. Соответственно,...

REST API - Домашнее задание (java)
Добрый день. Нуждаюсь в помощи выполнением задачи домашнего задания по REST API. Вот собственное задание -...

ExtJS + Rest
Помогите с проблемой: запустил rest сервис на .net в сетке - при url: http://localhost:1008/srv/adres/test/2 Вывод &quot;example 2&quot; при...

REST-сервис синхронизации устройства с данными
Задача стоит следующая: Есть клиент, он загружает XML файл с помощью FileUpload и вводит UDID устройства в TextBox далее нужно в теле POST...

Реализовать REST-ful веб-сервис
Здравствуйте, Уважаемые форумчане. Нужно реализовать REST-ful веб-сервис для получения списка сохраненных результатов и самих результатов в XML,...

Реализовать REST-ful веб-сервис
Здравствуйте, нужно реализовать REST-ful веб-сервис для получения списка сохраненных результатов и самих результатов в XML, отправки и сохранения...

работа с сокитами Warning: fsockopen(): unable to connect to rest.msun.ru:80 in /home/y/ydmty.h14.ru/cgi/filesize.php on line 6
ктто работал с хостом agava.com стартую скрипт http://ydmty.h14.ru/cgi-bin/filesize.php в ответ Warning: fsockopen(): unable to connect to...

WCF + RabbitMQ ограничение количества запросов
Здравствуйте, есть сервис на WCF и брокер очередей RabbitMQ Данный сервис может обработать ограниченное кол-во запросов и до момента пока он не...

Spring, Rest, Json, LocalData
REST method POST вот такой json мапитса и все ок { &quot;mark&quot;:false, &quot;surname&quot;:&quot;test&quot;, &quot;name&quot;:&quot;test&quot;, ...

Нужно написать на PHP REST и SOAP сервисы
Нужно написать на PHP REST и SOAP сервисы. Описание во воложении

Spring MVC, Spring REST
Всем привет! Изучаю фреймворк Spring и возникло несколько вопросов в процессе, на которые не уверен однозначно, что до конца понимаю. Могли бы...

Размещено в Без категории
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Ошибка "Cleartext HTTP traffic not permitted" в Android
hw_wired 13.02.2025
При разработке Android-приложений можно столнуться с неприятной ошибкой "Cleartext HTTP traffic not permitted", которая может серьезно затруднить отладку и тестирование. Эта проблема особенно. . .
Изменение версии по умолчанию в NVM
hw_wired 13.02.2025
Node Version Manager, или коротко NVM - незаменимый инструмент для разработчиков, использующих Node. js. Многие сталкивались с ситуацией, когда разные проекты требуют различных версий Node. js,. . .
Переименование коммита в Git (локального и удаленного)
hw_wired 13.02.2025
Git как система контроля версий предоставляет разработчикам множество средств для управления этой историей, и одним из таких важных средств является возможность изменения сообщений коммитов. Но зачем. . .
Отличия Promise и Observable в Angular
hw_wired 13.02.2025
В веб-разработки асинхронные операции стали неотъемлимой частью почти каждого приложения. Ведь согласитесь, было бы странно, если бы при каждом запросе к серверу или при обработке больших объемов. . .
Сравнение NPM, Gulp, Webpack, Bower, Grunt и Browserify
hw_wired 13.02.2025
В современной веб-разработке существует множество средств сборки и управления зависимостями проектов, каждое из которых решает определенные задачи и имеет свои особенности. Когда я начинаю новый. . .
Отличия AddTransient, AddScoped и AddSingleton в ASP.Net Core DI
hw_wired 13.02.2025
В современной разработке веб-приложений на платформе ASP. NET Core правильное управление зависимостями играет ключевую роль в создании надежного и производительного кода. Фреймворк предоставляет три. . .
Отличия между venv, pyenv, pyvenv, virtualenv, pipenv, conda, virtualenvwrapp­­er, poetry и другими в Python
hw_wired 13.02.2025
В Python существует множество средств для управления зависимостями и виртуальными окружениями, что порой вызывает замешательство даже у опытных разработчиков. Каждый инструмент создавался для решения. . .
Навигация с помощью React Router
hw_wired 13.02.2025
React Router - это наиболее распространенное средство для создания навигации в React-приложениях, без которого сложно представить современную веб-разработку. Когда мы разрабатываем сложное. . .
Ошибка "error:0308010C­­:dig­ital envelope routines::unsup­­ported"
hw_wired 13.02.2025
Если вы сталкиваетесь с ошибкой "error:0308010C:digital envelope routines::unsupported" при разработке Node. js приложений, то наверняка уже успели поломать голову над её решением. Эта коварная ошибка. . .
Подключение к контейнеру Docker и работа с его содержимым
hw_wired 13.02.2025
В мире современной разработки контейнеры Docker изменили подход к созданию, развертыванию и масштабированию приложений. Эта технология позволяет упаковать приложение со всеми его зависимостями в. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2025, CyberForum.ru