Pull to refresh

Comments 18

Свои скопы в дотнете и его встроенном DI в целом не выглядят удобно.

Это всегда смотрится как какой то костыль или хак.

Интересное решение и здорово, что сторонние инструменты (Autofac) так умеют.

Главная особенность встроенного ServiceProvider - он единый и глобальный.

Неправда. Каждый скоуп имеет свой собственный IServiceProvider. А, во-вторых, даже если все это создает какие-то проблемы, то значит вы как-то DI неправильно используете. Приложение про тот или иной контейнер вообще ничего не должно знать, за исключением той части кода на старте приложения, которая этот контейнер инициализирует.

Неправда. Каждый скоуп имеет свой собственный IServiceProvider.

Я искал и не нашёл. Судя по исходникам, всё дерево скоупов имеет общий контейнер, который и возвращается указанным вами вызовом. Также я не нашёл ни одного метода, позоволяющего пополнить регистрациями контейнер или отдельный скоуп после создания. Если вы знаете такой - напишите, мне самому интересно.

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

А у меня так и реализовано. Все DI-детали только в коде инициализации. Ни сервисы, ни контроллеры ни про какие Autofac и скоупы ничего не знают.

Легко ведь убедиться:

ServiceCollection services = new();
using var serviceProvider = services.BuildServiceProvider();
using var scope = serviceProvider.CreateScope();
Console.WriteLine(scope.ServiceProvider == serviceProvider); // выводит False

А у меня так и реализовано. Все DI-детали только в коде инициализации. Ни сервисы, ни контроллеры ни про какие Autofac и скоупы ничего не знают.

Ну, так а какие тогда проблемы были без использования Autofac?

Мне, как раз, на самом деле всегда нравился Autofac (несмотря на то, что среди всех известных контейнеров он, похоже, вообще самый медленный), и я его постоянно прикручивал к .NET (Core) проектам (через его штатное расширение), но позже я как-то понял, что просто нужды в этом никакой нет - те его возможности, которых нет в Microsoft.Extensions.DependencyInjection на самом деле нужны очень редко, а когда действительно нужны, то без особых проблем реализуются руками без него.

У меня сейчас родилась версия в чем у вас могло возникнуть недопонимание, если вы до этого, в "докоровские" времена именно с Autofac работали. В майкрософтовском DI скоупы, в отличии от AF, не иерархические.

Легко ведь убедиться

Если посмотреть в детали реализации, то видно, что скоуп делегирует все вызовы создания зависимостей к корневому контейнеру. Никаких модификаций списка зарегистрированных сервисов на уровне скоупа не предполагается.

По поводу остального, я и написал, что задача редкая. Но если нужна, то решается как описано в статье.

Никаких модификаций списка зарегистрированных сервисов на уровне скоупа не предполагается.

А зачем это? Инициализация DI всегда проводится только один раз, еще до того как какие-либо скоупы создаются. Если это не так, то это как раз и есть какое-то кривое использование этого самого DI. Контейнер, который на ходу меняет список регистраций во время работы приложения это какой-то архитектурный нонсенс. Если надо изменять поведение тех или иных сервисов в зависимости, допустим, от времени суток или фазы Луны - используйте стандартные подходы (например паттерны "состояние" или "стратегия").

Вы статью читали? Там не про "на ходу", а в процессе настройки. Задача простая: двум веткам HTTP-конвейера предоставить разные реализации одного интерфейса. И, в более сложном случае, сделать два варианта сервиса аутентификации для разных веток конвейера.

двум веткам HTTP-конвейера предоставить разные реализации одного интерфейса.

Сценарий вполне нормальный, но прикручивать ради такого целый отдельный фреймворк, это, по-моему избыточно (о чем я выше и говорил). Достаточно сделать самую обычную фабрику с параметром.

public interface IGreeting
{
    void Say();
}

public class DailyGreeting: IGreeting
{
    public void Say() => "Добрый день!";
}

public class NightlyGreeting: IGreeting
{
    public void Say() => "Доброй ночи!";
}

public interface IGreetingFactory
{
    IGreeting GetGreeting(DateTime now);
}

/// <summary>
/// Да, в общем случае использовать IServiceProvider это плохо,
/// но тут это допустимо, т.к. его использование локализовано в фабрике
/// и не приводит потом к нарушению "явности зависимостей".
/// </summary>
public class GreetingFactory: IGreetingFactory
{
    private readonly IService _serviceProvider;

    public GreetingFactory(IServiceProvider serviceProvider) =>
        _serviceProvider = serviceProvider;

