Ask or search…
K
Links

Hello Configuration

Resources

Want to watch the config and linking instead?
2_Configuration.zip
2KB
Binary
Completed part 2 code

Intro

Lets take a closer look at how the configuration works within Perigee.
To understand the configuration process we need to understand how the sections work and why we separate them. To keep it concise, sections create a logical separation of values for the different processes in our application.
As an example we have a section called Serilog. This section is entirely dedicated to setting up our logging, where we send logs to and in what format. It wouldn't make any sense to put a message queue URL or an API key inside of this section.
We create a logical separation between our logging needs and a specific threads' requirements by keeping those configuration values separate.
Configuration sections can be registered in our application in a number of ways including but not limited to:
  • Environment variables
  • main(string[] args)
  • appsettings.json file
  • Custom providers
There is a custom provider shipped with Perigee called the SQLConfigurationSource. It allows you to quickly wire up a MSSQL database table as a configuration section to use in your application. You can see this example here
Back in the Installation and Project Setup we used a configuration file. A section is a key off of the root json node and as you can see there are 4 different sections in our startup file.
{
"ConnectionStrings": {
},
"AppSettings": {
},
"Perigee": { "HideConsole": false },
"Serilog": {
"MinimumLevel": "Debug",
"WriteTo": [
{ "Name": "Console" }
]
}
}
Line 2 - ConnectionStrings is exactly as it sounds, it's the place to put connections to other services like a database.
Line 5 - AppSettingsis the default included location to put configuration values. Various ways of retrieving configuration values will default to this section
Line 8 - Perigeeis the section automatically read by Perigee itself. As an example, if you're running in a windows console you can hide it by setting the HideConsole flag to true.
Line 9 - Serilogis the configuration section for Serilog logging. There's a lot of info on this section, and we'll cover a lot more of them in Hello Logs

Demo Application

This is the final screenshot after all sections below have been completed. If your log doesn't look like this yet, keep going :)
Our goal is to write an application that logs out our custom configuration sections. We will do so by reading the configuration directly at runtime as well as binding the configuration section to a custom class.
You will also see some helpful ways you can plug the configuration system into managed threads

Configuration Direct Read

To start, we need to add another section to our appsettings.json file called HelloConfig and give it some values to read.
{
"ConnectionStrings": {
},
"AppSettings": {
},
"HelloConfig": {
"Enabled": true,
"Name": "HAL 9000",
"Year": 2001,
"Tags": [ "Heuristic", "Algorithmic" ]
},
"Perigee": { "HideConsole": false },
"Serilog": {
"MinimumLevel": "Debug",
"WriteTo": [
{ "Name": "Console" }
]
}
}
Notice the comma after the section? Make sure when you add new sections you keep the file valid JSON
🖖
This creates a section for us to read in our code. We will read it directly by calling taskConfig.GetValue<>().
This handy method is a quick way to reference ALL of the incoming configuration providers and it works by supplying two things:
  1. 1.
    The C# Type - This could be a string, or int, or whatever other value type you're reading.
  2. 2.
    A special format: Section:Key.
To read the Name from our configuration section the Type would be string and our Section:Key would be: HelloConfig:Name
Here's a fully working Perigee application
PerigeeApplication.ApplicationNoInit("HelloConfig", (taskConfig) => {
taskConfig.AddRecurring("TestMethod", (ct, log) => {
log.LogInformation("What is my name? It is {name}", taskConfig.GetValue<string>("HelloConfig:Name"));
});
});
  • Line 1 - This is a simplified Perigee start method that skips IPC tokens and initialization actions.
  • Line 3 - AddRecurring simply adds a managed thread that is automatically called every n milliseconds (supplied by an optional parameter, default 5000 (5 seconds))
  • Line 5 - We log out the configuration value reading it live at runtime.
The regular .Add() method doesn't recur. If the block exits then Perigee will consider that thread to be ended and it will enter its restart phase and conditions.
This is why we use .AddRecurring() here, because the efficient loop is automatically built into it.
If a cancellation request comes through, it will not call the block again and end. You can always use the same built in loop in your own code and it's what we recommend if you're adding your own managed threads.
while (PerigeeApplication.delayOrCancel(delay, cancelToken)) {
// This is only called after delay, and if it's not cancelled
// Otherwise, the loop block ends
}
You'll see below the results of running this application. We get a log every 5 seconds with the value captured from our configuration file.

