Pure.DI следующий шаг

Недавно в этом посте вы познакомились с библиотекой Pure.DI. Этот пакет с анализатором/генератором кода .NET 5 задумывался как помощник, который пишет простой код для композиции объектов в стиле чистого DI, используя подсказки для построения графа зависимостей. Он следит за изменениями, анализирует типы и зависимости между ними, подсвечивает проблемы и предлагает пути решения. Важно отметить, что библиотека Pure.DI - это не контейнер внедрения зависимостей, в её задачи входит:

По обсуждениям в предыдущем посте у меня сложилось впечатление, что необходимо решить следующие вопросы:

Сейчас, после небольших доработок анализатор кода автоматически определяет, является ли проект ASP.NET проектом, и генерирует код специального метода расширения, который обеспечивает интеграцию с ASP.NET. Для демонстрации возможности выбрано серверное Blazor приложение:

Описание графа зависимостей находится в этом классе:

DI.Setup()
  .Bind<IDispatcher>().As(Singleton).To<Dispatcher>()
  .Bind<IClockViewModel>().To<ClockViewModel>()
  .Bind<ITimer>().As(Scoped).To(_ => new Timer(TimeSpan.FromSeconds(1)))
  .Bind<IClock>().As(ContainerSingleton).To<SystemClock>();

Для того чтобы связать эти DI типы с инфраструктурой  ASP.NET нужно добавить всего лишь одну строку вызова метода расширения:

services.AddClockDomain();

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

Сейчас их нельзя использовать вне контекста ASP.NET, иначе появится ошибка компиляции с информацией об этом.

Для решения вопроса о нежелательной бинарной зависимости на API я “удалил” пакет Pure.DI.Contracts. Теперь весь API для описания графа зависимостей генерируются “на месте” и является частью инфраструктурного кода проекта, где этот API и используется. Как итог, в проекты не добавляется ни одной бинарной зависимости, а единственная зависимость типа analyzers на пакет Pure.DI будет использована только во время компиляции и забыта сразу после. И, конечно, ее можно использовать без ограничения в любых проектах, не опасаясь “зависеть от чего-то лишнего”.

ASP.NET инфраструктура вызывает метод Resolve() для каждого запроса. Чтобы уменьшить накладные расходы на этот вызов, был оптимизирован код, ответственный за сопоставление типа корневого элемента композиции объектов к методу создания этой композиции. С результатами сравнительных тестов можно ознакомиться здесь. Хотелось бы подчеркнуть, что в этом сравнении используется спорный способ получения показателей производительности. Поэтому, эти результаты, дают приблизительную оценку накладных расходов на многократный вызов метода Resolve().

Как обычно, любые конструктивные замечания и идеи очень приветствуются.

@NikolayPyanikov
25.04.2021 16:40 UTC
Первоисточник

Комментарии

@lair
25.04.2021 12:15 UTC
0

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

@NikolayPyanikov
25.04.2021 12:55 UTC
0
namespace BlazorServerApp
{
    internal static partial class ClockDomain
    {
        private static readonly Pure.DI.ResolversTable Resolvers = new Pure.DI.ResolversTable(new Pure.DI.Pair<System.Type, System.Func<object>>[]{new Pure.DI.Pair<System.Type, System.Func<object>>(typeof(Clock.Models.IClock), () =>
        {
            return (new Clock.Models.SystemClock());
        }), new Pure.DI.Pair<System.Type, System.Func<object>>(typeof(Clock.Models.ITimer), () =>
        {
            return (new Timer(TimeSpan.FromSeconds(1)));
        }), new Pure.DI.Pair<System.Type, System.Func<object>>(typeof(Clock.ViewModels.IDispatcher), () =>
        {
            return (Dispatcher__Singleton.Shared);
        }), new Pure.DI.Pair<System.Type, System.Func<object>>(typeof(Clock.ViewModels.IClockViewModel), () =>
        {
            return (new Clock.ViewModels.ClockViewModel((new Pure.DI.ServiceProviderInstance<Clock.Models.IClock>()).Value, (new Pure.DI.ServiceProviderInstance<Clock.Models.ITimer>()).Value, (Dispatcher__Singleton.Shared)));
        }), new Pure.DI.Pair<System.Type, System.Func<object>>(typeof(Pure.DI.ServiceProviderInstance<Clock.Models.ITimer>), () =>
        {
            return (new Pure.DI.ServiceProviderInstance<Clock.Models.ITimer>());
        }), new Pure.DI.Pair<System.Type, System.Func<object>>(typeof(Pure.DI.ServiceProviderInstance<Clock.Models.IClock>), () =>
        {
            return (new Pure.DI.ServiceProviderInstance<Clock.Models.IClock>());
        })}, default(System.Func<System.Type, object, object>));
       
