FsConfig is a F# library for reading configuration data from environment variables and AppSettings with type safety

PM> Install-Package FsConfig The FsConfig library can be installed from NuGet

To understand FsConfig, let's have a look at an use case from the FsTweet application.

The FsTweet application follows The Twelve-Factor App guideline for managing the configuration data. During the application bootstrap, it retrieves its ten configuration parameters from their respective environment variables.

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: open System let main argv = let fsTweetConnString = Environment . GetEnvironmentVariable "FSTWEET_DB_CONN_STRING" let serverToken = Environment . GetEnvironmentVariable "FSTWEET_POSTMARK_SERVER_TOKEN" let senderEmailAddress = Environment . GetEnvironmentVariable "FSTWEET_SENDER_EMAIL_ADDRESS" let env = Environment . GetEnvironmentVariable "FSTWEET_ENVIRONMENT" let streamConfig : GetStream . Config = { ApiKey = Environment . GetEnvironmentVariable "FSTWEET_STREAM_KEY" ApiSecret = Environment . GetEnvironmentVariable "FSTWEET_STREAM_SECRET" AppId = Environment . GetEnvironmentVariable "FSTWEET_STREAM_APP_ID" } let serverKey = Environment . GetEnvironmentVariable "FSTWEET_SERVER_KEY" let port = Environment . GetEnvironmentVariable "PORT" |> uint16 0

Though the code snippet does the job, there are some shortcomings.

The code is verbose. There is no error handling to deal with the absence of values or wrong values. Explicit type casting

With the help of FsConfig, we can overcome these limitations by specifying the configuration data as a F# Record type.

1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: type StreamConfig = { Key : string Secret : string AppId : string } [< Convention ( "FSTWEET" )>] type Config = { DbConnString : string PostmarkServerToken : string SenderEmailAddress : string ServerKey : string Environment : string [< CustomName ( "PORT" )>] Port : uint16 Stream : StreamConfig }

And then read all the associated environment variables in a single function call with type safety and error handling!

1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: let main argv = 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" envVarName value | NotSupported msg -> failwith msg

FsConfig supports the following data types and leverages their respective TryParse function to do the type conversion.

Int16 , Int32 , Int64 , UInt16 , UInt32 , UInt64

, , , , , Byte , SByte

, Single , Double , Decimal

, , Char , String

, Bool

TimeSpan , DateTimeOffset , DateTime

, , Guid

Enum

list of all the above types

of all the above types option of all the above types

FsConfig allows us to specify optional configuration parameters using the option type. In the previous example, if the configuration parameter Port is optional, we can define it like

1: 2: 3: 4: 5: type Config = { ... - Port : uint16 + Port : uint16 option }

FsConfig supports Discriminated Union Types that has cases alone.

1: 2: 3: 4: 5: 6: 7: 8: type Color = | Red | Green | Blue type Config = { ConsoleColor : Color }

With this configuration declaration, FsConfig read the environment variable CONSOLE_COLOR and populates the ConsoleColor field of type Color . List of Discriminated Union Types also supported: see under List Type

FsConfig can read enumerations using either their numeric values or their case names.

The [<Flags>] attribute is also supported, using only comma-separated values.

1: 2: 3: 4: 5: 6: 7: 8: 9: [< Flags >] type Status = | Inactive = 0 | Receiving = 1 | Transmitting = 2 type Config = { Status : Status }

This will result in the status Receiving ||| Transmitting 1: STATUS=Receiving,Transmitting This will result in the status Inactive 1: STATUS=0

When parsing enumerations or discriminated unions, FsConfig will first look for an exact match (case sensitive), but if no matches are found, it will also accept a case-insensitive match.

1: 2: 3: 4: 5: 6: 7: 8: 9: type LogLevel = | Debug = 0 | Informative = 1 | Warning = 2 | Error = 3 type Config = { LogLevel : LogLevel }

This is a valid configuration 1: LOG_LEVEL=warning

FsConfig also supports list type, and it expects comma separated individual values.

For example, to get mulitple ports, we can define the config as

1: 2: 3: type Config = { Port : uint16 list }

and then pass the value 8084,8085,8080 using the environment variable PORT .

The default separator for the list can be changed if needed using the ListSeparator attribute.

