FSharp.Configuration


The YamlConfig type provider

This tutorial shows the use of the YamlConfig type provider.

It's generated, hence the types can be used from any .NET language, not only from F# code.

It can produce mutable properties for Yaml scalars (leafs), which means the object tree can be loaded, modified and saved into the original file or a stream as Yaml text. Adding new properties is not supported, however lists can be replaced with new ones atomically. This is intentional, see below.

The main purpose for this is to be used as part of a statically typed application configuration system which would have a single master source of configuration structure - a Yaml file. Then any F#/C# project in a solution will able to use the generated read-only object graph.

When you push a system into production, you can modify the configs with scripts written in F# in safe, statically typed way with full intellisense.

Using Yaml type provider from F# scripts

Create a Config.yaml file like this:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
Mail:
    Smtp:
        Host: smtp.sample.com
        Port: 25
        User: user1
        Password: pass1
    Pop3:
        Host: pop3.sample.com
        Port: 110
        User: user2
        Password: pass2
        CheckPeriod: 00:01:00
    ErrorNotificationRecipients:
        - user1@sample.com
        - user2@sample.com
    ErrorMessageId: 9d165087-9b74-4313-ab90-89be897d3d93
DB:
    ConnectionString: Data Source=server1;Initial Catalog=Database1;Integrated Security=SSPI;
    NumberOfDeadlockRepeats: 5
    DefaultTimeout: 00:05:00

Reference the type provider assembly and configure it to use your yaml file:

1: 
2: 
3: 
4: 
5: 
6: 
#r "FSharp.Configuration.dll"
open FSharp.Configuration

// Let the type provider do it's work
type TestConfig = YamlConfig<"Config.yaml">
let config = TestConfig()

alt text

Reading and writing from the config

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
// read a value from the config
config.DB.ConnectionString

val it : string = 
  "Data Source=server1;Initial Catalog=Database1;Integrated Security=SSPI;"

// change a value in the config
config.DB.ConnectionString <- "Data Source=server2;"
config.DB.ConnectionString
val it : string = "Data Source=server2;"

// write the settings back to a yaml file
config.Save(__SOURCE_DIRECTORY__ + @"\ChangedConfig.yaml")

Using configuration from C#

Let's create a F# project named Config, add reference to FSharp.Configuration.dll, then add the following Config.yaml file:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
Mail:
  Smtp:
    Host: smtp.sample.com
    Port: 25
    User: user1
    Password: pass1
  Pop3:
    Host: pop3.sample.com
    Port: 110
    User: user2
    Password: pass2
    CheckPeriod: 00:01:00
  ErrorNotificationRecipients:
    - user1@sample.com
    - user2@sample.com
  ErrorMessageId: 9d165087-9b74-4313-ab90-89be897d3d93
DB:
  ConnectionString: Data Source=server1;Initial Catalog=Database1;Integrated Security=SSPI;
  NumberOfDeadlockRepeats: 5
  DefaultTimeout: 00:05:00

Declare a YamlConfig type and point it to the file above:

1: 
2: 
3: 
open FSharp.Configuration

type Config = YamlConfig<"Config.yaml">

Compile it. Now we have assembly Config.dll containing generated types with the default values "baked" into them (actually the values are set in the type constructors).

Let's test it in a C# project. Create a Console Application, add reference to FSharp.Configuration.dll and our F# Config project.

First, we'll try to create an instance of our generated Config type and check that all the values are there:

1: 
2: 
var config = new Config.Config();
Console.WriteLine(string.Format("Default configuration:\n{0}", config));

It should outputs this:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
Default settings:
Mail:
  Smtp:
    Host: smtp.sample.com
    Port: 25
    User: user1
    Password: pass1
  Pop3:
    Host: pop3.sample.com
    Port: 110
    User: user2
    Password: pass2
    CheckPeriod: 00:01:00
  ErrorNotificationRecipients:
  - user1@sample.com
  - user2@sample.com
  ErrorMessageId: 9d165087-9b74-4313-ab90-89be897d3d93
