Last Updated on December 14, 2023 by Aram
A shoutout for ilovedotnet.org, a great initiative to share the knowledge of .NET and its related technologies through a platform that provides live demos crafted by developers for developers with love using .NET.
Since the beginning of ASP.NET Core, there has been a great addition in the core features, one of which is the appsettings.json file. In this tutorial, you will learn how to apply API Configurations using appsettings.json in ASP.NET Core Web API.
Appsettings.json is a simple JSON-formatted file which typically resides within the root folder of your ASP.NET Core Web Application project, it can include anything from connection strings, API keys or secrets, application-related settings, URLs to different API integrations, logging configurations and many more.
In the previous version of ASP.NET, we used to have the web.config file which included such details alongside other runtime configurations for some dependency libraries.
Now in .NET Core and most recent versions of .NET, this has been replaced with the improved configuration file which is the appsettings.json
In fact, the appsettings.json can be split into multiple files, each one targeting a specific platform, so you can have a file that includes settings targeting development environment which having another file targeting the production environment.
so namely we can have appsettings.development.json and appsettings.json files, and if you have a pre-prod or qa environment, then you can just name it accordingly: appsettings.qa.json.
Once you create the new file, In the solution explorer, you will notice that the development version of the file is actually hidden within the appsettings.json file, which is a logical grouping that VS will show it for you.
Let’s start this tutorial to apply API Configurations using appsettings.json
Create a new project using Visual Studio 2022, choose ASP.NET Core Web API.
Let’s choose .NET 7 and then press Create.
Once VS 2022 finishes project setup, in the solution explorer you will notice an expandable view of the file appsettings.json, as mentioned previously.
It will expand to show the appsettings.development.json
Opening the 2 files will show the below JSON-formatted content:
You will notice that each one of the files has almost matching structure, with values that might be different between the environments and some file might have an extra key or object.
We will build a Web API that expose endpoints related to hashing, we will be mainly implementing 2 types of hashing functions
PbKFD2 which stands for Password-based Key Derivation Function
and HMAC-256, this function calculates the message authentication code using SHA-256 as the hashing function
Now we are ready to start preparing the structure of the tutorial, so let’s get started:
appsettings.json
Open appsettings.json
add the below section:
1
2
3
4
5
|
"ApiSettings": {
"TestingEndpointEnabled": false,
"ApiKeyHash": "wBtawFVBT+1AfZDMyimi04GP1RoYGKymkpwaA5twD8s=",
"PbKDF2IterationsCount": 500000
}
|
Then open the file appsettings.development.json and add the below ApiSettings Section:
1
2
3
4
5
|
"ApiSettings": {
"TestingEndpointEnabled": true,
"ApiKeyHash": "+KhteeZa4ydjFaCQ+QXmYIli5XKEztJTocmbuoE65Eg=",
"PbKDF2IterationsCount": 300000
}
|
API Key
You might wonder from where did we bring the values of the ApiKeyHash for both environments.
So, I simple generated a random alphanumeric series of 30 characters from any online generation tool, so I got the below API Keys
atw35lrqs12cqvrwhaeee7366em6ky for production which is represented by the hash +493O7g2eN1Q2KOMa8+2pvT2aX83hcCmfxkdPpGRy/g=
gkgw3vqares09fr2m5dh6lpkwf1b9k for development which is represented by the hash +KhteeZa4ydjFaCQ+QXmYIli5XKEztJTocmbuoE65Eg=
So usually the best practice for API key is to share it with client and never store it anywhere in your code or even settings.
Better to hash it (with a strong hashing algorithm) and save it on settings
For the knowledge, I’ve used the pbkdf2 key-derviation function to find the hash for the API Keys mentioned above.
Now, in order to be able to use the Options pattern in the code, we need to have a model (class) that would bind to the ApiSettings Section within the appsettings.json file
So let’s go and create a new class with name ApiSettingsModel inside a new folder with name Models:
1
2
3
4
5
6
7
8
9
|
namespace ApiConfigurationsUsingAppSettings.Models
{
public class ApiSettingsModel
{
public bool? TestingEndpointEnabled { get; set; }
public string? ApiKeyHash { get; set; }
public int PbKDF2IterationsCount { get; set; }
}
}
|
Interfaces
Create an Interfaces Folder.
Create a new Interface with name IHashingService.
We will have our 2 methods signatures defined in the IHashingService Interface
1
2
3
4
5
6
7
8
|
namespace ApiConfigurationsUsingAppSettings.Interfaces
{
public interface IHashingService
{
string HashUsingPbkdf2(byte[] password);
string ComputeHmacUsingSha256(byte[] data);
}
}
|
Services
Now let’s create the service that would implement the hashing functions defined in the IHashingService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
using ApiConfigurationsUsingAppSettings.Interfaces;
using ApiConfigurationsUsingAppSettings.Models;
using Microsoft.Extensions.Options;
using System.Security.Cryptography;
namespace ApiConfigurationsUsingAppSettings.Services
{
public class HashingService : IHashingService
{
private readonly IOptions<apisettingsmodel> options;
public HashingService(IOptions<apisettingsmodel> options)
{
this.options = options;
}
public string HashUsingPbkdf2(byte[] password)
{
string saltString = "ThisIsAStringToBeUsedAsSalt!@#$%";
byte[] salt = System.Text.Encoding.ASCII.GetBytes(saltString);
using var bytes = new Rfc2898DeriveBytes(password, salt, options.Value.PbKDF2IterationsCount, HashAlgorithmName.SHA256);
var derivedRandomKey = bytes.GetBytes(32);
var hash = Convert.ToBase64String(derivedRandomKey);
return hash;
}
public string ComputeHmacUsingSha256(byte[] data)
{
string keyString = "ThisIsAnotherStringToBeUsedAsKey*&^%$#";
byte[] key = System.Text.Encoding.ASCII.GetBytes(keyString);
byte[] hmacBytes = HMACSHA256.HashData(key, data);
return Convert.ToBase64String(hmacBytes);
}
}
}
|
The most important part of this service class is the usage of IOptions<T> , since IOptions is a singleton instance service, any change after loading the appsettings.json will not be reflected .
Middleware
So we will be using a middleware to provide basic authentication for the APIs.
Side Note: I don’t prefer to pluralize Middleware and name it Middlewares, it doesn’t sound or rhyme right, and I guess it falls under the same category of Software. Here is a long discussion about this topic if anyone wants to read further.
This is mainly intended to showcase that we can easily use any section from the appsettings.json file within any component of our ASP.NET Core application, and the middleware is an example for that.
Create a new class with name ApiKeyMiddleware inside a new folder ‘Middleware’:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
using ApiConfigurationsUsingAppSettings.Interfaces;
using ApiConfigurationsUsingAppSettings.Models;
using Microsoft.Extensions.Options;
namespace ApiConfigurationsUsingAppSettings.Middleware
{
public class ApiKeyMiddleware
{
private readonly RequestDelegate _next;
private readonly IHashingService _hashingService;
private readonly IOptions<apisettingsmodel> _options;
private const string APIKEYNAME = "ApiKey";
public ApiKeyMiddleware(RequestDelegate next, IHashingService hashingService, IOptions<apisettingsmodel> options)
{
_next = next;
_hashingService = hashingService;
_options = options;
}
public async Task InvokeAsync(HttpContext context)
{
if (!context.Request.Headers.TryGetValue(APIKEYNAME, out var extractedApiKey))
{
context.Response.StatusCode = 401;
await context.Response.WriteAsync("Api Key was not provided. (Using ApiKeyMiddleware) ");
return;
}
byte[] apiKey = System.Text.Encoding.ASCII.GetBytes(extractedApiKey!);
var extractedApiKeyHashed = _hashingService.HashUsingPbkdf2(apiKey);
var apiKeyHash = _options.Value.ApiKeyHash ?? "";
if (!apiKeyHash.Equals(extractedApiKeyHashed))
{
context.Response.StatusCode = 401;
await context.Response.WriteAsync("Unauthorized client. (Using ApiKeyMiddleware)");
return;
}
await _next(context);
}
}
}
|
Controllers
Let’s define our endpoints of hashing functions.
Also we will be using another setting definedi in ApiSettings section here in the controller to be able to enable/disable one of the test controllers.
And that will happen using the IOptions<T>
Here is the final code for the HashingController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
|
using ApiConfigurationsUsingAppSettings.Interfaces;
using ApiConfigurationsUsingAppSettings.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using System.Text;
namespace ApiConfigurationsUsingAppSettings.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class HashingController : ControllerBase
{
private readonly IHashingService _hashingService;
private readonly IOptions<apisettingsmodel> _options;
public HashingController(IHashingService hashingService, IOptions<apisettingsmodel> options)
{
_hashingService = hashingService;
_options = options;
}
[HttpGet("Test")]
public IActionResult TestEndpoint()
{
if (_options.Value.TestingEndpointEnabled.HasValue && _options.Value.TestingEndpointEnabled.Value)
{
return Ok();
}
else
{
return NotFound();
}
}
[HttpPost("PbKdf2")]
public IActionResult HashUsingPbKdf2([FromBody] string password)
{
if (string.IsNullOrEmpty(password))
{
return BadRequest("Password must be provided");
}
var passwordBytes = Encoding.ASCII.GetBytes(password);
var hash = _hashingService.HashUsingPbkdf2(passwordBytes);
if (string.IsNullOrEmpty(hash))
{
return BadRequest($"Unable to calcuate hash for {password}");
}
return Ok(hash);
}
[HttpPost("HMAC256")]
public IActionResult ComputeHmac([FromBody] string data)
{
if (string.IsNullOrEmpty(data))
{
return BadRequest("Data must be provided");
}
var dataBytes = Encoding.ASCII.GetBytes(data);
var hash = _hashingService.ComputeHmacUsingSha256(dataBytes);
if (string.IsNullOrEmpty(hash))
{
return BadRequest($"Unable to compute HMAC-SHA256 for {data}");
}
return Ok(hash);
}
}
}
|
launchSettings.json
When working locally, you have the launchSettings.json file that includes the different build profiles and with each profile you have the ASPNETCore_Environment variable.
If you keep the Environment value as empty string, the default appsettings.json file should be loaded, and the same goes with setting the value as Development.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:3106",
"sslPort": 44333
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5091",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7026;http://localhost:5091",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": ""
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
|
Swagger Testing
Now In order to test the API with Swagger, we need to define an operation filter that will introduce a new paramter to pass the APIKey value as header in Swagger UI
Filters
Create a new file with name ApiKeyHeaderOperationFilter under a new folder with name Filters
Add the below:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace ApiConfigurationsUsingAppSettings.Filters
{
public class ApiKeyHeaderOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
if (operation == null)
{
throw new Exception("Invalid operation");
}
operation.Parameters.Add(new OpenApiParameter
{
In = ParameterLocation.Header,
Name = "ApiKey",
Description = "pass the Api Key here",
Schema = new OpenApiSchema
{
Type = "String"
}
});
}
}
}
|
Program.cs
Now open program.cs file and make sure it looks like the below:
1
2
3
4
5
6
7
8
9
10
11
12
|
using ApiConfigurationsUsingAppSettings.Filters;
using ApiConfigurationsUsingAppSettings.Interfaces;
using ApiConfigurationsUsingAppSettings.Middleware;
using ApiConfigurationsUsingAppSettings.Models;
using ApiConfigurationsUsingAppSettings.Services;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options => options.OperationFilter<apikeyheaderoperationfilter>());
builder.Services.AddTransient</apikeyheaderoperationfilter>
|
So in the builder.Services.Configure<T> we are binding the ApiSettings section of appsettings.json to the ApiSettingsModel
Notice as well the OperationFilter that we added to be able to test the APIKey header with the Swagger UI
And see how we are using the ApiKeyMiddleware by injecting it into ASP.NET Core Application’s pipeline.
Testing with Swagger UI
Now let’s run our API project to test our work on appsettings.json
We will start with testing the endppint /PbKdf2
Notice that you have to provide the ApiKey in order to get authenticated into the API
And here is the Response with the cURL:
Let’s test the /HMAC256 endpoint:
And the response for calculating the HMAC for CodingSonata.com is:
Now let’s test the above endpoint without passing the ApiKey, let’s see what will happen:
And the response is:
Yes, it is 401 which means unauthorized or more accurately (unauthenticated)
We have another endpoint that includes a flag setting whether it is enabled or not:
In appsettings.development.json, it was set to true, let’s test it here:
And the result is:
Let’s do a final test to see how changing the ASPNETCore_Environment variable would reflect on the output of the above endpoint.
Now let’s stop the application and open launchSetting.json
Navigate to https in the profiles and remove the ASPNETCORE_ENVIRONMENT.
1
2
3
|
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": ""
}
|
And also open program.cs file and comment the if statement that only enables Swagger UI for development environment, since we want to test our application on a different environment.
1
2
3
4
5
6
|
// Configure the HTTP request pipeline.
//if (app.Environment.IsDevelopment())
//{
app.UseSwagger();
app.UseSwaggerUI();
//}
|
Now let’s run the API and test with Swagger:
And the result is:
Why 404?
Because in the appsetting.json file we defined the setting “TestingEndpointEnabled” as false, while it was true in the appsettings.development.json file.
More on Options Pattern
One of the recommended and heavily used patterns to load the appsettings.json with its different environment variations, is through the options pattern.
Options pattern is a design pattern that is mainly used in ASP.NET Core to bind and group configurations that are defined in appsettings.json to strongly-typed classes in C#
This is rather helpful when you want to use your configurations in different parts of your ASP.NET Core application.
Earlier in this tutorial, we have seen how we can use IOptions to bind the ApiSettings section of the appsettings.json with the ApiSettingsModel and inject it across the different components of the ASP.NET Core Web API project, and these include the Controller, Middleware, Services.
In fact, with the great dependency injection feature of ASP.NET Core, you can inject the IOptions virtually anywhere in your application.
There are 3 different types and Interfaces for Options, all 3 of them have very similar functionality of loading the appsettings.json file and binding to a strongly-typed models.
IOptions
This is the most commonly used pattern, the IOptions service is injected as singleton instance, so you can have the same value across all the different parts of your project and during the lifetime of the application session, which can include multiple requests.
Even if you try changing the setting on the json file, the settings will remain the same until the session is terminated, (i.e.) the IIS pool that is hosting the ASP.NET Core application is recycled .
IOptionsSnapshot
This is injected as a scoped service, a scoped service would keep persisting the same values throughout the same HTTP request and across multiple requests for the same injected instance.
IOptionsMonitor
Similar to IOptions, this is injected as a singleton instance, with IOptionsMonitor you can have a live update for any changes related to the appsettings.json file
What makes this unique and very useful is in the extra method that it exposes:
As per the documentation:
1
2
|
public IDisposable? OnChange (Action<out toptions,string?=""> listener);
</out>
|
It registers a listener (callback event) whenever an option (or setting) is changed.
Also note that this method returns IDisposable, so this should be disposed to stop listening to changes and avoid memory issues.
Also you can access the values provided by the IOptionsMonitor using the Property CurrentValue rather than Value
Coding Exercise
To get your hand-dirty learning the about , I would like you to continue working on the source code with the below:
- Implement the above 2 other types of the Options Pattern
- Run the API with Swagger
- See how the updating the ApiSettings values would reflect on the endpoints behavior at runtime
Try this out and let me know your experience with the different types of Options.
Summary
In this tutorial we got introduced to a core component in the new versions of .NET and ASP.NET Core, which is the appsettings.json
It is a great way to implement settings with different environments in your application to build robust and flexible Web API
We learned how we can inject the appsettings.json ApiSettings section into your ASP.NET Core project and bind it to a strongly-typed model
Also we have seen how using the Options Pattern represented by IOptions<T> as one of the options types to inject a singleton instance of your bound ApiSettings section into different parts of your ASP.NET Core application
And that includes injecting them within Controllers, Middleware and Services
We tested the application using Swagger to see the appsettings.json working in action.
Lastly, we got introduced to the other types of Options Pattern, IOptionsSnapshot and IOptionsMonitor to
You can find the complete source code for this project in my GitHub Account.
References
Extensive read about Configurations in ASP.NET Core
Options pattern in ASP.NET Core
Collaborations
I am always open to discuss any protentional opportunities and collaborations.
Check this page to learn more about how we can benefit each other.
Sponsorships and Collaborations
Bonus
Enjoy the captivating tunes of this highly dynamic baroque masterpeice by George Frideric Handel
Air and Variations (The Harmonious Blacksmith) on harpsichord
Thanks for sharing such great information. It is really helpful to me. I always search to read the quality content and finally I found this in your post. Keep it up! Your work is so good your writing is so clear I liked it you are a great writer. I appreciate your work.
https://sensationsolutions.com/training/asp-dot-net-training-in-chandigarh/