Generate code through Reflection over the same ass

2019-05-07 08:50发布

问题:

I have started dabbling in T4 and first got along pretty well, but then ran into an issue that's actually pretty obvious and might not be solvable, but maybe there is a way that I just lack the experience to know or see.

Given the following class:

public class T4Test : CodeActivity
{
    protected override void Execute(CodeActivityContext context)
    {
    }

    [Input("InX")]
    public InArgument<string> InX { get; set; }

    [Output("OutX")]
    public OutArgument<string> OutX { get; set; }
}

I want this as the output:

public class ActivityWrapper
{
    private readonly T4Test _activity;
    private readonly ActivityContext _context;

    public ActivityWrapper(T4Test activity, ActivityContext context)
    {
        this._activity = activity;
        this._context = context;
    }

    public string InX
    {
        get { return this._activity.InX.Get(this._context); }
    }

    public string OutX
    {
        get { return this._activity.OutX.Get(this._context); }
        set { this._activity.OutX.Set(this._context, value); }
    }
}

I have figured out the Reflection stuff I need, and I know what the T4 code should look like, but there's one problem: I need it in the same project as the T4Test class. However, to load the assembly and reflect over it, it needs to be compiled - but of course that's a bit difficult if I intend to modify that same assembly's code. (And I guess NCrunch doesn't simplify things.)

Now here's the things that I hope might still make it possible to solve this:

  • The project will compile without the generated class. This is because the class will implement interfaces that will be auto-registered/-resolved by an IoC container. It is also not testable anyway, because ActivityContext can't be mocked.
  • For that reason it doesn't have to be there or correct all the time. I just need to be able to say "generate this now" before actually delivering the DLL.
  • For the same reason I also don't care whether the T4 template actually sits in the project - as long as the generated file ends up in the project (though without needing another project for the template and constructing PostBuild events to copy a .cs file around).
  • To be precise, it doesn't even need to be T4. If there's any other feasible way to do it, I'll be happy to use that as well.

Is there any way to achieve this? (And was that even clear enough?)

回答1:

T4Test.tt

<#@ include file="Activities.tt" #>
<#
var t4test = new Activity("T4Test")
{
    Input("InX"),
    Output("OutX"),
};
GenerateCode(t4test);
#>

Activities.tt

<#@ template language="C#" #>
<#@ import namespace="System.Collections" #>
<#@ import namespace="System.Collections.Generic" #>
<#+
class Activity : IEnumerable<Property>
{
    private string name, wrapper;
    private List<Property> properties;
    public Activity(string name, string wrapper = null)
    {
        this.name = name;
        this.wrapper = wrapper ?? name + "Wrapper";
        this.properties = new List<Property>();
    }
    public void Add(Property property)
    {
        this.properties.Add(property);
    }
    public IEnumerator<Property> GetEnumerator()
    {
        return this.properties.GetEnumerator();
    }
    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
    public void GenerateCode()
    {
        // ...
    }
}
class Property
{
    private bool output;
    private string name, type;
    public Property(bool output, string name, string type)
    {
        this.output = output;
        this.name = name;
        this.type = type;
    }
}
Property Input(string name, string type = "string")
{
    return new Property(false, name, type);
}
Property Output(string name, string type = "string")
{
    return new Property(true, name, type);
}
void GenerateCode(params Activity[] activities)
{
    WriteLine("namespace Foo");
    WriteLine("{");
    PushIndent("   ");
    foreach (var activity in activities)
    {
        WriteLine("class " + activity.name);
        WriteLine("{");
        PushIndent("   ");
        // ...
        PopIndent();
        WriteLine("}");
    }
    PopIndent();
    WriteLine("}");
}
#>


回答2:

I would like to propose an alternative to reflecting the generated assembly, since transforming the T4 only works when the project successfully built and generates proper output iff the assembly is not outdated.

If you use a hostspecific T4 template you gain access to the Visual Studio automation model through the EnvDTE interfaces. Using this you can walk the CodeModel of your currently loaded Visual Studio solution without the need to building it first.

Have a look at my answer to this SO question: Design Time Reflection. Using the aid of the free template from tangible's Template Gallery you could easily "reflect" your existing classes at design time and detect properties decorated with the desired attributes:

<#
var project = VisualStudioHelper.CurrentProject;

// get all class items from the code model
var allClasses = VisualStudioHelper.GetAllCodeElementsOfType(project.CodeModel.CodeElements, EnvDTE.vsCMElement.vsCMElementClass, false);

// iterate all classes
foreach(EnvDTE.CodeClass codeClass in allClasses)
{
    // iterate all properties
    var allProperties = VisualStudioHelper.GetAllCodeElementsOfType(codeClass.Members, EnvDTE.vsCMElement.vsCMElementProperty, true);
    foreach(EnvDTE.CodeProperty property in allProperties)
    {
        // check if it is decorated with an "Input"-Attribute
        if (property.Attributes.OfType<EnvDTE.CodeAttribute>().Any(a => a.FullName == "Input"))
        {
            ...
        }
    }
}
#>