DB:
  ConnectionString: Data Source=server1;Initial Catalog=Database1;Integrated Security=SSPI;
  NumberOfDeadlockRepeats: 5
  DefaultTimeout: 00:05:00

And, of course, we now able to access all the config data in a nice typed way like this:

1: 
2: 
3: 
4: 
5: 
let pop3host = config.Mail.Pop3.Host
val pop3host : string = "pop3.sample.com"

let dbTimeout = config.DB.DefaultTimeout
val dbTimeout : System.TimeSpan = 00:05:00

It's not very interesting so far, as the main purpose of any configuration is to be loaded from a config file at runtime. So, add the following RuntimeConfig.yaml into the C# console project:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
Mail:
  Smtp:
    Host: smtp2.sample.com
    Port: 26
    User: user11
    Password: pass11
  Pop3:
    Host: pop32.sample.com
    Port: 111
    User: user2
    Password: pass2
    CheckPeriod: 00:02:00
  ErrorNotificationRecipients:
    - user11@sample.com
    - user22@sample.com
    - new_user@sample.com
  ErrorMessageId: 9d165087-9b74-4313-ab90-89be897d3d93
DB:
  ConnectionString: Data Source=server2;Initial Catalog=Database1;Integrated Security=SSPI;
  NumberOfDeadlockRepeats: 5
  DefaultTimeout: 00:10:00

We changed almost every setting here. Update our default config with this file:

1: 
2: 
3: 
4: 
// ...as before
config.Load(@"RuntimeConfig.yaml");
Console.WriteLine(string.Format("Loaded config:\n{0}", config));
Console.ReadLine();

The output should be:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
Loaded settings:
Mail:
  Smtp:
    Host: smtp2.sample.com
    Port: 26
    User: user11
    Password: pass11
  Pop3:
    Host: pop32.sample.com
    Port: 111
    User: user2
    Password: pass2
    CheckPeriod: 00:02:00
  ErrorNotificationRecipients:
  - user11@sample.com
  - user22@sample.com
  - new_user@sample.com
  ErrorMessageId: 9d165087-9b74-4313-ab90-89be897d3d93
DB:
  ConnectionString: Data Source=server2;Initial Catalog=Database1;Integrated Security=SSPI;
  NumberOfDeadlockRepeats: 5
  DefaultTimeout: 00:10:00

Great! Values have been updated properly, the new user has been added into ErrorNotificationRecipients list.

The Changed event

Every type in the hierarchy contains Changed: EventHandler event. It's raised when an instance is updated (Loaded), not when the writable properties are assigned.

Let's show the event in action:

 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: 
// ...reference assemblies and open namespaces as before...
let c = Config()
let log name _ = printfn "%s changed!" name
// add handlers for the root and all down the Mail hierarchy 
c.Changed.Add (log "ROOT")
c.Mail.Changed.Add (log "Mail")
c.Mail.Smtp.Changed.Add (log "Mail.Smtp")
c.Mail.Pop3.Changed.Add (log "Mail.Pop3")
// as a marker, add a handler for DB
c.DB.Changed.Add (log "DB")
c.LoadText """
Mail:
  Smtp:
    Host: smtp.sample.com
    Port: 25
    User:       => first changed value <=
    Password:   => second changed value on the same level (in the same Map) <=
    Ssl: true   
  Pop3:
    Host: pop3.sample.com
    Port: 110
    User: user2
    Password: pass2
    CheckPeriod: 00:01:00
  ErrorNotificationRecipients:
    - user1@sample.com
    - user2@sample.com
  ErrorMessageId: 9d165087-9b74-4313-ab90-89be897d3d93
DB:
  ConnectionString: Data Source=server1;Initial Catalog=Database1;Integrated Security=SSPI;
  NumberOfDeadlockRepeats: 5
  DefaultTimeout: 00:05:00
""" |> ignore

