T4 运行时模板引擎

之前的文章都介绍的是通过保存的方式来使用T4,或者是通过VS下的TextTransform.exe文件来编译T4(还需要考虑传参长度等各种问题)T4模板引擎 参数调用
这些方法都比较繁琐,可以适用在不同的使用场景下,本文介绍一下运行时的T4模板

运行时T4只需要在创建时候选择为运行时模板即可,和之前的模板引擎类型不同。
运行时T4生成工具为TextTemplatingFilePreprocessor,普通模板引擎是 TextTemplatingFileGenerator,需要注意区分。

模板引擎默认保存生成的是txt,运行时模板默认保存生成的是cs代码(生成与文件名一致的类)

运行时模板会在保存时生成对应的类

后续只需要调用这个TransformText方法就能获得生成的字符串结果。

例如

var page = new RuntimeTextTemplate1();
            String pageContent = page.TransformText();
            System.IO.File.WriteAllText(@"D:\work\code\outputPage.html", pageContent);

framework & .net core

framework下引用nuget: 【Microsoft.VisualStudio.TextTemplating】、【Microsoft.VisualStudio.TextTemplating.Interfaces】
.netcore下引用nuget:【Mono.TextTemplating】
另外替代方法可以通过安装【Microsoft.VisualStudio.SDK】,版本号为【17.0.31902.203】也可以正常获取依赖。

参数传递

运行时T4生成的代码是分部类,因此可以手动创建另外的代码来添加公共属性,然后在模板中使用

如果需要动态读取T4文件内容在运行时候执行代码,可以参考
https://learn.microsoft.com/zh-cn/visualstudio/modeling/walkthrough-creating-a-custom-text-template-host?view=vs-2022&tabs=csharp
核心内容为继承ITextTemplatingEngineHost,随后调用

  CustomCmdLineHost host = new CustomCmdLineHost();
            Engine engine = new Engine();
            host.TemplateFileValue = templateFileName;
            //Read the text template.
            string input = File.ReadAllText(templateFileName);
            //Transform the text template.
            string output = engine.ProcessTemplate(input, host);
            string outputFileName = Path.GetFileNameWithoutExtension(templateFileName);
            outputFileName = Path.Combine(Path.GetDirectoryName(templateFileName), outputFileName);
            outputFileName = outputFileName + "1" + host.FileExtension;
            File.WriteAllText(outputFileName, output, host.FileEncoding);

            foreach (CompilerError error in host.Errors)
            {
                Console.WriteLine(error.ToString());
            }

ITextTemplatingEngineHost实现代码文档中以给出,内容如下

using System;
using System.IO;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Text;
using Microsoft.VisualStudio.TextTemplating;

