Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generate T4 file when same-class code files have been modified

Tags:

c#

t4

I have a C# regex-parser program with three files in it, each containing a static class:

1) one static class filled with string dictionaries

static class MyStringDicts
{
    internal static readonly Dictionary<string, string> USstates =
        new Dictionary<string, string>()
        {
            { "ALABAMA", "AL" },
            { "ALASKA", "AK" },
            { "AMERICAN SAMOA", "AS" },
            { "ARIZONA", "AZ" },
            { "ARKANSAS", "AR" }
             // and so on
        }
    // and some other dictionaries
}

2) A class that compiles these values into Regex

public static class Patterns
{       
    Public static readonly string StateUS =
        @"\b(?<STATE>" + CharTree.GenerateRegex(Enumerable.Union(
            AddrVals.USstates.Keys,
            AddrVals.USstates.Values))
        + @")\b";

    //and some more like these
}

3) some code that runs regular expressions based on these strings:

public static class Parser
{   
    // heavily simplified example
    public static GroupCollection SearchStringForStates(string str)
    {
        return Regex.Match(str, 
            "^" + Patterns.StateUS, 
            RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase).Groups;
    }
}

I'd like to be able to generate 2) as with a T4 template, as all of this concatenation is identical on every execution:

@"\b(?<STATE><#=CharTree.GenerateRegex(Enumerable.Union(
    AddrVals.USstates.Keys,
    AddrVals.USstates.Values)#>)\b";

This works, but if I create a new member of MyStringDicts, or add/remove some values from its dictionaries, the T4 template won't recognize them until exclude Patterns.cs from compilation and recompile. As Parser depends on Patterns, this really isn't an option - I need the T4 transformation to take into account changes to other files in the same build.

Things I don't want do do:

  • Split MyStringDicts into its own project. I'd like to keep the files in one project, as they are a logical unit.
  • Just move MyStringDicts into the top of Patterns.cs. I need the MyStringDicts members for other purposes, too (for dictionary lookups, or in other T4 templates, for example.)

I adopted the advice here about using T4Toolbox's VolatileAssembly and such, but that seems to only work for the reverse direction, when the class files need to be recompiled after editing the T4 template.

Is what I want possible?

edited for clarity

like image 320
Arithmomaniac Avatar asked Jun 27 '12 16:06

Arithmomaniac


People also ask

What is the purpose of using T4 templates?

T4 is used within Microsoft in ASP.NET MVC for the creation of the views and controllers, ADO.NET Entity Framework for entity generation, and ASP.NET Dynamic Data. It is also used outside of Microsoft in projects such as SubSonic. T4 templating is supported in Visual Studio, MonoDevelop and JetBrains Rider.

What is transform all T4 templates?

t4 is basically a tool built into VS for doing text transformation, typically for doing code generation. Transform All T4 Templates searches your solution for *. tt files and executes them to create other text, again typically source code, files.


2 Answers

I just created a small test template which uses EnvDte (Visual Studio Automation) and the T4Toolbox to run through the first file. It picks up the file through the project, so there's no need to compile before running the template. In fact, it even picks up unsaved changes...

This is basically the same approach as FullSnabel uses, but without the need for Roslyn.

<#@ template debug="false" hostspecific="True" language="C#" #>
<#@ output extension=".cs" #>
<#@ Assembly Name="System.Core.dll" #>
<#@ dte processor="T4Toolbox.DteProcessor" #>
<#@ TransformationContext processor="T4Toolbox.TransformationContextProcessor" #>
<#@ assembly name="System.Xml" #>
<#@ assembly name="EnvDTE" #>
<#@ assembly name="EnvDTE80" #>
<#@ import namespace="T4Toolbox" #>
<#@ import namespace="EnvDTE" #> 
<#@ import namespace="EnvDTE80" #>
<#
    ProjectItem projectItem = TransformationContext.FindProjectItem("Dictionaries.cs");
    FileCodeModel codeModel = projectItem.FileCodeModel;

    foreach (CodeElement element in codeModel.CodeElements)
    {
        CodeNamespace ns = element as CodeNamespace;
        if(ns != null)
        {
            foreach(CodeElement ele in ns.Children)
            {
                CodeClass cl = ele as CodeClass;

                if(cl != null && cl.Name == "Dictionaries")
                {
                    foreach(CodeElement member in cl.Members)
                    {
                        // Generate stuff...
                        this.WriteLine(member.Name);
                    }
                }
            }
        }
    }
