So injizieren oder verwenden Sie IConfiguration in Azure Function V3 mit Dependency Injection beim Konfigurieren eines Dienstes

9

Normalerweise würde ich in einem .NET Core-Projekt eine 'Boostrap'-Klasse erstellen, um meinen Dienst zusammen mit den DI-Registrierungsbefehlen zu konfigurieren. Dies ist normalerweise eine Erweiterungsmethode, IServiceCollectionbei der ich eine Methode wie aufrufen kann .AddCosmosDbServiceund alles Notwendige in der statischen Klasse, die diese Methode enthält, in sich geschlossen ist. Der Schlüssel ist jedoch, dass die Methode eine IConfigurationvon der StartupKlasse erhält .

Ich habe in der Vergangenheit in Azure-Funktionen mit DI gearbeitet, bin jedoch noch nicht auf diese spezielle Anforderung gestoßen.

Ich verwende die IConfigurationOption zum Binden an eine konkrete Klasse mit Eigenschaften, die den Einstellungen sowohl meiner local.settings.jsonals auch der Entwicklungs- / Produktionsanwendungseinstellungen entsprechen, wenn die Funktion in Azure bereitgestellt wird.

CosmosDbClientSettings.cs

/// <summary>
/// Holds configuration settings from local.settings.json or application configuration
/// </summary>    
public class CosmosDbClientSettings
{
    public string CosmosDbDatabaseName { get; set; }
    public string CosmosDbCollectionName { get; set; }
    public string CosmosDbAccount { get; set; }
    public string CosmosDbKey { get; set; }
}

BootstrapCosmosDbClient.cs

public static class BootstrapCosmosDbClient
{
    /// <summary>
    /// Adds a singleton reference for the CosmosDbService with settings obtained by injecting IConfiguration
    /// </summary>
    /// <param name="services"></param>
    /// <param name="configuration"></param>
    /// <returns></returns>
    public static async Task<CosmosDbService> AddCosmosDbServiceAsync(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        CosmosDbClientSettings cosmosDbClientSettings = new CosmosDbClientSettings();
        configuration.Bind(nameof(CosmosDbClientSettings), cosmosDbClientSettings);

        CosmosClientBuilder clientBuilder = new CosmosClientBuilder(cosmosDbClientSettings.CosmosDbAccount, cosmosDbClientSettings.CosmosDbKey);
        CosmosClient client = clientBuilder.WithConnectionModeDirect().Build();
        CosmosDbService cosmosDbService = new CosmosDbService(client, cosmosDbClientSettings.CosmosDbDatabaseName, cosmosDbClientSettings.CosmosDbCollectionName);
        DatabaseResponse database = await client.CreateDatabaseIfNotExistsAsync(cosmosDbClientSettings.CosmosDbDatabaseName);
        await database.Database.CreateContainerIfNotExistsAsync(cosmosDbClientSettings.CosmosDbCollectionName, "/id");

        services.AddSingleton<ICosmosDbService>(cosmosDbService);

        return cosmosDbService;
    }
}

Startup.cs

public class Startup : FunctionsStartup
{

    public override async void Configure(IFunctionsHostBuilder builder)
    {
        builder.Services.AddHttpClient();
        await builder.Services.AddCosmosDbServiceAsync(**need IConfiguration reference**); <--where do I get IConfiguration?
    }
}

Offensichtlich funktioniert das Hinzufügen eines privaten Felds für IConfigurationin Startup.csnicht, da es mit etwas gefüllt werden muss, und ich habe auch gelesen, dass die Verwendung von DI für IConfigurationkeine gute Idee ist .

Ich habe auch versucht, das hier beschriebene und als solches implementierte Optionsmuster zu verwenden :

builder.Services.AddOptions<CosmosDbClientSettings>()
    .Configure<IConfiguration>((settings, configuration) => configuration.Bind(settings));

Während dies funktionieren würde, um IOptions<CosmosDbClientSettings>eine nicht statische Klasse zu injizieren , verwende ich eine statische Klasse, um meine Konfigurationsarbeit zu speichern.

Irgendwelche Vorschläge, wie ich diese Arbeit machen kann oder eine mögliche Problemumgehung? Ich würde es vorziehen, die gesamte Konfiguration an einem Ort zu speichern (Bootstrap-Datei).

Jason Shave
quelle

Antworten:

5

Das verknüpfte Beispiel ist schlecht gestaltet (Meiner Meinung nach). Es fördert die enge Kopplung und das Mischen von asynchronen Warte- und Blockierungsanrufen.

IConfigurationwird standardmäßig als Teil des Startvorgangs zur Service-Sammlung hinzugefügt. Ich würde daher empfehlen, Ihr Design zu ändern, um die verzögerte Auflösung von Abhängigkeiten zu nutzen, damit die Auflösung mithilfe eines Factory-Delegaten IConfigurationgelöst werden kann IServiceProvider.