The output is as follows:

1: 
2: 
3: 
ROOT changed!
Mail changed!
Mail.Smtp changed!

So, we can see that all the events have been raised from the root's one down to the most close to the changed value one. And note that there're no duplicates - even though two value was changed in Mail.Smpt map, its Changed event has been raised only once.

Multiple items
namespace FSharp

--------------------
namespace Microsoft.FSharp
namespace FSharp.Configuration
Multiple items
type TestConfig =
  inherit Root
  new : unit -> TestConfig
  event Changed : EventHandler
  member DB : DB_Type
  member Error : IEvent<Exception>
  member Mail : Mail_Type
  nested type DB_Type
  nested type Mail_Type

Full name: YamlConfigProvider.TestConfig

--------------------
TestConfig() : TestConfig
type YamlConfig =
  inherit Root
  member Error : IEvent<Exception>

Full name: FSharp.Configuration.YamlConfig


<summary>Statically typed YAML config.</summary>
           <param name='FilePath'>Path to YAML file.</param>
           <param name='ReadOnly'>Whether the resulting properties will be read-only or not.</param>
           <param name='YamlText'>Yaml as text. Mutually exclusive with FilePath parameter.</param>
val config : TestConfig

Full name: YamlConfigProvider.config
property TestConfig.DB: TestConfig.DB_Type
property TestConfig.DB_Type.ConnectionString: string
member YamlConfigTypeProvider.Root.Save : unit -> unit
member YamlConfigTypeProvider.Root.Save : filePath:string -> unit
member YamlConfigTypeProvider.Root.Save : writer:System.IO.TextWriter -> unit
member YamlConfigTypeProvider.Root.Save : stream:System.IO.Stream -> unit
Multiple items
type Config =
  inherit Root
  new : unit -> Config
  event Changed : EventHandler
  member DB : DB_Type
  member Error : IEvent<Exception>
  member Mail : Mail_Type
  nested type DB_Type
  nested type Mail_Type

Full name: YamlConfigProvider.Config

--------------------
Config() : Config
val pop3host : string

Full name: YamlConfigProvider.pop3host
property TestConfig.Mail: TestConfig.Mail_Type
property TestConfig.Mail_Type.Pop3: TestConfig.Mail_Type.Pop3_Type
property TestConfig.Mail_Type.Pop3_Type.Host: string
val dbTimeout : System.TimeSpan

Full name: YamlConfigProvider.dbTimeout
property TestConfig.DB_Type.DefaultTimeout: System.TimeSpan
val c : Config

Full name: YamlConfigProvider.c
val log : name:string -> 'a -> unit

Full name: YamlConfigProvider.log
val name : string
val printfn : format:Printf.TextWriterFormat<'T> -> 'T

Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.printfn
event Config.Changed: IEvent<System.EventHandler,System.EventArgs>
member System.IObservable.Add : callback:('T -> unit) -> unit
property Config.Mail: Config.Mail_Type
event Config.Mail_Type.Changed: IEvent<System.EventHandler,System.EventArgs>
property Config.Mail_Type.Smtp: Config.Mail_Type.Smtp_Type
event Config.Mail_Type.Smtp_Type.Changed: IEvent<System.EventHandler,System.EventArgs>
property Config.Mail_Type.Pop3: Config.Mail_Type.Pop3_Type
event Config.Mail_Type.Pop3_Type.Changed: IEvent<System.EventHandler,System.EventArgs>
property Config.DB: Config.DB_Type
event Config.DB_Type.Changed: IEvent<System.EventHandler,System.EventArgs>
member YamlConfigTypeProvider.Root.LoadText : yamlText:string -> unit
val ignore : value:'T -> unit

Full name: Microsoft.FSharp.Core.Operators.ignore
Fork me on GitHub