修改同类代码文件时生成T4文件

时间:2012-06-27 16:51:22

标签: c# t4

我有一个C#regex-parser程序,里面有三个文件,每个文件都包含一个静态类:

1)一个填充字符串字典的静态类

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)将这些值编译为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)基于这些字符串运行正则表达式的一些代码:

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

我希望能够像T4模板那样生成2),因为所有这些连接在每次执行时都是相同的:

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

这样可行,但是如果我创建MyStringDicts的新成员,或者添加/删除其词典中的某些值,T4模板将无法识别它们,直到从编译中排除Patterns.cs并重新编译。由于Parser取决于Patterns,这实际上不是一个选项 - 我需要T4转换来考虑对同一版本中其他文件的更改。

我不想做的事情:

  • MyStringDicts拆分为自己的项目。我想将文件保存在一个项目中,因为它们是一个逻辑单元。
  • 只需将MyStringDicts移到Patterns.cs的顶部即可。我也需要将MyStringDicts成员用于其他目的(例如,对于字典查找或其他T4模板。)

我采用了关于使用T4Toolbox的VolatileAssembly之类的建议here,但这似乎只适用于反向,因为在编辑T4模板后需要重新编译类文件。

想要什么?

为了清晰起见而编辑

2 个答案:

答案 0 :(得分:4)

我刚创建了一个小测试模板,它使用EnvDte(Visual Studio Automation)和T4Toolbox来运行第一个文件。它通过项目获取文件,因此在运行模板之前无需编译。事实上,它甚至可以获得未保存的更改......

这与FullSnabel使用的方法基本相同,但不需要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);
                    }
                }
            }
        }
    }
#>

如果您想坚持原始方法,这应该有用。

您似乎正在做的是将数据存储在类文件中。您可以考虑将代码存储在代码之外(在xml或ini文件中),并根据该数据生成这两个文件。这样你可以一起避免这个问题,它也可以使管理列表更容易。 如果您对列表的更改不太在意,您也可以将字典放在T4模板中。

另一种选择可能在代码中完全处理它。您可以创建一个Dictionary的子类,它具有'Pattern'属性(或GetPattern()函数)。然后解析器将使用AddrVals.USstates.Pattern,并且将不再需要patterns类。这样您就不需要任何代码生成了。

也许实际字典的包装器会更好,因为它允许您隐藏实际的集合以确保它在运行时不会被更改。有关示例,请参阅Is there a read-only generic dictionary available in .NET?

答案 1 :(得分:3)

看看roslyn。它允许您将源文件编译为语法树,然后您可以检查和生成代码。这是一个CTP,但对我来说效果很好。

(添加了Roslyn样本)。

我在我的解决方案中创建了一个名为class2.cs的文件:

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

使用Roslyn CTP(你还需要Visual studio SDK)我创建了这个简单的T4模板,该模板使用Roslyn来解析Class2.cs并根据它生成输出:

<#@ 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()");
<#
    }
#>
        }
    }
}

此模板基于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()");
            }
    }
}

希望这有帮助