        private const System.UInt32 ResolversDivisor = 28U;
        private static Pure.DI.Pair<System.Type, System.Func<object>>[] ResolversBuckets = Resolvers.ResolversBuckets;
        private static System.Func<System.Type, object, object> ResolversDefaultFactory = Resolvers.ResolversDefaultFactory;

        [MethodImplAttribute((MethodImplOptions)256)]
        public static object Resolve(System.Type type)
        {
            var pair = ResolversBuckets[(uint)type.GetHashCode() % ResolversDivisor];
            do
            {
                if (pair.Key == type)
                {
                    return pair.Value();
                }

                pair = pair.Next;
            }
            while (pair != null);
            return ResolversDefaultFactory != null ? ResolversDefaultFactory(type, null) ?? new ArgumentException($"Cannot resolve an instance of the type {type.Name}.") : throw new ArgumentException($"Cannot resolve an instance of the type {type.Name}.");
        }

        private static class Dispatcher__Singleton
        {
            public static readonly Clock.ViewModels.Dispatcher Shared = new Clock.ViewModels.Dispatcher();
        }

        public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddClockDomain(this Microsoft.Extensions.DependencyInjection.IServiceCollection services)
        {
            Microsoft.Extensions.DependencyInjection.IMvcBuilder builder = Microsoft.Extensions.DependencyInjection.MvcServiceCollectionExtensions.AddControllers(services);
            builder.PartManager.PopulateFeature(new Microsoft.AspNetCore.Mvc.Controllers.ControllerFeature());
            Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.Replace(builder.Services, Microsoft.Extensions.DependencyInjection.ServiceDescriptor.Transient<Microsoft.AspNetCore.Mvc.Controllers.IControllerActivator, Microsoft.AspNetCore.Mvc.Controllers.ServiceBasedControllerActivator>());
            Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient(services, serviceProvider =>
            {
                System.IServiceProvider prevServiceProvider = Pure.DI.ServiceProviderInstance.ServiceProvider;
                try
                {
                    Pure.DI.ServiceProviderInstance.ServiceProvider = serviceProvider;
                    return (Clock.ViewModels.IDispatcher)new Clock.ViewModels.Dispatcher();
                }
                finally
                {
                    Pure.DI.ServiceProviderInstance.ServiceProvider = prevServiceProvider;
                }
            });
            Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient(services, serviceProvider =>
            {
                System.IServiceProvider prevServiceProvider = Pure.DI.ServiceProviderInstance.ServiceProvider;
                try
                {
                    Pure.DI.ServiceProviderInstance.ServiceProvider = serviceProvider;
                    return (Clock.ViewModels.IClockViewModel)new Clock.ViewModels.ClockViewModel((new Pure.DI.ServiceProviderInstance<Clock.Models.IClock>()).Value, (new Pure.DI.ServiceProviderInstance<Clock.Models.ITimer>()).Value, (Dispatcher__Singleton.Shared));
                }
                finally
                {
                    Pure.DI.ServiceProviderInstance.ServiceProvider = prevServiceProvider;
                }
            });
            Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddScoped(services, serviceProvider =>
            {
                System.IServiceProvider prevServiceProvider = Pure.DI.ServiceProviderInstance.ServiceProvider;
                try
                {
                    Pure.DI.ServiceProviderInstance.ServiceProvider = serviceProvider;
                    return (Clock.Models.ITimer)new Timer(TimeSpan.FromSeconds(1));
                }
                finally
                {
                    Pure.DI.ServiceProviderInstance.ServiceProvider = prevServiceProvider;
                }
            });
            Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton(services, serviceProvider =>
            {
                System.IServiceProvider prevServiceProvider = Pure.DI.ServiceProviderInstance.ServiceProvider;
                try
                {
                    Pure.DI.ServiceProviderInstance.ServiceProvider = serviceProvider;
                    return (Clock.Models.IClock)new Clock.Models.SystemClock();
                }
                finally
                {
                    Pure.DI.ServiceProviderInstance.ServiceProvider = prevServiceProvider;
                }
            });
            return services;
        }
    }

