T4 - TT - Using custom classes in TT files
I was having the same problem myself - my solution was like @Tehseen's except I'll provide an actual solution with explanation :)
The trick to include arbitrary C# into a T4 file (where you want the imported C# to be used by T4 instead of simply included as raw text) is to hide the parts of the *.cs file that T4 will choke on - things like using
directives, and ensuring the types are declared within a <#+
(instead of <#
) block.
Here's my solution:
My "entrypoint" MyScript.tt
T4 script looks like this:
<#@ template debug="true" hostspecific="false" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Text.RegularExpressions" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ include file="IncludedCSFile.cs" #>
<#@ output extension=".cs" #>
<#
MyClass foo = new MyClass(); // This is using a type from `IncludedCSFile.cs` in the T4 script.
#>
Hello, <#= foo.Name #>
My IncludedCSFile.cs
looks like this:
// <#+ /*
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
namespace MyNamespace
{
// */
public class MyClass
{
// etc...
}
}
// #>
Explanation:
// <#+ /*
- The initial
//
stops the main C# parser (from the project) from seeing the T4<#+
delimiter which would cause a project syntax error. - The
<#+
however is parsed by the T4 parser and causes the C# within the file to be interpreted as code that the T4 script can use. - The
/*
following starts a new comment that causes T4's C# parser to ignore theusing...
statements and openingnamespace MyNamespace
lines which would otherwise cause a T4 syntax error.- This is because T4 requires
using
statements to be expressed as<#@ import namespace="" #>
directives.
- This is because T4 requires
// */
- This is the terminating delimiter for the opening block comment behind the first
<#+
. - The
//
hides the*/
from the project C# compiler (which doesn't see the/*
), while T4's C# compiler will see it because the//
is overridden by the previous/*
.- This works because in C# block-comments will "comment-out" other comments (if that makes sense!).
// #>
- Finally, T4 requires a T4 block terminator before EOF, so we use the same leading-
//
trick to hide it from C# while T4 can still see it.
Disadvantages:
There are a few downsides to this approach:
- A leading
//
will be rendered to the final output file.- I don't believe this can be mitigated.
- If you know of a solution, please edit this answer or let me know in a comment reply!
- The included T4 file cannot declare its own namespace imports.
- Though this isn't a problem for small T4 scripts where it isn't a problem to ensure they're all added to the entrypoint T4 script.
- Another workaround is to create an actual
*.ttinclude
file which has only the necessary<#@ import namespace="" #>
directives and then includes the*.cs
file.
- The included file cannot be executed as its own T4 file due to a lack of
<#@ template #>
and<#@ output #>
directives which I understand must be placed at the start of a file.- Granted, most included
*.ttinclude
files cannot be executed by themselves anyway.
- Granted, most included
Including the C# file into the T4 template using:
<#@ include file="$(ProjectDir)ClassDefinition.cs" #>
Will add the text to the output of the T4 template. It does not compile the class.
You have debug=true set in your T4 template so you can see what T4 is generating if you look in your %TEMP% directory. When you run your T4 template you should see a .cs file generated in the TEMP directory. In this file you will have something similar to:
this.Write("public class ClassDefinition\r\n{\r\n public string NameSpace { get; set; }\r\n p" +
"ublic string Name { get; set; }\r\n public string Protection { get; set; }\r\n\r\n " +
" List<ClassProperty> Properties { get; set; }\r\n}");
So all that is happening to your C# class is that it will be written out in the generated T4 output.
What you probably want to do is include the ClassDefinition.cs file in your project so it is compiled as part of your project. Then you can reference the assembly that includes ClassDefinition class. So if your project output is MyLibrary.dll, which contains the ClassDefinition.cs compiled then you should be able to use:
<#@ assembly name="$(SolutionDir)$(OutDir)MyLibrary.dll" #>
The line that includes the ClassDefinition.cs file should be removed.
If I understand you correctly, you're trying to reuse a class as part of your template generation.
That class needs to be in a tt file itself, build action is set to none, custom tool - nothing. What I have is a template manager class with the following at the top:
<#@ template language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Diagnostics" #>
<#+
public class TemplateManager
{
Then in the other t4 templates I use:
<#@ include file="TemplateManager.tt"#>
and then
List<Values> values = TemplateManager.PrepareVariables(code, container, itemCollection.OfType<EntityType>())
In your case that ClassDefinition.tt file would contain:
<#@ template language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Diagnostics" #>
<#+
public class ClassDefinition
{
public string NameSpace { get; set; }
public string Name { get; set; }
public string Protection { get; set; }
List<ClassProperty> Properties { get; set; }
}
#>
Then you can include
<#@ include file="ClassDefinition.tt"#>
Here is the complete solution:
Separate the classes into another project
Include the reference to these classes through the TT via
<#@ assembly name="$(TargetDir)MyOwnLibraryProject.dll" #> <#@ import namespace="MyOwnNamespace" #>
Do not forget to include a reference of this Library into your TT project
You have to copy the MyOwnLibraryProject.dll into the BIN\DEBUG folder of the TT solution
The magic appears !!!
Every time you changed your DLL, do not forget to put the new version into the folder :) Or just configure your Library project output to be the same as your TT one. I would like to thank you all for giving guide lines and ideas.