Often you want the behaviour of your application to be configurable. Maybe you want to have the same executable in different environments interacting with different services, or to make a parameter tunable by the ops team without needing to redeploy your app. ASP.NET Core makes a variety of different configuration sources available, exposing them via the IConfiguration
interface.
At Compositional IT, we often add type extensions to IConfiguration
, to provide type-safe access to configuration settings. For example:
type Microsoft.Extensions.Configuration.IConfiguration with
member this.AdDomain = this.["AdDomain"]
Writing that mapping by hand is pretty easy. But if you'd rather mapping was done for you by convention, the interesting library FsConfig can help you out!
Getting Started
FsConfig is available as a NuGet package. If you're using SAFE Stack, you can add it via Paket:
dotnet paket add -p Server FsConfig
Demo
I've made a small sample repo demonstrating how to use FsConfig starting from the SAFE Template. The key changes are below.
First we open the namespace, which allows us to annotate a record type declaration with the Convention
attribute. FsConfig will combine this with the names of the record's fields when mapping environment variables to the fields.
open FsConfig
[<Convention("APP_NAME")>]
type Config = {
TodoPrefix: string
TodoSuffix: string
}
Next, we use EnvConfig.Get
to read values from environment variables into a record of the new Config
type.
let config =
match EnvConfig.Get<Config>() with
| Ok config -> config
| Error error ->
match error with
| NotFound envVarName -> failwithf "Environment variable %s not found" envVarName
| BadValue(envVarName, value) -> failwithf "Environment variable %s has invalid value %s" envVarName value
| NotSupported msg -> failwith msg
Because the binding is done up-front, you'll discover any invalid configuration immediately on app launch. Then, after passing the config through the various functions, we use it to add a prefix and suffix to the todos that we read from storage.
Storage.todos
|> Seq.map (fun t -> {
t with
Description = $"%s{config.TodoPrefix}%s{t.Description}%s{config.TodoSuffix}"
})
All that's left to do is set the environment variables and run the app:
export APP_NAME_TODO_PREFIX='TODO: '; export APP_NAME_TODO_SUFFIX=' 💪'; dotnet run
Conclusion
That was a very quick introduction to FsConfig! We only scratched the surface, but already hints of its utility are apparent. Some other useful features that I've not shown here include:
- Parsing strings into other types such as
int
s andGuid
s. - Supporting optional configuration via the
option
type. - Supporting discriminated unions and lists!
- Setting default values for when a variable isn't present in the environment.
- Reading from variables that don't follow the convention you've specified.
- Reading from appSettings files.
- And more!
Check out the README for more details.
Thanks to demystifyfp and the other contributors for a great library!