    public IGreeting GetGreeing(DateTime now) =>
       (9 < now.Hour && now.Hour < 22) ?
           (IGreeting)_serviceProvider.GetService<DailyGreeting>() :
           (IGreeting)_serviceProvider.GetService<NightlyGreeting>();
}

// инициализация контейнера
ServiceCollection services = new();
services.AddTransient<DailyGreeting>();
services.AddTransient<NightlyGreeting>();
services.AddTransient<IGreetingFactory, GreetingFactory>();

// некий класс куда фабрика вставляется.
public class Foo
{
    private readonly IGreetingFactory greetingFactory;

    public Foo(IGreetingFactory greetingFactory) =>
        _greetingFactory = greetingFactiory;

   public void Greet() => _greetingFactory.GetGreeting(DateTime.Now).Say();
}

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

Каким образом вы сможете в вашем примере сделать два вызова стандартного AddAuthentication() для разных веток конвейера, со своими параметрами аутентификации для каждой ?

Вы хотите для разных частей приложения сделать разную аутентификацию? Ну так это делается совсем не так. Потому что всегда есть аутентификация "вообще" (определяем кто это у нас такой), а потом вы уже даете или не даете доступ (авторизация) на основе, допустим, не только identity пользователя, но и на основе того, через какой метод аутентификации этот identity был получен. Впрочем, это уже даже не имеет отношения к DI, а больше к общему подходу - не должно быть отдельных "аутентификации для /api/foo/"" и "аутентификации для /api/bar/". Я допускаю, что кто-то может такой изврат придумать, но в таком случае вы просто создаете костыль для решения изначально кривой задачи.

Да, еще, к тому же вы поймите, что во время выполнения пайплайна DI уже создал "composition root" и его дерево зависимостей, и менять уже после этого что-либо в контейнере это какой-то костыльный подход.

не должно быть отдельных "аутентификации для /api/foo/"" и "аутентификации для /api/bar/".

Жизнь несколько шире ваших категоричных "не должно". Я же не зря написал, что задача редкая. Но это не значит, что её нельзя решить. Дискуссию о прямоте/кривизне задач предлагаю вывести за скобки технического обсуждения, иногда это просто данность.

Да, еще, к тому же вы поймите, что во время выполнения пайплайна DI уже создал "composition root" и его дерево зависимостей, и менять уже после этого что-либо в контейнере это какой-то костыльный подход.

Я уже акцентировал, что в процессе исполнения ничего не меняется. Вы или невнимательно читали, или спорите с кем-то другим.

Жизнь несколько шире ваших категоричных "не должно". Я же не зря написал, что задача редкая. Но это не значит, что её нельзя решить.

Я с этим полностью согласен. Но решение костыльной задачи костылями не делает эти костыли правильным паттерном :)

Это же идентификация?

Потому что всегда есть аутентификация "вообще" (определяем кто это у нас такой)

Аутентификация это проверка подлинности, а авторизация - проверка прав.

Формально, да, согласен. Но, в случае ASP.NET "аутентификацией" чаще всего (в том числе) называют и механизм получения уже готового identity клиента из куки (по-старинке), или из заголовка Authorization (по-современному). Получение клиентом самого этого identity может быть вообще за скоупом приложения (выделенный провайдер аутентификации). Так чтобы специально и отдельно в ASP.NET использовался термин "идентификация" лично я не встречал.

Вы не делаете несколько вызовов AddAuthentication; вы добавляете несколько authn schemas и настраиваете ForwardDefaultSelector, например:

services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddScheme<AuthenticationSchemeOptions, ApiAuthHandler>("Api", o => { })
    .AddCookie(options =>
    {
        // Foward any requests that start with /api to that scheme
        options.ForwardDefaultSelector = ctx =>
        {
            return ctx.Request.Path.StartsWithSegments("/api") ? "Api" : null;
        };
        options.AccessDeniedPath = "/account/denied";
        options.LoginPath = "/account/login";
    });

(см. пример, доки).

Так а как надо было делать правильно? Текущее решение выглядит удобно и корректно, имхо.

Посмотрите на named options, это одно из решений, которое успешно применяется для разделения ресурсов на множество веток. Например, можно зарегистрировать один и тот же HttpClient с разными именами, и совершенно разными настройками. Тоже самое касается EF / DbContext.

Да, при разработке своих компонентов, желательно это всегда учитывать. Что у вас появится просто отдельные "ветки" экземпляров.

А создавать полностью независимые DI контейнеры, это может быть как простой удар по производительности, так и невозможность использовать адекватно вполне легальные синглтоны. В общем, решаете проблему, как мне кажется, не там.

Sign up to leave a comment.

Articles