Конфигурация .NET Core позволяет добавлять множество параметров для добавления значений (переменные среды, файлы json, аргументы командной строки).
Я просто не могу понять и найти ответ, как заполнить его с помощью кода.
Я пишу модульные тесты для методов расширения конфигураций, и я думал, что заполнение их в модульных тестах с помощью кода будет проще, чем загрузка выделенных файлов json для каждого теста.
Мой текущий код:
[Fact]
public void Test_IsConfigured_Positive()
{
// test against this configuration
IConfiguration config = new ConfigurationBuilder()
// how to populate it via code
.Build();
// the extension method to test
Assert.True(config.IsConfigured());
}
Обновлять:
Одним из особых случаев является «пустой раздел», который будет выглядеть так в json.
{
"MySection": {
// the existence of the section activates something triggering IsConfigured to be true but does not overwrite any default value
}
}
Обновление 2:
Как отметил Мэтью в комментариях, наличие пустого раздела в json дает тот же результат, что и отсутствие раздела вообще. Я привел пример, и да, это так. Я ошибся, ожидая другого поведения.
Итак, что мне делать и чего я ожидал:
Я пишу модульные тесты для 2 методов расширения для IConfiguration (собственно потому, что привязка значений в методе Get...Settings по какой-то причине не работает (но это другая тема). Они выглядят так:
public static bool IsService1Configured(this IConfiguration configuration)
{
return configuration.GetSection("Service1").Exists();
}
public static MyService1Settings GetService1Settings(this IConfiguration configuration)
{
if (!configuration.IsService1Configured()) return null;
MyService1Settings settings = new MyService1Settings();
configuration.Bind("Service1", settings);
return settings;
}
Мое неправильное понимание заключалось в том, что если я помещу пустой раздел в настройки приложения, метод IsService1Configured() вернет true (что сейчас явно неправильно). Разница, которую я ожидал, заключается в наличии пустого раздела, теперь метод GetService1Settings() возвращает null, а не то, что я ожидал, MyService1Settings со всеми значениями по умолчанию.
К счастью, это все еще работает для меня, так как у меня не будет пустых разделов (или теперь я знаю, что мне нужно избегать таких случаев). Это был всего лишь один теоретический случай, с которым я столкнулся при написании модульных тестов.
Далее по дороге (для интересующихся).
Для чего я его использую? Активация/деактивация услуги на основе конфигурации.
У меня есть приложение, в котором есть сервис/некоторые сервисы, скомпилированные в него. В зависимости от развертывания мне нужно полностью активировать/деактивировать службы. Это связано с тем, что некоторые (локальные или тестовые установки) не имеют полного доступа ко всей инфраструктуре (вспомогательные службы, такие как кэширование, метрики...). И я делаю это через настройки приложений. Если сервис настроен (раздел config существует), он будет добавлен. Если раздел конфигурации отсутствует, он не будет использоваться.
Полный код дистиллированного примера приведен ниже.
Service1 и Service2using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System;
namespace WebApplication1
{
public class MyService1Settings
{
public int? Value1 { get; set; }
public int Value2 { get; set; }
public int Value3 { get; set; } = -1;
}
public static class Service1Extensions
{
public static bool IsService1Configured(this IConfiguration configuration)
{
return configuration.GetSection("Service1").Exists();
}
public static MyService1Settings GetService1Settings(this IConfiguration configuration)
{
if (!configuration.IsService1Configured()) return null;
MyService1Settings settings = new MyService1Settings();
configuration.Bind("Service1", settings);
return settings;
}
public static IServiceCollection AddService1(this IServiceCollection services, IConfiguration configuration, ILogger logger)
{
MyService1Settings settings = configuration.GetService1Settings();
if (settings == null) throw new Exception("loaded MyService1Settings are null (did you forget to check IsConfigured in Startup.ConfigureServices?) ");
logger.LogAsJson(settings, "MyServiceSettings1: ");
// do what ever needs to be done
return services;
}
public static IApplicationBuilder UseService1(this IApplicationBuilder app, IConfiguration configuration, ILogger logger)
{
// do what ever needs to be done
return app;
}
}
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.ConfigureLogging
(
builder =>
{
builder.AddDebug();
builder.AddConsole();
}
)
.UseStartup<Startup>();
}
public class Startup
{
public IConfiguration Configuration { get; }
public ILogger<Startup> Logger { get; }
public Startup(IConfiguration configuration, ILoggerFactory loggerFactory)
{
Configuration = configuration;
Logger = loggerFactory.CreateLogger<Startup>();
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// flavour 1: needs check(s) in Startup method(s) or will raise an exception
if (Configuration.IsService1Configured()) {
Logger.LogInformation("service 1 is activated and added");
services.AddService1(Configuration, Logger);
} else
Logger.LogInformation("service 1 is deactivated and not added");
// flavour 2: checks are done in the extension methods and no Startup cluttering
services.AddOptionalService2(Configuration, Logger);
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment()) app.UseDeveloperExceptionPage();
// flavour 1: needs check(s) in Startup method(s) or will raise an exception
if (Configuration.IsService1Configured()) {
Logger.LogInformation("service 1 is activated and used");
app.UseService1(Configuration, Logger); }
else
Logger.LogInformation("service 1 is deactivated and not used");
// flavour 2: checks are done in the extension methods and no Startup cluttering
app.UseOptionalService2(Configuration, Logger);
app.UseMvc();
}
}
public class MyService2Settings
{
public int? Value1 { get; set; }
public int Value2 { get; set; }
public int Value3 { get; set; } = -1;
}
public static class Service2Extensions
{
public static bool IsService2Configured(this IConfiguration configuration)
{
return configuration.GetSection("Service2").Exists();
}
public static MyService2Settings GetService2Settings(this IConfiguration configuration)
{
if (!configuration.IsService2Configured()) return null;
MyService2Settings settings = new MyService2Settings();
configuration.Bind("Service2", settings);
return settings;
}
public static IServiceCollection AddOptionalService2(this IServiceCollection services, IConfiguration configuration, ILogger logger)
{
if (!configuration.IsService2Configured())
{
logger.LogInformation("service 2 is deactivated and not added");
return services;
}
logger.LogInformation("service 2 is activated and added");
MyService2Settings settings = configuration.GetService2Settings();
if (settings == null) throw new Exception("some settings loading bug occured");
logger.LogAsJson(settings, "MyService2Settings: ");
// do what ever needs to be done
return services;
}
public static IApplicationBuilder UseOptionalService2(this IApplicationBuilder app, IConfiguration configuration, ILogger logger)
{
if (!configuration.IsService2Configured())
{
logger.LogInformation("service 2 is deactivated and not used");
return app;
}
logger.LogInformation("service 2 is activated and used");
// do what ever needs to be done
return app;
}
}
public static class LoggerExtensions
{
public static void LogAsJson(this ILogger logger, object obj, string prefix = null)
{
logger.LogInformation(prefix ?? string.Empty) + ((obj == null) ? "null" : JsonConvert.SerializeObject(obj, Formatting.Indented)));
}
}
}





