提问人:mu88 提问时间:11/16/2023 更新时间:11/21/2023 访问量:30
使用选项模式在测试中将应用配置替换为实例
Replace app config with instance in tests using Options pattern
问:
我使用 C# 选项模式,例如,配置类型如下:
public class MySettings
{
public bool IsEnabled { get; set; }
public int NumberOfDays { get; set; }
}
在我的测试中,我想配置 ASP。NET 的 DI 容器,以便它始终提供相同的实例 ,例如像这样(它不编译,因为它不接受 的实例):MySettings
Configure<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
感谢!
答:
对于一个非常简单的问题来说,这个答案可能有点矫枉过正,但让我们继续吧!
为了强制 ASP.NET Core 内置的 DI 容器始终返回您提供的相同实例,我们可以在现有系统之上添加另一层。通常,选项是使用创建的,因此可以通过多个配置回调。让我们重用 available 并覆盖它的方法,这样我们就可以拦截源自属性 getter 的调用,并将它们引导到替代路径(如果我们决定这样做的话)。IOptions<T>
IOptions<T>
IOptionsFactory
OptionsManager<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; }
}
但我们仍然需要注册这些商店和其他商店。在这里,扩展方法进来了。将启用单实例选项的功能。它还添加了选项的存储,并用我们的自定义替换了默认实现。相同。如果需要,下面是具有命名选项重载的扩展方法。OptionsInstances
AddOptionsSingleInstance()
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>
评论
services.Configure<MySettings>(_ => _settings)