нейрокартинка, куда ж без них
нейрокартинка, куда ж без них

Зачем всё это

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

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

[ApiController]
[Route("[controller]")]
public class ExampleController(ApplicationService application) : ControllerBase
{
    public IActionResult List()
    {
        var userId = User.Claims.FirstOrDefault(c => c.Type == "uid")?.Value;
        var permissions = User.Claims.Where(c => c.Type == "role").Select(c => c.Value).ToList();
        var userContext = new UserContext
        {
            UserId = userId,
            Roles = permissions
        };

        application.PerformApplicationOperation(userContext);

        return Ok();
    }
}

public class ApplicationService(Layer2Service layer2)
{
    public void PerformApplicationOperation(UserContext userContext)
    {
        layer2.PerformLayerOperation(userContext);
    }
}

public class Layer2Service(Layer3Service layer3)
{
   public void PerformLayerOperation(UserContext userContext)
    {
        layer3.PerformLayer3Operation(userContext);
    }
}

public class Layer3Service
{
    public void PerformLayer3Operation(UserContext userContext)
    {
        // some logic here
    }
}

public class UserContext
{
    public string? UserId { get; set; }

    public List<string> Roles { get; set; } = [];
}

Это работает, но в каждом из промежуточных методов неожиданно оказывается UserId, даже если этим сервисам сам он вообще не особо нужен.

С CQRS всё становится еще хуже

С CQRS все становится еще хуже, так как там из ниоткуда появляется ещёё более глубокая иерархия вызовов. И каждая команда и каждый запрос в итоге тащат за собой пользовательские параметры:

public record ImportDocumentsCommand(
    Guid ExternalSystemDocumentIג
    Guid? UserId,
    HashSet<InspectionCompany> UserCompanies,
    bool IsSuperAdmin
) : IQuery<List<MyAmazingDocuments>;

Давайте вместе поможем Даше понять зачем тут все пераметры кроме первого, ага.

Более чистый подход: Scoped‑сервис через DI

Как это можно улучшись, особенно для ситуаций, когда вам нужен UserContext только в Layer 3?

Тут можем помочь DI‑сервис, который хранит UserContext и делает его доступным в течение всего запроса. В ASP.NET Core у чаще всего запрос = di scope, поэтому сервис должен быть зарегистрирован как Scoped.

builder.Services.AddControllers();
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();
builder.Services.AddScoped<ApplicationService>();
builder.Services.AddScoped<Layer2Service>();
builder.Services.AddScoped<Layer3Service>();

К счастью, или к сожалению (в зависимости от взгляда) после создания ServiceProvider вы не можете напихать в него новые сервисы. Но мутабельность пропертей в серсиах никто не отменял:

public class ExampleController(ApplicationService application, UserContext userContext) : ControllerBase
{
    [HttpGet]
    public IActionResult List()
    {
        userContext.UserId = User.Claims.FirstOrDefault(c => c.Type == "uid")?.Value;
        userContext.Roles = User.Claims.Where(c => c.Type == "role").Select(c => c.Value).ToList();
        application.PerformApplicationOperation();
        return Ok();
    }
}

Менять промерти у UserScope относительно безопасно, так как он scoped и соседние запросы не затронет. Тут было бы конечно здорово сделать UserContext immutable за пределами контроллера, но это для целей данной статьи — оверкилл.

public class Layer3Service(UserContext userContext)
{
    public void PerformDeepestOperation()
    {
        Log.LogInformation($"Тут был {userContext.UserId}. И всё испортил(а)");
    }
}

Ну и теперь больше никакого проброса UserContext через ApplicationService и Layer2Service.

Убираем бойлерплейт с помощью Middleware

Копипастить логику создания пользовательского контекста в каждый экшн контроллера — фу, поэтому вынесем ее в middleware:

public async Task Invoke(HttpContext httpContext, UserContext userContext)
{
    userContext.UserId = httpContext.User.Claims.FirstOrDefault(c => c.Type == "uid")?.Value;
    userContext.Roles = httpContext.User.Claims.Where(c => c.Type == "role").Select(c => c.Value).ToList();

    await next(httpContext);
}

Теперь контекст заполняется один раз за запрос, до того как он дойдет до контроллера, и любой сервис в цепочке может просто заинжектить UserContext, когда он ему надо.

А минусы будут

У этого подхода есть очевидный минус, так как тут работает старое правило: явное лучше неявного. Когда вы передаете параметры пользователя как параметры метода, зависимость полностью видна в сигнатуре. Это прям сильно упрощает отслеживание потока данных, анализ и тестирование. С мутабельным контекстом у новичка на проекте возникнет очевидный вопрос «а это вообще откуда». И кто‑нибудь обязательно воспользуется методом, в окружении без пользователя. В большой семье это легком может деградировать с нормальную такую глобальную переменную посыпанную сахаром, скрывающую реальные архитектурные зависимости.

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


  1. for7raid
    20.10.2026 14:59

    Какой жуть!

    Для решения этой проблемы давно придумали паттерн Provider, именно он инжектится и методом а-ля GetCurrentUserId возвращает вам пользователя из http контекста.


    1. alehcka Автор
      20.10.2026 14:59

      и вот только сейчас, после много лет написания кода, я нагуглил что IHttpContextAccessor вообще говоря существует и инжектится. Штош