Сделайте библиотеки настраиваемыми как для Azure Functions, так и для проектов .NET


Не правда ли, ужасное название?

Инъекция зависимостей (DI) в .NET действительно мощна, если вы привыкнете думать так в начале проекта. Часто мы используем один из методов расширения AddScoped или AddSingleton для создания объектов на основе наших классов или объектов сторонних библиотек. Однако если сторонние библиотеки не позволяют нам передавать данные конфигурации через объекты, раскрывающие IConfiguration или IOption, наши цепочки команд HostBuilder могут выйти из-под контроля. И если этого недостаточно, Azure Functions обычно передают данные конфигурации через переменные окружения.

Что делает наши цепочки команд беспорядочными?

Часто, когда нам нужно внедрить зависимость, мы должны передать функцию для создания объекта с параметрами, которые не могут быть внедрены ранее в IServicesCollection:

// Here we have a constructor with two parameters neither of which is of type IConfiguration
AddScoped((sp) =>
{
   var config = sp.GetService<IConfiguration>();
   return new Client(param1:config["param1"], param2:config["param2"]);
});
Вход в полноэкранный режим Выход из полноэкранного режима

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

// Here we have a constructor with one parameters which is of type IConfiguration
AddScoped<IClient, Client>();
Вход в полноэкранный режим Выход из полноэкранного режима

Мне часто нравится передавать IConfigurationSection, оставляя без внимания конкретную конфигурацию, относящуюся к инжектируемому объекту, из большого файла конфигурации в командную цепочку построения сервисов. Это позволит сделать инжектируемый класс более гибким при использовании в проектах Web API, консольных приложениях и Azure Functions.

Давайте заставим наши библиотеки легко работать в Azure Functions, а также в других проектах .NET

Допустим, у нас есть библиотека, и в этой библиотеке есть любое количество классов. Мы видим это в клиентских библиотеках API (они же SDK). Мы можем позволить пользователю вводить те объекты, которые ему нужны, или предложить возможность вводить все сразу в красивом чистом методе расширения. Это часто делается, когда многие объекты не являются необязательными. Это может выглядеть следующим образом:

AddMyClassLibrary();
Войти в полноэкранный режим Выйти из полноэкранного режима

И это вызовет все базовые методы расширения инъекции:

AddScoped<IClient, Client>()
.AddScoped<IClient1, Client1>()
.AddScoped<IClient2, Client2>()
//   ...
.AddScoped<IClientN, ClientN>();
Войти в полноэкранный режим Выход из полноэкранного режима

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

Но как насчет Azure Functions?

Azure Functions обычно используют переменные среды для хранения своих конфигураций, будь то из host.json, local.host.json или конфигурация в проекте функций на хосте Azure. Другими словами, Azure Functions не использует IConfiguration. Именно здесь нам следует задуматься о том, как мы позволяем нашим потребителям создавать наши библиотечные объекты с помощью инъекции зависимостей.

Методы расширения, которые работают вместе

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

    /// <summary>Add all classes with the IConfiguration object</summary>
    /// <remarks>Ironically, we dig out the configuration to find the default section in the settings file.
    /// It might seem a bit clunky, but it's very reuseable and makes for an easy reference in your consumers' start up files.
    /// </remarks>
    public static IServiceCollection AddClassLibrary(this IServiceCollection services)
    {
        services
            .AddScoped((sp) =>
            {
                var configuration = sp.GetService<IConfiguration>();
                return new Class1(configuration!.GetSection(DefaultSectionName));
            })
            .AddScoped((sp) =>
            {
                var configuration = sp.GetService<IConfiguration>();
                return new Class2(configuration!.GetSection(DefaultSectionName));
            })
            .AddScoped((sp) =>
            {
                var configuration = sp.GetService<IConfiguration>();
                return new Class3(configuration!.GetSection(DefaultSectionName));
            });

        return services;
    }

Войти в полноэкранный режим Выход из полноэкранного режима

Во-вторых, давайте предложим аналогичную перегрузку, которая позволяет пользователю иметь именованный раздел конфигурации в своем файле настроек:

    /// <summary>Add all classes with the IConfiguration object but with a named configuration section.</summary>
    /// <remarks>Ironically, we dig out the configuration to find the default section in the settings file.
    /// It might seem a bit clunky, but it's very reuseable and makes for an easy reference in your consumers' start up files.
    /// </remarks>
    public static IServiceCollection AddClassLibrary(this IServiceCollection services, string configurationSectionName)
    {
        services
            .AddScoped((sp) =>
            {
                var configuration = sp.GetService<IConfiguration>();
                return new Class1(configuration!.GetSection(configurationSectionName));
            })
            .AddScoped((sp) =>
            {
                var configuration = sp.GetService<IConfiguration>();
                return new Class2(configuration!.GetSection(configurationSectionName));
            })
            .AddScoped((sp) =>
            {
                var configuration = sp.GetService<IConfiguration>();
                return new Class3(configuration!.GetSection(configurationSectionName));
            });

        return services;
    }