namespace CustomHost
{
    //The text template transformation engine is responsible for running
    //the transformation process.
    //The host is responsible for all input and output, locating files,
    //and anything else related to the external environment.
    //-------------------------------------------------------------------------
    class CustomCmdLineHost : ITextTemplatingEngineHost
    {
        //the path and file name of the text template that is being processed
        //---------------------------------------------------------------------
        internal string TemplateFileValue;
        public string TemplateFile
        {
            get { return TemplateFileValue; }
        }
        //This will be the extension of the generated text output file.
        //The host can provide a default by setting the value of the field here.
        //The engine can change this value based on the optional output directive
        //if the user specifies it in the text template.
        //---------------------------------------------------------------------
        private string fileExtensionValue = ".txt";
        public string FileExtension
        {
            get { return fileExtensionValue; }
        }
        //This will be the encoding of the generated text output file.
        //The host can provide a default by setting the value of the field here.
        //The engine can change this value based on the optional output directive
        //if the user specifies it in the text template.
        //---------------------------------------------------------------------
        private Encoding fileEncodingValue = Encoding.UTF8;
        public Encoding FileEncoding
        {
            get { return fileEncodingValue; }
        }
        //These are the errors that occur when the engine processes a template.
        //The engine passes the errors to the host when it is done processing,
        //and the host can decide how to display them. For example, the host
        //can display the errors in the UI or write them to a file.
        //---------------------------------------------------------------------
        private CompilerErrorCollection errorsValue;
        public CompilerErrorCollection Errors
        {
            get { return errorsValue; }
        }
        //The host can provide standard assembly references.
        //The engine will use these references when compiling and
        //executing the generated transformation class.
        //--------------------------------------------------------------
        public IList<string> StandardAssemblyReferences
        {
            get
            {
                return new string[]
                {
                    //If this host searches standard paths and the GAC,
                    //we can specify the assembly name like this.
                    //---------------------------------------------------------
                    //"System"

                    //Because this host only resolves assemblies from the
                    //fully qualified path and name of the assembly,
                    //this is a quick way to get the code to give us the
                    //fully qualified path and name of the System assembly.
                    //---------------------------------------------------------
                    typeof(System.Uri).Assembly.Location
                };
            }
        }
        //The host can provide standard imports or using statements.
        //The engine will add these statements to the generated
        //transformation class.
        //--------------------------------------------------------------
        public IList<string> StandardImports
        {
            get
            {
                return new string[]
                {
                    "System"
                };
            }
        }
        //The engine calls this method based on the optional include directive
        //if the user has specified it in the text template.
        //This method can be called 0, 1, or more times.
        //---------------------------------------------------------------------
        //The included text is returned in the context parameter.
        //If the host searches the registry for the location of include files,
        //or if the host searches multiple locations by default, the host can
        //return the final path of the include file in the location parameter.
        //---------------------------------------------------------------------
        public bool LoadIncludeText(string requestFileName, out string content, out string location)
        {
            content = System.String.Empty;
            location = System.String.Empty;

            //If the argument is the fully qualified path of an existing file,
            //then we are done.
            //----------------------------------------------------------------
            if (File.Exists(requestFileName))
            {
                content = File.ReadAllText(requestFileName);
                return true;
            }
            //This can be customized to search specific paths for the file.
            //This can be customized to accept paths to search as command line
            //arguments.
            //----------------------------------------------------------------
            else
            {
                return false;
            }
        }
        //Called by the Engine to enquire about
        //the processing options you require.
        //If you recognize that option, return an
        //appropriate value.
        //Otherwise, pass back NULL.
        //--------------------------------------------------------------------
        public object GetHostOption(string optionName)
        {
        object returnObject;
        switch (optionName)
        {
        case "CacheAssemblies":
                    returnObject = true;
     break;
        default:
        returnObject = null;
        break;
        }
        return returnObject;
        }
        //The engine calls this method to resolve assembly references used in
        //the generated transformation class project and for the optional
        //assembly directive if the user has specified it in the text template.
        //This method can be called 0, 1, or more times.
        //---------------------------------------------------------------------
        public string ResolveAssemblyReference(string assemblyReference)
        {
            //If the argument is the fully qualified path of an existing file,
            //then we are done. (This does not do any work.)
            //----------------------------------------------------------------
            if (File.Exists(assemblyReference))
            {
                return assemblyReference;
            }
            //Maybe the assembly is in the same folder as the text template that
            //called the directive.
            //----------------------------------------------------------------
            string candidate = Path.Combine(Path.GetDirectoryName(this.TemplateFile), assemblyReference);
            if (File.Exists(candidate))
            {
                return candidate;
            }
            //This can be customized to search specific paths for the file
            //or to search the GAC.
            //----------------------------------------------------------------
            //This can be customized to accept paths to search as command line
            //arguments.
            //----------------------------------------------------------------
            //If we cannot do better, return the original file name.
            return "";
        }
        //The engine calls this method based on the directives the user has
        //specified in the text template.
        //This method can be called 0, 1, or more times.
        //---------------------------------------------------------------------
        public Type ResolveDirectiveProcessor(string processorName)
        {
            //This host will not resolve any specific processors.
            //Check the processor name, and if it is the name of a processor the
            //host wants to support, return the type of the processor.
            //---------------------------------------------------------------------
            if (string.Compare(processorName, "XYZ", StringComparison.OrdinalIgnoreCase) == 0)
            {
                //return typeof();
            }
            //This can be customized to search specific paths for the file
            //or to search the GAC
            //If the directive processor cannot be found, throw an error.
            throw new Exception("Directive Processor not found");
        }
        //A directive processor can call this method if a file name does not
        //have a path.
        //The host can attempt to provide path information by searching
        //specific paths for the file and returning the file and path if found.
        //This method can be called 0, 1, or more times.
        //---------------------------------------------------------------------
        public string ResolvePath(string fileName)
        {
            if (fileName == null)
            {
                throw new ArgumentNullException("the file name cannot be null");
            }
            //If the argument is the fully qualified path of an existing file,
            //then we are done
            //----------------------------------------------------------------
            if (File.Exists(fileName))
            {
                return fileName;
            }
            //Maybe the file is in the same folder as the text template that
            //called the directive.
            //----------------------------------------------------------------
            string candidate = Path.Combine(Path.GetDirectoryName(this.TemplateFile), fileName);
            if (File.Exists(candidate))
            {
                return candidate;
            }
            //Look more places.
            //----------------------------------------------------------------
            //More code can go here...
            //If we cannot do better, return the original file name.
            return fileName;
        }
        //If a call to a directive in a text template does not provide a value
        //for a required parameter, the directive processor can try to get it
        //from the host by calling this method.
        //This method can be called 0, 1, or more times.
        //---------------------------------------------------------------------
        public string ResolveParameterValue(string directiveId, string processorName, string parameterName)
        {
            if (directiveId == null)
            {
                throw new ArgumentNullException("the directiveId cannot be null");
            }
            if (processorName == null)
            {
                throw new ArgumentNullException("the processorName cannot be null");
            }
            if (parameterName == null)
            {
                throw new ArgumentNullException("the parameterName cannot be null");
            }
            //Code to provide "hard-coded" parameter values goes here.
            //This code depends on the directive processors this host will interact with.
            //If we cannot do better, return the empty string.
            return String.Empty;
        }
        //The engine calls this method to change the extension of the
        //generated text output file based on the optional output directive
        //if the user specifies it in the text template.
        //---------------------------------------------------------------------
        public void SetFileExtension(string extension)
        {
            //The parameter extension has a '.' in front of it already.
            //--------------------------------------------------------
            fileExtensionValue = extension;
        }
        //The engine calls this method to change the encoding of the
        //generated text output file based on the optional output directive
        //if the user specifies it in the text template.
        //----------------------------------------------------------------------
        public void SetOutputEncoding(System.Text.Encoding encoding, bool fromOutputDirective)
        {
            fileEncodingValue = encoding;
        }
        //The engine calls this method when it is done processing a text
        //template to pass any errors that occurred to the host.
        //The host can decide how to display them.
        //---------------------------------------------------------------------
        public void LogErrors(CompilerErrorCollection errors)
        {
            errorsValue = errors;
        }
        //This is the application domain that is used to compile and run
        //the generated transformation class to create the generated text output.
        //----------------------------------------------------------------------
        public AppDomain ProvideTemplatingAppDomain(string content)
        {
            //This host will provide a new application domain each time the
            //engine processes a text template.
            //-------------------------------------------------------------
            return AppDomain.CreateDomain("Generation App Domain");
            //This could be changed to return the current appdomain, but new
            //assemblies are loaded into this AppDomain on a regular basis.
            //If the AppDomain lasts too long, it will grow indefintely,
            //which might be regarded as a leak.
            //This could be customized to cache the application domain for
            //a certain number of text template generations (for example, 10).
            //This could be customized based on the contents of the text
            //template, which are provided as a parameter for that purpose.
        }
    }
    //This will accept the path of a text template as an argument.
    //It will create an instance of the custom host and an instance of the
    //text templating transformation engine, and will transform the
    //template to create the generated text output file.
    //-------------------------------------------------------------------------
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                ProcessTemplate(args);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }
        static void ProcessTemplate(string[] args)
        {
            string templateFileName = null;
            if (args.Length == 0)
            {
                throw new System.Exception("you must provide a text template file path");
            }
            templateFileName = args[0];
            if (templateFileName == null)
            {
                throw new ArgumentNullException("the file name cannot be null");
            }
            if (!File.Exists(templateFileName))
            {
                throw new FileNotFoundException("the file cannot be found");
            }
            CustomCmdLineHost host = new CustomCmdLineHost();
            Engine engine = new Engine();
            host.TemplateFileValue = templateFileName;
            //Read the text template.
            string input = File.ReadAllText(templateFileName);
            //Transform the text template.
            string output = engine.ProcessTemplate(input, host);
            string outputFileName = Path.GetFileNameWithoutExtension(templateFileName);
            outputFileName = Path.Combine(Path.GetDirectoryName(templateFileName), outputFileName);
            outputFileName = outputFileName + "1" + host.FileExtension;
            File.WriteAllText(outputFileName, output, host.FileEncoding);

            foreach (CompilerError error in host.Errors)
            {
                Console.WriteLine(error.ToString());
            }
        }
    }
}