    internal struct ServiceProviderInstance
    {
        [System.ThreadStatic] public static System.IServiceProvider ServiceProvider;
    }

    internal struct ServiceProviderInstance<T>
    {
        public T Value => ServiceProviderInstance.ServiceProvider != null ? (T)ServiceProviderInstance.ServiceProvider.GetService(typeof(T)) : throw new System.InvalidOperationException("Cannot resolve an instance outside a container.");
    }
}
25.04.2021 13:02 UTC
0
try
{
Pure.DI.ServiceProviderInstance.ServiceProvider = serviceProvider;
return (Clock.ViewModels.IClockViewModel)new Clock.ViewModels.ClockViewModel((new Pure.DI.ServiceProviderInstance<Clock.Models.IClock>()).Value, (new Pure.DI.ServiceProviderInstance<Clock.Models.ITimer>()).Value, (Dispatcher__Singleton.Shared));
}
finally
{
Pure.DI.ServiceProviderInstance.ServiceProvider = prevServiceProvider;
}

internal struct ServiceProviderInstance
{
[System.ThreadStatic] public static System.IServiceProvider ServiceProvider;
}

Ээээ.


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


Ну и не очень понятно, зачем вы что-то делаете с MVC-шными сервисами, будем честными. Это невежливо по отношению к пользователю контейнера.


Короче говоря, как я и боялся: получившийся код очень тяжело читать. Понять, что в нем пошло не так… более чем рискованно.

25.04.2021 13:44 UTC
0
Можете случайно получить много смешных багов. Да, очень маловероятно, прямо очень. Но не надо так все равно.

Я, причем, не очень понимаю, что вам мешает сделать "резолвер" для каждого "вашего" сервиса в виде Func<IServiceProvider,T>, и просто передавать сервис-провайдер вниз по цепочке. Грубо говоря, вот так:


class A {}
class B { B(ILogger<B> logger) } //системная зависимость
class C { C(A a, B b) } //прикладные зависимости

Func<IServiceProvider, A> FactoryA = _ => new A();
Func<IServiceProvider, B> FactoryB = sp => new B(sp.GetRequiredService<ILogger<B>>());
Func<IServiceProvider, C> FactoryC = sp => new C(FactoryA(sp), FactoryB(sp));

//регистрация

services.AddSingleton<A>(FactoryA);
services.AddScoped<B>(FactoryB);
services.AddTransient<C>(FactoryC);

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

25.04.2021 21:50 UTC
0
Func<IServiceProvider, C> FactoryC = sp => new C(FactoryA(sp), FactoryB(sp));

Тут, на самом деле, должно быть хитрее, и если зависимости Scoped или Singleton, то надо тоже вызывать sp.GetRequiredService<T>, а Factory вызывать только если временем жизни зависимости управляет не контейнер. Но это вполне очевидные (и просто реализуемые) мелочи.

@NikolayPyanikov
25.04.2021 19:02 UTC
0
Можете случайно получить много смешных багов. Да, очень маловероятно, прямо очень. Но не надо так все равно.

Например?

Ну и не очень понятно, зачем вы что-то делаете с MVC-шными сервисами, будем честными. Это невежливо по отношению к пользователю контейнера.


Я решал вопрос интеграции с ASP.NET, Microsoft DI контейнер — не лучше и не хуже других

Короче говоря, как я и боялся: получившийся код очень тяжело читать. Понять, что в нем пошло не так… более чем рискованно.


Если убрать интеграцию с ASP.NET, то код получается проще. Для понимания того, что происходит важна только таблица зависимостей, там все просто. Возможно, стоит улучшить форматирование и добавить комментарии в сгенерированном коде. Нужно понять как бы написал этот код программист руками.

Я, причем, не очень понимаю, что вам мешает сделать «резолвер» для каждого «вашего» сервиса в виде Func<IServiceProvider,T>, и просто передавать сервис-провайдер вниз по цепочке. Грубо говоря, вот так:

IServiceProvider нужен только привязкам с временем жизни ContainerSingleton или Scoped и только для случая интеграции с ASP.NET. Ваш подход не сработает для привязок To<>(…) с lambda функцией и я пока не вижу смысла держать отдельную таблицу для этих методов и дополнительную логику для обработки этого. Если возникнет необходимость, я подумаю над решением
@lair
25.04.2021 20:53 UTC
0
Например?