Configuration Binding

Next let's look at binding the section we added to a custom class. Here's the file if you prefer to import it.
HelloConfig.cs
273B
Text
If you import the class file above, don't forget to change the namespace if you want it to auto-link to whatever your applications default namespace is.
The class simply has 3 properties that are an exact case sensitive name match to the configuration section we created.
public class HelloConfig
{
public string Name { get; set; } // HelloConfig:Name
public int Year { get; set; } // HelloConfig:Year
public List<string> Tags { get; set; } // HelloConfig:Tags
}
To bind a class, simply use the taskConfig and call GetConfigurationAs<>():
taskConfig.GetConfigurationAs<HelloConfig>("HelloConfig");
  • The Generic T Parmater, HelloConfig, is the class we're creating and assigning. It's a type reference.
  • The string, "HelloConfig", is what section to get from our configuration so that we can assign it's properties to the referenced type.
This is all that is needed to bind our configuration section to a class. Here's the full application up to this point!
PerigeeApplication.ApplicationNoInit("HelloConfig", (taskConfig) => {
taskConfig.AddRecurring("TestMethod", (ct, log) => {
//Directly by reading
log.LogInformation("What is my name? It is {name}", taskConfig.GetValue<string>("HelloConfig:Name"));
//Binding a class
HelloConfig config = taskConfig.GetConfigurationAs<HelloConfig>("HelloConfig");
log.LogInformation("{name} first appeared in {year:N0} and was {@tagged}", config.Name, config.Year, config.Tags);
});
});
Line 10 - We got the configuration as a custom class by binding to it.
Line 11 - We use the custom classes properties to write to our log.
Curious what those special characters are in the template string?
The first one, :N0 tells the template string to treat it's input as a number with 0 precision, adding the grouping character (thousands separator). This is why the logged out version has a thousands separator.
The second one has an @ symbol which tells the template string to deconstruct the input. This is why you see the array of tags written out.
Perigee will, by default, hot-reload the appsettings.json files. This is a fantastic way to give control to a configuration file to enable or disable a thread without shutting down the entire application.
Perigee has a great helper method built into it which enables the thread-to-configuration linking for enabling or disabling managed threads through a configuration boolean.
Our example config section we added (HelloConfig) which has a key called Enabled, and we're going to use that Boolean to tell Perigee whether or not to start or stop our thread.
1
PerigeeApplication.ApplicationNoInit("HelloConfig", (taskConfig) => {
2
3
taskConfig.AddRecurring("TestMethod", (ct, log) => {
4
5
//Directly by reading
6
log.LogInformation("What is my name? It is {name}", taskConfig.GetValue<string>("HelloConfig:Name"));
7
8
//Binding a class
9
HelloConfig config = taskConfig.GetConfigurationAs<HelloConfig>("HelloConfig");
10
log.LogInformation("{name} first appeared in {year:N0} and was {@tagged}", config.Name, config.Year, config.Tags);
11
12
}, started: false).LinkToConfig("HelloConfig:Enabled");
13
});
Line 12 - We added started: false to the methods optional parameter so that with the addition of the linking command, it will be in charge of starting or stopping this thread. LinkToConfig was added and we supplied our Section:Key to it.
Because configurations are loaded, hot-reloaded, and maintained internally by the configuration system, please also use the started: false flag on the thread itself. This will prevent the thread from starting while the configuration value is "false".
Now if you run the application and navigate over to the debug folder to open up the appsettings.json file, you can change that HelloConfig:Enabled value from true to false or false to true and watch Perigee start and gracefully stop that thread
🧙
🪄
The "debugging" version of the app config should be at: {projectRoot}\bin\Debug\net{version}\appsettings.json
If you change the appsettings.json file in your project you aren't actually modifying the version of the file the debugged application is reading.

Change Notification