此逻辑仅为基础的调用,如果需要传递参数则还需要继承ITextTemplatingSessionHost,
如果需要导入引用、程序集,则需要通过StandardAssemblyReferences、StandardImports这两个属性传递、调整后代码如下

using System;
using System.IO;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Text;
using Microsoft.VisualStudio.TextTemplating;

namespace CustomHost
{
    //The text template transformation engine is responsible for running
    //the transformation process.
    //The host is responsible for all input and output, locating files,
    //and anything else related to the external environment.
    //-------------------------------------------------------------------------
    class CustomCmdLineHost : ITextTemplatingEngineHost, ITextTemplatingSessionHost
    {
        //the path and file name of the text template that is being processed
        //---------------------------------------------------------------------
        internal string TemplateFileValue;
        public string TemplateFile
        {
            get { return TemplateFileValue; }
        }
        //This will be the extension of the generated text output file.
        //The host can provide a default by setting the value of the field here.
        //The engine can change this value based on the optional output directive
        //if the user specifies it in the text template.
        //---------------------------------------------------------------------
        private string fileExtensionValue = ".txt";
        public string FileExtension
        {
            get { return fileExtensionValue; }
        }
        //This will be the encoding of the generated text output file.
        //The host can provide a default by setting the value of the field here.
        //The engine can change this value based on the optional output directive
        //if the user specifies it in the text template.
        //---------------------------------------------------------------------
        private Encoding fileEncodingValue = Encoding.UTF8;
        public Encoding FileEncoding
        {
            get { return fileEncodingValue; }
        }
        //These are the errors that occur when the engine processes a template.
        //The engine passes the errors to the host when it is done processing,
        //and the host can decide how to display them. For example, the host
        //can display the errors in the UI or write them to a file.
        //---------------------------------------------------------------------
        private CompilerErrorCollection errorsValue;
        public CompilerErrorCollection Errors
        {
            get { return errorsValue; }
        }
        //The host can provide standard assembly references.
        //The engine will use these references when compiling and
        //executing the generated transformation class.
        //--------------------------------------------------------------
        public IList<string> StandardAssemblyReferences
        {
            get; set;
        } = new
            List<string>()
        { typeof(System.Uri).Assembly.Location};
        //The host can provide standard imports or using statements.
        //The engine will add these statements to the generated
        //transformation class.
        //--------------------------------------------------------------
        public IList<string> StandardImports
        {
            get;
            set;
        } = new List<string>() { "System" };

