Использование DI в DotNet чтобы не таскать UserId сквозь все слои приложения

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

Зачем всё это

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

На самом глубоком уровне вам неожиданно нужна информация о текущем пользователе: его 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, когда он ему надо.

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

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

Источник: https://habr.com/ru/articles/1096610/


Интересные статьи

Интересные статьи

В июле 2026 года мы соединили две технологические эпохи. С одной стороны был классический ICQ начала 2000-х. С другой — корпоративный мессенджер Битрикс24. Мы хотели, чтобы коллеги появились в ко...
Всем привет! CLI-инструменты являются неотъемлемой частью любого PHP-фреймворка, они удобны для выполнения службных операций. В данной статье поговорим о том, какой инструментарий предоставляет нам Би...
В результате проведенного мной исследования безопасности 3000 российских frontend-приложений было обнаружено, что CMS Битрикс более 11 лет передает персональные данные посетителей сайтов на территорию...
В недавно опубликованной статье «Три наблюдения о командной строке и путях в файловой системе» были рассмотрены некоторые особенности интерпретации командной строки оболочками в операционных системах ...
Когда ты уже не начинающий ардуинщик и погодных информеров у тебя уже несколько штук, хочется чего-то более «оригинального» и «полезного». Предположим у нас имеется: Arduino Leonardo – ...