1: 2: 3: 4: 5: 6: 7: 8: [< Convention ( "MYENV" )>] type CustomListSeparatorSampleConfig = { ProcessNames : string list [< ListSeparator ( ';' )>] ProcessIds : uint16 list [< ListSeparator ( '|' )>] PipedFlow : int list }

With this configuration declaration, FSConfig would be able to read the following entries from App.settings.

1: 2: 3: < add key ="MYENVProcessNames" value ="conhost.exe,gitter.exe" /> < add key ="MYENVProcessIds" value ="4700;15680" /> < add key ="MYENVPipedFlow" value ="4700|15680|-1" />

A definition similar to the one shown below will allow parsing of standalone lists.

1: 2: 3: 4: type IntListUsingSemiColonsConfig = { [< ListSeparator ( ';' )>] IntListUp : int list }

E.g. an environment variable containing 1: INT_LIST_UP=42;43;44

As shown in the initial example, FsConfig allows us to group similar configuration into a record type.

1: 2: 3: 4: 5: 6: 7: 8: 9: type AwsConfig = { AccessKeyId : string DefaultRegion : string SecretAccessKey : string } type Config = { Aws : AwsConfig }

With this configuration declaration, FsConfig read the environment variables AWS_ACCESS_KEY_ID , AWS_SECRET_ACCESS_KEY , and AWS_DEFAULT_REGION and populates the Aws field of type AwsConfig .

If you'd like to use a default value in the absence of a field value, you can make use of the DefaultValue attribute.

1: 2: 3: 4: 5: 6: type Config = { [< DefaultValue ( "8080" )>] HttpServerPort : int16 [< DefaultValue ( "Server=localhost;Port=5432;Database=FsTweet;User Id=postgres;Password=test;" )>] DbConnectionString : string }

By default, FsConfig follows Underscores with uppercase convention, as in UPPER_CASE , for deriving the environment variable name.

1: 2: 3: type Config = { ServerKey : string }

Using this configuration declaration, FsConfig read the environment variable SERVER_KEY and populates the ServerKey field

To specify a custom prefix in the environment variables, we can make use of the Convention attribute.

1: 2: 3: 4: [< Convention ( "FSTWEET" )>] type Config = { ServerKey : string }

For this configuration declaration, FsConfig read the environment variable FSTWEET_SERVER_KEY and populates the ServerKey field.

We can also override the separator character _ using the Convention attribute's optional field Separator

1: 2: 3: 4: [< Convention ( "FSTWEET" , Separator = "-" )>] type Config = { ServerKey : string }

In this case, FsConfig derives the environment variable name as FSTWEET-SERVER-KEY .

If an environment variable name is not following a convention, we can override the environment variable name at the field level using the CustomName attribute.

1: 2: 3: 4: type Config = { [< CustomName ( "MY_SERVER_KEY" )>] ServerKey : string }

Here, FsConfig uses the environment variable name MY_SERVER_KEY to get the ServerKey.

We can also merely customise (or control) the environment variable name generation by passing an higher-order function while calling the Get function

1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: open FsConfig // Prefix -> string -> string let lowerCaseConfigNameCanonicalizer ( Prefix prefix ) ( name : string ) = let lowerCaseName = name . ToLowerInvariant () if String . IsNullOrEmpty prefix then name . ToLowerInvariant () else sprintf "%s-%s" ( prefix . ToLowerInvariant ()) lowerCaseName [< Convention ( "FSTWEET" )>] type Config = { ServerKey : string } let main argv = let config = match EnvConfig . Get < Config > lowerCaseConfigNameCanonicalizer with | Ok config -> config | Error error -> failwithf "Error : %A " error 0

FsConfig computes the environment variable name as fstweet-server-key in this scenario.

FsConfig also supports reading value directly by explicitly specifying the environment variable name

1: EnvConfig . Get < decimal > "MY_APP_INITIAL_BALANCE" // Result<decimal, ConfigParseError>

FsConfig supports App Config for both DotNet Core and Non DotNet Core Applications.

FsConfig abstracts the configuration provider by depending on IConfigurationRoot.

1: 2: let configurationRoot : IConfigurationRoot = "{...}" let appConfig = new AppConfig ( configurationRoot )

