Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Actual Method Signature Using Reflection

I am trying to build a T4 template that will take the method definitions in an interface and reproduce the signature and call a base method with the passed parameters. The interface defines a multitude of methods so rewriting them every time the interface changes becomes very challenging. Another complication is the interface is a generic interface with possible generic methods and generic parameters. So far, the only way I can find to reproduce the actual signature (without "`1" definitions for generics) is to completely rebuild it, which becomes very cumbersome.

In the case I have a signature like this in my interface:

ICar Drive<TCar>(Expression<Func<TWheel, bool>> wheels, int miles)

Is there any way to completely reproduce that with reflection without having to disect the entire MethodInfo details, or is there a quick way to get the string above out so I can write it in my T4?

Any help would be greatly appreciated!

like image 688
Benny Avatar asked Sep 15 '11 18:09

Benny


1 Answers

When I need to generate code, I often look to the System.CodeDom namespace. It lets you build up a logical representation of code and then get the corresponding source code for what you built. However, I don't know if I can say that this way isn't also 'cumbersome' as you said in your answer (and this certainly involves 'dissecting' the MethodInfo. However, it does give you a pretty decent foundation. Just by passing in the interface you want to 'clone', the name of the new class and the base class that you want to extend like so:

var code = GenerateCode(typeof(TestInterface<>),
                        "MyNewClass",
                        typeof(TestBaseClass<>));

will result in this:

//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//     Runtime Version:4.0.30319.237
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

namespace MyNamespace {
    using System;
    using System.Linq.Expressions;


    public class MyNewClass<TWheel> : TestInterface<TWheel>, TestBaseClass<TWheel>
     {

        public MyNamespace.ICar Drive<TCar>(Expression<Func<TWheel, bool>> wheels, int miles)
         {
            return base.Drive(wheels, miles);
        }
    }
}

Also, you can change a few characters in the code and switch to the VB provider and you'll get Visual Basic output (perhaps not useful but kinda cool):

'------------------------------------------------------------------------------
' <auto-generated>
'     This code was generated by a tool.
'     Runtime Version:4.0.30319.237
'
'     Changes to this file may cause incorrect behavior and will be lost if
'     the code is regenerated.
' </auto-generated>
'------------------------------------------------------------------------------

Option Strict Off
Option Explicit On

Imports System
Imports System.Linq.Expressions

Namespace MyNamespace

    Public Class MyNewClass(Of TWheel)
        Inherits TestInterface(Of TWheel)
        Implements TestBaseClass(Of TWheel)

        Public Function Drive(Of TCar)(ByVal wheels As Expression(Of Func(Of TWheel, Boolean)), ByVal miles As Integer) As MyNamespace.ICar
            Return MyBase.Drive(wheels, miles)
        End Function
    End Class
End Namespace

Here is the GenerateCode beast. Hopefully the comments can explain what's going on:

public static string GenerateCode(Type interfaceType, string generatedClassName, Type baseClass)
{
    //Sanity check
    if (!interfaceType.IsInterface)
        throw new ArgumentException("Interface expected");

    //I can't think of a good way to handle closed generic types so I just won't support them
    if (interfaceType.IsGenericType && !interfaceType.IsGenericTypeDefinition)
        throw new ArgumentException("Closed generic type not expected.");

    //Build the class
    var newClass = new CodeTypeDeclaration(generatedClassName)
    {
        IsClass = true,
        TypeAttributes = TypeAttributes.Public,
        BaseTypes =
                                {
                                    //Include the interface and provided class as base classes
                                    MakeTypeReference(interfaceType),
                                    MakeTypeReference(baseClass)
                                }
    };

    //Add type arguments (if the interface is generic)
    if (interfaceType.IsGenericType)
        foreach (var genericArgumentType in interfaceType.GetGenericArguments())
            newClass.TypeParameters.Add(genericArgumentType.Name);

    //Loop through each method
    foreach (var mi in interfaceType.GetMethods())
    {
        //Create the method
        var method = new CodeMemberMethod
        {
            Attributes = MemberAttributes.Public | MemberAttributes.Final,
            Name = mi.Name,
            ReturnType = MakeTypeReference(mi.ReturnType)
        };

        //Add any generic types
        if (mi.IsGenericMethod)
            foreach (var genericParameter in mi.GetGenericArguments())
                method.TypeParameters.Add(genericParameter.Name);

        //Add the parameters
        foreach (var par in mi.GetParameters())
            method.Parameters.Add(new CodeParameterDeclarationExpression(MakeTypeReference(par.ParameterType),
                                                                            par.Name));

        //Call the same method on the base passing all the parameters
        var allParameters =
            mi.GetParameters().Select(p => new CodeArgumentReferenceExpression(p.Name)).ToArray();
        var callBase = new CodeMethodInvokeExpression(new CodeBaseReferenceExpression(), mi.Name, allParameters);

        //If the method is void, we just call base
        if (mi.ReturnType == typeof(void))
            method.Statements.Add(callBase);
        else
            //Otherwise, we return the value from the call to base
            method.Statements.Add(new CodeMethodReturnStatement(callBase));

        //Add the method to our class
        newClass.Members.Add(method);
    }

    //TODO: Also add properties if needed?

    //Make a "CompileUnit" that has a namespace with some 'usings' and then
    //  our new class.
    var unit = new CodeCompileUnit
    {
        Namespaces =
        {
            new CodeNamespace(interfaceType.Namespace)
            {
                Imports =
                {
                    new CodeNamespaceImport("System"),
                    new CodeNamespaceImport("System.Linq.Expressions")
                },
                Types =
                {
                    newClass
                }
            }
        }
    };

    //Use the C# prvider to get a code generator and generate the code
    //Switch this to VBCodeProvider to generate VB Code
    var gen = new CSharpCodeProvider().CreateGenerator();
    using (var tw = new StringWriter())
    {
        gen.GenerateCodeFromCompileUnit(unit, tw, new CodeGeneratorOptions());
        return tw.ToString();
    }
}

/// <summary>
/// Helper method for expanding out a type with all it's generic types.
/// It seems like there should be an easier way to do this but this work.
/// </summary>
private static CodeTypeReference MakeTypeReference(Type interfaceType)
{
    //If the Type isn't generic, just wrap is directly
    if (!interfaceType.IsGenericType)
        return new CodeTypeReference(interfaceType);

    //Otherwise wrap it but also pass the generic arguments (recursively calling this method
    //  on all the type arguments.
    return new CodeTypeReference(interfaceType.Name,
                                    interfaceType.GetGenericArguments().Select(MakeTypeReference).ToArray());
}
like image 54
Stephen McDaniel Avatar answered Oct 25 '22 17:10

Stephen McDaniel