        public ITextTemplatingSession Session { get; set; }

        //The engine calls this method based on the optional include directive
        //if the user has specified it in the text template.
        //This method can be called 0, 1, or more times.
        //---------------------------------------------------------------------
        //The included text is returned in the context parameter.
        //If the host searches the registry for the location of include files,
        //or if the host searches multiple locations by default, the host can
        //return the final path of the include file in the location parameter.
        //---------------------------------------------------------------------
        public bool LoadIncludeText(string requestFileName, out string content, out string location)
        {
            content = System.String.Empty;
            location = System.String.Empty;

            //If the argument is the fully qualified path of an existing file,
            //then we are done.
            //----------------------------------------------------------------
            if (File.Exists(requestFileName))
            {
                content = File.ReadAllText(requestFileName);
                return true;
            }
            //This can be customized to search specific paths for the file.
            //This can be customized to accept paths to search as command line
            //arguments.
            //----------------------------------------------------------------
            else
            {
                return false;
            }
        }
        //Called by the Engine to enquire about
        //the processing options you require.
        //If you recognize that option, return an
        //appropriate value.
        //Otherwise, pass back NULL.
        //--------------------------------------------------------------------
        public object GetHostOption(string optionName)
        {
            object returnObject;
            switch (optionName)
            {
                case "CacheAssemblies":
                    returnObject = true;
                    break;
                default:
                    returnObject = null;
                    break;
            }
            return returnObject;
        }
        //The engine calls this method to resolve assembly references used in
        //the generated transformation class project and for the optional
        //assembly directive if the user has specified it in the text template.
        //This method can be called 0, 1, or more times.
        //---------------------------------------------------------------------
        public string ResolveAssemblyReference(string assemblyReference)
        {
            //If the argument is the fully qualified path of an existing file,
            //then we are done. (This does not do any work.)
            //----------------------------------------------------------------
            if (File.Exists(assemblyReference))
            {
                return assemblyReference;
            }
            //Maybe the assembly is in the same folder as the text template that
            //called the directive.
            //----------------------------------------------------------------
            string candidate = Path.Combine(Path.GetDirectoryName(this.TemplateFile), assemblyReference);
            if (File.Exists(candidate))
            {
                return candidate;
            }
            //This can be customized to search specific paths for the file
            //or to search the GAC.
            //----------------------------------------------------------------
            //This can be customized to accept paths to search as command line
            //arguments.
            //----------------------------------------------------------------
            //If we cannot do better, return the original file name.
            return "";
        }
        //The engine calls this method based on the directives the user has
        //specified in the text template.
        //This method can be called 0, 1, or more times.
        //---------------------------------------------------------------------
        public Type ResolveDirectiveProcessor(string processorName)
        {
            //This host will not resolve any specific processors.
            //Check the processor name, and if it is the name of a processor the
            //host wants to support, return the type of the processor.
            //---------------------------------------------------------------------
            if (string.Compare(processorName, "XYZ", StringComparison.OrdinalIgnoreCase) == 0)
            {
                //return typeof();
            }
            //This can be customized to search specific paths for the file
            //or to search the GAC
            //If the directive processor cannot be found, throw an error.
            throw new Exception("Directive Processor not found");
        }
        //A directive processor can call this method if a file name does not
        //have a path.
        //The host can attempt to provide path information by searching
        //specific paths for the file and returning the file and path if found.
        //This method can be called 0, 1, or more times.
        //---------------------------------------------------------------------
        public string ResolvePath(string fileName)
        {
            if (fileName == null)
            {
                throw new ArgumentNullException("the file name cannot be null");
            }
            //If the argument is the fully qualified path of an existing file,
            //then we are done
            //----------------------------------------------------------------
            if (File.Exists(fileName))
            {
                return fileName;
            }
            //Maybe the file is in the same folder as the text template that
            //called the directive.
            //----------------------------------------------------------------
            string candidate = Path.Combine(Path.GetDirectoryName(this.TemplateFile), fileName);
            if (File.Exists(candidate))
            {
                return candidate;
            }
            //Look more places.
            //----------------------------------------------------------------
            //More code can go here...
            //If we cannot do better, return the original file name.
            return fileName;
        }
        //If a call to a directive in a text template does not provide a value
        //for a required parameter, the directive processor can try to get it
        //from the host by calling this method.
        //This method can be called 0, 1, or more times.
        //---------------------------------------------------------------------
        public string ResolveParameterValue(string directiveId, string processorName, string parameterName)
        {
            if (directiveId == null)
            {
                throw new ArgumentNullException("the directiveId cannot be null");
            }
            if (processorName == null)
            {
                throw new ArgumentNullException("the processorName cannot be null");
            }
            if (parameterName == null)
            {
                throw new ArgumentNullException("the parameterName cannot be null");
            }
            //Code to provide "hard-coded" parameter values goes here.
            //This code depends on the directive processors this host will interact with.
            //If we cannot do better, return the empty string.
            return String.Empty;
        }
        //The engine calls this method to change the extension of the
        //generated text output file based on the optional output directive
        //if the user specifies it in the text template.
        //---------------------------------------------------------------------
        public void SetFileExtension(string extension)
        {
            //The parameter extension has a '.' in front of it already.
            //--------------------------------------------------------
            fileExtensionValue = extension;
        }
        //The engine calls this method to change the encoding of the
        //generated text output file based on the optional output directive
        //if the user specifies it in the text template.
        //----------------------------------------------------------------------
        public void SetOutputEncoding(System.Text.Encoding encoding, bool fromOutputDirective)
        {
            fileEncodingValue = encoding;
        }
        //The engine calls this method when it is done processing a text
        //template to pass any errors that occurred to the host.
        //The host can decide how to display them.
        //---------------------------------------------------------------------
        public void LogErrors(CompilerErrorCollection errors)
        {
            errorsValue = errors;
        }
        //This is the application domain that is used to compile and run
        //the generated transformation class to create the generated text output.
        //----------------------------------------------------------------------
        public AppDomain ProvideTemplatingAppDomain(string content)
        {
            //This host will provide a new application domain each time the
            //engine processes a text template.
            //-------------------------------------------------------------
            return AppDomain.CreateDomain("Generation App Domain");
            //This could be changed to return the current appdomain, but new
            //assemblies are loaded into this AppDomain on a regular basis.
            //If the AppDomain lasts too long, it will grow indefintely,
            //which might be regarded as a leak.
            //This could be customized to cache the application domain for
            //a certain number of text template generations (for example, 10).
            //This could be customized based on the contents of the text
            //template, which are provided as a parameter for that purpose.
        }

        public ITextTemplatingSession CreateSession()
        {
            return Session;
        }
    }
}

参数传递方法如下

            CustomCmdLineHost host = new CustomCmdLineHost();
            Engine engine = new Engine();
            host.TemplateFileValue = $@"{ResourceHelper.TargetDir}{SelectTemplate.Path}";

            //参数传递
            var sessionHost = (ITextTemplatingSessionHost)host;
            sessionHost.Session = new TextTemplatingSession();
            sessionHost.Session.Add("ParameterName", base64Path);

            //Read the text template.
            string input = File.ReadAllText($@"{ResourceHelper.TargetDir}{SelectTemplate.Path}");
            host.StandardAssemblyReferences.Add("Mono.TextTemplating.dll");
            //Transform the text template.
            string output = engine.ProcessTemplate(input, host);

尾注

如果需要在编译期间生成可以考虑使用Source Generate

[参考]
使用 T4 文本模板生成运行时文本
在程序中动态执行T4模板,如何传参数给T4模板?

posted @ 2023-04-06 21:13  Hey,Coder!  阅读(294)  评论(0编辑  收藏  举报