Friday, May 20, 2011

CodeDOM -- generating an interface to a type

This is a fairly simple process -- reflect over the public properties and methods, and generate a matching interface member, rather like this:

let GenerateInterface (target:Type) =
let compileUnit = new CodeCompileUnit()
let scope = new CodeNamespace("Generated")
let namespaces = new HashSet<string>()
compileUnit.Namespaces.Add( scope ) |> ignore
let wrapper = new CodeTypeDeclaration("I" + target.Name + "Wrapper")
wrapper.IsInterface <- true
scope.Types.Add(wrapper) |> ignore
IncludeProperties wrapper target namespaces
IncludeMethods wrapper target namespaces
namespaces
|> Seq.sort
|> Seq.iter (fun n-> scope.Imports.Add(new CodeNamespaceImport(n)))
compileUnit
view raw gistfile1.fs hosted with ❤ by GitHub

which puts the required namespaces into order (TODO: sort System.* ahead of the rest).

The two functions look much the same, so could be factored further:

let IncludeProperties (klass:CodeTypeDeclaration) (target:Type) (namespaces:HashSet<string>) =
target.GetProperties(BindingFlags.Instance ||| BindingFlags.Public)
|> Seq.sortBy (fun p -> p.Name)
|> Seq.iter (fun p ->
let m = new CodeMemberProperty()
m.Name <- p.Name
namespaces.Add(p.PropertyType.Namespace) |> ignore
m.Type <- TypeRef(p.PropertyType)
m.HasGet <- p.CanRead
m.HasSet <- p.CanWrite
p.GetIndexParameters()
|> Seq.iter ( fun t ->
let px = new CodeParameterDeclarationExpression()
px.Direction <- FieldDirection.In
px.Name <- t.Name
namespaces.Add(t.ParameterType.Namespace) |> ignore
px.Type <- TypeRef(t.ParameterType)
m.Parameters.Add(px) |> ignore
)
if m.Parameters.Count > 0 then m.Name <- "Item"
klass.Members.Add( m ) |> ignore
)
view raw gistfile1.fs hosted with ❤ by GitHub

which special cases the indexer property; and

let IncludeMethods (klass:CodeTypeDeclaration) (target:Type) (namespaces:HashSet<string>) =
target.GetMethods(BindingFlags.Instance ||| BindingFlags.Public)
|> Seq.filter (fun x -> not x.IsSpecialName )
|> Seq.sortBy (fun p -> p.Name)
|> Seq.iter (fun p ->
let m = new CodeMemberMethod()
m.Name <- p.Name
m.ReturnType <- TypeRef(p.ReturnType)
namespaces.Add(p.ReturnType.Namespace) |> ignore
p.GetParameters()
|> Seq.iter ( fun (t:ParameterInfo) ->
let px = new CodeParameterDeclarationExpression()
px.Direction <- if t.IsIn && t.IsOut then FieldDirection.Ref
else if t.IsOut then FieldDirection.Out
else FieldDirection.In
px.Name <- t.Name
namespaces.Add(t.ParameterType.Namespace) |> ignore
px.Type <- TypeRef(t.ParameterType)
m.Parameters.Add(px) |> ignore
)
klass.Members.Add( m ) |> ignore
)
view raw gistfile1.fs hosted with ❤ by GitHub

(TODO: generics and attributes, which I don't need for my immediate use case)

To make the code look less cluttered, to get type specifications which aren't fully namespace qualified everywhere, we want to specify types with keyword synonyms by type object, and the rest by name (with the namespaces included by using or open). So we have helper functions

let SysType (t:Type) =
if t = typeof<System.Object> ||
t = typeof<System.Char> ||
t = typeof<System.Boolean> ||
t = typeof<System.String> ||
t = typeof<System.Int32> ||
// also uint, float, etc... ; probably also Nullables
t = typeof<System.Void> then true
else false
let TypeRef (t:Type) =
if t.IsArray then
if SysType <| t.GetElementType() then new CodeTypeReference(t)
else new CodeTypeReference(t.Name)
else if SysType t then new CodeTypeReference(t)
else new CodeTypeReference(t.Name)
view raw gistfile1.fs hosted with ❤ by GitHub

where again, TODO handling generic types, especially collections, to get them looking pretty.

So if we use string as the type, and dump to C# we get

namespace Generated {
using System;
using System.Globalization;
using System.Text;
public interface IStringWrapper {
char this[int index] {
get;
}
int Length {
get;
}
object Clone();
...
string TrimStart(char[] trimChars);
}
}
view raw gistfile1.cs hosted with ❤ by GitHub

and in F#

namespace Generated
// Generated by F# CodeDom stuff omitted...
open System
open System.Globalization
open System.Text
type
IStringWrapper = interface
abstract Item : int -> char with get
abstract Length : int with get
abstract TrimStart : char[] -> string
...
abstract Clone : unit -> obj
end
view raw gistfile1.fs hosted with ❤ by GitHub

where interestingly, the sorted order of the inserted members is reversed.

No comments :