Adding RESTful API Configuration Provider For Our ASP.NET Core Application

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.
  1. [ApiController]  
  2. [Route("[controller]")]  
  3. public class ConfigController : ControllerBase  
  4. {  
  5.     [HttpGet]  
  6.     public IActionResult Get([FromQuery]string appName, [FromQuery]string env)  
  7.     {  
  8.         if (string.IsNullOrWhiteSpace(appName))  
  9.             return BadRequest("appName is empty");  
  10.       
  11.         if (string.IsNullOrWhiteSpace(env))  
  12.             return BadRequest("env is empty");  
  13.       
  14.         return Ok(ConfigResult.GetResult(appName, env));  
  15.     }  
  16.       
  17.     public class ConfigResult  
  18.     {  
  19.         public int Code { getset; }  
  20.       
  21.         public string Msg { getset; }  
  22.       
  23.         public Dictionary<stringstring> Data { getset; }  
  24.       
  25.         public static ConfigResult GetResult(string appName, string env)  
  26.         {  
  27.             var rd = new Random();  
  28.             var dict = new Dictionary<stringstring>  
  29.             {  
  30.                 { "appName", appName },  
  31.                 { "env", env },  
  32.                 { "key1", $"val1-{rd.NextDouble()}" },  
  33.                 { "key2", $"val2-{rd.NextDouble()}" },  
  34.                 { "SC1__key1", $"sc1_val1-{rd.NextDouble()}" },  
  35.                 { "SC2:key1", $"sc2_val1-{rd.NextDouble()}" },  
  36.             };  
  37.       
  38.             return new ConfigResult  
  39.             {  
  40.                 Code = 0,  
  41.                 Msg = "OK",  
  42.                 Data = dict  
  43.             };  
  44.         }  
  45.     }  

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.
  1. public class ApiConfigurationSource : IConfigurationSource  
  2. {  
  3.     /// <summary>  
  4.     /// Specifies the url of RESTful API.  
  5.     /// </summary>  
  6.     public string ReqUrl { getset; }  
  7.   
  8.     /// <summary>  
  9.     /// Specifies the polling period.  
  10.     /// </summary>  
  11.     public int Period { getset; }  
  12.   
  13.     /// <summary>  
  14.     /// Specifies whether this source is optional.  
  15.     /// </summary>  
  16.     public bool Optional { getset; }  
  17.   
  18.     /// <summary>  
  19.     /// Specifies the name of the application.  
  20.     /// </summary>  
  21.     public string AppName { getset; }  
  22.   
  23.     /// <summary>  
  24.     /// Specifies the env of the application.  
  25.     /// </summary>  
  26.     public string Env { getset; }  
  27.   
  28.     public IConfigurationProvider Build(IConfigurationBuilder builder)  
  29.     {  
  30.         return new ApiConfigurationProvider(this);  
  31.     }  

Create the custom configuration provider by inheriting from ConfigurationProvider 
  1. internal class ApiConfigurationProvider : ConfigurationProvider, IDisposable  
  2. {  
  3.     private readonly Timer _timer;  
  4.     private readonly ApiConfigurationSource _apiConfigurationSource;  
  5.   
  6.     public ApiConfigurationProvider(ApiConfigurationSource apiConfigurationSource)  
  7.     {  
  8.         _apiConfigurationSource = apiConfigurationSource;  
  9.         _timer = new Timer(x => Load(),   
  10.             null,   
  11.             TimeSpan.FromSeconds(_apiConfigurationSource.Period),   
  12.             TimeSpan.FromSeconds(_apiConfigurationSource.Period));  
  13.     }  
  14.   
  15.     public void Dispose()  
  16.     {  
  17.         _timer?.Change(Timeout.Infinite, 0);  
  18.         _timer?.Dispose();  
  19.         Console.WriteLine("Dispose timer");  
  20.     }  
  21.   
  22.     public override void Load()  
  23.     {  
  24.         try  
  25.         {  
  26.             var url = $"{_apiConfigurationSource.ReqUrl}?appName={_apiConfigurationSource.AppName}&env={_apiConfigurationSource.Env}";  
  27.   
  28.             using (HttpClient client = new HttpClient())  
  29.             {  
  30.                 var resp = client.GetAsync(url).ConfigureAwait(false).GetAwaiter().GetResult();  
  31.                 var res = resp.Content.ReadAsStringAsync().ConfigureAwait(false).GetAwaiter().GetResult();  
  32.                 var config = Newtonsoft.Json.JsonConvert.DeserializeObject<ConfigResult>(res);  
  33.   
  34.                 if (config.Code == 0)  
  35.                 {  
  36.                     Data = config.Data;  
  37.                     OnReload();  
  38.                     Console.WriteLine($"update at {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}");  
  39.                     Console.WriteLine($"{res}");  
  40.                 }  
  41.                 else  
  42.                 {  
  43.                     CheckOptional();  
  44.                 }  
  45.             }  
  46.         }  
  47.         catch  
  48.         {  
  49.             CheckOptional();  
  50.         }  
  51.     }  
  52.   
  53.     private void CheckOptional()  
  54.     {  
  55.         if (!_apiConfigurationSource.Optional)  
  56.         {  
  57.             throw new Exception($"can not load config from {_apiConfigurationSource.ReqUrl}");  
  58.         }  
  59.     }  

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
  1. public static class ApiExtensions  
  2. {  
  3.     public static IConfigurationBuilder AddApiConfiguration(  
  4.         this IConfigurationBuilder builder, Action<ApiConfigurationSource> action)  
  5.     {  
  6.         var source = new ApiConfigurationSource();  
  7.   
  8.         action(source);  
  9.   
  10.         return builder.Add(source);  
  11.     }  

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.
  1. public class Program  
  2. {  
  3.     public static void Main(string[] args)  
  4.     {  
  5.         CreateHostBuilder(args).Build().Run();  
  6.     }  
  7.       
  8.     public static IHostBuilder CreateHostBuilder(string[] args) =>  
  9.         Host.CreateDefaultBuilder(args)  
  10.             .ConfigureAppConfiguration((context, builder) =>  
  11.             {  
  12.                 builder.AddApiConfiguration(x =>   
  13.                 {  
  14.                     x.AppName = "Demo";  
  15.                     x.Env = context.HostingEnvironment.EnvironmentName;  
  16.                     x.ReqUrl = "http://localhost:9632/config";  
  17.                     x.Period = 60;  
  18.                     x.Optional = false;  
  19.                 });  
  20.             })  
  21.             .ConfigureWebHostDefaults(webBuilder =>  
  22.             {  
  23.                 webBuilder.UseStartup<Startup>().UseUrls("http://*:9633");  
  24.             });  
  25. }  
Define an entity for configuration values.
  1. public class AppSettings  
  2. {  
  3.     public string appName { getset; }  
  4.   
  5.     public string env { getset; }  
  6.   
  7.     public string key1 { getset; }  
  8.   
  9.     public string key2 { getset; }  
  10.   
  11.     public SC1 SC1 { getset; }  
  12.   
  13.     public SC2 SC2 { getset; }  
  14. }  
  15.   
  16. public class SC1  
  17. {  
  18.     public string key1 { getset; }  
  19. }  
  20.   
  21. public class SC2  
  22. {  
  23.     public string key1 { getset; }  

Configure this entity in `Startup` class. 
  1. public class Startup  
  2. {  
  3.     // ....  
  4.   
  5.     public void ConfigureServices(IServiceCollection services)  
  6.     {  
  7.         services.Configure<AppSettings>(Configuration);  
  8.         services.AddControllers();  
  9.     }  

Using the configuration just like what you normally do. Here is a sample about controller. 
  1. [ApiController]  
  2. [Route("[controller]")]  
  3. public class WeatherForecastController : ControllerBase  
  4. {  
  5.     private readonly IConfiguration _configuration;  
  6.     private readonly AppSettings _settings;  
  7.     private readonly AppSettings _sSettings;  
  8.     private readonly AppSettings _mSettings;  
  9.   
  10.     public WeatherForecastController(  
  11.         IConfiguration configuration,  
  12.         IOptions<AppSettings> options,  
  13.         IOptionsSnapshot<AppSettings> sOptions,  
  14.         IOptionsMonitor<AppSettings> _mOptions  
  15.         )  
  16.     {  
  17.         _configuration = configuration;  
  18.         _settings = options.Value;  
  19.         _sSettings = sOptions.Value;  
  20.         _mSettings = _mOptions.CurrentValue;  
  21.     }  
  22.   
  23.     [HttpGet]  
  24.     public string Get()  
  25.     {  
  26.         Console.WriteLine($"===============================================");  
  27.   
  28.         var other = _configuration["other"];  
  29.         Console.WriteLine($"other = {other}");  
  30.   
  31.         var appName = _configuration["appName"];  
  32.         var env = _configuration["env"];  
  33.         var key1 = _configuration["key1"];  
  34.         var SC1key1 = _configuration["SC1__key1"];  
  35.         var SC2key1 = _configuration["SC2:key1"];  
  36.   
  37.         Console.WriteLine($"appName={appName},env={env},key1={key1},SC1key1={SC1key1},SC2key1={SC2key1}");  
  38.   
  39.         var str1 = Newtonsoft.Json.JsonConvert.SerializeObject(_settings);  
  40.         Console.WriteLine($"IOptions");  
  41.         Console.WriteLine($"{str1}");  
  42.   
  43.         var str2 = Newtonsoft.Json.JsonConvert.SerializeObject(_sSettings);  
  44.         Console.WriteLine($"IOptionsSnapshot");  
  45.         Console.WriteLine($"{str2}");  
  46.   
  47.         var str3 = Newtonsoft.Json.JsonConvert.SerializeObject(_mSettings);  
  48.         Console.WriteLine($"IOptionsMonitor");  
  49.         Console.WriteLine($"{str3}");  
  50.   
  51.         Console.WriteLine($"===============================================");  
  52.         Console.WriteLine("");  
  53.   
  54.         return "OK";  
  55.     }  

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!