Introduction
For unified management of the application's configuration information, some of us will use open source solutions such as Consul, Etcd and so on, and some of us will maintain them by their private center of configuration and provide RESTful APIs for developers.
In this article, I will introduce a simple way to integrate RESTful APIs into the configuration system of ASP.NET Core, and it belongs to custom configuration provider.
If you have no idea of the configuration of ASP.NET Core, you can read the following document at first
Simple RESTful API
We will prepare the datasource of configuration at this section. The response result of the RESTful APIs decides how we can parse and integrate.
Create an ASP.NET Core Web API project to finish this one.
- [ApiController]
- [Route("[controller]")]
- public class ConfigController : ControllerBase
- {
- [HttpGet]
- public IActionResult Get([FromQuery]string appName, [FromQuery]string env)
- {
- if (string.IsNullOrWhiteSpace(appName))
- return BadRequest("appName is empty");
-
- if (string.IsNullOrWhiteSpace(env))
- return BadRequest("env is empty");
-
- return Ok(ConfigResult.GetResult(appName, env));
- }
-
- public class ConfigResult
- {
- public int Code { get; set; }
-
- public string Msg { get; set; }
-
- public Dictionary<string, string> Data { get; set; }
-
- public static ConfigResult GetResult(string appName, string env)
- {
- var rd = new Random();
- var dict = new Dictionary<string, string>
- {
- { "appName", appName },
- { "env", env },
- { "key1", $"val1-{rd.NextDouble()}" },
- { "key2", $"val2-{rd.NextDouble()}" },
- { "SC1__key1", $"sc1_val1-{rd.NextDouble()}" },
- { "SC2:key1", $"sc2_val1-{rd.NextDouble()}" },
- };
-
- return new ConfigResult
- {
- Code = 0,
- Msg = "OK",
- Data = dict
- };
- }
- }
- }
To simulate modifing the configuration, here it concatenates some random value.
When accessing this API, you will get a different result.
RESTful API Configuration Provider
This is the most important section of this article. There are three steps that we should do.
Create a class that implements IConfigurationSource.
- public class ApiConfigurationSource : IConfigurationSource
- {
-
-
-
- public string ReqUrl { get; set; }
-
-
-
-
- public int Period { get; set; }
-
-
-
-
- public bool Optional { get; set; }
-
-
-
-
- public string AppName { get; set; }
-
-
-
-
- public string Env { get; set; }
-
- public IConfigurationProvider Build(IConfigurationBuilder builder)
- {
- return new ApiConfigurationProvider(this);
- }
- }
Create the custom configuration provider by inheriting from ConfigurationProvider
- internal class ApiConfigurationProvider : ConfigurationProvider, IDisposable
- {
- private readonly Timer _timer;
- private readonly ApiConfigurationSource _apiConfigurationSource;
-
- public ApiConfigurationProvider(ApiConfigurationSource apiConfigurationSource)
- {
- _apiConfigurationSource = apiConfigurationSource;
- _timer = new Timer(x => Load(),
- null,
- TimeSpan.FromSeconds(_apiConfigurationSource.Period),
- TimeSpan.FromSeconds(_apiConfigurationSource.Period));
- }
-
- public void Dispose()
- {
- _timer?.Change(Timeout.Infinite, 0);
- _timer?.Dispose();
- Console.WriteLine("Dispose timer");
- }
-
- public override void Load()
- {
- try
- {
- var url = $"{_apiConfigurationSource.ReqUrl}?appName={_apiConfigurationSource.AppName}&env={_apiConfigurationSource.Env}";
-
- using (HttpClient client = new HttpClient())
- {
- var resp = client.GetAsync(url).ConfigureAwait(false).GetAwaiter().GetResult();
- var res = resp.Content.ReadAsStringAsync().ConfigureAwait(false).GetAwaiter().GetResult();
- var config = Newtonsoft.Json.JsonConvert.DeserializeObject<ConfigResult>(res);
-
- if (config.Code == 0)
- {
- Data = config.Data;
- OnReload();
- Console.WriteLine($"update at {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}");
- Console.WriteLine($"{res}");
- }
- else
- {
- CheckOptional();
- }
- }
- }
- catch
- {
- CheckOptional();
- }
- }
-
- private void CheckOptional()
- {
- if (!_apiConfigurationSource.Optional)
- {
- throw new Exception($"can not load config from {_apiConfigurationSource.ReqUrl}");
- }
- }
- }
Here we use a timer to load the configuation so that it can reload the application's configuration.
Add an extension method which permits adding the configuration source to a ConfigurationBuilder
- public static class ApiExtensions
- {
- public static IConfigurationBuilder AddApiConfiguration(
- this IConfigurationBuilder builder, Action<ApiConfigurationSource> action)
- {
- var source = new ApiConfigurationSource();
-
- action(source);
-
- return builder.Add(source);
- }
- }
After those three steps, we have integrated RESTful APIs into the configuration system of ASP.NET Core! Let's take a look at how to use it.
Integration Sample
At first, we should modify the
Program.cs to add our custom configuration provider.
- public class Program
- {
- public static void Main(string[] args)
- {
- CreateHostBuilder(args).Build().Run();
- }
-
- public static IHostBuilder CreateHostBuilder(string[] args) =>
- Host.CreateDefaultBuilder(args)
- .ConfigureAppConfiguration((context, builder) =>
- {
- builder.AddApiConfiguration(x =>
- {
- x.AppName = "Demo";
- x.Env = context.HostingEnvironment.EnvironmentName;
- x.ReqUrl = "http://localhost:9632/config";
- x.Period = 60;
- x.Optional = false;
- });
- })
- .ConfigureWebHostDefaults(webBuilder =>
- {
- webBuilder.UseStartup<Startup>().UseUrls("http://*:9633");
- });
- }
Define an entity for configuration values.
- public class AppSettings
- {
- public string appName { get; set; }
-
- public string env { get; set; }
-
- public string key1 { get; set; }
-
- public string key2 { get; set; }
-
- public SC1 SC1 { get; set; }
-
- public SC2 SC2 { get; set; }
- }
-
- public class SC1
- {
- public string key1 { get; set; }
- }
-
- public class SC2
- {
- public string key1 { get; set; }
- }
Configure this entity in `Startup` class.
- public class Startup
- {
-
-
- public void ConfigureServices(IServiceCollection services)
- {
- services.Configure<AppSettings>(Configuration);
- services.AddControllers();
- }
- }
Using the configuration just like what you normally do. Here is a sample about controller.
- [ApiController]
- [Route("[controller]")]
- public class WeatherForecastController : ControllerBase
- {
- private readonly IConfiguration _configuration;
- private readonly AppSettings _settings;
- private readonly AppSettings _sSettings;
- private readonly AppSettings _mSettings;
-
- public WeatherForecastController(
- IConfiguration configuration,
- IOptions<AppSettings> options,
- IOptionsSnapshot<AppSettings> sOptions,
- IOptionsMonitor<AppSettings> _mOptions
- )
- {
- _configuration = configuration;
- _settings = options.Value;
- _sSettings = sOptions.Value;
- _mSettings = _mOptions.CurrentValue;
- }
-
- [HttpGet]
- public string Get()
- {
- Console.WriteLine($"===============================================");
-
- var other = _configuration["other"];
- Console.WriteLine($"other = {other}");
-
- var appName = _configuration["appName"];
- var env = _configuration["env"];
- var key1 = _configuration["key1"];
- var SC1key1 = _configuration["SC1__key1"];
- var SC2key1 = _configuration["SC2:key1"];
-
- Console.WriteLine($"appName={appName},env={env},key1={key1},SC1key1={SC1key1},SC2key1={SC2key1}");
-
- var str1 = Newtonsoft.Json.JsonConvert.SerializeObject(_settings);
- Console.WriteLine($"IOptions");
- Console.WriteLine($"{str1}");
-
- var str2 = Newtonsoft.Json.JsonConvert.SerializeObject(_sSettings);
- Console.WriteLine($"IOptionsSnapshot");
- Console.WriteLine($"{str2}");
-
- var str3 = Newtonsoft.Json.JsonConvert.SerializeObject(_mSettings);
- Console.WriteLine($"IOptionsMonitor");
- Console.WriteLine($"{str3}");
-
- Console.WriteLine($"===============================================");
- Console.WriteLine("");
-
- return "OK";
- }
- }
Here is the result of this sample.
As you can see, before the application starts up, the provider will load the configuration values from our RESTful API.
After the application starts up successfully, we can read configuration values.
When the values were changed, it can also read the newest values, but IOptions<T> cannot read a new one due to its design.
If your applications should read values that can be modified, I suggest that you should not use IOptions<T>!
Here is the source code you can find in my GitHub page.
Summary
This article showed you a simple solution of how to integrate RESTful APIs into the configuration system of ASP.NET Core.
This is a very small case, it has a lot of room for improvement, such as caching the result of RESTful API, etc.
I hope this will help you!