PunchKwara doctors lament poor pay, illegal salary deductionsEl Comercio¿Meta favorece a Flávio Bolsonaro y perjudica a Lula? Las claves de la polémica electoral en BrasilRTP DesportoMotoGP: terminou a temporada para RazgatliogluESPNTransfer rumors, news: Premier League clubs eye Alexander-Arnold amid Real Madrid uncertaintyThe Jerusalem PostMandelblit says he approved questioning Netanyahu as suspect, defense challenges wordingDaily MaverickHegseth meets Netanyahu in unannounced visit to Israel, source saysInquirerMarcos vows reforms to boost investor confidence, stock marketCollider‘Sense and Sensibility’ Officially Posts One of the Weekend’s Best Per-Theater AveragesObservador DesportoMoçambique. Supostos terroristas abatidos por fuzileirosSky TG24I titoli di Sky TG24 del 20 ottobre, edizione delle 8
The Daily Newsstand · Free, Always
Tuesday, October 20, 2026

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

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

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

Зачем всё это

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

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

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

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

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.