使用选项模式在测试中将应用配置替换为实例

Replace app config with instance in tests using Options pattern

提问人:mu88 提问时间:11/16/2023 更新时间:11/21/2023 访问量:30

问:

我使用 C# 选项模式,例如,配置类型如下:

public class MySettings
{
    public bool IsEnabled { get; set; }

    public int NumberOfDays { get; set; }
}

在我的测试中,我想配置 ASP。NET 的 DI 容器,以便它始终提供相同的实例 ,例如像这样(它不编译,因为它不接受 的实例):MySettingsConfigure<T>T

public class CustomWebApplicationFactory : WebApplicationFactory<Startup>
{
    private readonly MySettings settings;

    public CustomWebApplicationFactory(MySettings settings)
        : base()
     => _settings = settings;

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        base.ConfigureWebHost(builder);

        builder.ConfigureServices(services => services.Configure<MySettings>(_settings));
    }
}

目前,我正在通过指定如下所示的配置 lambda 来解决此问题:

public class CustomWebApplicationFactory : WebApplicationFactory<Startup>
{
    private readonly Action<MySettings> _configureSettings;

    public CustomWebApplicationFactory(Action<MySettings> configureSettings)
        : base()
     => _configureSettings = configureSettings;

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        base.ConfigureWebHost(builder);

        builder.ConfigureServices(services => services.Configure<MySettings>(_configureSettings));
    }
}

如果我可以传递 的实例,它将使测试代码更具可读性。MySettings

感谢!

C# ASP.NET-CORE 测试 依赖项注入

评论

0赞 Johnathan Barclay 11/16/2023
services.Configure<MySettings>(_ => _settings)
0赞 Emre Bener 11/16/2023
我建议看看这篇文章 andrewlock.net/......

答:

1赞 Prolog 11/19/2023 #1

对于一个非常简单的问题来说,这个答案可能有点矫枉过正,但让我们继续吧!

为了强制 ASP.NET Core 内置的 DI 容器始终返回您提供的相同实例,我们可以在现有系统之上添加另一层。通常,选项是使用创建的,因此可以通过多个配置回调。让我们重用 available 并覆盖它的方法,这样我们就可以拦截源自属性 getter 的调用,并将它们引导到替代路径(如果我们决定这样做的话)。IOptions<T>IOptions<T>IOptionsFactoryOptionsManager<T>Get()Value

public class CachedOptionsManager<TOptions> : OptionsManager<TOptions>, IOptions<TOptions>
    where TOptions : class
{
    private readonly OptionsInMemoryStore _optionsInMemoryStore;

    public CachedOptionsManager(
        OptionsInMemoryStore optionsInMemoryStore,
        IOptionsFactory<TOptions> optionsFactory)
        : base(optionsFactory)
    {
        _optionsInMemoryStore = optionsInMemoryStore;
    }

    public override TOptions Get(string name)
    {
        if (_optionsInMemoryStore.TryGet<TOptions>(name, out var options))
        {
            return options;
        }
        else
        {
            return base.Get(name);
        }
    }
}

随着调用被拦截,我们需要从某个地方存储和获取选项实例。下面是选项的内存存储。请注意它是如何从 DI 提供的实例创建的。Get()OptionsInstance

public class OptionsInMemoryStore
{
    private readonly Dictionary<Type, Dictionary<string, object>> _options;

    public OptionsInMemoryStore(IEnumerable<OptionsInstance> optionsInstances)
    {
        var values = optionsInstances
            .GroupBy(x => x.Instance.GetType())
            .ToDictionary(g => g.Key, g => g.ToDictionary(x => x.Name ?? Options.DefaultName, x => x.Instance));

        _options = new Dictionary<Type, Dictionary<string, object>>(values);
    }

    public bool TryGet<T>(string name, out T options)
        where T : class
    {
        options = default;

        if (!_options.TryGetValue(typeof(T), out var optionsInstances))
        {
            return false;
        }

        if (!optionsInstances.TryGetValue(name, out var instance))
        {
            return false;
        }

        options = (T)instance;

        return true;
    }
}

选项的简单包装器,即:OptionsInstance

public class OptionsInstance
{
    public OptionsInstance(object instance)
    {
        Instance = instance;
    }

    public OptionsInstance(string name, object instance)
    {
        Instance = instance;
        Name = name;
    }

    public object Instance { get; }

    public string Name { get; }
}

但我们仍然需要注册这些商店和其他商店。在这里,扩展方法进来了。将启用单实例选项的功能。它还添加了选项的存储,并用我们的自定义替换了默认实现。相同。如果需要,下面是具有命名选项重载的扩展方法。OptionsInstancesAddOptionsSingleInstance()IOptions<T>CachedOptionsManager<T>IOptionsSnapshot<T>

public static class OptionsServiceCollectionExtensions
{
    public static IServiceCollection AddOptionsSingleInstance(this IServiceCollection services)
    {
        services.TryAddSingleton<OptionsInMemoryStore>();
        services.AddSingleton(typeof(IOptions<>), typeof(CachedOptionsManager<>));
        services.AddScoped(typeof(IOptionsSnapshot<>), typeof(CachedOptionsManager<>));

        return services;
    }

    public static IServiceCollection Configure<T>(this IServiceCollection services, T instance)
        where T : class
    {
        return services.AddSingleton(new OptionsInstance(instance));
    }

    public static IServiceCollection Configure<T>(this IServiceCollection services, string name, T instance)
        where T : class
    {
        return services.AddSingleton(new OptionsInstance(name, instance));
    }
}

用法展示:

var williamSettings = new MySettings
{
    NumberOfDays = 22,
};

var services = new ServiceCollection();
services.AddOptions<MySettings>().Configure(x => x.NumberOfDays = 101);
services.AddOptions<MySettings>("Jean settings").Configure(x => x.NumberOfDays = 303);
services.AddOptionsSingleInstance();
services.Configure(williamSettings);

using var serviceProvider = services.BuildServiceProvider();
using var scope = serviceProvider.CreateScope();

var options1 = scope.ServiceProvider.GetRequiredService<IOptions<MySettings>>();
var options2 = scope.ServiceProvider.GetRequiredService<IOptionsSnapshot<MySettings>>();
var options1Name = options1.Value.NumberOfDays;
var options2Name = options2.Get("Jean settings").NumberOfDays;

Debug.Assert(options1Name.Equals(22));
Debug.Assert(options2Name.Equals(303));

就您而言,它只是:WebApplicationFactory

protected override void ConfigureWebHost(IWebHostBuilder builder)
{
    base.ConfigureWebHost(builder);

    builder.ConfigureServices(services => 
    {
        services.AddOptionsSingleInstance();
        services.Configure<MySettings>(_settings);
    });
}

这将在解析或 时起作用。高于它的任何内容,例如从工厂手动创建选项或使用,都需要额外的代码。IOptions<T>IOptionsSnapshot<T>IOptionsMonitor<T>