After creating an instance appConfig (of type AppConfig from FsConfig), you can use it to read the configuration values as 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: 26: 27: 28: 29: // Reading Primitive let result = appConfig . Get < int > "processId" // Result<int, ConfigParseError> // A Sample Record type SampleConfig = { ProcessId : int ProcessName : string } // Reading a Record type let result = appConfig . Get < SampleConfig > () // Result<SampleConfig, ConfigParseError> // A Sample Nested Record type AwsConfig = { AccessKeyId : string DefaultRegion : string SecretAccessKey : string } type Config = { MagicNumber : int Aws : AwsConfig } // Reading a Nested Record type let result = appConfig . Get < Config > () // Result<Config, ConfigParseError>

Refer below for creating configurationRoot based on the file type and using FsConfig to read the values.

1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: { "processId" : "123", "processName" : "FsConfig", "magicNumber" : 42, "aws" : { "accessKeyId" : "Id-123", "defaultRegion" : "us-east-1", "secretAccessKey" : "secret123" }, "colors" : "Red,Green" }

This JSON file can be read using

1: 2: 3: 4: 5: 6: 7: 8: 9: // Requires NuGet package // Microsoft.Extensions.Configuration.Json let configurationRoot = ConfigurationBuilder () . SetBasePath ( Directory . GetCurrentDirectory ()) . AddJsonFile ( "settings.json" ) . Build () let appConfig = new AppConfig ( configurationRoot ) let result = appConfig . Get < Config > () // Result<Config, ConfigParseError>

1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: < Settings > < ProcessId > 123 </ ProcessId > < ProcessName > FsConfig </ ProcessName > < MagicNumber > 42 </ MagicNumber > < Aws > < AccessKeyId > Id-123 </ AccessKeyId > < DefaultRegion > us-east-1 </ DefaultRegion > < SecretAccessKey > secret123 </ SecretAccessKey > </ Aws > < Colors > Red,Green </ Colors > </ Settings >

This XML file can be read using

1: 2: 3: 4: 5: 6: 7: 8: 9: // Requires NuGet package // Microsoft.Extensions.Configuration.Xml let configurationRoot = ConfigurationBuilder () . SetBasePath ( Directory . GetCurrentDirectory ()) . AddXmlFile ( "settings.xml" ) . Build () let appConfig = new AppConfig ( configurationRoot ) let result = appConfig . Get < Config > () // Result<Config, ConfigParseError>

1: 2: 3: 4: 5: 6: 7: 8: 9: ProcessId=123 ProcessName=FsConfig MagicNumber=42 Colors=Red,Green [Aws] AccessKeyId=Id-123 DefaultRegion=us-east-1 SecretAccessKey=secret123

This INI file can be read using

1: 2: 3: 4: 5: 6: 7: 8: 9: // Requires NuGet package // Microsoft.Extensions.Configuration.Ini let configurationRoot = ConfigurationBuilder () . SetBasePath ( Directory . GetCurrentDirectory ()) . AddIniFile ( "settings.ini" ) . Build () let appConfig = new AppConfig ( configurationRoot ) let result = appConfig . Get < Config > () // Result<Config, ConfigParseError>

We can read the appSettings values using the AppConfig type instead of EnvConfig type.

FsConfig uses the exact name of the field to derive the appSettings key name and doesn't use any separator by default.

1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: type AwsConfig = { AccessKeyId : string DefaultRegion : string SecretAccessKey : string } type Config = { Port : uint16 Aws : AwsConfig } let main argv = let config = match AppConfig . Get < Config > () with | Ok config -> config | Error error -> failwithf "Error : %A " error 0

The above code snippet looks for appSettings values with the name Port , AwsAccessKeyId , AwsDefaultRegion , AwsSecretAccessKey and populates the respective fields.

All the customisation that we have seen for EnvConfig is applicable for AppConfig as well.

If you are curious to know how FsConfig works and its internals then you might be interested in my blog post, Generic Programming Made Easy that deep dives into the initial implementation of FsConfig.

The idea of FsConfig is inspired by Kelsey Hightower's golang library envconfig.

FsConfig uses Eirik Tsarpalis's TypeShape library for generic programming.

The project is hosted on GitHub where you can report issues, fork the project and submit pull requests. If you're adding a new public API, please also consider adding samples that can be turned into a documentation. You might also want to read the library design notes to understand how it works.

