Hello Configuration
Resources
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.
Line 2 - ConnectionStrings
is exactly as it sounds, it's the place to put connections to other services like a database.
Line 5 - AppSettings
is the default included location to put configuration values. Various ways of retrieving configuration values will default to this section
Line 8 - Perigee
is 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 - Serilog
is 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
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.
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:
The C# Type - This could be a string, or int, or whatever other value type you're reading.
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
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.
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.
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.
To bind a class, simply use the taskConfig
and call GetConfigurationAs<>()
:
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!
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.
Link to Config
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.
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.
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
.
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.
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:
If you're reading encrypted configuration values, there's a handy little helper than does the read and decrypt in one pass:
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.
Last updated