Chapter 5. Write Your Own Type Provider

There are four built-in type providers in F# 3.0. If you’ve wondered how to make a type provider that can be used to interpret data—such as the data in a database, in the cloud, or in an XML file—this chapter will show you how to do it.

What Is a Type Provider?

As I mentioned at the beginning of Chapter 4, the type provider is an adapter component that reads the underlying information schema and converts it to types in .NET. The information can be from data stores, services, or any other source of structured data. The type-generation process is known as the provided type. The type generated from a type provider can be picked up by Microsoft IntelliSense. In addition to the class type and members, the type provider can provide metadata such as a description of each column in a database table. These descriptions appear as tooltips. The type provider provides compile-time meta-programming support. This definition might seem heavy and difficult to digest. If that is the case, don’t worry. As more and more type provider samples are presented, different aspects of the type provider will be examined and things will become clear.

You need to understand that the type provider types and methods are generated in a lazy way. The lazy generation decreases the code size. Imagine you have a complex database schema and your code accesses only one table in the database. The erased type provider generates only the code for that particular table. This allows the provided type space to be large and even infinite. The built-in type providers use type generation, which involves wrapping existing code-generation tools. This chapter focuses on how to write the erased type provider and the generated type provider. The erased type provider provides types that will be erased to other types, such as System.Object. The following statement is from MSDN (http://msdn.microsoft.com/en-us/library/hh361034.aspx#BK_Erased):

Each provided type is erased to type obj, and all uses of the type will appear as type obj in compiled code. In fact, the underlying objects in these examples are strings, but the type will appear as Object in .NET compiled code. As with all uses of type erasure, you can use explicit boxing, unboxing, and casting to subvert erased types. In this case, a cast exception that isn’t valid may result when the object is used. A provider runtime can define its own private representation type to help protect against false representations. You can’t define erased types in F# itself. Only provided types may be erased. You must understand the ramifications, both practical and semantic, of using either erased types for your type provider or a provider that provides erased types. An erased type has no real .NET type. Therefore, you cannot do accurate reflection over the type, and you might subvert erased types if you use runtime casts and other techniques that rely on exact runtime type semantics. Subversion of erased types frequently results in type cast exceptions at runtime.

The erased type provider generates types that exist only at design time, whereas the generated type provider generates a real type that is compiled in the assembly.

Setting Up the Development Environment

F# provides an API that you can use to write your own type providers. The type-provider author requires several files. A type-provider template is available to help make the type-provider authoring process easier. The type-provider template contains several API files that are also published on the F# Sample Pack site (http://fsharp3sample.codeplex.com/). The type-provider feature is new in F# 3.0 and, as such, will not work with previous versions of F#.

Note

It is highly recommended that you use Microsoft Visual Studio 2012 to author type providers.

As an alternative to performing a manual setup of the Visual Studio solution and project, you can use a Visual Studio template package published at the Visual Studio Gallery, which can be downloaded from http://visualstudiogallery.msdn.microsoft.com/43d00ffd-1b9a-4581-a942-da85b6a83b9c. After the package is installed, the type-provider project can be created via the New Project dialog box, as shown in Figure 5-1.

Type-provider project template
Figure 5-1. Type-provider project template

The type-provider project is a class library project, and it contains four files. The type-provider class, which is named TypeProvider1.fs, is the backbone of the project. It contains the type-provider code. TestScript.fsx is a script file that can be used to test the type provider. The generated project is shown in Figure 5-2.

Generated files from the Type Provider template
Figure 5-2. Generated files from the Type Provider template

Example 5-1 contains the code needed to reference a type provider. The type-provider type is denoted as the provided type, and the method and property is denoted as a provided method and provided property. The first line adds a reference to the Type Provider dynamic-link library (DLL). After the reference is ready, the second and third lines create the provided type and assign it to the value named t.

It is a good practice to first design how an end user will invoke the type provider. For example, TPTestType should have a parameterless constructor that does not require any special initialization code. Also, it has a property called Property1.

Example 5-1. Type-provider test file
#r @".inDebugTypeProviderTemplate9.dll"

type T = Samples.ShareInfo.TPTest.TPTestType
let t = T()

let c = t.Property1
t.Property1 <- "aa"
printfn "methodName() = %A" (t.methodName())

Execution result

setter code
methodName() = 2

The type-provider class code is shown in Example 5-2. As you saw in Example 5-1, the class should have a parameterless constructor and one property. The type-provider class should inherit from the TypeProviderFromNamespaces class and be decorated with the TypeProvider attribute. The type-provider type is created by the ProvidedTypeDefinition class. There are four parameters and, for now, only two of them are of interest to us. The namespace is specified when the ProvidedTypeDefinition instance is created. Like all types in the .NET world, it needs a base class. In the sample code, the base type is typeof<obj>. The provided type definition, named newT, is added at the end of the code, by calling AddNamespace with the namespace variable.

Once the type is created, the method and property are easy to understand. ProvidedMethod takes a method name, parameters, a return type, a Boolean that indicates whether or not the property is static, and a function that contains a code quotation that will be executed whenever the method is invoked. In the sample code, InvokeCode is 1+1, so it will always return 2. The property is added by creating a ProvidedProperty with a property name, a property type, an indication of whether it is static or not, and getter/setter code.

The function logic has to be a code quotation. The code quotation is a tree structure that represents F# code. There is no direct equivalent in C#. You can think of it as being like C# LINQ. There are two ways to generate an F# code quotation. One is to use <@@ and @@> operators, and the other way is to use Expr functions. Both approaches can generates the same code quotation, so you can choose either way. The sample code in this chapter demonstrates both ways to generate code quotation.

The F# class cannot be instantiated without a constructor; therefore, the constructor is mandatory for a type-provider type, although in most cases it does nothing. ProvidedConstructor takes no parameters, and the constructor code does nothing. The provided method, property, and constructor are finally added to the ProvidedTypeDefinition variable newT by invoking the AddMember function.

Example 5-2. Generated type-provider code
namespace Samples.FSharp.HelloWorldTypeProvider

open System.Reflection
open Microsoft.FSharp.Core.CompilerServices
open Samples.FSharp.ProvidedTypes

[<TypeProvider>]
type public TypeProvider1() as this =
    inherit TypeProviderForNamespaces()

    // Get the assembly and namespace used to house the provided types
    let thisAssembly = Assembly.GetExecutingAssembly()
    let rootNamespace = "Samples.ShareInfo.TPTest"
    let baseTy = typeof<obj>

    let newT = ProvidedTypeDefinition(thisAssembly, rootNamespace, "TPTestType", Some
baseTy)

    // add other property and method definition
    let m = ProvidedMethod(
                methodName = "methodName",
                parameters = [],
                returnType = typeof<int>,
                IsStaticMethod = false,
                InvokeCode = fun args ->
                    <@@ 1 + 1 @@>
                )

    let ctor = ProvidedConstructor(parameters = [], InvokeCode = fun args -> <@@ (* base class
initialization or null*) () @@>)

    let prop2 = ProvidedProperty(propertyName = "Property1",
                                 propertyType = typeof<string>,
                                 IsStatic=false,
                                 GetterCode= (fun args -> <@@ "Hello!" @@>),
                                 SetterCode = (fun args -> <@@ printfn "setter code" @@>))

    do prop2.AddXmlDocDelayed(fun () -> "xml comment")
    do
        newT.AddMember(m)
        newT.AddMember(prop2)
        newT.AddMember(ctor)

    do this.AddNamespace(rootNamespace, [newT])

[<TypeProviderAssembly>]
do ()

Note

Other functions and attributes will be covered in later sections.

The test script is lightweight and good for testing and debugging a small type provider, but it is not the only way to do that. For complex type providers, using two instances of Visual Studio is a better choice. Use one instance of Visual Studio to develop the type provider and refer to that instance as Developing Visual Studio. Use the other instance of Visual Studio to host the testing project, and refer to it as Testing Visual Studio. Developing Visual Studio needs to attach to the testing Visual Studio.

Tip

Because the type-provider DLL is locked by Visual Studio, restarting Visual Studio is unavoidable. If the test script is used, make sure that the script file is not the current file when you are closing the project; otherwise, it will be opened automatically and consequently lock the DLL the next time that the project is opened.

Exploring the HelloWorld Type Provider

If you understood the F# type-provider template code from the prior section, the HelloWorld type-provider example in this section will be easy for you to follow. However, if any of the code presented in the last section is not clear, the HelloWorld type provider example will provide some clarity. This type-provider example has five methods and five properties. Example 5-3, which defines how users invoke the HelloWorld type provider, can be used to design and test the type provider.

Example 5-3. HelloWorld type-provider test script
#r @".inDebugHelloWorldTypeProvider.dll"

let assertFunction x =
    if not x then failwith "expression is false"

type T = Samples.ShareInfo.TPTest.HelloTypeProvider
let t = T()
assertFunction( t.Method1() = 1 )
assertFunction( t.Method2() = 2 )
assertFunction( t.Method3() = 3 )
assertFunction( t.Method4() = 4 )
assertFunction( t.Method5() = 5 )
assertFunction( t.Property6 = "Property 6")
assertFunction( t.Property7 = "Property 7")
assertFunction( t.Property8 = "Property 8")
assertFunction( t.Property9 = "Property 9")
assertFunction( t.Property10 = "Property 10")

Note

In Example 5-3, assert is the F# keyword used to check for a TRUE/FALSE value, but it works only when the DEBUG symbol is defined. In the sample, a user-defined function named assert is used instead.

First create the Type Provider project using the Type Provider template with HelloWorldTypeProvider as the project name. (See Figure 5-3.) The template, which can be downloaded from http://visualstudiogallery.msdn.microsoft.com/43d00ffd-1b9a-4581-a942-da85b6a83b9c, has four files created by default. The TypeProvider1.fs file is where the type-provider code will be placed.

Create the HelloWorld type-provider project
Figure 5-3. Create the HelloWorld type-provider project

Because the type provider generates a type named HelloWorldTypeProvider, ProvidedTypeDefinition needs Example 5-7 to be provided as the third parameter in the constructor:

let newT = ProvidedTypeDefinition(thisAssembly, rootNamespace, "HelloWorldTypeProvider", Some
baseTy)

As mentioned previously, the type provider has five methods and five properties. The template adds the code to generate a single method and property (as shown in Example 5-4) by creating ProvidedMethod and ProvidedProperty. As a result, it is not difficult to add a List.map to the generated code so that five methods and five properties (which you can see in Example 5-5) are generated by the type provider.

Example 5-4. ProvidedMethod and ProvidedProperty
let m = ProvidedMethod(
            methodName = "methodName",
            parameters = [],
            returnType = typeof<int>,
            IsStaticMethod = false,
            InvokeCode = fun args ->
                <@@ 1 + 1 @@>
            )

let prop2 = ProvidedProperty(propertyName = "Property1",
                             propertyType = typeof<string>,
                             IsStatic=false,
                             GetterCode= (fun args -> <@@ "Hello!" @@>),
                             SetterCode = (fun args -> <@@ printfn "setter code" @@>))
Example 5-5. Generating five ProvidedMethod and ProvidedProperty instances
    let ms =
        [ 1..5 ]
        |> List.map (fun i ->
                        let m = ProvidedMethod(
                                    methodName = sprintf "Method%d" i,
                                    parameters = [],
                                    returnType = typeof<int>,
                                    IsStaticMethod = false,
                                    InvokeCode = fun args ->
                                    <@@ i @@>)
                        m)

    let props =
        [ 6..10 ]
        |> List.map (fun i ->
                        let prop2 = ProvidedProperty(propertyName = sprintf "Property%d"
i,
                                 propertyType = typeof<string>,
                                 IsStatic=false,
                                 GetterCode= (fun args -> <@@ sprintf "Property %d" i
@@>),
                                 SetterCode = (fun args -> <@@ () @@>))
                        prop2.AddXmlDocDelayed(fun () -> sprintf "xml comment for
Property%d" i)
                        prop2)

Because the default constructor is good enough for the HelloWorldTypeProvider project, you can now add a ProvidedMethod list, a ProvidedProperty list, and a constructor to the ProvidedTypeDefinition. (See Example 5-6.) The code for the type provider after these changes are made is shown in Example 5-7.

Example 5-6. Adding ProvidedMethod, ProvidedProperty, and constructor instances
do
    ms |> Seq.iter newT.AddMember
    props |> Seq.iter newT.AddMember
    newT.AddMember(ctor)
Example 5-7. HelloWorldTypeProvider code
// Learn more about F# at http://fsharp.net
// See the 'F# Tutorial' project for more help.

namespace Samples.FSharp.HelloWorldTypeProvider


open System.Reflection
open Microsoft.FSharp.Core.CompilerServices
open Samples.FSharp.ProvidedTypes

[<TypeProvider>]
type public TypeProvider1() as this =
    inherit TypeProviderForNamespaces()

    // Get the assembly and namespace used to house the provided types
    let thisAssembly = Assembly.GetExecutingAssembly()
    let rootNamespace = "Samples.ShareInfo.TPTest"
    let baseTy = typeof<obj>

    let newT = ProvidedTypeDefinition(thisAssembly, rootNamespace,
"HelloWorldTypeProvider",
                                            Some baseTy)


    // add other property and method definition
    let ms =
        [ 1..5 ]
        |> List.map (fun i ->
                        let m = ProvidedMethod(
                                    methodName = sprintf "Method%d" i,
                                    parameters = [],
                                    returnType = typeof<int>,
                                    IsStaticMethod = false,
                                    InvokeCode = fun args ->
                                    <@@ i @@>)
                        m)

    let ctor = ProvidedConstructor(parameters = [],
                                         InvokeCode = fun args ->
                                                      <@@ (* base class initialization or
null*) () @@>)

    let props =
        [ 6..10 ]
        |> List.map (fun i ->
                        let prop2 =
                             ProvidedProperty(
                                 propertyName = sprintf "Property%d" i,
                                 propertyType = typeof<string>,
                                 IsStatic=false,
                                 GetterCode= (fun args -> <@@ sprintf "Property %d" i
@@>),
                                 SetterCode = (fun args -> <@@ () @@>))
                        prop2.AddXmlDocDelayed(fun () -> sprintf "xml comment for
Property%d" i)
                        prop2)

    do
        ms |> Seq.iter newT.AddMember
        props |> Seq.iter newT.AddMember
        newT.AddMember(ctor)

    do this.AddNamespace(rootNamespace, [newT])

[<TypeProviderAssembly>]
do ()

Because most of the work is done by the template, the basic rules to write a simple type provider are as follows:

  • The type-provider type is defined by ProvidedTypeDefinition.

  • If a method is needed, create an instance of the ProvidedMethod type and add it to the ProvidedTypeDefinition instance.

  • If a property is needed, create an instance of the ProvidedProperty type and add it to the ProvidedTypeDefinition instance.

  • You will always need a constructor, and it also needs to be added to the ProvidedTypeDefinition instance. The default constructor that is generated by the template is good for most cases.

As long as the F# snippet add-on and latest snippet-file Visual Studio extensions are installed, the snippet to generate ProvidedMethod, ProvidedProperty, and ProvidedConstructor is under Type Providers And Query, Write Type Provider, as shown in Figure 5-4. The F# code snippet add-on can be downloaded from http://visualstudiogallery.msdn.microsoft.com/d19080ad-d44c-46ae-b65c-55cede5f708b, and the latest snippet file is located as an open source project at http://fsharpcodesnippet.codeplex.com/.

Code snippet to generate ProvidedConstructor, ProvidedProperty, and ProvidedMethod
Figure 5-4. Code snippet to generate ProvidedConstructor, ProvidedProperty, and ProvidedMethod

Using the Regular-Expression Type Provider

As you know, the type provider is used to generate a new type with class members, such as methods and properties. HelloWorldTypeProvider generates class members from a predefined data schema. The predefined data schema in this case requires five methods and five properties. As software developers, we always look for something that can be created programmatically. Is there any way to create the class members from some variable passed in by a user? The answer is ”Yes.” The regular-expression type provider dynamically generates class members based on a regular-expression parameter passed in by a user, as shown in Example 5-8.

Example 5-8. Invoking a regular-expression type provider
type T = Samples.ShareInfo.TPTest.RegularExpressionTypeProvider<
               @"(?<AreaCode>^d{3})-(?<PhoneNumber>d{3}-d{4}$)" >
let reg = T()
let result = T.IsMatch("425-555-2345")
let r = reg.Match("425-555-2345").AreaCode.Value

The first line in the listing shows how the type provider takes the parameters between < and >. This is the most common way for a type provider to get parameters. The third line shows a static method, but this is not a problem because ProvidedMethod can take IsStaticMethod = true. The last line is the most interesting part. The Match function returns a variable that has an AreaCode property. But the AreaCode property does not look like any standard .NET property. It is a generated property from the regular-expression group AreaCode. From the preceding analysis, something is needed to take and interpret the regular-expression pattern string and the type provider needs at least two types. One is the type-provider type, and the other one is a type with an AreaCode property.

First you need to get the regular-expression pattern string. To parameterize the type provider, you pass a list of ProvidedStaticParameters containing the name and type of each parameter to the DefineStaticParameter method, along with an instantiation callback function, as shown in Example 5-9. At runtime, values the user supplies for each static parameter will be passed into the second argument of the callback function in the same order that they were defined.

Example 5-9. Getting the type-provider static parameter
 let staticParams = [ProvidedStaticParameter("pattern", typeof<string>)]

do newT.DefineStaticParameters(
        parameters=staticParams,
        instantiationFunction=(fun typeName parameterValues ->
            ... .
            ))

Therefore, when using static parameters, the creation of additional types and members must be performed inside the callback function. Consequently, the type provider’s code structure changes as shown in Example 5-10. The only change from HelloWorldTypeProvider is that now the operation must be performed on the ty variable defined inside the instantiation function. The instantiation function must return a new type. If RegexTypeProvider can be viewed as a generic type and the regular-expression pattern string as a type parameter, the type returned from the instantiation function is a new type after applying the type parameter. In Example 5-8, the type T was the new type returned from the instantiation function. This new type is the target to which you attach properties and methods.

Example 5-10. Type with a static parameter
    let staticParams = [ProvidedStaticParameter("pattern", typeof<string>)]

    do newT.DefineStaticParameters(
            parameters=staticParams,
            instantiationFunction=(fun typeName parameterValues ->
                match parameterValues with
                | [| :? string as pattern|] ->
                    let ty = ProvidedTypeDefinition(
                        thisAssembly,
                        rootNamespace,
                        typeName,
                        Some baseTy,
                        HideObjectMethods = true)

                    let ctor = ProvidedConstructor(
                                        parameters = [],
                                        InvokeCode = fun args ->
                                                <@@ (* base class initialization or null*)
() @@>)
                    ty.AddMember(ctor)

                    ty
                ))

Note

The HideObjectMethods property determines whether the object method, such as ToString, can be shown in IntelliSense.

To make the IsMatch static method work, a few problems need to be solved:

  • How to define a provided function with a parameter

  • How to get parameter values inside of the provided method

In Example 5-11, function parameters are defined using ProvidedParameter, which requires a parameter name and parameter type. The parameter passed into the function is named args, and it is of type Expr list. The way to get the value out of the Expr list is to use a quotation splicing operator (%%) and specify its type. Because the function is a static method, the first element in the args list is the parameter you want. If the method is not a static method, %%args.[0] is the pointer to the instance.

Example 5-11. The IsMatch method
let m = ProvidedMethod(
                    methodName = "IsMatch",
                    parameters = [ ProvidedParameter("input", typeof<string>) ],
                    returnType = typeof<bool>,
                    IsStaticMethod = true,
                    InvokeCode = fun args ->
                    <@@
                        System.Text.RegularExpressions.Regex.IsMatch((%%args.[0]:string),
pattern)
                    @@>)

ty.AddMember(m)

Note

Both % and %% are quotation splicing operators. Their job is to enable a user to insert a code expression object into a quotation. The difference between % and %% is that % inserts a typed expression but %% inserts an untyped expression. In the code sample just shown, args is an F# expression, and the %% operator converts the expression into a quotation.

The last line in Example 5-8 returns a type that has an AreaCode property, which is a group name defined in the regular-expression pattern string. As a result, a new type is needed to host this property, and it can be returned from the Match method. (See Example 5-12.) The new type is named matchTy. It is created and used as the return type for matchMethod. The group names are retrieved, and the AreaCode property is added to matchTy. The property assumes its parent object is System.Text.RegularExpressions.Match type.

Example 5-12. Implementing the Match method
let matchTy = ProvidedTypeDefinition("Match",
                                        baseType = Some baseTy,
                                        HideObjectMethods = true)

for group in r.GetGroupNames() do
    // ignore the group named 0, which represents all input
    if group <> "0" then
        let prop = ProvidedProperty(
                   propertyName = group,
                   propertyType = typeof<System.Text.RegularExpressions.Group>,
                   GetterCode =
                     fun args ->
                     <@@
                      ((%%args.[0]:obj):?>System.Text.RegularExpressions.Match).Groups.
[group]
                    @@>)

        matchTy.AddMember prop

let matchMethod =
      ProvidedMethod(
           methodName = "Match",
           parameters = [ProvidedParameter("input", typeof<string>)],
           returnType = matchTy,
           InvokeCode =
             fun args ->
             <@@
            ((%%args.[0]:obj):?>System.Text.RegularExpressions.Regex).Match(%%args.
[1]):>obj
            @@>)

ty.AddMember(matchMethod)

ty.AddMember(matchTy)

A little extra code is needed in the matchTy constructor, because the AreaCode property needs its calling object to be a Match type. Example 5-13 shows the necessary code additions.

Example 5-13. Constructor for ty
let ctor = ProvidedConstructor(
              parameters = [],
              InvokeCode = fun args -> <@@ System.Text.RegularExpressions.Regex(pattern)
:> obj @@>)

ty.AddMember(ctor)

Before presenting the complete code for the regular expression type provider, there is one thing I need to clarify. The code ((%%args.[0]:obj) :?> System.Text.RegularExpressions.Match) in Example 5-12 might seem odd when you review the value type that is then converted to a Match type. The reason this is needed is that the matchTy type needs to have a base type of System.Object. Although you could use a base type of System.Text.RegularExpression.Match, and consequently avoid the conversion, the end-user experience would be diminished by this. The problem with using System.Text.RegularExpression is that viewing Match via IntelliSense shows all the built-in members from the Match type. This makes the generated AreaCode property more difficult to find. Figure 5-5 shows a side-by-side comparison that illustrates this issue. The complete code for the regular-expression type provider is shown in Example 5-14.

IntelliSense affected by the base type; Match is used as the base type in the image on the right
Figure 5-5. IntelliSense affected by the base type; Match is used as the base type in the image on the right
Example 5-14. Completing the regular-expression type-provider code
// Learn more about F# at http://fsharp.net
// See the 'F# Tutorial' project for more help.

namespace Samples.FSharp.RegularExpressionTypeProvider

open System.Reflection
open Microsoft.FSharp.Core.CompilerServices
open Samples.FSharp.ProvidedTypes

[<TypeProvider>]
type public TypeProvider1() as this =
    inherit TypeProviderForNamespaces()

    // Get the assembly and namespace used to house the provided types
    let thisAssembly = Assembly.GetExecutingAssembly()
    let rootNamespace = "Samples.ShareInfo.TPTest"
    let baseTy = typeof<obj>

    let newT = ProvidedTypeDefinition(thisAssembly, rootNamespace,
                     "RegularExpressionTypeProvider", Some baseTy)

    let staticParams = [ProvidedStaticParameter("pattern", typeof<string>)]
    do newT.DefineStaticParameters(
            parameters=staticParams,
            instantiationFunction=(fun typeName parameterValues ->
                match parameterValues with
                | [| :? string as pattern|] ->
                    let ty = ProvidedTypeDefinition(
                                   thisAssembly,
                                   rootNamespace,
                                   typeName,
                                   Some baseTy,
                                   HideObjectMethods = true)
                    let r = System.Text.RegularExpressions.Regex(pattern)
                    let m = ProvidedMethod(
                                methodName = "IsMatch",
                                parameters = [ProvidedParameter("input", typeof<string>)],
                                returnType = typeof<bool>,
                                IsStaticMethod = true,
                                InvokeCode = fun args ->
                                    <@@
                                     System.Text.RegularExpressions.Regex.IsMatch(
                                       (%%args.[0]:string),
                                       pattern)
                                    @@>
                                )
                    ty.AddMember(m)

                    let ctor = ProvidedConstructor(
                                    parameters = [],
                                    InvokeCode =
                                      fun args ->
                                        <@@
                                          System.Text.RegularExpressions.Regex(pattern) :>
obj
                                        @@>)
                    ty.AddMember(ctor)

                    let matchTy = ProvidedTypeDefinition("Match",
                                        baseType = Some baseTy,
                                        HideObjectMethods = true)

                    for group in r.GetGroupNames() do
                        // ignore the group named 0, which represents all input
                        if group <> "0" then
                            let prop =
                              ProvidedProperty(
                                propertyName = group,
                                propertyType = typeof<System.Text.RegularExpressions.
Group>
                                GetterCode =
                                  fun args ->
                                    <@@
                                       ((%%args.[0]:obj)
                                       :?>System.Text.RegularExpressions.Match).Groups.
[group]
                                    @@>)

                            matchTy.AddMember prop

                    let matchMethod =
                            ProvidedMethod(
                              methodName = "Match",
                              parameters = [ProvidedParameter("input", typeof<string>)],
                              returnType = matchTy,
                              InvokeCode =
                                fun args ->
                                  <@@
                                        ((%%args.[0]:obj)
                                        :?>System.Text.RegularExpressions.Regex)
                                              .Match(%%args.[1])
                                        :>obj
                                        @@>)

                    ty.AddMember(matchMethod)

                    ty.AddMember(matchTy)

                    ty
                ))

    do this.AddNamespace(rootNamespace, [newT])

[<TypeProviderAssembly>]
do ()

Using the CSV Type Provider

The regular-expression type provider uses a static parameter to provide the type information. The static parameter can also point to a file that contains the type information. The current implementation allows a user to generate only .NET 1.x-compatible types. One exception is unit-of-measure types. Type providers do support the generation of unit-of-measure types. The CSV type provider demonstrates how to get type information from a local CSV file and how to generate unit-of-measure types.

As with the previous examples, the first step is to design how the API should look for the end user. (See Example 5-15.) The sample CSV file is shown in Example 5-16.

Example 5-15. CSV type-provider test script
#r @".inDebugCSVTypeProvider.dll"

type T = Samples.ShareInfo.TPTest.CSVTypeProvider<"TextFile1.csv">
let t = T()
for row in t.Data do
    let time = row.Time
    printfn "%f" (float time)
Example 5-16. Sample CSV file
Distance (metre),Time (second)
50,3.73
100,4.22
150,7.3

The type information is stored in the first line of the CSV file. Each row, except the first row, also needs a type to hold all of the fields. ProvidedMeasureBuilder is needed when creating unit-of-measure types. Example 5-17 shows how to create kg, meter, and kg/m2.

Example 5-17. Creating unit-of-measure types in type provider
 let measures = ProvidedMeasureBuilder.Default

// make the kilogram and meter
let kg = measures.SI "Kilogram"
let m = measures.SI "Meter"

// make float<kg> type
let float_kg = measures.AnnotateType(typeof<float>,[kg])

// make float<m2> type
let kgpm2 = measures.Ratio(kg, measures.Square m)

// make float<kg/m2> type
let dkgpm2 = measures.AnnotateType(typeof<float>,[kgpm2])

// make Nullable<float< kg/m2>> type
let nullableDecimal_kgpm2 = typedefof<System.Nullable<_>>.MakeGenericType [|dkgpm2 |]

Referencing a local file is an easy task as long as the user gives the correct file path. If a relative path is preferred, TypeProviderConfig can be used. The instance of a type provider can take an instance of a type named TypeProviderConfig during construction, which contains the resolution folder for the type provider, the list of referenced assemblies, and other information. Example 5-18 shows how to use the TypeProviderConfig value.

Example 5-18. Using the TypeProviderConfig value to get a resolution path
[<TypeProvider>]
type public CsvProvider(cfg:TypeProviderConfig) as this =
    inherit TypeProviderForNamespaces()
    ...

    // Resolve the filename relative to the resolution folder.
    let resolvedFilename = System.IO.Path.Combine(cfg.ResolutionFolder, filename)

Now that the two problems are solved, the CSV type provider can be implemented as shown in Example 5-19. The structure of the code for the CSV type provider is similar to that of the regular-expression type provider. The Data property returns a CSV file line, which is represented as a Row.

Example 5-19. CSV type provider
// Learn more about F# at http://fsharp.net
// See the 'F# Tutorial' project for more help.

namespace Samples.FSharp.CSVTypeProvider

open System.Reflection
open Microsoft.FSharp.Core.CompilerServices
open Samples.FSharp.ProvidedTypes
open System.IO
open System.Text.RegularExpressions

module CsvFileModule =
    let data filename =
        [| for line in File.ReadAllLines(filename) |> Seq.skip 1 do
                yield line.Split(',') |> Array.map float |]

[<TypeProvider>]
type public CsvProvider(cfg:TypeProviderConfig) as this =
    inherit TypeProviderForNamespaces()

    // Get the assembly and namespace used to house the provided types.
    let asm = System.Reflection.Assembly.GetExecutingAssembly()
    let ns = "Samples.ShareInfo.TPTest"

    // Create the main provided type.
    let csvTy = ProvidedTypeDefinition(asm, ns, "CSVTypeProvider", Some(typeof<obj>))

    // Parameterize the type by the file to use as a template.
    let filename = ProvidedStaticParameter("filename", typeof<string>)
    do csvTy.DefineStaticParameters([filename], fun tyName [| :? string as filename |] ->

        // Resolve the filename relative to the resolution folder.
        let resolvedFilename = Path.Combine(cfg.ResolutionFolder, filename)

        // Get the first line from the file.
        let headerLine = File.ReadLines(resolvedFilename) |> Seq.head

        // Define a provided type for each row, erasing to a float[].
        let rowTy = ProvidedTypeDefinition("Row", Some(typeof<float[]>))

        // Extract header names from the file, splitting on commas.
        // use Regex matching to get the position in the row at which the field occurs
        let headers = Regex.Matches(headerLine, "[^,]+")

        // Add one property per CSV field.
        for i in 0 .. headers.Count - 1 do
            let headerText = headers.[i].Value

            // Try to decompose this header into a name and unit.
            let fieldName, fieldTy =
                let m = Regex.Match(headerText, @"(?<field>.+) ((?<unit>.+))")
                if m.Success then
                    let fieldName = m.Groups.["field"].Value
                    let unitName = m.Groups.["unit"].Value
                    let units = ProvidedMeasureBuilder.Default.SI unitName
                    let fieldType =
                         ProvidedMeasureBuilder.Default.AnnotateType(typeof<float>,[uni
ts])
                    (fieldName, fieldType)
                else
                    // no units, just treat it as a normal float
                    headerText, typeof<float>

            let fieldProp = ProvidedProperty(fieldName, fieldTy,
                                             GetterCode = fun [row] -> <@@
(%%row:float[]).[i] @@>)

            // Add metadata that defines the property's location in the referenced file.
            fieldProp.AddDefinitionLocation(1, headers.[i].Index + 1, filename)
            rowTy.AddMember fieldProp

        // Define the provided type
        let ty = ProvidedTypeDefinition(asm, ns, tyName, Some(typeof<obj>))

        // Add a parameterless constructor that loads the file that was used to define the
schema.
        let ctor0 = ProvidedConstructor([],
                                        InvokeCode = fun _ -> <@@ () @@>)

        // Add a more strongly typed Data property, which uses the existing property at
runtime.
        let prop = ProvidedProperty("Data", typedefof<seq<_>>.MakeGenericType(rowTy),
                                GetterCode = fun _ -> <@@ CsvFileModule.data
resolvedFilename @@>)

        ty.AddMember prop
        ty.AddMember ctor0

        // Add the row type as a nested type.
        ty.AddMember rowTy
        ty)

    // Add the type to the namespace.
    do this.AddNamespace(ns, [csvTy])

[<TypeProviderAssembly>]
do ()

Note

Wrapping the functions in modules helps make the type provider tidy.

Another approach is to use a user-defined CSV class to process the file and return a list of data. The type provider’s job is to provide unit-of-measure types and extra class members to the design-time IntelliSense. The CSV class and type provider code are shown in Example 5-20. The CSV file’s first line is parsed by using a regular expression; the column name, which also contains the unit-of-measure types, is retrieved and generated. The CSV class returns the Data property, and its content is passed into the generated type rowTy. The rowTy type provides the unit-of-measure type and named property for each data item.

Example 5-20. CSV file class
// Learn more about F# at http://fsharp.net
// See the 'F# Tutorial' project for more help.

namespace Samples.FSharp.CSVTypeProvider

open System.Reflection
open Microsoft.FSharp.Core.CompilerServices
open Samples.FSharp.ProvidedTypes
open System.IO
open System.Text.RegularExpressions

// CSV file class
type CsvFile(filename) =
    let data =
        [| for line in File.ReadAllLines(filename) |> Seq.skip 1 do
                yield line.Split(',') |> Array.map float |]
    member __.Data = data

// CSV file type provider code with unit-of-measure support
[<TypeProvider>]
type public CsvProvider(cfg:TypeProviderConfig) as this =
    inherit TypeProviderForNamespaces()

    // Get the assembly and namespace used to house the provided types.
    let asm = System.Reflection.Assembly.GetExecutingAssembly()
    let ns = "Samples.ShareInfo.TPTest"

    // Create the main provided type.
    let csvTy = ProvidedTypeDefinition(asm, ns, "CSVTypeProvider", Some(typeof<obj>))

    // Parameterize the type by the file to use as a template.
    let filename = ProvidedStaticParameter("filename", typeof<string>)

    do csvTy.DefineStaticParameters([filename], fun tyName [| :? string as filename |] ->

        // Resolve the filename relative to the resolution folder.
        let resolvedFilename = Path.Combine(cfg.ResolutionFolder, filename)

        // Get the first line from the file.
        let headerLine = File.ReadLines(resolvedFilename) |> Seq.head

        // Define a provided type for each row, erasing to a float[].
        let rowTy = ProvidedTypeDefinition("Row", Some(typeof<float[]>))

        // Extract header names from the file and use
        // Regex to get the position in the row at which the field occurs
        let headers = Regex.Matches(headerLine, "[^,]+")

        // One property per CSV field.
        for i in 0 .. headers.Count - 1 do
            let headerText = headers.[i].Value

            // Try to decompose this header into a name and unit.
            let fieldName, fieldTy =
                let m = Regex.Match(headerText, @"(?<field>.+) ((?<unit>.+))")
                if m.Success then
                    let fieldName = m.Groups.["field"].Value
                    let unitName = m.Groups.["unit"].Value
                    let units = ProvidedMeasureBuilder.Default.SI unitName
                    let fieldType =
                      ProvidedMeasureBuilder.Default.AnnotateType(typeof<float>,[units])
                    (fieldName, fieldType)
                else
                    // no units, just treat it as a normal float
                    headerText, typeof<float>

            let fieldProp = ProvidedProperty(fieldName, fieldTy,
                                             GetterCode = fun [row] -> <@@
(%%row:float[]).[i] @@>)

            // Add metadata that defines the property's location in the referenced file.
            fieldProp.AddDefinitionLocation(1, headers.[i].Index + 1, filename)
            rowTy.AddMember fieldProp

        // Define the provided type
        let ty = ProvidedTypeDefinition(asm, ns, tyName, Some(typeof<CsvFile>))

        // Add a parameterless constructor that loads the file that was used to define the
schema.
        let ctor0 = ProvidedConstructor([],
                                        InvokeCode = fun [] -> <@@
CsvFile(resolvedFilename) @@>)

        // Add a more strongly typed Data property, which uses the existing property at
runtime.
        let prop = ProvidedProperty("Data", typedefof<seq<_>>.MakeGenericType(rowTy),
                                    GetterCode = fun [csvFile] -> <@@ (%%csvFile:CsvFile).
Data @@>)

        ty.AddMember prop
        ty.AddMember ctor0

        // Add the row type as a nested type.
        ty.AddMember rowTy
        ty)

    // Add the type to the namespace.
    do this.AddNamespace(ns, [csvTy])

[<TypeProviderAssembly>]
do ()

Note

The csvFile value in the fun [csvFile] -> <@@ (%%csvFile:CsvFile).Data @@>) uses an active pattern to decompose the arguments into a single element list. This code could also be written as fun args -> <@@ ((%%args.[0]):CsvFile).Data @@>).

Two interesting concepts have been introduced in the implementation of this type provider. The type-provider type and provided class members are derived from the schema information, which defines the meaning of the data. The regular-expression pattern and CSV file header in the schema defines how the data is presented. Any schematized data can potentially have a type provider that helps to interpret and process the data. A few examples include image data and network transmission data.

Using the Excel-File Type Provider

Starting with Excel 2007, a new file format with extension XLSX was introduced. This file format is the Office Open XML File format, which is an open international, ECMA-376, Second Edition, and ISO/IEC 29500 standard. This new format is an XML-format file that can be accessed by using the Open XML SDK. The Open XML SDK can be downloaded from http://www.microsoft.com/en-us/download/details.aspx?id=5124. After the SDK is installed successfully, DocumentFormat.OpenXml.dll is installed under the C:Program FilesOpen XML SDK folder.

The sample Excel file Book1.xlsx has three cells. The C1 cell value is computed by adding the values in A1 and B1. The screen shot is shown in Figure 5-6. This file needs to be put in the project folder where the type-provider project file is located.

Excel file content
Figure 5-6. Excel file content

Example 5-21 creates an ExcelFile class to access the XLSX file using the Open XML SDK. To run this code, DocumentFormat.OpenXml.dll, System.Xml.dll, and WindowsBase.dll have to be added to the project reference.

Example 5-21. Accessing the Excel XLSX file using the OpenXML SDK
module ExcelClass

open DocumentFormat.OpenXml.Packaging
open DocumentFormat.OpenXml.Spreadsheet
open DocumentFormat.OpenXml

type CellContent = { Name: string* string; Value : string; Formula : string; Cell : Cell}
    with
        override this.ToString() = sprintf "%s%s(%s, %s)" (fst this.Name) (snd this.Name)
                                           this.Value this.Formula

let (|CellName|) (name:string) =
    let filterBy f =
        name
        |> f System.Char.IsLetter
        |> Seq.toArray
        |> fun l -> System.String l
    let col = filterBy Seq.takeWhile
    let row = filterBy Seq.skipWhile
    (col, row)

type ExcelFile(filename:string, editable:bool) =
    let myWorkbook = SpreadsheetDocument.Open(filename, editable)
    let part = myWorkbook.WorkbookPart

    let cells = part.Workbook.Descendants<Sheet>()
                |> Seq.map (fun sheet ->
                                    let a = part.GetPartById sheet.Id.Value
                                    let c =
                                        match a with
                                        | :? WorksheetPart as part ->
                                            let rows = part.Worksheet.Descendants<Row>()
                                            [ for row in rows do
                                                for cell in row.Descendants<Cell>() do
                                                    let formula =
                                                        if cell.CellFormula = null then
                                                            ""
                                                        else
                                                            cell.CellFormula.Text
                                                    match cell.CellReference.Value with
                                                    | CellName(col,row) ->
                                                        yield
                                                            {   Cell = cell
                                                                Name = col, row
                                                                Formula = formula
                                                                Value = cell.CellValue.Text } ]
                                        | _ -> [ ]
                                    (sheet.Name.Value, c))
                |> Seq.map snd
                |> Seq.collect id
                |> Seq.toList

    member this.Cells = cells
    member this.Cell(col,row) =
        cells |> Seq.tryFind (fun cell->cell.Name=(col,row))
    member this.Cell(col,row, v) =
        let cell = this.Cell(col,row)
        match cell with
        | Some c ->
            if c.Cell.CellFormula = null then
                c.Cell.CellValue.Text <- v
        | None -> ()
    member this.Close() = myWorkbook.Close()

Note

The Close method saves and closes the XLSX file.

The type-provider script, presented in Example 5-22, shows what class members are exposed to the end user. The code opens Book1.xlsx and reads the A1 cell content first. Then the A1 content is changed to 99.

Example 5-22. ExcelTypeProvider script file
#r @".inDebugTypeProviderTemplate2.dll"

type T = Samples.Excel.ExcelTypeProvider<"Book1.xlsx">

let t = T()
let (Some(cell)) = t.GetCell(t.A, t.''1'')
printfn "%A" cell.Value

t.SetCell(t.A, t.''1'', "99")
t.Close()

let t2 = T()

let (Some(cell2)) = t2.GetCell(t2.A, t2.''1'')
printfn "%A" cell2.Value

t2.Close()

Note

The Close method needs to be invoked; otherwise, the script will generate an error after it is executed once. The error message is “Cannot access Book1.xlsx because it is opened by another process.”

The type provider code, shown in Example 5-23, provides several methods. It uses System.Object as a base class to hide the ExcelFile members. It also provides Excel columns and row names as read-only properties.

Example 5-23. ExcelTypeProvider implementation code
// Learn more about F# at http://fsharp.net
// See the 'F# Tutorial' project for more help.

namespace Samples.FSharp.TypeProviderTemplate2

open System.Reflection
open Microsoft.FSharp.Core.CompilerServices
open Samples.FSharp.ProvidedTypes
open ExcelClass

[<TypeProvider>]
type public TypeProvider1(cfg:TypeProviderConfig) as this =
    inherit TypeProviderForNamespaces()

    // Get the assembly and namespace used to house the provided types
    let thisAssembly = Assembly.GetExecutingAssembly()
    let rootNamespace = "Samples.Excel"
    let baseTy = typeof<obj>

    let newT = ProvidedTypeDefinition(
                      thisAssembly,
                      rootNamespace,
                      "ExcelTypeProvider",
                      Some baseTy)

    let filename = ProvidedStaticParameter("filename", typeof<string>)
    do newT.DefineStaticParameters(
            [filename],
            fun tyName [| :? string as filename |] ->
                let path = System.IO.Path.Combine (cfg.ResolutionFolder, filename)
                let ty = ProvidedTypeDefinition(
                               thisAssembly, rootNamespace, tyName, Some(typeof<obj>))
                ty.AddMember(
                    ProvidedConstructor([], InvokeCode = fun _ -> <@@ ExcelFile(path,
true) @@>))

                let file = ExcelFile(path, false)
                let rows = file.Cells
                            |> Seq.map (fun cell -> cell.Name)
                            |> Seq.map (fun (col,row) -> row)
                            |> Seq.distinct
                let cols = file.Cells
                            |> Seq.map (fun cell -> cell.Name)
                            |> Seq.map (fun (col,row) -> col)
                            |> Seq.distinct
                rows
                |> Seq.append cols
                |> Seq.iter (fun n ->
                        ty.AddMember(
                            ProvidedProperty(
                                n,
                                typeof<string>,
                                GetterCode= fun _ -> <@@ n @@>)))

                file.Close()

                let mi = ProvidedMethod(
                               "GetCell",
                               [ProvidedParameter("row", typeof<string>);
                                ProvidedParameter("col", typeof<string>)],
                                typeof<CellContent option>,
                                InvokeCode =
                                    fun [me; row; col] ->
                                      <@@
                                        ((%%me:obj):?>ExcelFile).Cell((%%row:string),
                                                                            (%%col:string))
                                      @@>)

                let close = ProvidedMethod(
                                   "Close", [], typeof<unit>,
                                   InvokeCode =
                                       fun [me] -> <@@ ((%%me:obj) :?> ExcelFile).Close()
@@>)

                let save = ProvidedMethod(
                                 "SetCell",
                                 [ProvidedParameter("row", typeof<string>)
                                  ProvidedParameter("col", typeof<string>)
                                  ProvidedParameter("value", typeof<string>)],
                                  typeof<unit>,
                                  InvokeCode =
                                      fun [me; row; col; v] ->
                                          <@@ ((%%me:obj) :?> ExcelFile).
Cell((%%row:string),

(%%col:string),

(%%v:string)) @@>)

                ty.AddMember(mi)
                ty.AddMember(save)
                ty.AddMember(close)
                ty)

    do this.AddNamespace(rootNamespace, [newT])

[<TypeProviderAssembly>]
do ()

Note

The “Incomplete pattern matches on this expression” warnings are expected and can be ignored.

Using the Type-Provider Base Class

Until now, there has been no way to share information or coordinate the actions between class methods within a type provider. The type-provider methods do not show relationships when invoked. The interesting part about Example 5-24 is that it stores the file name in the base class and enables methods to share that information between method calls.

Sharing Information Among Members

A simple example is shown in Example 5-24. The type-provider base provides a non-object-based property that can be used to store information that can be shared between method calls. The type provider defines two methods, and these two provided methods provide a wrapper around this existing property. The code is very close to the HelloWorld type provider except for the base class.

One thing that needs to be called out is the constructor. Because the provided type is erased to the base type, the constructor needs to return the base type instance instead of nothing. Example 5-25 shows how to invoke the type provider. The F1 function sets the underlying base class value and the F2 function can retrieve it. This code seems to do nothing, but it opens the door to a more interesting scenario, which will be presented in the next section.

Example 5-24. Type provider sharing information between members
namespace Samples.FSharp.ShareInfoProvider

open System.Reflection
open Microsoft.FSharp.Core.CompilerServices
open Samples.FSharp.ProvidedTypes
open System.IO
open System.Collections.Generic

// base class the type provider erased to
type BaseType2() =
     member val X = "" with get, set

[<TypeProvider>]
type public CheckedRegexProvider() as this =
    inherit TypeProviderForNamespaces()

    // Get the assembly and namespace used to house the provided types
    let thisAssembly = Assembly.GetExecutingAssembly()
    let rootNamespace = "Samples.ShareInfo.TPTest"
    let baseTy = typeof<BaseType2>

let regexTy = ProvidedTypeDefinition(thisAssembly, rootNamespace, "TPTestType", Some
baseTy)

    let f1 = ProvidedMethod(
                            methodName = "F1",
                            parameters = [ProvidedParameter("input", typeof<string>)],
                            returnType = typeof<unit>,
                            IsStaticMethod = false,
                            InvokeCode = fun args -> <@@ (%%args.[0]:BaseType2).X <-
(%%args.[1]) @@>)

    let f2 = ProvidedMethod(
                            methodName = "F2",
                            parameters = [],
                            returnType = typeof<string>,
                            IsStaticMethod = false,
                            InvokeCode = fun args -> <@@ (%%args.[0]:BaseType2).X @@>)

    // constructor needs to return the BaseType2 instance
    let ctor = ProvidedConstructor(
                        parameters = [],
                        InvokeCode = fun args -> <@@ BaseType2() @@>)

    do
        regexTy.AddMember ctor
        regexTy.AddMember f1
        regexTy.AddMember f2

    do this.AddNamespace(rootNamespace, [regexTy])

[<TypeProviderAssembly>]
do ()
Example 5-25. Test script for the TPTestType provider sharing information between methods
#r @".inDebugShareInfoSampleTypeProvider.dll"

type T = Samples.ShareInfo.TPTest.TPTestType
let t = T()
t.F1("hello")
let a = t.F2()

printfn "%A" a // the print result is hello

Because the provided type is based on BaseType2 and because the constructor returns an instance of BaseType2, the variable t in the test script is of type BaseType2.

Using a Wrapper Type Provider

Imagine that you have a sealed class and need to add some logic for each member function, such as logging the function usage. The most common technique for a C# developer is to write a wrapper class with the sealed class instance as a variable, as shown in Example 5-26. It would be a tedious and boring task to refactor the sealed class. Reflection can be an option, but the performance impact of this approach is something that cannot be ignored.

Example 5-26. Wrapper class used to expand a sealed class
// sealed class A
public sealed class A
{
    public void F1() { ... }
}

// new class to add logging function around the F1 defined in sealed class A
public class MyClass
{
    private A a = new A();
    public void F1()
    {
        LogBefore();
        a.F1();
        LogAfter();
    }
}

A custom type provider is another way to perform meta-programming, and it has a performance advantage over reflection. The type provider logic is simple:

  • Generate the same provided method with the same function name and signature.

  • In the function implementation, first invoke the LogBefore method, then invoke the base class’ method, and finally invoke the LogAfter method.

As usual, the way to invoke the type provider is defined first, as you can see in Example 5-27. The sealed class and the logging function are defined in Example 5-28.

Example 5-27. Invoking the wrapper type provider and the expected result
#r @".inDebugShareInfoSampleTypeProvider.dll"

type T = Samples.ShareInfo.TPTest.TPTestType
let t = T()

//invoke F1 and also output logging info
t.F1("hello")

//invoke F2 and also output logging info
t.F2(2)

Execution result

log before Samples.FSharp.ShareInfoProvider.BaseType2 "F1"
hello
log after Samples.FSharp.ShareInfoProvider.BaseType2 "F1"

log before Samples.FSharp.ShareInfoProvider.BaseType2 "F2"
2
log after Samples.FSharp.ShareInfoProvider.BaseType2 "F2"
Example 5-28. Sealed class for the wrapper type provider
[< Sealed >]
type BaseType2() =
     member this.F1(s) = printfn "%s" s
     member this.F2(i) = printfn "%d" i

// logging functions
type LoggingFunctions =
    static member LogBeforeExecution(obj:BaseType2, methodName:string) =
         printfn "log before %A %A" obj methodName
    static member LogAfterExecution(obj:BaseType2, methodName:string) =
         printfn "log after %A %A" obj methodName

The next task is determining how to invoke the logging functions and base class method by using a quotation. The following two problems need to be solved, and the code that provides the solutions is shown in Example 5-29:

  • Invoke a method. The method call is performed by Expr.Call, which takes three parameters if the method is not a static method and only two parameters if it is a static method. The parameter to Expr.Call is created by using Expr.Value.

  • Invoke a sequence of statements. Invoking statements is handled by Expr.Sequential. It might seem like a problem that this only takes two elements, especially when more than two statements need to be invoked. Actually, this is not a problem at all. If the second parameter is an Expr.Sequential, it has an extra space to hold the following statement.

Example 5-29. Quotation code to invoke the base class method and logging function
let baseTExpression = <@@ (%%args.[0]:BaseType2) @@>
let mi = baseTy.GetMethod(methodName)

// get LogBefore function code quotation
let logExpr = Expr.Call(typeof<LoggingFunctions>.GetMethod("LogBeforeExecution"),
                   [ baseTExpression; Expr.Value(methodName) ])

// invoke the base class's method
let invokeExpr = Expr.Call(baseTExpression, mi, args.Tail)

// get logAfter function code quotation
let logAfterExpr = Expr.Call(typeof< LoggingFunctions >.GetMethod("LogAfterExecution"),
                         [ baseTExpression; Expr.Value(methodName) ])

// invoke logBefore, base method, and logAfter
Expr.Sequential(logExpr, Expr.Sequential(invokeExpr, logAfterExpr))

The complete code is presented in Example 5-30. Reflection is used to get the method names from the base class and to generate the provided methods. Because the reflection code is only executed at compile time (design time), the generated IL (which is shown in Example 5-31) does not contain any reflection code. This wrapper class has visibility to the base class’ public methods and properties, and the cost to generate and maintain it is significantly lower than the manual way shown in Example 5-26.

The wrapper type provider can work for cases in which source code is not available. The Activator.CreateInstance method is not something new for C# developers. The DLL path and type name can be passed in as a string, and CreateInstance can be used to create the sealed class instance.

Example 5-30. The complete type-provider code
namespace Samples.FSharp.ShareInfoProvider

open System.Reflection
open Microsoft.FSharp.Core.CompilerServices
open Samples.FSharp.ProvidedTypes
open System.IO
open System.Collections.Generic
open Microsoft.FSharp.Quotations

[<Sealed>]
type BaseType2() =
     member this.F1(s) = printfn "%s" s
     member this.F2(i) = printfn "%d" i

type LoggingFunctions =
    static member LogBeforeExecution(obj:BaseType2, methodName:string) =
        printfn "log before %A %A" obj methodName
    static member LogAfterExecution(obj:BaseType2, methodName:string) =
        printfn "log after %A %A" obj methodName

[<TypeProvider>]
type public CheckedRegexProvider() as this =
    inherit TypeProviderForNamespaces()

    // Get the assembly and namespace used to house the provided types
    let thisAssembly = Assembly.GetExecutingAssembly()
    let rootNamespace = "Samples.ShareInfo.TPTest"
    let baseTy = typeof<BaseType2>

    let regexTy = ProvidedTypeDefinition(thisAssembly, rootNamespace, "TPTestType", Some
baseTy)

    let methods = baseTy.GetMethods()
    let getParameters (mi:MethodInfo) =
        let parameters = mi.GetParameters()
        parameters
        |> Seq.map (fun p -> ProvidedParameter(p.Name, p.ParameterType))
        |> Seq.toList

    let providedMethods =
        methods
        |> Seq.map (fun m ->let methodName = m.Name
                            ProvidedMethod(
                               methodName = methodName,
                               parameters = (getParameters m),
                               returnType = m.ReturnType,
                               IsStaticMethod = false,
                               InvokeCode = fun args ->
                                   let baseTExpression = <@@ (%%args.[0]:BaseType2) @@>
                                   let mi = baseTy.GetMethod(methodName)
                                   let logExpr = Expr.Call(
                                                  typeof<LoggingFunctions>
                                                     .GetMethod("LogBeforeExecution"),
                                                     [baseTExpression; Expr.
Value(methodName) ])
                                   let invokeExpr = Expr.Call(baseTExpression, mi, args.
Tail)
                                   let logAfterExpr = Expr.Call(
                                                           typeof<LoggingFunctions>
                                                            .GetMethod("LogAfterExecuti
on"),
                                                            [baseTExpression;Expr.
Value(methodName)]
                                                            )
                                   Expr.Sequential(
                                               logExpr,
                                               Expr.Sequential(invokeExpr, logAfterExpr))
                                    ))


    let ctor = ProvidedConstructor(
                        parameters = [],
                        InvokeCode = fun args -> <@@ BaseType2() @@>)

    do
        regexTy.AddMember ctor
        providedMethods |> Seq.iter regexTy.AddMember

    do this.AddNamespace(rootNamespace, [regexTy])

[<TypeProviderAssembly>]
do ()
Example 5-31. IL code from the compiled binary reference to the type provider
IL_0000: nop
IL_0001: newobj instance void
[ShareInfoSampleTypeProvider]Samples.FSharp.ShareInfoProvider.BaseType2::.ctor()
IL_0006: box [ShareInfoSampleTypeProvider]Samples.FSharp.ShareInfoProvider.BaseType2
IL_000b: unbox.any [ShareInfoSampleTypeProvider]Samples.FSharp.ShareInfoProvider.BaseType2
IL_0010: dup
IL_0011: stsfld class
[ShareInfoSampleTypeProvider]Samples.FSharp.ShareInfoProvider.BaseType2 '<StartupCode$Cons
oleApplication1>.$Program'::t@5
IL_0016: stloc.0
IL_0017: call class [ShareInfoSampleTypeProvider]Samples.FSharp.ShareInfoProvider.
BaseType2
Program::get_t()
IL_001c: ldstr "F1"
IL_0021: call void
[ShareInfoSampleTypeProvider]Samples.FSharp.ShareInfoProvider.InsertFunctions::LogBeforeEx
ecution(class [ShareInfoSampleTypeProvider]Samples.FSharp.ShareInfoProvider.BaseType2,
string)
IL_0026: call class [ShareInfoSampleTypeProvider]Samples.FSharp.ShareInfoProvider.
BaseType2
Program::get_t()
IL_002b: ldstr "hello"
IL_0030: callvirt instance void
[ShareInfoSampleTypeProvider]Samples.FSharp.ShareInfoProvider.BaseType2::F1(string)
IL_0035: call class [ShareInfoSampleTypeProvider]Samples.FSharp.ShareInfoProvider.BaseType2
Program::get_t()
IL_003a: ldstr "F1"
IL_003f: call void
[ShareInfoSampleTypeProvider]Samples.FSharp.ShareInfoProvider.InsertFunctions::LogAfterExe
cution(class [ShareInfoSampleTypeProvider]Samples.FSharp.ShareInfoProvider.BaseType2,
string)
IL_0044: call class [ShareInfoSampleTypeProvider]Samples.FSharp.ShareInfoProvider.BaseType2
Program::get_t()
IL_0049: ldstr "F2"
IL_004e: call void
[ShareInfoSampleTypeProvider]Samples.FSharp.ShareInfoProvider.InsertFunctions::LogBeforeEx
ecution(class [ShareInfoSampleTypeProvider]Samples.FSharp.ShareInfoProvider.BaseType2,
string)
IL_0053: call class [ShareInfoSampleTypeProvider]Samples.FSharp.ShareInfoProvider.
BaseType2
Program::get_t()
IL_0058: ldc.i4.2
IL_0059: callvirt instance void
[ShareInfoSampleTypeProvider]Samples.FSharp.ShareInfoProvider.BaseType2::F2(int32)
IL_005e: call class [ShareInfoSampleTypeProvider]Samples.FSharp.ShareInfoProvider.
BaseType2
Program::get_t()
IL_0063: ldstr "F2"
IL_0068: call void
[ShareInfoSampleTypeProvider]Samples.FSharp.ShareInfoProvider.InsertFunctions::LogAfterExe
cution(class [ShareInfoSampleTypeProvider]Samples.FSharp.ShareInfoProvider.BaseType2,
string)
IL_006d: ret

At first glance, the wrapper type provider does not appear to derive its type and members from a schema. In this case, the schema is being defined by the data returned from TypeInfo and MethodInfo via reflection. From this sample, I can say that the base class is another way to get schema data in addition to static type parameters and local files. The type provider has the ability to rearrange the data, which are methods and properties, to its new inheritance location and insert customized code during the relocation. The next sample shows how to make an inheritance structure with a type provider.

Using the Multi-Inheritance Type Provider

With C#, the decision was made not to support multi-inheritance. Discussions about the advantages or disadvantages of multi-inheritance often open a can of worms. I am not going to pick a side in this discussion. Instead, this example attempts to demonstrate that a type provider can create a new inheritance hierarchy. Because the user has full control over how to generate the class members, she can decide how to handle any duplicated function names or other nasty problems. This example uses only unique function names and leaves the duplicated-function-name problem to the reader.

First assume C# classes need to be handled by the type provider. The C# class definition is presented in Example 5-32. The C# classes that are sealed make the traditional inheritance more difficult. Note that if the class is located in another assembly, you to make sure that the assembly can be found (as shown in Example 5-33) if the DLL is located next to the type provider DLL.

Example 5-32. C# classes
namespace ClassLibrary1
{
    public sealed class Class1
    {
        public void F1()
        {
            Console.WriteLine("from Class1.F1");
        }

        public void F2()
        {
            Console.WriteLine("from Class1.F2");
        }
    }

    public sealed class Class2
    {
        public void F11()
        {
            Console.WriteLine("from Class2.F11");
        }

        public void F12()
        {
            Console.WriteLine("from Class2.F12");
        }
    }
}
Example 5-33. Code to resolve the reference assembly
[<TypeProvider>]
type public CheckedRegexProvider(tpc :
Microsoft.FSharp.Core.CompilerServices.TypeProviderConfig) as this =
    inherit TypeProviderForNamespaces()

    let handler = System.ResolveEventHandler(fun _ args ->
        let asmName = AssemblyName(args.Name)
        // assuming that we reference only dll files
        let expectedName = asmName.Name + ".dll"
        let expectedLocation =
            // we expect to find this assembly near the dll with type provider
            let d = System.IO.Path.GetDirectoryName(tpc.RuntimeAssembly)
            System.IO.Path.Combine(d, expectedName)
        if System.IO.File.Exists expectedLocation then
          Assembly.LoadFrom expectedLocation
        else
          null
        )
    do System.AppDomain.CurrentDomain.add_AssemblyResolve handler

    interface System.IDisposable with
        member this.Dispose() = System.AppDomain.CurrentDomain.remove_AssemblyResolve
handler

Example 5-34 shows how to use the type provider to generate the provided method whenever the user wants to. The straightforward way is to use a module to hold instances of C# classes and then create provided methods. From the test script, the inheritance is not obvious, but compared to the next example, this approach is more straightforward.

Example 5-34. Multi-inheritance type provider with a test script
namespace Samples.FSharp.ShareInfoProvider

open System.Reflection
open Microsoft.FSharp.Core.CompilerServices
open Samples.FSharp.ProvidedTypes
open System.IO
open System.Collections.Generic
open Microsoft.FSharp.Quotations
open ClassLibrary1

[<AutoOpen>]
module Module =
    let c1 = Class1()
    let c2 = Class2()

[<TypeProvider>]
type public CheckedRegexProvider(tpc :
Microsoft.FSharp.Core.CompilerServices.TypeProviderConfig) as this =
    inherit TypeProviderForNamespaces()

    let handler = System.ResolveEventHandler(fun _ args ->
        let asmName = AssemblyName(args.Name)
        // assuming that we reference only dll files
        let expectedName = asmName.Name + ".dll"
        let expectedLocation =
            // we expect to find this assembly near the dll with type provider
            let d = System.IO.Path.GetDirectoryName(tpc.RuntimeAssembly)
            System.IO.Path.Combine(d, expectedName)
        if System.IO.File.Exists expectedLocation then Assembly.LoadFrom expectedLocation
else null
        )
    do System.AppDomain.CurrentDomain.add_AssemblyResolve handler

    // Get the assembly and namespace used to house the provided types
    let thisAssembly = Assembly.GetExecutingAssembly()
    let rootNamespace = "Samples.ShareInfo.TPTest"
    let baseTy = typeof<obj>

    let regexTy = ProvidedTypeDefinition(
                         thisAssembly,
                         rootNamespace,
                         "MultiInheritanceTypeProvider",
                         Some baseTy)

    let getParameters (mi:MethodInfo) =
        let parameters = mi.GetParameters()
        parameters
        |> Seq.map (fun p -> ProvidedParameter(p.Name, p.ParameterType))
        |> Seq.toList

    let methodsFromC1 = c1.GetType().GetMethods()
    let providedMethodsFromC1 =
        methodsFromC1
        |> Seq.map (fun m ->let methodName = m.Name
                            ProvidedMethod(
                                    methodName = methodName,
                                    parameters = (getParameters m),
                                    returnType = m.ReturnType,
                                    IsStaticMethod = false,
                                    InvokeCode = fun args ->
                                        let baseTExpression = <@@ c1 @@>
                                        let mi = c1.GetType().GetMethod(methodName)
                                        let invokeExpr = Expr.Call(baseTExpression, mi,
args.Tail)
                                        invokeExpr
                                    ))

    let methodsFromC2 = c2.GetType().GetMethods()
    let providedMethodsFromC2 =
        methodsFromC2
        |> Seq.map (fun m ->let methodName = m.Name
                            ProvidedMethod(
                                    methodName = methodName,
                                    parameters = (getParameters m),
                                    returnType = m.ReturnType,
                                    IsStaticMethod = false,
                                    InvokeCode = fun args ->
                                        let baseTExpression = <@@ c2 @@>
                                        let mi = c2.GetType().GetMethod(methodName)
                                        let invokeExpr = Expr.Call(baseTExpression, mi,
args.Tail)
                                        invokeExpr
                                    ))

    let ctor = ProvidedConstructor(
                        parameters = [],
                        InvokeCode = fun args -> <@@ System.Object() @@>)

    do
        regexTy.AddMember ctor
        providedMethodsFromC1 |> Seq.iter regexTy.AddMember
        providedMethodsFromC2 |> Seq.iter regexTy.AddMember

    do this.AddNamespace(rootNamespace, [regexTy])

    interface System.IDisposable with
        member this.Dispose() = System.AppDomain.CurrentDomain.remove_AssemblyResolve
handler

[<TypeProviderAssembly>]
do ()

Test script

#r @".inDebugShareInfoSampleTypeProvider.dll"
#r @".inDebugClassLibrary1.dll"

type T = Samples.ShareInfo.TPTest.MultiInheritanceTypeProvider
let t = T()
t.F1()
t.F2()
t.F11()
t.F12()

If the class does not have complex constructors and creating an object is straightforward, you can use the static parameter and Activator.CreateInstance to create the instance and host it in the base class. Because the static parameter passed into the type provider can only be a basic type such as int and string, this approach is not the best choice for the object that needs a non-basic type parameter. The base class uses a dictionary that creates a relationship between the type name and type instance, as shown in Example 5-35. This version of a type provider takes a DLL file path and two type names, which are used to create the instance for that type. This version is more general, but it requires that the instance creation be simple.

Example 5-35. Base class for a multi-inheritance type provider
type MyBase(dll:string, names:string array) =
    let assembly = Assembly.LoadFile(dll)
    let myDict =
        names
        |> Array.map (fun name -> name, System.Activator.CreateInstance(System.Type.
GetType(name)))
        |> dict
    member this.GetObj(name:string) = myDict.[name]
    member this.GetMethods(name) =
        myDict.[name].GetType().GetMethods()
    member this.GetMethod(name, methodName) =
        myDict.[name].GetType().GetMethod(methodName)
Example 5-36. Multi-inheritance provider with static parameters and its test script
namespace Samples.FSharp.ShareInfoProvider

open System.Reflection
open Microsoft.FSharp.Core.CompilerServices
open Samples.FSharp.ProvidedTypes
open System.IO
open System.Collections.Generic
open Microsoft.FSharp.Quotations
open ClassLibrary1

type MyBase(dll:string, names:string array) =
    let assembly = Assembly.LoadFile(dll)
    let myDict =
        names
        |> Array.map (fun name -> name, System.Activator.CreateInstance(System.Type.
GetType(name)))
        |> dict
    member this.GetObj(name:string) = myDict.[name]
    member this.GetMethods(name) =
        myDict.[name].GetType().GetMethods()
    member this.GetMethod(name, methodName) =
        myDict.[name].GetType().GetMethod(methodName)


[<TypeProvider>]
type public CheckedRegexProvider(tpc :
Microsoft.FSharp.Core.CompilerServices.TypeProviderConfig) as this =
    inherit TypeProviderForNamespaces()

    let handler = System.ResolveEventHandler(fun _ args ->
        let asmName = AssemblyName(args.Name)
        // assuming that we reference only dll files
        let expectedName = asmName.Name + ".dll"
        let expectedLocation =
            // we expect to find this assembly near the dll with type provider
            let d = System.IO.Path.GetDirectoryName(tpc.RuntimeAssembly)
            System.IO.Path.Combine(d, expectedName)
        if System.IO.File.Exists expectedLocation then
            Assembly.LoadFrom expectedLocation
        else
            null
        )
    do System.AppDomain.CurrentDomain.add_AssemblyResolve handler

    // Get the assembly and namespace used to house the provided types
    let thisAssembly = Assembly.GetExecutingAssembly()
    let rootNamespace = "Samples.ShareInfo.TPTest"
    let baseTy = typeof<obj>

    let regexTy = ProvidedTypeDefinition(thisAssembly,
                                                 rootNamespace,
                                                 "MultiInheritanceTypeProvider",
                                                 Some baseTy)
    let getParameters (mi:MethodInfo) =
        let parameters = mi.GetParameters()
        parameters
        |> Seq.map (fun p -> ProvidedParameter(p.Name, p.ParameterType))
        |> Seq.toList

    let staticParams = [ProvidedStaticParameter("dll", typeof<string>);
                            ProvidedStaticParameter("type", typeof<string>);
                            ProvidedStaticParameter("type2", typeof<string>)]
    do regexTy.DefineStaticParameters(
            parameters=staticParams,
            instantiationFunction=(fun typeName parameterValues ->

                let name = parameterValues.[1] :?> string;
                let name2 = parameterValues.[2] :?> string;
                let dll = System.IO.Path.Combine(
                                System.IO.Path.GetDirectoryName(tpc.RuntimeAssembly),
                                parameterValues.[0] :?> string)
                let r = MyBase(dll, [| name; name2 |])

                let ty = ProvidedTypeDefinition(
                             thisAssembly,
                             rootNamespace,
                             typeName,
                             Some typeof<obj>,
                             HideObjectMethods = true)
                [| name; name2 |]
                |> Seq.iter (fun n ->
                    let methods = r.GetMethods(n)
                    let providedMethods =
                        methods
                        |> Seq.map (fun m ->let methodName = m.Name
                                            ProvidedMethod(
                                                    methodName = methodName,
                                                    parameters = (getParameters m),
                                                    returnType = m.ReturnType,
                                                    IsStaticMethod = false,
                                                    InvokeCode = fun args ->
                                                      let baseValue = r
                                                      let baseTExpression = <@@
                                                        ((%%args.[0]:obj) :?> MyBase)
.GetObj(n) @@>
                                                      let mi = baseValue.GetObj(n)
                                                                            .GetType()

.GetMethod(methodName)
                                                      let t = System.Type.GetType(n)
                                                      let invokeExpr =
                                                        Expr.Call(Expr.
Coerce(baseTExpression, t),
                                                                    mi,
                                                                    args.Tail)
                                                      invokeExpr
                                                    ))
                    providedMethods |> Seq.iter ty.AddMember)

                let ctor =
                    ProvidedConstructor(
                        parameters = [],
                        InvokeCode = fun args -> <@@ MyBase(dll, [| name; name2 |]) :> obj
@@>)

                ty.AddMember(ctor)

                ty
                ))

    let ctor = ProvidedConstructor(parameters = [],
                                         InvokeCode = fun args -> <@@ System.Object() @@>)

    do
        regexTy.AddMember ctor

    do this.AddNamespace(rootNamespace, [regexTy])

    interface System.IDisposable with
        member this.Dispose() = System.AppDomain.CurrentDomain.remove_AssemblyResolve
handler

[<TypeProviderAssembly>]
do ()

Test script

#r @".inDebugShareInfoSampleTypeProvider.dll"
#r @".inDebugClassLibrary1.dll"

type T = Samples.ShareInfo.TPTest.MultiInheritanceTypeProvider<
    "ClassLibrary1.dll",
    "ClassLibrary1.Class1, ClassLibrary1, Version=1.0.0.0, Culture=neutral,
PublicKeyToken=null",
    "ClassLibrary1.Class2, ClassLibrary1, Version=1.0.0.0, Culture=neutral,
PublicKeyToken=null">

let t = T()
t.F1()
t.F2()
t.F11()
t.F12()

Note

Because the dictionary returns a System.Object type, and because it needs to be converted to the correct type, the conversion code Expr.Coerce(baseTExpression, t) is required.

The wrapper and multi-inheritance type provider sample shows how a type provider can be used for meta-programming. The data passed into the type provider can originate from a base class or module inside the type-provider code in addition to the static parameter, local files, or both.

Using the XML Type Provider

This section shows you how to use the XML type provider’s design-time feature. The definition of the type provider shows that it is a design-time adapter component. The example I present in this section explores the design-time part of the definition. Many applications use XML files as feeders of input data. The problem with this approach is that XML can become complicated and errors in this text file can be checked only at run time. The XML type provider moves validation that is typically done during run time to design time.

InvokeCode in ProvidedMethod returns a code quotation, and it can also contain other code. The quotation will be executed at run time, whereas the other code is executed at design time. This is where the validation code can be inserted and executed.

The production code and the sample XML file are shown in Example 5-37 and Example 5-38, respectively. Both listings require that the XML file meet certain criteria: the start value must be equal or smaller than the end value. If the validation can happen only at run time, that could be a disaster. Another approach is to use a validation tool, such as a check-in gate validation. None of these approaches can provide a comparable coding experience if this validation can be performed at design time. Ideally, when a user invokes the method, he will see the error right away.

Example 5-37. Production code to be invoked by the type provider
type MyProductionCode() =
    member this.Work() =
        let xml = XDocument.Load("XmlFile1.xml")
        let start = xml.Descendants(XName.Get("Start")) |> Seq.head
        let end' = xml.Descendants(XName.Get("End")) |> Seq.head

        // if start > end, something bad will happen
        if Convert.ToInt32(start.Value) > Convert.ToInt32(end'.Value) then
            failwith "report illegal parameter from runtime"
Example 5-38. Sample XML file
<?xml version="1.0" encoding="utf-8" ?>

<Config>
  <Start>6</Start>
  <End>5</End>
</Config>

As usual, the test script is defined first. (See Example 5-39.) If the XML file is invalid, the error will be shown in Visual Studio, as you can see in Figure 5-7. The developer will know immediately that the configuration file is invalid.

Example 5-39. Test script for XMLTypeProvider
#r @".inDebugXMLTypeProvider.dll"

type T = Samples.ShareInfo.TPTest.XMLTypeProvider<"XmlFile1.xml">
let t = T()
let a = t.Work()
Design-time error from the type provider
Figure 5-7. Design-time error from the type provider

The beauty of this type provider is that the validation code will be executed only at design time. Unlike the test case that executed regardless of whether the function was invoked, the final binary allows the function to be invoked only if the validation passes. To recap, the type provider moves the run-time failure check to design time. The completed code is shown in Example 5-40.

Example 5-40. XMLTypeProvider code
// Learn more about F# at http://fsharp.net
// See the 'F# Tutorial' project for more help.

namespace Samples.FSharp.XMLTypeProvider

open System.Reflection
open Microsoft.FSharp.Core.CompilerServices
open Samples.FSharp.ProvidedTypes
open System.Xml.Linq
open Microsoft.FSharp.Quotations
open System

type MyProductionCode() =
    member this.Work() =
        let xml = XDocument.Load("XmlFile1.xml")
        let start = xml.Descendants(XName.Get("Start")) |> Seq.head
        let end' = xml.Descendants(XName.Get("End")) |> Seq.head

        // if start > end, something bad will happen
        if Convert.ToInt32(start.Value) > Convert.ToInt32(end'.Value) then
            failwith "report illegal parameter from runtime"

type MyTest() =
    member this.Test1(xml:string) =
        let xml = XDocument.Load(xml)
        let start = xml.Descendants(XName.Get("Start")) |> Seq.head
        let end' = xml.Descendants(XName.Get("End")) |> Seq.head
        if Convert.ToInt32(start.Value) > Convert.ToInt32(end'.Value) then
            failwith "report illegal parameter from design-time"

[<AutoOpen>]
module TestModule =
    let test = MyTest()

[<TypeProvider>]
type public TypeProvider1(cfg : Microsoft.FSharp.Core.CompilerServices.TypeProviderConfig)
    as this =
    inherit TypeProviderForNamespaces()

    // Get the assembly and namespace used to house the provided types
    let thisAssembly = Assembly.GetExecutingAssembly()
    let rootNamespace = "Samples.ShareInfo.TPTest"
    let baseTy = typeof<obj>

    let newT = ProvidedTypeDefinition(thisAssembly, rootNamespace,
                                             "XMLTypeProvider", Some baseTy)

    let ctor = ProvidedConstructor(
                   parameters = [],
                   InvokeCode = fun args -> <@@ (* base class initialization or null*) ()
@@>)

    let staticParams = [ProvidedStaticParameter("xml file name", typeof<string>)]
    do newT.DefineStaticParameters(
        parameters = staticParams,
        instantiationFunction=(fun typeName parameterValues ->
                let baseTy = typeof<MyProductionCode>

                let ty = ProvidedTypeDefinition(
                             thisAssembly,
                             rootNamespace,
                             typeName,
                             baseType = Some baseTy)

                let xmlFileName = System.IO.Path.Combine(
                                        cfg.ResolutionFolder,
                                        parameterValues.[0] :?> string)
                let m = ProvidedMethod(
                            methodName = "Work",
                            parameters = [],
                            returnType = typeof<unit>,
                            IsStaticMethod = false,
                            InvokeCode = fun args ->
                                test.Test1(xmlFileName)
                                <@@ (%%args.[0]:MyProductionCode).Work() @@>
                            )

                let ctor = ProvidedConstructor(
                                        parameters = [],
                                        InvokeCode = fun args -> <@@ MyProductionCode()
@@>)
                ty.AddMember(ctor)
                ty.AddMember(m)

                ty.AddXmlDoc "xml comment"
                ty))

    do
        newT.AddMember(ctor)

    do this.AddNamespace(rootNamespace, [newT])

[<TypeProviderAssembly>]
do ()

Note

This code requires a reference to System.Xml and System.Xml.Linq.

The type provider gives the user a place to design his own design-time logic. You can use type providers to better take advantage of the editing power of Visual Studio without getting into the details of the Visual Studio SDK.

Using the DGML-File Type Provider

Directed Graph Markup Language (DGML) is supported in Visual Studio. DGML is used to draw a diagram representing a graph structure that contains nodes and edges. You can use DGML to represent a state machine, where the node is the state and the edge is the possible transition between states. Figure 5-8 illustrates how the DGML is shown in Visual Studio. It represents a state machine with four states: State0 to State3. The DGML file is actually an XML file. Example 5-41 shows the content of the DGML file that is shown in Figure 5-8. The Nodes section defines all the nodes in the graph, and the Links section defines all the edges.

DGML file that represents a state machine with four states
Figure 5-8. DGML file that represents a state machine with four states
Example 5-41. DGML file content
<?xml version="1.0" encoding="utf-8"?>
<DirectedGraph GraphDirection="LeftToRight" Layout="Sugiyama"
xmlns="http://schemas.microsoft.com/vs/2009/dgml">
  <Nodes>
    <Node Id="State0" Bounds="0,19.02,53.36,25.96" />
    <Node Id="State1" Bounds="170.095,93.0200000000001,53.36,25.96" />
    <Node Id="State2" Bounds="85.0474999999999,93.02,53.36,25.96" />
    <Node Id="State3" Bounds="255.1425,93.02,53.3600000000001,25.96" />
  </Nodes>

  <Links>
    <Link Source="State0" Target="State1"
Bounds="53.3600006103516,32,131.471939086914,53.2166442871094" />
    <Link Source="State0" Target="State2"
Bounds="41.5977897644043,44.9799995422363,48.4222755432129,42.1323204040527" />
    <Link Source="State0" Target="State3"
Bounds="43.9286956787109,1.24344978758018E-14,229.335739135742,84.6670761108398" />
    <Link Source="State1" Target="State2"
Bounds="132.513122558594,118.980003356934,50.1091003417969,16.2699966430664" />
    <Link Source="State2" Target="State3"
Bounds="138.407501220703,82.2404556274414,108.355163574219,16.3045043945313" />
    <Link Source="State3" Target="State1"
Bounds="217.560623168945,118.980003356934,50.1091156005859,16.2699966430664" />
  </Links>

  <Properties>
    <Property Id="Bounds" DataType="System.Windows.Rect" />
    <Property Id="GraphDirection"
DataType="Microsoft.VisualStudio.Diagrams.Layout.LayoutOrientation" />
    <Property Id="Layout" DataType="System.String" />
  </Properties>

</DirectedGraph>

As I did with the other type-provider samples, I first define how a user uses the DGML-file type provider. The DGML file defines a state machine shown in Figure 5-8. Example 5-42 has the following three parts and shows how a user can use the type provider:

  • Part I creates the type provider, which takes two parameters. One parameter specifies where the DGML file is. The other parameter specifies the initial state of the state machine. At this point, the state machine is created from the DGML file.

  • Part II defines a set of transition functions. The transition is encapsulated in an object expression that implements the IState interface, which is defined in the following code sample. EnterFunction and ExitFunction will be invoked when the current state is going into the enter or exit state.

    type IState =
        // function which will be invoked when entering a state
        abstract member EnterFunction : unit -> unit
    
        // function which will be invoked when exiting a state
        abstract member ExitFunction : unit->unit
  • Part III performs a series of transition operations. From the DGML definition, you can see that the first three transitions are valid. The last transition, which is from the current state of State3 to State2, is invalid and thus the transition function is not executed.

Example 5-42. Test script for the DGML-file type provider
// Part I
#r @".inDebugTypeProviderTemplate1.dll"

type T =
  Samples.ShareInfo.TPTest.TPTestType<
    """C:MyCodeTypeProviderTemplate1TypeProviderTemplate1Graph1.dgml""", "State0">
let t = T()

// Part II
// define print function
let syncRoot = ref 0
let print str = lock(syncRoot) (fun _ -> printfn "%s" str)

// object expression for transition
let printObj = { new StateMachineTypeProvider.IState with
                            member this.EnterFunction() = print ("Enter " +
t.CurrentState)
                            member this.ExitFunction() = print ("Exit " + t.CurrentState)
}

// set the transition functions
t.SetFunction(t.State0, printObj)
t.SetFunction(t.State1, printObj)
t.SetFunction(t.State2, printObj)
t.SetFunction(t.State3, printObj)

// Part III
// valid transitions
t.TransitTo_State1()
t.TransitTo_State2()
t.TransitTo_State3()

// invalid transition
t.TransitTo_State2()

The state machine reads the DGML file and generates a graph structure. MailboxProcessor is applied to process the message in an asynchronous way. The actual implementation defines two classes, as shown in Example 5-43. The DGMLClass type represents the DGML file with a list of nodes and links defined in the file. The StateMachine class inherits from DGMLClass. It adds MailboxProcessor to process the transition asynchronously and several member methods to set the transition functions. These two classes can be treated like a library that can process any DGML file.

Example 5-43. DGML-file type provider state machine implementation

DGMLClass representing the DGML file

namespace StateMachineTypeProvider

open System.Xml
open System.Xml.Linq

// define the state interface
type IState =
    // function which will be invoked when entering a state
    abstract member EnterFunction : unit -> unit

    // function which will be invoked when exiting a state
    abstract member ExitFunction : unit->unit

// define a node record
type Node = { Name : string; NextNodes : Node list}

// define DGML DU
type DGML =
    | Node of string
    | Link of string * string

// define DGML class
type DGMLClass() =

    let mutable nodes = Unchecked.defaultof<Node list>
    let mutable links = Unchecked.defaultof<DGML list>
    let mutable currentState = System.String.Empty

    // current state
    member this.CurrentState
        with get() = currentState
        and private set(v) = currentState <- v
    // all links in the DGML file
    member this.Links
        with get() = links
        and private set(v) = links <- v

    // all nodes in the DGML file
    member this.Nodes
        with get() = nodes
        and private set(v) = nodes <- v

    // initialize the state machine from fileName and set the initial state to initState
    member this.Init(fileName:string, initState) =
        let file = XDocument.Load(fileName, LoadOptions.None)
        this.Links <-
            file.Descendants()
            |> Seq.filter (fun node -> node.Name.LocalName = "Link")
            |> Seq.map (fun node ->
                            let sourceName = node.Attribute(XName.Get("Source")).Value
                            let targetName = node.Attribute(XName.Get("Target")).Value
                            DGML.Link(sourceName, targetName))
            |> Seq.toList

        let getNextNodes fromNodeName=
                this.Links
                |> Seq.filter (fun (Link(a, b)) -> a = fromNodeName)
                |> Seq.map (fun (Link(a,b)) -> this.FindNode(b))
                |> Seq.filter (fun n -> match n with Some(x) -> true | None -> false)
                |> Seq.map (fun (Some(n)) -> n)
                |> Seq.toList

        this.Nodes <-
            file.Descendants()
            |> Seq.filter (fun node -> node.Name.LocalName = "Node")
            |> Seq.map (fun node -> DGML.Node( node.Attribute(XName.Get("Id")).Value) )
            |> Seq.map (fun (Node(n)) -> { Node.Name=n; NextNodes = [] })
            |> Seq.toList

        this.Nodes <-
            this.Nodes
            |> Seq.map (fun n -> { n with NextNodes = (getNextNodes n.Name) } )
            |> Seq.toList

        this.CurrentState <- initState

    // find the node by the given nodeName
    member this.FindNode(nodeName) : Node option=
        let result =
            this.Nodes
            |> Seq.filter (fun n -> n.Name = nodeName)
        if result |> Seq.isEmpty then None
        else result |> Seq.head |> Some

    // current node
    member this.CurrentNode
        with get() =
            this.Nodes
            |> Seq.filter (fun n -> n.Name = this.CurrentState)
            |> Seq.head

    // determine if one node can transit to another one represented by the nodeName
    member this.CanTransitTo(nodeName:string) =
        this.CurrentNode.NextNodes |> Seq.exists (fun n -> n.Name = nodeName)

    // force current state to a new state
    member this.ForceStateTo(args) =
        this.CurrentState <- args

    // assert function used for debugging
    member this.Assert(state) =
        if this.CurrentState <> state then
            failwith "assertion failed"

StateMachine class inheriting from DGMLClass

// state machine class which inherits the DGML class
// and uses a MailboxProcessor to perform asynchronous message processing
type StateMachine() as this =
    inherit DGMLClass()

    let functions = System.Collections.Generic.Dictionary<string, IState>()
    let processor = new MailboxProcessor<string>(fun inbox ->
                    let rec loop () =
                        async {
                            let! msg = inbox.Receive()
                            if this.CanTransitTo(msg) then
                                this.InvokeExit(this.CurrentNode.Name)
                                this.ForceStateTo(msg)
                                this.InvokeEnter(msg)
                            return! loop ()
                        }
                    loop ())

    do
        processor.Start()

    // define the second constructor taking the file name and initial state name
    new(fileName, initState) as secondCtor=
        new StateMachine()
        then
            secondCtor.Init(fileName, initState)

    // asynchronously transit to a new state
    member this.TransitTo(state) =
        processor.Post(state)

    // set the transition function
    member this.SetFunction(name:string, state:IState) =
        if functions.ContainsKey(name) then
            functions.[name] <- state
        else
            functions.Add(name, state)

    // invoke the Exit function
    member private this.InvokeExit(name:string) =
        if functions.ContainsKey(name) then
            functions.[name].ExitFunction()

    // invoke the Enter function
    member private this.InvokeEnter(name:string) =
        if functions.ContainsKey(name) then
            functions.[name].EnterFunction()

The type-provider implementation uses the StateMachine type as a base class. It generates the transition function TransitTo_<target node name> and the assert function Assert_<node name>. Because it is easy to make a typo when specifying the state name, I chose to generate the state name as a read-only property. SetFunction is not generated from the type provider; it is a function defined on the StateMachine class. The code is shown in Example 5-44.

Example 5-44. DGML-file type provider code
namespace Samples.FSharp.TypeProviderTemplate1

open System.Reflection
open Microsoft.FSharp.Core.CompilerServices
open Samples.FSharp.ProvidedTypes

open StateMahcineTypeProvider

[<TypeProvider>]
type public TypeProvider1() as this =
    inherit TypeProviderForNamespaces()

    // Get the assembly and namespace used to house the provided types
    let thisAssembly = Assembly.GetExecutingAssembly()
    let rootNamespace = "Samples.ShareInfo.TPTest"
    let baseTy = typeof<StateMachine>

    let newT = ProvidedTypeDefinition(thisAssembly, rootNamespace, "TPTestType", Some
baseTy)

    // define two static parameters: one is the DGML file name and
    // the other is the initial state
    let staticParams = [ProvidedStaticParameter("dgml file name", typeof<string>);
                            ProvidedStaticParameter("init state", typeof<string>)]

    do newT.DefineStaticParameters(
        parameters=staticParams,
        instantiationFunction=(fun typeName parameterValues ->
                let ty = ProvidedTypeDefinition(
                            thisAssembly,
                            rootNamespace,
                            typeName,
                            baseType = Some baseTy)

                let dgml, initState = parameterValues.[0] :?> string, parameterValues.[1]
:?> string

                // generate a new StateMachine instance to generate the read-only state
properties
                let stateMachine = StateMachine()
                stateMachine.Init(dgml, initState)

                let stateProperties =
                    stateMachine.Nodes
                    |> Seq.map (fun n ->
                                let name = n.Name
                                let prop1 = ProvidedProperty(propertyName = n.Name,
                                             propertyType = typeof<string>,
                                             IsStatic=false,
                                             GetterCode= (fun args -> <@@ name @@>))
                                prop1
                                )

                stateProperties
                |> Seq.iter ty.AddMember

                // generate the assert functions
                let asserts =
                    stateMachine.Nodes
                    |> Seq.map (fun n ->
                                    let name = n.Name
                                    let assertFunction =
                                        ProvidedMethod(
                                            methodName = sprintf "Assert_%s" name,
                                            parameters = [],
                                            returnType = typeof<unit>,
                                            IsStaticMethod = false,
                                            InvokeCode = fun args ->
                                                <@@ (%%args.[0] :> StateMachine).
Assert(name)  @@>
                                            )
                                    assertFunction)
                asserts
                |> Seq.iter ty.AddMember

                // generate the transition functions
                let transits =
                    stateMachine.Nodes
                    |> Seq.map (fun node ->
                                    let name = node.Name
                                    let m = ProvidedMethod(
                                                methodName = sprintf "TransitTo_%s" name,
                                                parameters = [],
                                                returnType = typeof<unit>,
                                                IsStaticMethod = false,
                                                InvokeCode = fun args ->
                                                    <@@
                                                      (%%args.[0] :> StateMachine).
TransitTo(name)
                                                    @@>
                                                )
                                    m)
                transits
                |> Seq.iter ty.AddMember

                let setFunction =
                         ProvidedMethod(
                            methodName = "SetFunction",
                            parameters = [ProvidedParameter("name", typeof<string>);
                                             ProvidedParameter("state
class",typeof<IState>)],
                            returnType = typeof<unit>,
                            IsStaticMethod = false,
                            InvokeCode = fun args ->
                                                  <@@ (%%args.[0] :> StateMachine)
                                                         .SetFunction(%%args.[1] :>
string,
                                                                         %%args.[2] :>
IState)
                                                  @@>
                                              )
                ty.AddMember(setFunction)

                // define the constructor
                let ctor = ProvidedConstructor(
                                    parameters = [],
                                    InvokeCode = fun args -> <@@ StateMachine(dgml,
initState) @@>)
                ty.AddMember(ctor)

                ty.AddXmlDoc "xml comment"
                ty))

    do this.AddNamespace(rootNamespace, [newT])

[<TypeProviderAssembly>]
do ()

Note

The type provider does not erase the type to System.Object anymore; therefore, IntelliSense shows a full list of class members from the base class. In the sample, SetFunction is visible in the IntelliSense drop-down list.

This sample shows a way to enhance the experience of working with a DGML file using a general-purpose library. The type provider can customize the property according to a specific DGML file, and any breaking change on the file will be reflected at compile time.

Separating Run Time and Design Time

The XML type provider shows how type providers handle design-time logic and run-time logic. Actually, design-time logic and run-time logic can be separated into different assemblies. The design-time DLL not only provides the design-time logic, it also inserts extra logic to change the final assembly’s logic. Example 5-45 shows the run-time DLL. The TypeProvider attribute is the key that indicates which DLL is the type-provider assembly.

Note

The run-time DLL and type-provider DLL should be located in the same folder.

The sample solution has two projects. One is for the run-time assembly, and the other is the design-time type-provider code. The structure of the solution is shown in Figure 5-9.

Type-provider solution separating run time and design time into two assemblies
Figure 5-9. Type-provider solution separating run time and design time into two assemblies
Example 5-45. Run-time logic for the type provider in the Library1 project
namespace Samples.MiniTypeSpace.Runtime

type ObjectBase() =
    member x.Kind = "I'm an object"

type ObjectRepresentation(contents:string) =
    inherit ObjectBase()
    member x.Contents = contents

type RuntimeMethods() =
   static member Create(s:string) = ObjectRepresentation(s + " - hello!") :> ObjectBase
   static member GetContents(s:ObjectBase) = (s :?> ObjectRepresentation).Contents

// This declaration points the compiler to the right design-time DLL for the selected
runtime DLL.
open Microsoft.FSharp.Core.CompilerServices
[<assembly:TypeProviderAssembly("DesignTimeSeparation")>]

do()

Note

The type provider project will generate an assembly named DesignTimeSeparation.

The design-time type-provider code is listed in Example 5-46. It generates class members to invoke the functions present in Example 5-45.

Example 5-46. Type-provider code in the DesignTimeSeparation project associated with the run-time DLL in Example 5-45
// Learn more about F# at http://fsharp.net
// See the 'F# Tutorial' project for more help.

namespace Samples.FSharp.DesignTimeSeparation

open System.Reflection
open Microsoft.FSharp.Core.CompilerServices
open Samples.FSharp.ProvidedTypes
open Microsoft.FSharp.Quotations

[<TypeProvider>]
type public TypeProvider1(config: TypeProviderConfig) as this =
    inherit TypeProviderForNamespaces()
    let assem = config.RuntimeAssembly
    let runtimeAssembly = System.Reflection.Assembly.ReflectionOnlyLoadFrom(assem)

    let objectBase = runtimeAssembly.GetType("Samples.MiniTypeSpace.Runtime.ObjectBase")
    let runtimeMethods = runtimeAssembly.GetType("Samples.MiniTypeSpace.Runtime.
RuntimeMethods")
    let createMethod = runtimeMethods.GetMethod("Create")
    let getContentsMethod = runtimeMethods.GetMethod("GetContents")

    // Get the assembly and namespace used to house the provided types
    let rootNamespace = "Samples.ShareInfo.TPTest"
    let baseTy = objectBase

    let newT = ProvidedTypeDefinition(runtimeAssembly, rootNamespace, "TPTestType", Some
baseTy)

    let ctor = ProvidedConstructor([ProvidedParameter("Contents",typeof<string>)],
                                       InvokeCode = (fun args -> Expr.Call(createMethod, [
args.[0] ])))

    // This is a property. At runtime, the property just gets the string value
    let prop2 = ProvidedProperty(
                     "Property2",
                     typeof<string>,
                     GetterCode= (fun args -> Expr.Call(getContentsMethod, [ args.[0] ])))

    do prop2.AddXmlDocDelayed(fun () -> "xml comment")
    do
        newT.AddMember(prop2)
        newT.AddMember(ctor)

    do this.AddNamespace(rootNamespace, [newT])

[<TypeProviderAssembly>]
do ()

Invoking the type provider, as shown in Example 5-47, is similar to the other example. The reference part is the main difference. In the other examples, there is only one assembly, but now there are two assemblies. The run-time DLL is what our test code needs to reference in order to use this type provider.

Example 5-47. Invoking the type provider
#r @".inDebugLibrary1.dll"

type T = Samples.ShareInfo.TPTest.TPTestType
let t = T("AA")
let c = t.Kind
let d = t.Property2

Generated Type Provider

The last example in this chapter is a generated type provider. The generated type provider behaves like a code generator. It does not support lazy type generation like an erased type provider does. However, there are several advantages to using a generated type provider versus an erased type provider. The generated type provider generates an assembly that can be used from other language. Because the generated assembly is present, you can use .NET reflection to retrieve type information. I’ll start by showing you how to write a generated type provider. The code needed to create a generated type provider, which is shown in Example 5-48, is similar to the erased version except that a new ProvidedAssembly class is needed. Let’s start from a simple HelloWorld type-provider project, TypeProviderTemplate3, which is created from the TypeProvider template. If you compare Example 5-48 and Example 5-7, there are a few differences. There is an assembly variable whose type is ProvidedAssembly. The ProvidedType newT needs to set its IsErased and SuppressRelocation properties to false before it can be added to ProvidedAssembly.

Example 5-48. HelloWorld generated type provider
namespace Samples.FSharp.TypeProviderTemplate3

open System.Reflection
open Microsoft.FSharp.Core.CompilerServices
open Samples.FSharp.ProvidedTypes

[<TypeProvider>]
type public TypeProvider1() as this =
    inherit TypeProviderForNamespaces()

    // Get the assembly and namespace used to house the provided types
    let assembly = ProvidedAssembly(@"c:mycode	t.dll")
    let thisAssembly = Assembly.GetExecutingAssembly()
    let rootNamespace = "Samples.ShareInfo.TPTest"
    let baseTy = typeof<obj>

    let newT = ProvidedTypeDefinition(thisAssembly, rootNamespace, "TPTestType", Some
baseTy)

    // add other property and method definition
    let m = ProvidedMethod(
                methodName = "methodName",
                parameters = [],
                returnType = typeof<int>,
                IsStaticMethod = false,
                InvokeCode = fun args ->
                    <@@ 1 + 1 @@>
                )

    let ctor = ProvidedConstructor(parameters = [], InvokeCode = fun args -> <@@ obj()
@@>)

    let prop2 = ProvidedProperty(propertyName = "Property1",
                                 propertyType = typeof<string>,
                                 IsStatic=false,
                                 GetterCode= (fun args -> <@@ "Hello!" @@>),
                                 SetterCode = (fun args -> <@@ printfn "setter code" @@>))

    do prop2.AddXmlDocDelayed(fun () -> "xml comment")
    do
        newT.AddMember(m)
        newT.AddMember(prop2)
        newT.AddMember(ctor)
        newT.IsErased <- false
        newT.SuppressRelocation <- false
        assembly.AddTypes [newT]

    do this.AddNamespace(rootNamespace, [newT])

[<TypeProviderAssembly>]
do ()

Note

Be sure that the C:MyCode folder is created because the DLL will be generated there.

The constructor needs to return obj(). If the default <@@ () @@> is used, the constructor will return a NULL value and generate a runtime error.

You need to create another project to consume the generated type or types, so you create an F# console project for that purpose. Instead of adding a reference to the generated tt.dll under C:MyCode, you need to add a reference to the output of the type-provider project TypeProviderTemplate3. Figure 5-10 shows the Console project references.

Project reference in a generated type-provider solution
Figure 5-10. Project reference in a generated type-provider solution

Example 5-49 shows Program.fs, which creates the generated type and prints out the property value.

Example 5-49. Program.fs, which accesses the generated types
type T = Samples.ShareInfo.TPTest.TPTestType
let t = T()
let p = t.Property1

printfn "t.Property1 value is %A" p

ignore <| System.Console.ReadKey()

For a generated type provider, you can create fields to hold values by using ProvidedField. Example 5-50 shows how to create a field and expose it.

Example 5-50. Using ProvidedField to create a field in a generated type provider
namespace Samples.FSharp.TypeProviderTemplate3

open System.Reflection
open Microsoft.FSharp.Core.CompilerServices
open Samples.FSharp.ProvidedTypes

[<TypeProvider>]
type public TypeProvider1() as this =
    inherit TypeProviderForNamespaces()

    // Get the assembly and namespace used to house the provided types
    let assembly = ProvidedAssembly(@"c:mycode	t.dll")
    let thisAssembly = Assembly.GetExecutingAssembly()
    let rootNamespace = "Samples.ShareInfo.TPTest"
    let baseTy = typeof<obj>

    let newT = ProvidedTypeDefinition(thisAssembly, rootNamespace, "TPTestType", Some
baseTy)

    // add other property and method definition
    let m = ProvidedMethod(
                methodName = "methodName",
                parameters = [],
                returnType = typeof<int>,
                IsStaticMethod = false,
                InvokeCode = fun args ->
                    <@@ 1 + 1 @@>
                )

    let ctor = ProvidedConstructor(parameters = [], InvokeCode = fun args -> <@@ obj()
@@>)

    let prop2 = ProvidedProperty(propertyName = "Property1",
                                 propertyType = typeof<string>,
                                 IsStatic=false,
                                 GetterCode= (fun args -> <@@ "Hello!" @@>),
                                 SetterCode = (fun args -> <@@ printfn "setter code" @@>))

    let field1 = ProvidedField("myField", typeof<string>)
    let prop3 = ProvidedProperty(
                  propertyName = "Property2",
                  propertyType = typeof<string>,
                  IsStatic=false,
                  GetterCode= (fun [me] -> Microsoft.FSharp.Quotations.Expr.
FieldGet(me,field1)),
                  SetterCode = (fun [me;v] -> Microsoft.FSharp.Quotations.Expr.
FieldSet(me,field1, v)))

    do prop2.AddXmlDocDelayed(fun () -> "xml comment")
    do
        newT.AddMember(m)
        newT.AddMember(prop2)
        newT.AddMember(field1)
        newT.AddMember(prop3)
        newT.AddMember(ctor)

        newT.IsErased <- false

        newT.SuppressRelocation <- false
        assembly.AddTypes [newT]


    do this.AddNamespace(rootNamespace, [newT])

[<TypeProviderAssembly>]
do ()

Note

Assigning SuppressRelocation generates a warning, which you can ignore.

The test code is presented in Example 5-51. From the execution result, you can see the field is created in the assembly and that you can use Property2 to access the field.

Example 5-51. Test code for a generated type provider with a generated field
type T = Samples.ShareInfo.TPTest.TPTestType
let t = T()
let p = t.Property1

printfn "t.Property1 value is %A" p

let b = t.GetType().GetField(
             "myField",
             System.Reflection.BindingFlags.Instance |||
             System.Reflection.BindingFlags.NonPublic)

printfn "found myField? %A" ( b<>null )

printfn "t.Property2 value is %A" t.Property2
t.Property2 <- "aa"
printfn "t.Property2 value is %A" t.Property2

ignore <| System.Console.ReadKey()

Execution result

t.Property1 value is "Hello!"
found myField? True
t.Property2 value is <null>
t.Property2 value is "aa"

The type-provider API also provides a way to decorate a property with an attribute. Example 5-52 shows how to add an attribute to a property and how to generate the type that is returned from the DefineStaticParameters function.

Example 5-52. Adding an attribute to a generated property
namespace Samples.FSharp.TypeProviderTemplate4

open System.Reflection
open Microsoft.FSharp.Core.CompilerServices
open Samples.FSharp.ProvidedTypes

type MyAttribute() =
    inherit System.Attribute()

[<TypeProvider>]
type public TypeProvider1() as this =
    inherit TypeProviderForNamespaces()

    // Get the assembly and namespace used to house the provided types
    let thisAssembly = Assembly.GetExecutingAssembly()
    let rootNamespace = "Samples.ShareInfo.TPTest"
    let baseTy = typeof<obj>

    let newT = ProvidedTypeDefinition(thisAssembly, rootNamespace, "TPTestType", Some
baseTy)
    let assembly = ProvidedAssembly(@"c:Mycode	t.dll")
    let data = ProvidedStaticParameter("filename", typeof<string>)
    do newT.DefineStaticParameters(
            [data],
            fun tyName [| :? string as data |] ->
                let ty = ProvidedTypeDefinition(
                               thisAssembly,
                               rootNamespace,
                               tyName,
                               Some(typeof<obj>))
                ty.AddMember(ProvidedConstructor([], InvokeCode = fun _ -> <@@ obj() @@>))
                let p = ProvidedProperty(data, typeof<string>,
                                                GetterCode = fun [me] -> <@@ data @@>)
                p.AddAttribute(typeof<MyAttribute>)
                ty.AddMember(p)

                ty.IsErased <- false
                ty.SuppressRelocation <- false
                assembly.AddTypes [ty]
                ty)

    do
        newT.IsErased <- false
        newT.SuppressRelocation <- false
        this.AddNamespace(rootNamespace, [newT])
        assembly.AddTypes [newT]

[<TypeProviderAssembly>]
do ()

Example 5-53 shows the attribute is correctly added to the property.

Example 5-53. A test-provided property that has an attribute
type T = Samples.ShareInfo.TPTest.TPTestType<"PropertyA">
let t = T()
printfn "propertyA value is %A" t.PropertyA
let pi = t.GetType().GetProperty("PropertyA")
printfn "propertyA has attribute %s"
    ( pi.GetCustomAttributes(typeof<Samples.FSharp.TypeProviderTemplate4.MyAttribute>,
true)
      |> Seq.head
      |> fun attribute -> attribute.ToString() )

ignore <| System.Console.ReadKey()

Execution result

propertyA value is "PropertyA"
propertyA has attribute Samples.FSharp.TypeProviderTemplate4.MyAttribute

Using Type-Provider Snippets

The F# snippets are designed to make the act of authoring a type provider even easier. The snippet project is published at http://fsharpcodesnippet.codeplex.com/ as an open source project. Each type provider snippet does not have shortcut support; however, you can press Ctrl+K,X to bring up a list of all snippets, as shown in Figure 5-11. Table 5-1 lists the commonly used code snippets for type-provider authoring.

Code snippets for type-provider writing
Figure 5-11. Code snippets for type-provider writing
Table 5-1. Code snippets for type-provider writing

Snippet Name

Description

Type provider define static parameter

Generates skeleton code for the DefineStaticParameters function that takes the static parameter. Here is the default code generated:

let staticParams = [ProvidedStaticParameter("pattern", typeof<string>)]
    do regexTy.DefineStaticParameters(
        parameters=staticParams,
        instantiationFunction=(fun typeName parameterValues ->
                let ty = ProvidedTypeDefinition(
                    thisAssembly,
                    rootNamespace,
                    typeName,
                    baseType = Some baseTy)

                ty.AddXmlDoc "xml comment"
                ty))

Type provider provided constructor

Generates the type-provider constructor. Here is the default code generated:

let ctor = ProvidedConstructor(
               parameters = [],
               InvokeCode = fun args ->
                                <@@
                                 (* base class initialization or null*) ()
                                @@>)

Type provider provided method

Generates the type-provider method. Here is the default code generated:

let m = ProvidedMethod(
            methodName = "methodName",
            parameters = [],
            returnType = typeof<int>,
            IsStaticMethod = false,
            InvokeCode = fun args ->
                <@@ 1 + 1 @@>
            )

Type provider provided property

Generates the type-provider property. Here is the default code generated:

let prop1 = ProvidedProperty(
                  propertyName = "Property1",
                  propertyType = typeof<string>,
                  IsStatic=false,
                  GetterCode= (fun args -> <@@ "Hello!" @@>),
                  SetterCode = (fun args -> <@@ printfn "setter code" @@>))

Type provider skeleton

Generates the skeleton for a type provider. This is the default code generated:

namespace Samples.FSharp.ShareInfoProvider

open System.Reflection
open Microsoft.FSharp.Core.CompilerServices
open Samples.FSharp.ProvidedTypes

[<TypeProvider>]
type public CheckedRegexProvider() as this =
    inherit TypeProviderForNamespaces()

    // Get the assembly and namespace used to house the provided types
    let thisAssembly = Assembly.GetExecutingAssembly()
    let rootNamespace = "Samples.ShareInfo.TPTest"
    let baseTy = typeof<obj>

    let regexTy = ProvidedTypeDefinition(thisAssembly, rootNamespace,
                                                 "TPTestType", Some baseTy)

    // add other property and method definition

    do this.AddNamespace(rootNamespace, [regexTy])

[<TypeProviderAssembly>]
do ()

Type provider xmlcomment

Generates the XML comment. This is the default code generated:

ctor.AddXmlDocDelayed(fun () -> "xml comment")

Provided measure builder

Generates the unit of measure for the type-provider type. Here is the default code generated:

let units = ProvidedMeasureBuilder.Default.SI “metre” let unitType = ProvidedMeasureBuilder.Default.AnnotateType(typeof<float>, [units] )

Type provider provided parameter

Generates the type-provider function parameter. This is the default code generated:

ProvidedParameter(“var”, typeof<string>)

Type-Provider Limitations

The type-provider mechanism that shipped with F# 3.0 has some limitations. The first limitation that a reader might have already noticed is that the static parameter can only take basic types, such as integer and string. The other limitation is that the type provider can only generate .NET 1.x types. Most of the F# specific types such as record are not supported. The only exception is the units of measure types, which are supported in this release. These limitations will be eliminated in a future release, but for now we have to live with them.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset