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.
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()
|
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")
|
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.
Every type in the hierarchy contains Changed: EventHandler
event. It's raised when an instance is updated (Load
ed), 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