Pokyny k použití vzoru "Options pattern" pro autory knihoven .NET

Díky injektáži závislostí může registrace služeb a jejich odpovídající konfigurace využít vzor možností. Vzor možností umožňuje uživatelům vaší knihovny (a vašim službám) vyžadovat instance rozhraní možností, kde TOptions je třída možností. Využívání možností konfigurace prostřednictvím objektů silného typu pomáhá zajistit konzistentní reprezentaci hodnot, umožňuje ověřování pomocí datových poznámek a odstraňuje zátěž ruční analýzy řetězcových hodnot. Existuje mnoho poskytovatelů konfigurace pro uživatele vaší knihovny. S těmito poskytovateli můžou uživatelé konfigurovat vaši knihovnu mnoha způsoby.

Jako autor knihovny .NET se dozvíte obecné pokyny, jak správně vystavit vzor možností uživatelům knihovny. Existují různé způsoby, jak dosáhnout stejné věci, a několik aspektů, které je potřeba vzít v úvahu.

Názvoslovné konvence

Podle konvence jsou metody rozšíření zodpovědné za registraci služeb pojmenovány Add{Service}, kde {Service} je smysluplný a popisný název. Add{Service} Metody rozšíření jsou v ASP.NET Core a .NET běžné.

✔️ Zvažte názvy, které odliší vaši službu od jiných nabídek.

❌ NEPOUŽÍVEJTE názvy, které jsou již součástí ekosystému .NET z oficiálních balíčků Microsoftu.

✔️ ZVAŽTE pojmenování statických tříd, které zpřístupňují rozšiřující metody jako {Type}Extensions, kde {Type} je typ, který rozšiřujete.

Pokyny k názvovému prostoru

Balíčky Microsoftu využívají Microsoft.Extensions.DependencyInjection obor názvů ke sjednocení registrace různých nabídek služeb.

✔️ ZVAŽTE obor názvů, který jasně identifikuje vaši nabídku balíčků.

❌ NEPOUŽÍVEJTE Microsoft.Extensions.DependencyInjection obor názvů pro jiné než oficiální balíčky Společnosti Microsoft.

Bez parametrů

Pokud vaše služba může pracovat s minimální nebo žádnou explicitní konfigurací, zvažte metodu rozšíření bez parametrů.

using Microsoft.Extensions.DependencyInjection;

namespace ExampleLibrary.Extensions.DependencyInjection;

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddMyLibraryService(
        this IServiceCollection services)
    {
        services.AddOptions<LibraryOptions>()
            .Configure(options =>
            {
                // Specify default option values
            });

        // Register lib services here...
        // services.AddScoped<ILibraryService, DefaultLibraryService>();

        return services;
    }
}

V předchozím kódu je AddMyLibraryService

IConfiguration parametr

Když vytvoříte knihovnu, která uživatelům zpřístupňuje mnoho možností, můžete zvážit vyžadování metody rozšíření parametru IConfiguration . Očekávaná IConfiguration instance by měla být vymezena na pojmenovanou část konfigurace pomocí IConfiguration.GetSection funkce.

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace ExampleLibrary.Extensions.DependencyInjection;

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddMyLibraryService(
      this IServiceCollection services,
      IConfiguration namedConfigurationSection)
    {
        // Default library options are overridden
        // by bound configuration values.
        services.Configure<LibraryOptions>(namedConfigurationSection);

        // Register lib services here...
        // services.AddScoped<ILibraryService, DefaultLibraryService>();

        return services;
    }
}

V předchozím kódu je AddMyLibraryService

Spotřebitelé v tomto vzoru poskytují omezenou IConfiguration instanci pojmenované části:

using ExampleLibrary.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddMyLibraryService(
    builder.Configuration.GetSection("LibraryOptions"));

using IHost host = builder.Build();

// Application code should start here.

await host.RunAsync();

Volání .AddMyLibraryService se provede u IServiceCollection typu.

Záleží na vás jako autorovi knihovny, zda zadáte výchozí hodnoty.

Poznámka:

Konfiguraci je možné svázat s instancí možností. Existuje však riziko kolizí názvů , což způsobí chyby. Navíc když tímto způsobem ručně vytvoříte vazbu, omezíte spotřebu vzorů možností na čtení jednou. Změny nastavení nebudou znovu vázány, protože uživatelé nebudou moci používat rozhraní IOptionsMonitor .

services.AddOptions<LibraryOptions>()
    .Configure<IConfiguration>(
        (options, configuration) =>
            configuration.GetSection("LibraryOptions").Bind(options));

Místo toho byste měli použít rozšiřující metodu BindConfiguration. Tato metoda rozšíření sváže konfiguraci s instancí možností a také zaregistruje zdroj tokenu změn pro oddíl konfigurace. To umožňuje uživatelům používat rozhraní IOptionsMonitor .

Konfigurační oddíl s parametrem cesty

Příjemci vaší knihovny mohou chtít zadat cestu ke konfiguračnímu oddílu pro vytvoření vazby souvisejícího TOptions typu. V tomto scénáři definujete string parametr v metodě rozšíření.