Например, скачок кода между тредами, и получение не того сервис-провайдера.


Я решал вопрос интеграции с ASP.NET

Зачем для этого регистрировать в контейнере что-то, кроме ваших собственных сервисов? Зачем вы вот это делаете:


Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.Replace(builder.Services, Microsoft.Extensions.DependencyInjection.ServiceDescriptor.Transient<Microsoft.AspNetCore.Mvc.Controllers.IControllerActivator, Microsoft.AspNetCore.Mvc.Controllers.ServiceBasedControllerActivator>());

Если убрать интеграцию с ASP.NET

Вы очень, очень зря приравниваете Microsoft.Extensions.DependencyInjection к ASP.NET.


IServiceProvider нужен только привязкам с временем жизни ContainerSingleton или Scoped

Гм. Так я еще раз спрошу: а откуда ваши сервисы получают зависимости вида ILogger<T>?


Ваш подход не сработает для привязок To<>(…) с lambda функцией

Зависит исключительно от того, как вы сделаете контракт этой функции.

@NikolayPyanikov
26.04.2021 14:33 UTC
0
Например, скачок кода между тредами, и получение не того сервис-провайдера

Если Pure.DI будет строить композиции объектов «скачками кода между тредами» я подумаю над решением этой проблемы.
Зачем для этого регистрировать в контейнере что-то, кроме ваших собственных сервисов? Зачем вы вот это делаете:

Этот код нужен для интеграции с ASP.NET
Гм. Так я еще раз спрошу: а откуда ваши сервисы получают зависимости вида ILogger

Чтобы получить зависимость в DI нужно делать «привязку». Для ILogger это можно сделать так:
.Bind<ILogger<TT>>().To(ctx => (ILogger<TT>)ctx.Resolve<IServiceProvider>().GetService(typeof(ILogger<TT>)));


Но я добил привязку по умолчанию для ILogger, так что в последней версии ни чего делать не нужно. Может быть стоит добавить функцию расширения, которая упрощает это. И собрать список того что чаще всего используется
@lair
26.04.2021 15:09 UTC
-1
Этот код нужен для интеграции с ASP.NET

Нет, не нужен. Вы где-то неправильно настраиваете ASP.NET Core-сервисы.


Чтобы получить зависимость в DI нужно делать «привязку».

… и так для всех потребляемых сервисов? Вместо того, чтобы научить вашу генерилку делать это автоматически?


Вы простите за прямоту, это неюзабельно.

26.04.2021 15:41 UTC
0
… и так для всех потребляемых сервисов? Вместо того, чтобы научить вашу генерилку делать это автоматически? Вы простите за прямоту, это неюзабельно.

Pure.DI это НЕ библиотека внедрения зависимостей, это генератор кода. Он анализирует зависимости в момент компиляции, соответственно в этот момент он должен иметь представление о графе. Я понимаю, сложно менять шаблоны и мне жаль, что это неюзабельно для вас.
В любом случае спасибо вам за то, что натолкнули меня на идею добавить привязки для основных типов ASP.NET, как уже было сделано для BCL типов .NET ранее
26.04.2021 15:46 UTC
+1
Pure.DI это НЕ библиотека внедрения зависимостей, это генератор кода. Он анализирует зависимости в момент компиляции, соответственно в этот момент он должен иметь представление о графе.

Я именно об этом и говорю. У вас есть представление о графе, поэтому вы знаете, что ILogger у вас отсутствует. И у вас есть IServiceProvider, из которого этот ILogger можно взять. Все, что осталось — это сгенерить код, который возьмет ILogger (или любой другой отсутствующий сервис) из IServiceProvider.


В любом случае спасибо вам за то, что натолкнули меня на идею добавить привязки для основных типов ASP.NET

Во-первых, не ASP.NET, а ASP.NET Core. Во-вторых, ни ILogger, ни IOptions, ни IHostEnvironment, ни IFileProvider, ни IHttpClientFactory не относятся к ASP.NET Core. Так что вы себе не представляете масштаб работы.


Я понимаю, сложно менять шаблоны и мне жаль, что это это неюзабельно для вас.

Дело не в шаблонах, дело в том, как это будет использоваться.