The library is available under Public Domain license, which allows modification and redistribution for both commercial and non-commercial purposes. For more information see the License file in the GitHub repository.

namespace System

val main : argv:'a -> int



Full name: Index.main

val argv : 'a

val fsTweetConnString : string

type Environment =

static member CommandLine : string

static member CurrentDirectory : string with get, set

static member CurrentManagedThreadId : int

static member Exit : exitCode:int -> unit

static member ExitCode : int with get, set

static member ExpandEnvironmentVariables : name:string -> string

static member FailFast : message:string -> unit + 1 overload

static member GetCommandLineArgs : unit -> string[]

static member GetEnvironmentVariable : variable:string -> string + 1 overload

static member GetEnvironmentVariables : unit -> IDictionary + 1 overload

...

nested type SpecialFolder

nested type SpecialFolderOption



Full name: System.Environment

Environment.GetEnvironmentVariable(variable: string) : string

Environment.GetEnvironmentVariable(variable: string, target: EnvironmentVariableTarget) : string

val serverToken : string

val senderEmailAddress : string

val env : string

val streamConfig : obj

val serverKey : string

val port : uint16

Multiple items

val uint16 : value:'T -> uint16 (requires member op_Explicit)



Full name: Microsoft.FSharp.Core.Operators.uint16



--------------------

type uint16 = UInt16



Full name: Microsoft.FSharp.Core.uint16

type StreamConfig =

{Key: string;

Secret: string;

AppId: string;}



Full name: Index.StreamConfig

StreamConfig.Key: string

Multiple items

val string : value:'T -> string



Full name: Microsoft.FSharp.Core.Operators.string



--------------------

type string = String



Full name: Microsoft.FSharp.Core.string

StreamConfig.Secret: string

StreamConfig.AppId: string

type Config =

{DbConnString: string;

PostmarkServerToken: string;

SenderEmailAddress: string;

ServerKey: string;

Environment: string;

Port: uint16;

Stream: StreamConfig;}



Full name: Index.Config

Config.DbConnString: string

Config.PostmarkServerToken: string

Config.SenderEmailAddress: string

Config.ServerKey: string

Multiple items

Config.Environment: string



--------------------

type Environment =

static member CommandLine : string

static member CurrentDirectory : string with get, set

static member CurrentManagedThreadId : int

static member Exit : exitCode:int -> unit

static member ExitCode : int with get, set

static member ExpandEnvironmentVariables : name:string -> string

static member FailFast : message:string -> unit + 1 overload

static member GetCommandLineArgs : unit -> string[]

static member GetEnvironmentVariable : variable:string -> string + 1 overload

static member GetEnvironmentVariables : unit -> IDictionary + 1 overload

...

nested type SpecialFolder

nested type SpecialFolderOption



Full name: System.Environment

Config.Port: uint16

Config.Stream: StreamConfig

val main : argv:'a -> 'b



Full name: Index.main

val config : obj

union case Result.Ok: ResultValue: 'T -> Result<'T,'TError>

union case Result.Error: ErrorValue: 'TError -> Result<'T,'TError>

val error : obj

val failwithf : format:Printf.StringFormat<'T,'Result> -> 'T



Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.failwithf

val failwith : message:string -> 'T



Full name: Microsoft.FSharp.Core.Operators.failwith

type Color =

| Red

| Green

| Blue



Full name: Index.Color

union case Color.Red: Color

union case Color.Green: Color

union case Color.Blue: Color

type ConsoleColor =

| Black = 0

| DarkBlue = 1

| DarkGreen = 2

| DarkCyan = 3

| DarkRed = 4

| DarkMagenta = 5

| DarkYellow = 6

| Gray = 7

| DarkGray = 8

| Blue = 9

...



Full name: System.ConsoleColor

Multiple items

type FlagsAttribute =

inherit Attribute

new : unit -> FlagsAttribute



Full name: System.FlagsAttribute



--------------------

FlagsAttribute() : unit

type Status =

| Inactive = 0

| Receiving = 1

| Transmitting = 2



Full name: Index.Status

Status.Inactive: Status = 0

Status.Receiving: Status = 1

Status.Transmitting: Status = 2

type LogLevel =

| Debug = 0

| Informative = 1

| Warning = 2

| Error = 3



Full name: Index.LogLevel

LogLevel.Debug: LogLevel = 0

