Recomendamos que você se familiarize com o material: Metanit - Configuração , Como funciona a configuração no .NET Core .
Formulação do problema
Você precisa implementar um aplicativo ASP NET Core com a capacidade de atualizar a configuração no formato JSON em tempo de execução. Durante uma atualização de configuração, as sessões atualmente em execução devem continuar a funcionar com as opções de configuração anteriores. Após atualizar a configuração, os objetos usados devem ser atualizados / substituídos por novos.
A configuração deve ser desserializada, não deve haver acesso direto aos objetos IConfiguration dos controladores. Os valores lidos devem ser verificados quanto à exatidão; se estiverem ausentes, devem ser substituídos pelos valores padrão. A implementação deve funcionar em um contêiner Docker.
Trabalho de configuração clássico
GitHub: ConfigurationTemplate_1
O projeto é baseado no modelo ASP NET Core MVC. O provedor de configuração JsonConfigurationProvider é usado para trabalhar com arquivos de configuração JSON . Para adicionar a capacidade de recarregar a configuração do aplicativo durante a operação, adicione o parâmetro: "reloadOnChange: true".
No arquivo Startup.cs, substitua:
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
Em
public Startup(IConfiguration configuration)
{
var builder = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
configuration = builder.Build();
Configuration = configuration;
}
.AddJsonFile - adiciona um arquivo JSON, reloadOnChange: true indica que quando os parâmetros do arquivo de configuração são alterados, eles serão recarregados sem a necessidade de recarregar o aplicativo.
Conteúdo do arquivo appsettings.json :
{
"AppSettings": {
"Parameter1": "Parameter1 ABC",
"Parameter2": "Parameter2 ABC"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
Os controladores de aplicativos usarão o serviço: ServiceABC em vez de acessar diretamente a configuração. ServiceABC é uma classe que obtém valores iniciais do arquivo de configuração. Neste exemplo, a classe ServiceABC contém apenas uma propriedade Title . Conteúdo do
arquivo ServiceABC.cs :
public class ServiceABC
{
public string Title;
public ServiceABC(string title)
{
Title = title;
}
public ServiceABC()
{ }
}
Para usar o ServiceABC, você precisa adicioná-lo como um serviço de middleware ao seu aplicativo. Adicione o serviço como AddTransient, que é criado toda vez que você acessa, usando a expressão:
services.AddTransient<IYourService>(o => new YourService(param));
Ótimo para serviços leves que não consomem memória ou recursos. A leitura dos parâmetros de configuração em Startup.cs é realizada por meio de IConfiguration , que utiliza uma string de consulta que indica o caminho completo da localização do valor, por exemplo: AppSettings: Parameter1.
No arquivo Startup.cs, adicione:
public void ConfigureServices(IServiceCollection services)
{
// "Parameter1" ServiceABC
var settingsParameter1 = Configuration["AppSettings:Parameter1"];
// "Parameter1"
services.AddScoped(s=> new ServiceABC(settingsParameter1));
//next
services.AddControllersWithViews();
}
Um exemplo de uso do serviço ServiceABC em um controlador, o valor Parameter1 será exibido na página html.
Para usar o serviço em controladores, adicione-o ao construtor, arquivo HomeController.cs
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly ServiceABC _serviceABC;
public HomeController(ILogger<HomeController> logger, ServiceABC serviceABC)
{
_logger = logger;
_serviceABC = serviceABC;
}
public IActionResult Index()
{
return View(_serviceABC);
}
Adicionar arquivo ServiceABC de visibilidade de serviço _ViewImports.cshtml
@using ConfigurationTemplate_1.Services
Vamos modificar Index.cshtml para exibir o parâmetro Parameter1 na página.
@model ServiceABC
@{
ViewData["Title"] = "Home Page";
}
<div class="text-center">
<h1> ASP.NET Core</h1>
<h4> </h4>
</div>
<div>
<p> ServiceABC,
Parameter1 = @Model.Title</p>
</div>
Vamos iniciar o aplicativo:

Resultado
Essa abordagem resolve parcialmente o problema. Esta solução não permite aplicar alterações de configuração durante a execução do aplicativo. o serviço recebe o valor do arquivo de configuração apenas na inicialização e, a seguir, funciona apenas com esta instância. Como resultado, as alterações subsequentes no arquivo de configuração não resultarão em alterações no aplicativo.
Usando IConfiguration como Singleton
GitHub: ConfigurationTemplate_2 A
segunda opção é colocar o IConfiguration (como um Singleton) nos serviços. Como resultado, IConfiguration pode ser chamado de controladores e outros serviços. Ao usar AddSingleton, o serviço é criado uma vez e, ao usar o aplicativo, a chamada vai para a mesma instância. Use este método com extremo cuidado, pois podem ocorrer vazamentos de memória e problemas de multithreading.
Vamos substituir o código do exemplo anterior em Startup.cs por um novo, onde
services.AddSingleton<IConfiguration>(Configuration);
adiciona IConfiguration como Singleton aos serviços.
public void ConfigureServices(IServiceCollection services)
{
// IConfiguration
services.AddSingleton<IConfiguration>(Configuration);
// "ServiceABC"
services.AddScoped<ServiceABC>();
//next
services.AddControllersWithViews();
}
Altere o construtor do serviço ServiceABC para aceitar o IConfiguration
public class ServiceABC
{
private readonly IConfiguration _configuration;
public string Title => _configuration["AppSettings:Parameter1"];
public ServiceABC(IConfiguration Configuration)
{
_configuration = Configuration;
}
public ServiceABC()
{ }
}
Como na versão anterior, adicione o serviço ao construtor e adicione um link para o namespace
, HomeController.cs
ServiceABC _ViewImports.cshtml:
Index.cshtml Parameter1 .
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly ServiceABC _serviceABC;
public HomeController(ILogger<HomeController> logger, ServiceABC serviceABC)
{
_logger = logger;
_serviceABC = serviceABC;
}
public IActionResult Index()
{
return View(_serviceABC);
}
ServiceABC _ViewImports.cshtml:
@using ConfigurationTemplate_2.Services;
Index.cshtml Parameter1 .
@model ServiceABC
@{
ViewData["Title"] = "Home Page";
}
<div class="text-center">
<h1> ASP.NET Core</h1>
<h4> IConfiguration Singleton</h4>
</div>
<div>
<p>
ServiceABC,
Parameter1 = @Model.Title
</p>
</div>
Vamos iniciar o aplicativo:

O serviço ServiceABC adicionado ao contêiner usando AddScoped significa que uma instância da classe será criada em cada solicitação de página. Como resultado, uma instância da classe ServiceABC será criada em cada solicitação http junto com o recarregamento da configuração IConfiguration e novas alterações em appsettings.json serão aplicadas.
Assim, se durante o funcionamento do aplicativo, altere o parâmetro Parameter1 para “NOVO !!! Parameter1 ABC ”, na próxima vez que você acessar a página inicial, o novo valor do parâmetro será exibido.
Vamos atualizar a página depois de alterar o arquivo appsettings.json :

Resultado
A desvantagem dessa abordagem é a leitura manual de cada parâmetro. E se você adicionar validação de parâmetro, a verificação será realizada não depois de alterar o arquivo appsettings.json, mas toda vez que você usar o ServiceABC , o que é uma ação desnecessária. Na melhor das hipóteses, os parâmetros devem ser validados apenas uma vez após cada mudança de arquivo.
Desserialização de configuração com validação (opção IOptions)
GitHub: ConfigurationTemplate_3
Leia sobre as opções aqui .
Esta opção elimina a necessidade de usar o ServiceABC . Em vez disso, é usada a classe AppSettings , que contém as configurações do arquivo de configuração e o objeto ClientConfig . O objeto ClientConfig precisa ser inicializado após a alteração da configuração, porque um objeto pronto é usado em controladores.
ClientConfig é uma classe que interage com sistemas externos, cujo código não pode ser alterado. Se você apenas desserializar os dados da classe AppSettings , então ClientConfigserá nulo. Portanto, é necessário assinar o evento de configuração de leitura e inicializar o objeto ClientConfig no manipulador .
Para transferir a configuração não na forma de pares de valores-chave, mas como objetos de certas classes, usaremos a interface IOptions . Além disso, IOptions, ao contrário do ConfigurationManager, permite desserializar seções individuais. Para criar um objeto ClientConfig , você precisará usar IPostConfigureOptions , que é executado após o processamento de todas as configurações. IPostConfigureOptions será executado toda vez que a configuração for lida, mais recentemente.
Vamos criar ClientConfig.cs :
public class ClientConfig
{
private string _parameter1;
private string _parameter2;
public string Value => _parameter1 + " " + _parameter2;
public ClientConfig(ClientConfigOptions configOptions)
{
_parameter1 = configOptions.Parameter1;
_parameter2 = configOptions.Parameter2;
}
}
Ele terá parâmetros como um construtor na forma de um objeto ClientConfigOptions :
public class ClientConfigOptions
{
public string Parameter1;
public string Parameter2;
}
Vamos criar a classe de configurações AppSettings e definir o método ClientConfigBuild () nela , que criará o objeto ClientConfig .
Arquivo AppSettings.cs :
public class AppSettings
{
public string Parameter1 { get; set; }
public string Parameter2 { get; set; }
public ClientConfig clientConfig;
public void ClientConfigBuild()
{
clientConfig = new ClientConfig(new ClientConfigOptions()
{
Parameter1 = this.Parameter1,
Parameter2 = this.Parameter2
}
);
}
}
Vamos criar um manipulador de configuração que será processado por último. Para fazer isso, ele deve ser herdado de IPostConfigureOptions . O último PostConfigure chamado executará ClientConfigBuild () , que criará ClientConfig .
Arquivo ConfigureAppSettingsOptions.cs :
public class ConfigureAppSettingsOptions: IPostConfigureOptions<AppSettings>
{
public ConfigureAppSettingsOptions()
{ }
public void PostConfigure(string name, AppSettings options)
{
options.ClientConfigBuild();
}
}
Agora resta fazer alterações apenas em Startup.cs , as alterações afetarão apenas a função ConfigureServices (IServiceCollection services) .
Primeiro, vamos ler a seção AppSettings em appsettings.json
// configure strongly typed settings objects
var appSettingsSection = Configuration.GetSection("AppSettings");
services.Configure<AppSettings>(appSettingsSection);
Além disso, para cada solicitação, uma cópia de AppSettings será criada para que o pós-processamento possa ser chamado:
services.AddScoped(sp => sp.GetService<IOptionsSnapshot<AppSettings>>().Value);
Vamos adicionar um pós-processamento da classe AppSettings como um serviço:
services.AddSingleton<IPostConfigureOptions<AppSettings>, ConfigureAppSettingsOptions>();
Adicionado código a Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// configure strongly typed settings objects
var appSettingsSection = Configuration.GetSection("AppSettings");
services.Configure<AppSettings>(appSettingsSection);
services.AddScoped(sp => sp.GetService<IOptionsSnapshot<AppSettings>>().Value);
services.AddSingleton<IPostConfigureOptions<AppSettings>, ConfigureAppSettingsOptions>();
//next
services.AddControllersWithViews();
}
Para obter acesso à configuração, será suficiente simplesmente injetar AppSettings do controlador .
Arquivo HomeController.cs :
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly AppSettings _appSettings;
public HomeController(ILogger<HomeController> logger, AppSettings appSettings)
{
_logger = logger;
_appSettings = appSettings;
}
Vamos alterar Index.cshtml para exibir o parâmetro Value do objeto lientConfig
@model AppSettings
@{
ViewData["Title"] = "Home Page";
}
<div class="text-center">
<h1> ASP.NET Core</h1>
<h4> ( IOptions)</h4>
</div>
<div>
<p>
ClientConfig,
= @Model.clientConfig.Value
</p>
</div>

Vamos iniciar o aplicativo: Se durante a operação do aplicativo, altere o parâmetro Parameter1 para “NOVO !!! Parâmetro1 ABC "e Parâmetro2 para" NOVO !!! Parameter2 ABC ", então na próxima vez que você acessar a página inicial, a nova propriedade Value será exibida :

Resultado
Essa abordagem permite desserializar todos os valores de configuração sem iterar manualmente os parâmetros. Cada solicitação http funciona com sua própria instância de AppSettings e lientConfig, o que elimina a situação de colisões. IPostConfigureOptions garante que ele seja executado por último, quando todas as opções forem relidas. A desvantagem dessa solução é a criação constante de uma instância do ClientConfig para cada solicitação, o que é inviável porque na verdade, o ClientConfig só deve ser recriado após alterações na configuração.
Desserialização da configuração com validação (sem usar IOptions)
GitHub: ConfigurationTemplate_4
usa abordagem usando IPostConfigureOptions leva à criação do objeto ClientConfig toda vez que você recebe uma solicitação do cliente. Isso não é racional o suficiente porque cada solicitação funciona com um estado ClientConfig inicial, que muda apenas quando o arquivo de configuração appsettings.json é alterado. Para fazer isso, abandonaremos IPostConfigureOptions e criaremos um manipulador de configuração que será chamado apenas quando appsettings.json for alterado, como resultado, ClientConfig será criado apenas uma vez e, em seguida, a instância ClientConfig já criada será fornecida para cada solicitação.
Crie uma classe SingletonAppSettingsconfiguração (Singleton) a partir da qual uma instância de configurações será criada para cada solicitação.
Arquivo SingletonAppSettings.cs :
public class SingletonAppSettings
{
public AppSettings appSettings;
private static readonly Lazy<SingletonAppSettings> lazy = new Lazy<SingletonAppSettings>(() => new SingletonAppSettings());
private SingletonAppSettings()
{ }
public static SingletonAppSettings Instance => lazy.Value;
}
Vamos voltar à classe Startup e adicionar uma referência à interface IServiceCollection .
Será usado no método de tratamento de configuração
public IServiceCollection Services { get; set; }
Vamos alterar ConfigureServices (serviços IServiceCollection) e passar uma referência para IServiceCollection .
Arquivo Startup.cs :
public void ConfigureServices(IServiceCollection services)
{
Services = services;
// AppSettings
var appSettings = Configuration.GetSection("AppSettings").Get<AppSettings>();
appSettings.ClientConfigBuild();
Vamos criar uma configuração Singleton e adicioná-la à coleção de serviços:
SingletonAppSettings singletonAppSettings = SingletonAppSettings.Instance;
singletonAppSettings.appSettings = appSettings;
services.AddSingleton(singletonAppSettings);
Vamos adicionar o objeto AppSettings como um Scoped, com cada solicitação uma cópia do Singleton será criada:
services.AddScoped(sp => sp.GetService<SingletonAppSettings>().appSettings);
ConfigureServices completamente (serviços IServiceCollection) :
public void ConfigureServices(IServiceCollection services)
{
Services = services;
// AppSettings
var appSettings = Configuration.GetSection("AppSettings").Get<AppSettings>();
appSettings.ClientConfigBuild();
SingletonAppSettings singletonAppSettings = SingletonAppSettings.Instance;
singletonAppSettings.appSettings = appSettings;
services.AddSingleton(singletonAppSettings);
services.AddScoped(sp => sp.GetService<SingletonAppSettings>().appSettings);
//next
services.AddControllersWithViews();
}
Agora adicione um manipulador para configuração em Configure (IApplicationBuilder app, IWebHostEnvironment env) . Um token é usado para rastrear a mudança no arquivo appsettings.json. OnChange é a função a ser chamada quando o arquivo muda. Manipulador de configuração OnChange () :
ChangeToken.OnChange(() => Configuration.GetReloadToken(), onChange);
Primeiro, lemos o arquivo appsettings.json e desserializamos a classe AppSettings . Então, da coleção de serviço, obtemos uma referência ao Singleton que armazena o objeto AppSettings e o substitui por um novo.
private void onChange()
{
var newAppSettings = Configuration.GetSection("AppSettings").Get<AppSettings>();
newAppSettings.ClientConfigBuild();
var serviceAppSettings = Services.BuildServiceProvider().GetService<SingletonAppSettings>();
serviceAppSettings.appSettings = newAppSettings;
Console.WriteLine($"AppSettings has been changed! {DateTime.Now}");
}
No HomeController, injetaremos um link para AppSettings, como na versão anterior (ConfigurationTemplate_3)
HomeController.cs:
Index.cshtml Value lientConfig:
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly AppSettings _appSettings;
public HomeController(ILogger<HomeController> logger, AppSettings appSettings)
{
_logger = logger;
_appSettings = appSettings;
}
Index.cshtml Value lientConfig:
@model AppSettings
@{
ViewData["Title"] = "Home Page";
}
<div class="text-center">
<h1> ASP.NET Core</h1>
<h4> ( IOptions)</h4>
</div>
<div>
<p>
ClientConfig,
= @Model.clientConfig.Value
</p>
</div>
Vamos

iniciar o aplicativo: Tendo selecionado o modo de inicialização como um aplicativo de console, na janela do aplicativo você pode ver uma mensagem sobre o acionamento do evento de alteração do arquivo de configuração:

E os novos valores:

Resultado
Esta opção é melhor do que usar IPostConfigureOptions porque permite que você construa um objeto somente após alterar o arquivo de configuração, e não a cada solicitação. O resultado é uma redução no tempo de resposta do servidor. Depois que o token é disparado, o estado do token é redefinido.
Adicionando padrões e validando a configuração
GitHub: ConfigurationTemplate_5
Nos exemplos anteriores, se o arquivo appsettings.json estiver ausente, o aplicativo lançará uma exceção, então vamos tornar o arquivo de configuração opcional e adicionar as configurações padrão. Quando você publica um aplicativo de projeto criado a partir de um modelo no Visula Studio, o arquivo appsettings.json estará localizado na mesma pasta junto com todos os binários, o que é inconveniente durante a implantação no Docker. O arquivo appsettings.json foi movido para config / :
.AddJsonFile("config/appsettings.json")
Para ser capaz de iniciar o aplicativo sem appsettings.json, altere o optiona l parâmetro de verdade , que neste meio de casos que a presença de appsettings.json é opcional.
Arquivo Startup.cs :
public Startup(IConfiguration configuration)
{
var builder = new ConfigurationBuilder()
.AddJsonFile("config/appsettings.json", optional: true, reloadOnChange: true);
configuration = builder.Build();
Configuration = configuration;
}
Adicione a public void ConfigureServices (IServiceCollection services) à linha de desserialização de configuração o caso de lidar com a ausência do arquivo appsettings.json:
var appSettings = Configuration.GetSection("AppSettings").Get<AppSettings>() ?? new AppSettings();
Vamos adicionar validação de configuração com base na interface IValidatableObject . Se os parâmetros de configuração estiverem ausentes, o valor padrão será usado.
Vamos herdar a classe AppSettings de IValidatableObject e implementar o método:
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
Arquivo AppSettings.cs :
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
List<ValidationResult> errors = new List<ValidationResult>();
if (string.IsNullOrWhiteSpace(this.Parameter1))
{
errors.Add(new ValidationResult(" Parameter1. " +
" DefaultParameter1 ABC"));
this.Parameter1 = "DefaultParameter1 ABC";
}
if (string.IsNullOrWhiteSpace(this.Parameter2))
{
errors.Add(new ValidationResult(" Parameter2. " +
" DefaultParameter2 ABC"));
this.Parameter2 = "DefaultParameter2 ABC";
}
return errors;
}
Adicione um método para chamar a verificação de configuração a ser chamada a partir do arquivo Startup.cs da classe de inicialização :
private void ValidateAppSettings(AppSettings appSettings)
{
var resultsValidation = new List<ValidationResult>();
var context = new ValidationContext(appSettings);
if (!Validator.TryValidateObject(appSettings, context, resultsValidation, true))
{
resultsValidation.ForEach(
error => Console.WriteLine($" : {error.ErrorMessage}"));
}
}
Vamos adicionar uma chamada ao método de validação de configuração em ConfigureServices (serviços IServiceCollection). Se não houver nenhum arquivo appsettings.json, você precisará inicializar o objeto AppSettings com os valores padrão.
Arquivo Startup.cs :
var appSettings = Configuration.GetSection("AppSettings").Get<AppSettings>() ?? new AppSettings();
Verificando parâmetros. Se o valor padrão for usado, uma mensagem indicando o parâmetro será exibida no console.
//Validate
this.ValidateAppSettings(appSettings);
appSettings.ClientConfigBuild();
Vamos mudar a verificação da configuração em onChange ()
private void onChange()
{
var newAppSettings = Configuration.GetSection("AppSettings").Get<AppSettings>() ?? new AppSettings();
//Validate
this.ValidateAppSettings(newAppSettings);
newAppSettings.ClientConfigBuild();
var serviceAppSettings = Services.BuildServiceProvider().GetService<SingletonAppSettings>();
serviceAppSettings.appSettings = newAppSettings;
Console.WriteLine($"AppSettings has been changed! {DateTime.Now}");
}
Se você excluir a chave Parameter1 do arquivo appsettings.json , depois de salvar o arquivo, uma mensagem sobre a ausência do parâmetro aparecerá na janela do aplicativo de console:

Resultado
Alterar o caminho para a localização das configurações na pasta config é uma boa solução. permite que você não misture todos os arquivos em um heap. A pasta config é definida apenas para armazenar arquivos de configuração. Simplificou a tarefa de implantação e configuração do aplicativo para administradores por meio da validação da configuração. Se você adicionar a saída de erros de configuração ao log, o administrador, se parâmetros incorretos forem especificados, receberá informações precisas sobre o problema, e não porque os programadores começaram recentemente a gravar em qualquer exceção: "Algo deu errado . "
Não existe uma opção ideal para trabalhar com uma configuração, tudo depende da tarefa em mãos, cada opção tem seus prós e contras.
Todos os modelos de configuração estão disponíveis aqui .
Literatura:
- ASP.NET Core correto
- METANIT - Configuração. Noções básicas de configuração
- Singleton Design Pattern C # núcleo .net
- Recarregando a configuração no .NET core
- Recarregando opções fortemente tipadas em alterações de arquivo no ASP.NET Core RC2
- Configuração do aplicativo ASP.NET Core via IOptions
- METANIT - Configuração de passagem via IOptions
- Configuração do aplicativo ASP.NET Core via IOptions
- METANIT - Modelo de Auto-validação