#>

This should work if you want to stick to your original approach.

What you seem to be doing is storing data in a class file. You could consider storing your lists outside code (in an xml or ini file) and generate both files based on that data. That way you avoid the problem all together, it might also make managing the lists easier. If you don't care too much about changes to the list you could also put the dictionaries inside the T4 template itself.

Another alternative might dealing with it fully in code. You could create a subclass of Dictionary which has a 'Pattern' property (or GetPattern() function). The parser would then use AddrVals.USstates.Pattern, and the patterns class won't be needed anymore. This way you won't need any code generation.

Perhaps a wrapper around the actual dictionary would be better because it allows you to hide the actual collection to make sure it's not changed at runtime. See Is there a read-only generic dictionary available in .NET? for an example of that.

like image 94
AVee Avatar answered Sep 26 '22 00:09

AVee


Take a look at roslyn. It allows you to compile the source files into syntax trees which you then can inspect and generated code from. It's a CTP but it worked quite well for me.

(Added a Roslyn sample).

I have created a file called class2.cs in my solution:

namespace StackOverflow
{
    class Class2
    {
        public static int One() { return 8; }
        public static int Eight(int x, double z) { return 8; }
    }
}

Using the Roslyn CTP (you need the Visual studio SDK as well) I created this simple T4 template which uses Roslyn to parse Class2.cs and produce output based on that:

<#@ template    hostspecific= "true"                            #>
<#@ assembly    name        = "System.Core"                     #>
<#@ assembly    name        = "Roslyn.Compilers"                #>
<#@ assembly    name        = "Roslyn.Compilers.CSharp"         #>
<#@ import      namespace   = "System.IO"                       #>
<#@ import      namespace   = "System.Linq"                     #>
<#@ import      namespace   = "Roslyn.Compilers.CSharp"         #>

<#

    var host    = Path.GetFullPath(Host.ResolvePath(@".\Class2.cs"));
    var content = File.ReadAllText(host);

    var tree = SyntaxTree.ParseCompilationUnit(content);

    var methods = tree
        .GetRoot()
        .ChildNodes()
        .OfType<NamespaceDeclarationSyntax>()
        .SelectMany(x => x.ChildNodes())
        .OfType<ClassDeclarationSyntax>()
        .SelectMany(x => x.ChildNodes())
        .OfType<MethodDeclarationSyntax>()
        .ToArray()
        ;
#>            

namespace StackOverflow
{
    using System;

    static partial class Program
    {
        public static void Main()
        {
<#
    foreach (var method in methods)
    {
        var parent = (ClassDeclarationSyntax)method.Parent;
        var types = method
            .ParameterList
            .ChildNodes()
            .OfType<ParameterSyntax>()
            .Select(t => t.Type.PlainName)
            .ToArray()
            ;

        var plist = string.Join(", ", types);
#>
            Console.WriteLine("<#=parent.Identifier.ValueText#>.<#=method.Identifier.ValueText#>(<#=plist#>).ToString()");
<#
    }
#>
        }
    }
}

This template produces the following output based on Class2.cs:

namespace StackOverflow
{
    using System;

    static partial class Program
    {
        public static void Main()
        {
                Console.WriteLine("Class2.One().ToString()");
                Console.WriteLine("Class2.Eight(int, double).ToString()");
            }
    }
}

Hope this helps

like image 29
Just another metaprogrammer Avatar answered Sep 26 '22 00:09

Just another metaprogrammer