Ввести полноэкранный режим Выйти из полноэкранного режима

В-третьих, мы предлагаем простой способ поместить наши классы в проект Azure functions:

    /// <summary>Add all classes with the IConfiguration object but with configuration that you find in Azure functions.</summary>
    /// <remarks>We create a configuration as an in-memory collection, which is completely separate from any configuration already in the service collection),
    /// This is really handy for Azure Functions that rely more on the environment variables collection and not an IConfiguration object.
    /// </remarks>
    public static IServiceCollection AddClassLibraryFromEnvironment(this IServiceCollection services)
    {
        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string>
            {
                ["First"] = Environment.GetEnvironmentVariable("First")!,
                ["Second"] = Environment.GetEnvironmentVariable("Second")!,
                ["Third"] = Environment.GetEnvironmentVariable("Third")!
            })
            .Build();

        services
            .AddScoped(s => new Class1(configuration))
            .AddScoped(s => new Class2(configuration))
            .AddScoped(s => new Class3(configuration));

        return services;
    }
Войти в полноэкранный режим Выход из полноэкранного режима

Как это выглядит для потребителя

В примере ниже показаны различные методы расширений.

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

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

        services
            // Uncomment only the following line to add the class library with the 'Default' configuration:
            .AddClassLibrary();

            // Uncomment only the following line to add the class library with the named configuration:
            .AddClassLibrary("SecondSection");

            // Uncomment only the following two lines to add only two classes from the class library:
            .AddScoped<Class1>()
            .AddScoped<Class3>()

            // Uncomment only the following command to add only the second class configured with the named configuration:
            .AddScoped(sp =>
            {
                var configuration = sp.GetService<IConfiguration>();
                return new Class2(configuration!.GetSection("SecondSection"));
            })

            // Uncomment only the following line to configure using the command-line arguments in the launch.json:
            .AddClassLibraryFromCommandline()
            ;
Вход в полноэкранный режим Выход из полноэкранного режима

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

Что можно вынести:

  • Эти шаблоны отлично подходят для библиотек, в которых много классов и/или много параметров конфигурации.
  • Если вы создаете многократно используемые библиотеки, особенно те, которые станут пакетами NuGet, то это отличный способ облегчить работу пользователей, даже если эти пользователи находятся в другом отделе вашей компании.
  • Не стоит забывать, что все здесь добавлено как scoped. Вы, вероятно, обнаружите, что в вашей библиотеке некоторые классы лучше использовать как scoped, а другие как singleton или даже transitional, и вы лучше знаете, как следует использовать ваши классы.
  • Но помните, что ваши потребители всегда могут внедрить ваши классы ala carte любым удобным для них способом, а поскольку вы позаботились об использовании по крайней мере одного параметра во всех ваших конструкторах типа IConfiguration, это будет легко сделать, используя формы параметров типа методов расширений AddScoped/Singleton/Transitional.
  • И это, надеюсь, только некоторые основополагающие идеи, которые могут быть адаптированы или заменены замечательными идеями, которые вы придумаете для своей библиотеки.

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

Alfetta159 / ConfigurationDotNetAzFunc

Создание библиотек, легко конфигурируемых между Azure Functions и остальной частью .NET.

Конфигурируемость для Azure Functions и .NET-проектов

от dev.to

Инъекция зависимостей (DI) в .NET становится по-настоящему мощной, когда вы привыкаете думать так с самого начала проекта. Часто мы используем один из методов расширения AddScoped или AddSingleton для создания объектов на основе наших классов или объектов сторонних библиотек. Однако если сторонние библиотеки не позволяют нам передавать данные конфигурации через объекты, раскрывающие IConfiguration или IOption, наши цепочки команд HostBuilder могут выйти из-под контроля. И если этого недостаточно, Azure Functions обычно передают данные конфигурации через переменные окружения.

Что делает наши цепочки команд беспорядочными?

Часто, когда нам нужно ввести зависимость, мы должны передать функцию для создания объекта с параметрами, которые не могут быть введены ранее в IServicesCollection:

// Here we have a constructor with two parameters neither of which is

Вход в полноэкранный режим Выход из полноэкранного режима
Посмотреть на GitHub

Оцените статью
devanswers.ru
Добавить комментарий