LogLevel.Informative: LogLevel = 1

LogLevel.Warning: LogLevel = 2

LogLevel.Error: LogLevel = 3

type 'T list = List<'T>



Full name: Microsoft.FSharp.Collections.list<_>

type CustomListSeparatorSampleConfig =

{ProcessNames: string list;

ProcessIds: uint16 list;

PipedFlow: int list;}



Full name: Index.CustomListSeparatorSampleConfig

CustomListSeparatorSampleConfig.ProcessNames: string list

CustomListSeparatorSampleConfig.ProcessIds: uint16 list

CustomListSeparatorSampleConfig.PipedFlow: int list

Multiple items

val int : value:'T -> int (requires member op_Explicit)



Full name: Microsoft.FSharp.Core.Operators.int



--------------------

type int = int32



Full name: Microsoft.FSharp.Core.int



--------------------

type int<'Measure> = int



Full name: Microsoft.FSharp.Core.int<_>

type IntListUsingSemiColonsConfig =

{IntListUp: int list;}



Full name: Index.IntListUsingSemiColonsConfig

IntListUsingSemiColonsConfig.IntListUp: int list

type AwsConfig =

{AccessKeyId: string;

DefaultRegion: string;

SecretAccessKey: string;}



Full name: Index.AwsConfig

AwsConfig.AccessKeyId: string

AwsConfig.DefaultRegion: string

AwsConfig.SecretAccessKey: string

Multiple items

type DefaultValueAttribute =

inherit Attribute

new : unit -> DefaultValueAttribute

new : check:bool -> DefaultValueAttribute

member Check : bool



Full name: Microsoft.FSharp.Core.DefaultValueAttribute



--------------------

new : unit -> DefaultValueAttribute

new : check:bool -> DefaultValueAttribute

Multiple items

val int16 : value:'T -> int16 (requires member op_Explicit)



Full name: Microsoft.FSharp.Core.Operators.int16



--------------------

type int16 = Int16



Full name: Microsoft.FSharp.Core.int16



--------------------

type int16<'Measure> = int16



Full name: Microsoft.FSharp.Core.int16<_>

val lowerCaseConfigNameCanonicalizer : 'a -> name:string -> 'b



Full name: Index.lowerCaseConfigNameCanonicalizer

val name : string

String.ToLowerInvariant() : string

Multiple items

type String =

new : value:char -> string + 8 overloads

member Chars : int -> char

member Clone : unit -> obj

member CompareTo : value:obj -> int + 1 overload

member Contains : value:string -> bool

member CopyTo : sourceIndex:int * destination:char[] * destinationIndex:int * count:int -> unit

member EndsWith : value:string -> bool + 2 overloads

member Equals : obj:obj -> bool + 2 overloads

member GetEnumerator : unit -> CharEnumerator

member GetHashCode : unit -> int

...



Full name: System.String



--------------------

String(value: nativeptr<char>) : unit

String(value: nativeptr<sbyte>) : unit

String(value: char []) : unit

String(value: ReadOnlySpan<char>) : unit

String(c: char, count: int) : unit

String(value: nativeptr<char>, startIndex: int, length: int) : unit

String(value: nativeptr<sbyte>, startIndex: int, length: int) : unit

String(value: char [], startIndex: int, length: int) : unit

String(value: nativeptr<sbyte>, startIndex: int, length: int, enc: Text.Encoding) : unit

String.IsNullOrEmpty(value: string) : bool

val sprintf : format:Printf.StringFormat<'T> -> 'T



Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.sprintf

Multiple items

val decimal : value:'T -> decimal (requires member op_Explicit)



Full name: Microsoft.FSharp.Core.Operators.decimal



--------------------

type decimal = Decimal



Full name: Microsoft.FSharp.Core.decimal



--------------------

type decimal<'Measure> = decimal



Full name: Microsoft.FSharp.Core.decimal<_>

val configurationRoot : string



Full name: Index.configurationRoot

val appConfig : obj



Full name: Index.appConfig

val result : obj



Full name: Index.result

type SampleConfig =

{ProcessId: int;

ProcessName: string;}



Full name: Index.SampleConfig

SampleConfig.ProcessId: int

SampleConfig.ProcessName: string

val configurationRoot : obj



Full name: Index.configurationRoot