public static class BootstrapCosmosDbClient {

    private static event EventHandler initializeDatabase = delegate { };

    public static IServiceCollection AddCosmosDbService(this IServiceCollection services) {

        Func<IServiceProvider, ICosmosDbService> factory = (sp) => {
            //resolve configuration
            IConfiguration configuration = sp.GetService<IConfiguration>();
            //and get the configured settings (Microsoft.Extensions.Configuration.Binder.dll)
            CosmosDbClientSettings cosmosDbClientSettings = configuration.Get<CosmosDbClientSettings>();
            string databaseName = cosmosDbClientSettings.CosmosDbDatabaseName;
            string containerName = cosmosDbClientSettings.CosmosDbCollectionName;
            string account = cosmosDbClientSettings.CosmosDbAccount;
            string key = cosmosDbClientSettings.CosmosDbKey;

            CosmosClientBuilder clientBuilder = new CosmosClientBuilder(account, key);
            CosmosClient client = clientBuilder.WithConnectionModeDirect().Build();
            CosmosDbService cosmosDbService = new CosmosDbService(client, databaseName, containerName);

            //async event handler
            EventHandler handler = null;
            handler = async (sender, args) => {
                initializeDatabase -= handler; //unsubscribe
                DatabaseResponse database = await client.CreateDatabaseIfNotExistsAsync(databaseName);
                await database.Database.CreateContainerIfNotExistsAsync(containerName, "/id");
            };
            initializeDatabase += handler; //subscribe
            initializeDatabase(null, EventArgs.Empty); //raise the event to initialize db

            return cosmosDbService;
        };
        services.AddSingleton<ICosmosDbService>(factory);
        return service;
    }
}

Beachten Sie den Ansatz, mit dem die Verwendung async voidin einem nicht asynchronen Ereignishandler umgangen werden muss.

Referenz Async / Await - Best Practices in der asynchronen Programmierung .

So kann nun das Configurerichtig aufgerufen werden.

public class Startup : FunctionsStartup {

    public override void Configure(IFunctionsHostBuilder builder) =>
        builder.Services
            .AddHttpClient()
            .AddCosmosDbService();
}
Nkosi
quelle
4

Hier ist ein Beispiel, das ich aufpeppen konnte; Es stellt eine Verbindung zur Azure App-Konfiguration für die zentralisierte Konfiguration und Funktionsverwaltung her. Man sollte in der Lage sein, alle DI-Funktionen wie IConfigurationund IOptions<T>wie in einem ASP.NET Core-Controller zu verwenden.

NuGet-Abhängigkeiten

  • Install-Package Microsoft.Azure.Functions.Extensions
  • Install-Package Microsoft.Extensions.Configuration.AzureAppConfiguration

Startup.cs

[assembly: FunctionsStartup(typeof(Startup))]

public class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder hostBuilder) {
        var serviceProvider = hostBuilder.Services.BuildServiceProvider();
        var configurationRoot = serviceProvider.GetService<IConfiguration>();
        var configurationBuilder = new ConfigurationBuilder();
        var appConfigEndpoint = configuration["AppConfigEndpoint"];

        if (configurationRoot is IConfigurationRoot) {
            configurationBuilder.AddConfiguration(configurationRoot);
        }

        if (!string.IsNullOrEmpty(appConfigEndpoint)) {
            configurationBuilder.AddAzureAppConfiguration(appConfigOptions => {
                // possible to run this locally if refactored to use ClientSecretCredential or DefaultAzureCredential
                appConfigOptions.Connect(new Uri(appConfigEndpoint), new ManagedIdentityCredential());
            });
        }

        var configuration = configurationBuilder.Build();

        hostBuilder.Services.Replace(ServiceDescriptor.Singleton(typeof(IConfiguration), configuration));

        // Do more stuff with Configuration here...
    }
}

public sealed class HelloFunction
{
    private IConfiguration Configuration { get; }

    public HelloFunction(IConfiguration configuration) {
        Configuration = configuration;
    }

    [FunctionName("HelloFunction")]
    public void Run([TimerTrigger("0 */1 * * * *")]TimerInfo myTimer, ILogger log) {
        log.LogInformation($"Timer Trigger Fired: 'Hello {Configuration["Message"]}!'");
    }
}
Kittoes0124
quelle
Bei diesem Ansatz habe ich ein Problem, dass host.jsoninsbesondere keine Parameter verwendet werdenroutePrefix
Andrii vor
1
@Andrii Interessant, ich muss etwas recherchieren und werde meinen Beitrag bearbeiten, wenn eine Lösung gefunden wird. Vielen Dank für die Heads-up!
Kittoes0124 vor