Thursday, September 29, 2011

A Simple AppSettings Type Provider

With the F# 3.0 Developer Preview now available, I've been spending a decent chunk of my free time playing with Type Providers. With the announcement of the introductory guide and samples for authoring type providers I've had a hard time putting down the computer to sleep (let alone eat).

After a fair amount of experimentation, I've written my first type provider. It is admittedly quite simple and isn't production quality, but it has been a fun exercise. The basic idea is to read the appSettings elements from a config file, parse the values appropriately, and interact with them in a type safe way.

The sample config file looks like this:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <appSettings>
        <add key="test2" value="Some Test Value 5"/>
        <add key="TestInt" value="102"/>
        <add key="TestBool" value="True"/>
        <add key="TestDouble" value="10.01"/>
    </appSettings>
</configuration>
Here's the TypeProvider code:

namespace AppSettingsTypeProvider
open System
open System.IO
open System.Reflection
open Samples.FSharpPreviewRelease2011.ProvidedTypes
open Microsoft.FSharp.Core.CompilerServices
open System.Configuration
[<TypeProvider>]
type public AppSettingsProvider(config:TypeProviderConfig) as this =
inherit TypeProviderForNamespaces()
let namespaceName = "AppSettingsTypeProvider"
let currentAssembly = Assembly.GetExecutingAssembly()
let tySettings = ProvidedTypeDefinition(currentAssembly, namespaceName, "AppSettings", Some typeof<obj>)
let addSettings (configFileName:string) (tyDef:ProvidedTypeDefinition) =
tyDef.AddXmlDocDelayed(fun () -> sprintf "Provides a strongly typed representation of the appSettings in a provided config file.")
let filePath = Path.Combine(config.ResolutionFolder, configFileName)
let fileMap = ExeConfigurationFileMap(ExeConfigFilename=filePath)
let appSettings = ConfigurationManager.OpenMappedExeConfiguration(fileMap, ConfigurationUserLevel.None).AppSettings.Settings
let tryParseWith func = func >> function
| true, value -> Some value
| false, _ -> None
let (|Bool|_|) = tryParseWith Boolean.TryParse
let (|Int|_|) = tryParseWith Int32.TryParse
let (|Double|_|) = tryParseWith Double.TryParse
do Seq.iter (fun (key) ->
let keyElement = match (appSettings.Item key).Value with
| Int fieldValue -> ProvidedLiteralField(key, typeof<int>, fieldValue)
| Bool fieldValue -> ProvidedLiteralField(key, typeof<bool>, fieldValue)
| Double fieldValue -> ProvidedLiteralField(key, typeof<Double>, fieldValue)
| fieldValue -> ProvidedLiteralField(key, typeof<string>, fieldValue.ToString())
do keyElement.AddXmlDocDelayed(fun () -> sprintf "Returns the value from the appSetting with key %s" key)
do tyDef.AddMember keyElement ) appSettings.AllKeys
let staticParams = [ProvidedStaticParameter("configFilePath", typeof<string>)]
do tySettings.DefineStaticParameters(staticParams,
fun typeName args ->
match args with
| [| :? string as configFileName |] ->
let ty = ProvidedTypeDefinition(assembly = currentAssembly,
namespaceName = namespaceName, typeName = typeName,
baseType = Some typeof<obj>, HideObjectMethods = true)
addSettings configFileName ty
ty
| _ -> failwith "unexpected parameter values")
do this.AddNamespace(namespaceName, [tySettings])
[<assembly:TypeProviderAssembly>]
do()
Lastly, here's an example of how to use it.
#r "System"
#r @"bin\debug\AppSettingsTypeProvider.dll"

open System
open AppSettingsTypeProvider

type settings = AppSettings<"App.config">
printfn "Test2 String: %s" settings.test2 
printfn "Test Int: %i" settings.TestInt 
printfn "Test Bool: %b" settings.TestBool  
printfn "Test Double: %f" settings.TestDouble