using Microsoft.Extensions.DependencyInjection;

namespace ExampleLibrary.Extensions.DependencyInjection;

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddMyLibraryService(
      this IServiceCollection services,
      string configSectionPath)
    {
        services.AddOptions<SupportOptions>()
            .BindConfiguration(configSectionPath)
            .ValidateDataAnnotations()
            .ValidateOnStart();

        // Register lib services here...
        // services.AddScoped<ILibraryService, DefaultLibraryService>();

        return services;
    }
}

V předchozím kódu je AddMyLibraryService

V dalším příkladu se k povolení ověření datových poznámek použije balíček NuGet Microsoft.Extensions.Options.DataAnnotations . Třída SupportOptions je definována takto:

using System.ComponentModel.DataAnnotations;

public sealed class SupportOptions
{
    [Url]
    public string? Url { get; set; }

    [Required, EmailAddress]
    public required string Email { get; set; }

    [Required, DataType(DataType.PhoneNumber)]
    public required string PhoneNumber { get; set; }
}

Představte si, že se používá následující soubor JSON appsettings.json :

{
    "Support": {
        "Url": "https://support.example.com",
        "Email": "help@support.example.com",
        "PhoneNumber": "+1(888)-SUPPORT"
    }
}

Action<TOptions> parametr

Spotřebitelé vaší knihovny mohou mít zájem o poskytnutí výrazu lambda, který poskytuje instanci vaší třídy možností. V tomto scénáři definujete Action<LibraryOptions> parametr v metodě rozšíření.

using Microsoft.Extensions.DependencyInjection;

namespace ExampleLibrary.Extensions.DependencyInjection;

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddMyLibraryService(
        this IServiceCollection services,
        Action<LibraryOptions> configureOptions)
    {
        services.Configure(configureOptions);

        // Register lib services here...
        // services.AddScoped<ILibraryService, DefaultLibraryService>();

        return services;
    }
}

V předchozím kódu je AddMyLibraryService

Příjemci v tomto vzoru poskytují výraz lambda (nebo delegát, který tento Action<LibraryOptions> parametr splňuje):

using ExampleLibrary.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddMyLibraryService(options =>
{
    // User defined option values
    // options.SomePropertyValue = ...
});
                                                                        
using IHost host = builder.Build();

// Application code should start here.

await host.RunAsync();

Parametr instance možnosti

Uživatelé vaší knihovny mohou raději poskytnout přímo vloženou instanci parametrů. V tomto scénáři zveřejníte rozšiřující metodu, která přebírá instanci objektu options , LibraryOptions.

using Microsoft.Extensions.DependencyInjection;

namespace ExampleLibrary.Extensions.DependencyInjection;

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddMyLibraryService(
      this IServiceCollection services,
      LibraryOptions userOptions)
    {
        services.AddOptions<LibraryOptions>()
            .Configure(options =>
            {
                // Overwrite default option values
                // with the user provided options.
                // options.SomeValue = userOptions.SomeValue;
            });

        // Register lib services here...
        // services.AddScoped<ILibraryService, DefaultLibraryService>();

        return services;
    }
}

V předchozím kódu je AddMyLibraryService

Uživatelé v tomto vzoru poskytují instanci třídy LibraryOptions, definující požadované hodnoty vlastností přímo:

using ExampleLibrary.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddMyLibraryService(new LibraryOptions
{
    // Specify option values
    // SomePropertyValue = ...
});

using IHost host = builder.Build();

// Application code should start here.

await host.RunAsync();

Po konfiguraci

Po vázání nebo zadání všech hodnot možností konfigurace je k dispozici funkce po konfiguraci. Když odkryjete stejný Action<TOptions> parametr, který je podrobně popsán dříve, můžete se rozhodnout použít PostConfigure. Po dokončení konfigurace se spustí po všech .Configure voláních. Existuje několik důvodů, proč byste měli zvážit použití PostConfigure:

  • Pořadí provádění: Můžete přepsat všechny hodnoty konfigurace, které byly nastaveny ve .Configure voláních.
  • Ověření: Po použití všech ostatních konfigurací můžete ověřit, jestli jsou nastavené výchozí hodnoty.
using Microsoft.Extensions.DependencyInjection;

namespace ExampleLibrary.Extensions.DependencyInjection;

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddMyLibraryService(
      this IServiceCollection services,
      Action<LibraryOptions> configureOptions)
    {
        services.PostConfigure(configureOptions);

        // Register lib services here...
        // services.AddScoped<ILibraryService, DefaultLibraryService>();

        return services;
    }
}

V předchozím kódu je AddMyLibraryService

Spotřebitelé v tomto vzoru poskytují výraz lambda (nebo delegát, který splňuje tento parametr Action<LibraryOptions>), stejně jako u parametru Action<TOptions> ve scénáři konfigurace, která není post konfigurací.

using ExampleLibrary.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddMyLibraryService(options =>
{
    // Specify option values
    // options.SomePropertyValue = ...
});

using IHost host = builder.Build();

// Application code should start here.

await host.RunAsync();

Viz také