Реализация Vertical Slice Architecture
Когда использовать этот скилл
Используй этот скилл когда:
- •Реализуешь ЛЮБУЮ backend-фичу (команду или запрос)
- •Создаёшь новые вертикальные слайсы в
backend/src/Api/Features/ - •Нужны примеры валидации, авторизации или обработки ошибок
Для тестирования вертикальных слайсов: Используй скилл component-testing. Он предоставляет паттерны тестирования на основе harness, которые тестируют фичи как целые единицы через HTTP-эндпоинты.
Объяви в начале: "Я использую скилл vertical-slice-architecture для реализации этой фичи."
Структура проекта
Фичи (вертикальные слайсы)
Каждая фича = один статический класс-файл с вложенными типами:
Features/ ├── [Domain]/ │ ├── [Feature].cs # Один файл, все вложенные типы │ └── [Feature]Tests.cs # Совмещённый тест (или в проекте tests/)
Пример: Features/Blog/CreatePost.cs
Общая инфраструктура (организация по назначению)
Весь общий инфраструктурный код организован в папки по назначению. Каждый файл принадлежит конкретному назначению, а не общей категории вроде "Middleware".
Common/
├── Auth/ # Аутентификация и авторизация
│ └── UserContextMiddleware.cs # BFF заголовок → Claims конвертация
├── Exceptions/ # Обработка исключений и доменные исключения
│ ├── Exceptions.cs # NotFoundException, ForbiddenException и т.д.
│ └── ExceptionHandlerMiddleware.cs # Глобальная обработка исключений
├── Http/ # HTTP/endpoint инфраструктура
│ ├── IEndpoint.cs # Интерфейс для регистрации эндпоинтов
│ └── HttpContextExtensions.cs # Хелперы для извлечения контекста пользователя
├── Identity/ # Утилиты генерации ID
│ ├── IdFactory.cs # IdGen обёртка для DI
│ └── Base32Encoder.cs # Crockford Base32 кодирование/декодирование
└── Validation/ # Инфраструктура валидации
└── ValidationBehavior.cs # MediatR pipeline behavior
Соглашение о пространствах имён: DrimAgents.Api.Common.{Concern}
- •Пример:
DrimAgents.Api.Common.Auth,DrimAgents.Api.Common.Exceptions,DrimAgents.Api.Common.Http
При создании новых файлов в Common/:
- •Определи конкретное назначение (Auth, Exceptions, Http, Identity, Validation, Caching, Storage и т.д.)
- •Middleware принадлежит тому назначению, которому служит (например, auth middleware →
Common/Auth/, caching middleware →Common/Caching/) - •Создай новую папку назначения при необходимости (например,
Common/Caching/,Common/Storage/) - •Размести файл в соответствующей папке назначения
- •Используй пространство имён
DrimAgents.Api.Common.{Concern}
Анти-паттерн: Не создавай общие папки вроде Middleware/, Utilities/, Helpers/ — всегда используй конкретное назначение.
Основной паттерн
Каждый вертикальный слайс следует одной структуре:
- •Endpoint — маппит HTTP-маршрут, извлекает пользователя, вызывает MediatR
- •Request — MediatR команда/запрос со всеми входными данными
- •Response — явный DTO, возвращаемый обработчиком
- •RequestValidator — правила FluentValidation
- •RequestHandler — бизнес-логика, операции с базой данных
Краткий справочник
Минимальный пример команды
namespace Api.Features.Blog;
public static class CreatePost
{
public class Endpoint : IEndpoint
{
public void MapEndpoint(WebApplication app)
{
app.MapPost("/api/blog/posts", async (
[FromBody] Body body,
ISender sender,
CancellationToken ct) =>
{
var request = new Request(body.Title, body.Slug);
var response = await sender.Send(request, ct);
return Results.Created($"/api/blog/posts/{response.PostId}", response);
});
}
private record Body(string Title, string Slug);
}
public record Request(string Title, string Slug) : IRequest<Response>;
public record Response(Guid PostId);
public class RequestValidator : AbstractValidator<Request>
{
public RequestValidator()
{
RuleFor(x => x.Title).NotEmpty().MaximumLength(200);
RuleFor(x => x.Slug).NotEmpty().Matches("^[a-z0-9-]+$");
}
}
public class RequestHandler : IRequestHandler<Request, Response>
{
private readonly AppDbContext _db;
public RequestHandler(AppDbContext db) => _db = db;
public async Task<Response> Handle(Request request, CancellationToken ct)
{
var post = new Post
{
Id = Guid.NewGuid(),
Title = request.Title,
Slug = request.Slug.ToLower()
};
_db.Posts.Add(post);
await _db.SaveChangesAsync(ct);
return new Response(post.Id);
}
}
}
Минимальный пример запроса
namespace Api.Features.Blog;
public static class GetPost
{
public class Endpoint : IEndpoint
{
public void MapEndpoint(WebApplication app)
{
app.MapGet("/api/blog/posts/{slug}", async (
string slug,
ISender sender,
CancellationToken ct) =>
{
var response = await sender.Send(new Request(slug), ct);
return response != null ? Results.Ok(response) : Results.NotFound();
});
}
}
public record Request(string Slug) : IRequest<Response?>;
public record Response(Guid Id, string Title, string Slug);
public class RequestHandler : IRequestHandler<Request, Response?>
{
private readonly AppDbContext _db;
public RequestHandler(AppDbContext db) => _db = db;
public async Task<Response?> Handle(Request request, CancellationToken ct)
{
return await _db.Posts
.Where(p => p.Slug == request.Slug && p.IsPublished)
.Select(p => new Response(p.Id, p.Title, p.Slug))
.FirstOrDefaultAsync(ct);
}
}
}
Управление конфигурацией
Паттерн Options
Всегда используй паттерн Options для доступа к конфигурации в вертикальных слайсах. Никогда не инжектируй IConfiguration напрямую в обработчики.
Структура:
Features/
└── [Domain]/
├── Options/
│ └── [Domain]Options.cs # Класс конфигурации для домена
└── [Feature].cs # Фича, использующая IOptions<DomainOptions>
Пример: Features/Users/Options/UsersOptions.cs
namespace Api.Features.Users.Options;
public class UsersOptions
{
public string[] AdminEmails { get; set; } = [];
}
Использование в обработчике:
using Microsoft.Extensions.Options;
public class RequestHandler : IRequestHandler<Request, Response>
{
private readonly AppDbContext _db;
private readonly UsersOptions _usersOptions;
public RequestHandler(AppDbContext db, IOptions<UsersOptions> usersOptions)
{
_db = db;
_usersOptions = usersOptions.Value;
}
public async Task<Response> Handle(Request request, CancellationToken ct)
{
var isAdmin = _usersOptions.AdminEmails.Contains(request.Email);
// ...
}
}
Регистрация в Program.cs:
builder.Services.Configure<UsersOptions>(builder.Configuration.GetSection("Users"));
Конфигурация в appsettings.json:
{
"Users": {
"AdminEmails": ["admin@example.com"]
}
}
Преимущества:
- •Строго типизированная конфигурация
- •Тестируемость (легко мокать options)
- •Поддержка валидации (DataAnnotations или FluentValidation)
- •Перезагружаемая конфигурация (при использовании
IOptionsMonitor<T>)
Правила:
- •Один класс options на домен (например,
UsersOptions,CoursesOptions) - •Размещай в папке
Features/[Domain]/Options/ - •Используй
IOptions<T>для статической конфигурации - •Используй
IOptionsMonitor<T>для перезагружаемой конфигурации - •Никогда не инжектируй
IConfigurationнапрямую в обработчики
Руководство по реализации
Полные паттерны, примеры и анти-паттерны смотри в:
- •Паттерны реализации — полные шаблоны, типовые паттерны, авторизация, пагинация, тестирование, анти-паттерны
Чеклист для каждой фичи
При реализации нового вертикального слайса:
- • Создать файл фичи:
Features/[Domain]/[Feature].cs - • Реализовать вложенный класс
Endpointс методомMapEndpoint - • Определить запись
Request, реализующуюIRequest<TResponse> - • Определить запись
Response(илиIRequestдля команд без возврата) - • Создать
RequestValidator, наследующийAbstractValidator<Request> - • Реализовать правила валидации в конструкторе валидатора
- • Создать
RequestHandler, реализующийIRequestHandler<Request, Response> - • Инжектировать зависимости в конструкторе обработчика (
AppDbContext,ILoggerи т.д.) - • Реализовать бизнес-логику в методе
Handle - • Добавить политику авторизации к эндпоинту при необходимости (
.RequireAuthorization("PolicyName")) - • Извлечь пользователя из
HttpContext.Userв эндпоинте при необходимости - • Добавить структурированное логирование в обработчике (важные операции, ошибки)
- • Написать компонентные тесты (паттерны смотри в скилле
component-testing) - • Протестировать happy path, ошибки валидации, граничные случаи, авторизацию
- • Проверить HTTP-ответ и состояние базы данных в тестах
- • Закоммитить с описательным сообщением, следуя conventional commits
Ключевые правила
- •Одна фича = один файл — все вложенные типы в одном статическом классе
- •Вложенные типы internal/private — только
RequestиResponseдолжны быть public - •Всегда используй MediatR — валидация запускается автоматически через pipeline
- •Тонкие эндпоинты — делегируй всю логику обработчику
- •Используй DTO — никогда не возвращай EF-сущности напрямую
- •Компонентные тесты — тестируй через HTTP-эндпоинт, а не внутренние классы
Резюме
Vertical Slice Architecture = Одна фича, один файл
Преимущества:
- •Легко находить и модифицировать фичи (всё в одном месте)
- •Консистентная структура для всех фич
- •Автоматическая валидация через MediatR pipeline
- •Тестируемость в изоляции с помощью компонентных тестов
Используй этот скилл для каждой backend-фичи для поддержания консистентности и качества.