If you'd prefer more control over how to start or stop your threads, or even make different decisions based on the configuration changes, you can always subscribe the change notification directly. .LinkToConfig() is essentially the same code as below but managed by Perigee internally and automatically, saving you from a few lines of code.
taskConfig.Event_ConfigurationUpdated += (sender, Configuration) =>
{
bool enabled = Configuration.GetValue<bool>("HelloConfig:Enabled", false);
if (enabled) taskConfig.StartIfNotRunning("TestMethod");
else taskConfig.QueueStop("TestMethod", true);
};
To see other thread operations, see ThreadRegistry.

Configuring the logging

Perigee will automatically add an additional config property block to each of it's loggers that it passes to the managed threads. This property block is called ThreadName. As a fun aside, let's update our logging template to include this property block.
If you want to read up on all of the configurations for the console, simply go to that sinks dedicated support page.
Open the appsettings.json file and under the Serilog section, update the console sink to include the updated outputTemplate.
{
"Name": "Console",
"Args": {
"outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}]({ThreadName}) {Message:lj}{NewLine}{Exception}"
}
}
Now our application's logs have the name of the managed thread they are coming from right in the log!
You can add your own properties to the loggers, we'll show you how on the Hello Logs page.

Secure Values

Perigee has several ways built in that allow you to encrypt your configuration values which may provide an extra layer of security/obscurity. It uses AES256 bit encryption on the values for secure encrypting and decrypting values and has built in methods for reading them.
To secure a value you must provide the encryption key, encryption IV, and the source of where to find these values.
There are 3 sources for the Encryption keys
  • Environment Variable source
  • Arguments Source
  • Variable Source
Here's an example of setting this up all within code using the variable source. The methods are exactly the same though if you decide to pass in the encryption key from an argument, or read it from an environment variable.
PerigeeApplication.ApplicationNoInit("DemoApp", (c) => {
//Set the value source as variables, to which we assign next
c.SecureValueSource = ThreadRegistry.SecureKeyStorage.Variable;
//Create two new randoms, one AES 256 and the IV as AES 128
c.SecureValueKey = AesCrypto.GetNewRandom();
c.SecureValueIV = AesCrypto.GetNewRandom(16);
//Write out the two new generated values, so we can use them to decrypt values with later
c.GetLogger<Program>().LogInformation($"Copy these values out!!{Environment.NewLine}Key: {{key}}{Environment.NewLine}iv: {{iv}}", c.SecureValueKey, c.SecureValueIV);
});
With that done you should see something like this printed to the console:
These are your two encryption keys, and we will use them to decrypt values in the next step. Remember you can easily put these in the environment variables, command line arguments, or even set the variables in code. For the ease of this demonstration, we'll assign them directly:
PerigeeApplication.ApplicationNoInit("DemoApp", (c) => {
//Assign our keys
c.SecureValueSource = ThreadRegistry.SecureKeyStorage.Variable;
c.SecureValueKey = "e7zP37543Rbvn5a6NnN3FGlhPVAsdTmljcXZoTLOlkw=";
c.SecureValueIV = "UFn5LH+RLLCVkxt+qfjWKQ==";
//Encrypt and decrypt the same string
string encrypted = c.EncryptSecureValue("Hello World");
string decrypted = c.DecryptSecureValue(encrypted);
c.GetLogger<Program>().LogInformation($"Encrypted: {{Encrypted}}{Environment.NewLine}Decrypted: {{Decrypted}}", encrypted, decrypted);
});
If you're reading encrypted configuration values, there's a handy little helper than does the read and decrypt in one pass:
PerigeeApplication.ApplicationNoInit("DemoApp", (c) => {
c.GetSecureConfigurationValue("HelloConfig:EncryptedValue")
});
There's always an option of rolling your own or using custom methods of reading/decrypting values. If you want to see how to implement a custom property loader then check out the link!

Summary

Hopefully you understand a bit better about how configuration, sections, and providers work as well as how to use them in code.
PRO TIP: If you plan to support hot-reloading, never cache the configuration values in such a way that a hot-reload would be ignored. If you're binding a configuration section to a class do so at the time you need it and dispose of the class when you're finished so that in the event the configuration section is changed between iterations of your code you get the updated values by rebinding.