Поможет ли метод расширения AddInMemoryCollection?
Вы можете передать ему коллекцию ключ-значение:
IEnumerable<KeyValuePair<String,String>> с данными, которые могут понадобиться для теста.
var builder = new ConfigurationBuilder();
builder.AddInMemoryCollection(new Dictionary<string, string>
{
{ "key", "value" }
});
Вы можете использовать MemoryConfigurationBuilderExtensions, чтобы предоставить его через словарь.
using Microsoft.Extensions.Configuration;
var myConfiguration = new Dictionary<string, string>
{
{"Key1", "Value1"},
{"Nested:Key1", "NestedValue1"},
{"Nested:Key2", "NestedValue2"}
};
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(myConfiguration)
.Build();
Эквивалентным JSON будет:
{
"Key1": "Value1",
"Nested": {
"Key1": "NestedValue1",
"Key2": "NestedValue2"
}
}
Эквивалентные переменные среды будут (при условии отсутствия префикса/нечувствительного к регистру):
Key1=Value1
Nested__Key1=NestedValue1
Nested__Key2=NestedValue2
Эквивалентные параметры командной строки:
dotnet <myapp.dll> -- --Key1=Value1 --Nested:Key1=NestedValue1 --Nested:Key2=NestedValue2
Да, это сработает. Я обновил свой вопрос, чтобы отразить недостающую часть.
Вы должны обновить свой вопрос, чтобы включить то, что вы ожидаете. Наличие пустого узла JSON приводит к тому же результату, что и отсутствие этого узла вообще.
действительно ты был прав. Пустой раздел вроде бы удален и не существует. Я добавил обновление 2 к своему вопросу с полным примером того, что я (ошибочно) ожидал и почему.
ну, модульный тест показал, что привязка конфигурации не удалась, потому что я только что определил получение для свойства, но не для набора.
@Matthew добавление json в ваш пример было бы полезно
Решение, которое я выбрал (которое, по крайней мере, отвечает на заголовок вопроса!), Это использовать файл настроек в решении testsettings.json и установить для него значение «Копировать всегда».
private IConfiguration _config;
public UnitTestManager()
{
IServiceCollection services = new ServiceCollection();
services.AddSingleton<IConfiguration>(Configuration);
}
public IConfiguration Configuration
{
get
{
if (_config == null)
{
var builder = new ConfigurationBuilder().AddJsonFile($"testsettings.json", optional: false);
_config = builder.Build();
}
return _config;
}
}
Привет, ребята, AddJsonFile кажется немного измененным на стороне источника .net 5.0: https://docs.microsoft.com/tr-tr/dotnet/api/microsoft.extensions.configuration.jsonconfigurationextensions.addjsonfile? view = dotnet-plat-ext-5.0 # Microsoft_Extensions_Configuration_JsonConfigurationExtensions_AddJsonFile_Microonsoft
Я пытаюсь использовать это с .net 6.0 в VS 2022, но получаю сообщение об ошибке ConfigurationBuilder does not contain AddJsonFile
Вы можете использовать следующую технику, чтобы имитировать метод расширения IConfiguration.GetValue<T>(key).
var configuration = new Mock<IConfiguration>();
var configSection = new Mock<IConfigurationSection>();
configSection.Setup(x => x.Value).Returns("fake value");
configuration.Setup(x => x.GetSection("MySection")).Returns(configSection.Object);
//OR
configuration.Setup(x => x.GetSection("MySection:Value")).Returns(configSection.Object);
Этот ответ больше относится к этому (закрытому) вопросу: stackoverflow.com/questions/43618686/… Но я не смог найти ничего другого, связанного с этой темой, с работающим подходом Moq. Многие люди спрашивали об этом, но единственные ответы, которые вы можете найти, связаны с ConfigurationBuilder. Если бы вышеуказанный вопрос был открытым, я бы разместил его там.
Я предпочитаю, чтобы мои классы приложений не зависели от IConfiguration. Вместо этого я создаю класс конфигурации для хранения конфигурации с конструктором, который может инициализировать его из IConfiguration, например так:
public class WidgetProcessorConfig
{
public int QueueLength { get; set; }
public WidgetProcessorConfig(IConfiguration configuration)
{
configuration.Bind("WidgetProcessor", this);
}
public WidgetProcessorConfig() { }
}
то в вашем ConfigureServices вам просто нужно сделать:
services.AddSingleton<WidgetProcessorConfig>();
services.AddSingleton<WidgetProcessor>();
и для тестирования:
var config = new WidgetProcessorConfig
{
QueueLength = 18
};
var widgetProcessor = new WidgetProcessor(config);
наверняка это плохая практика, когда классы приложений зависят от IConfiguration. Но когда конфигурация становится сложной, простого связывания недостаточно. Я реализовал систему проверки для каждого класса конфигурации, чтобы при запуске возникали ошибки. И этот код нужно было протестировать. :-)
Да, это сработает. Я обновил свой вопрос, чтобы отразить недостающую часть.