Использование 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/openapibuilder.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/