T4 Toolbox: Generating files in different folders and projects
As scope and complexity of a code generator increases, as the number of files and file types it produces grows, it may become desirable or even necessary to save these output files to different folders in a Visual Studio project or even different projects in a Visual Studio solution. Consider models, like LINQ to SQL, that allow us to design both application and database parts of an information system. When generating code from such a model, it may be beneficial to place generated DataContext class in a data access layer project, entity classes in a business logic layer project and SQL schema scripts in a database project of your Visual Studio solution. This scenario is also frequent in frameworks, like S#arp Architecture, that provide code generators to create project structure scaffolding for developers to use as a starting point.
To accomplish this task in a T4 code generator today, we would have to place a separate T4 file in each folder where one or more output files need to be generated. This approach may be feasible, however it increases the number of code generation files you need to maintain and leads to errors when developers forget to regenerate one of many files after changing the model it uses. In addition to this, not all types of projects in Visual Studio currently support T4. Most notably, Database projects of VSTS Development edition don’t support custom tools and thus don’t allow using T4. It would be ideal to have a single T4 code generation file per model that creates all required output files in various folders and projects. However, this approach presents several major challenges.
Automation model for newer types of Visual Studio projects is implemented in .NET and cannot be accessed from T4 code running in templating AppDomain directly. Instead, the T4 code generator has to pass information about output files, their locations and target projects to the main AppDomain of Visual Studio and execute the code that updates Visual Studio solution and projects there.
Managing a set of output files saved in different folders and different projects also becomes more difficult. In order to automatically remove previously generated output files that are no longer necessary, we store the list of all generated files in the Visual Studio project that contains the T4 file. The generated files are stored as “DependentUpon” the T4 file that generated them and appear as nested items under the T4 file in the Solution Explorer. This approach doesn’t work for files in other folders because Visual Studio project model doesn’t support nested items in different folders or nested links. Instead, the T4 code generator has to store the list of generated output files in a separate file.
Detailed discussion of these problems and available solutions is outside of scope of this article and probably outside of reader’s interest.
T4 Toolbox provides support for generating output files in different folders and different projects of Visual Studio solution. Complete details can be found in the source code on CodePlex.
Usage
- Download and install the latest version of T4 Toolbox from CodePlex.
- Create a Visual Studio solution with two C# Class Library projects - ClassLibrary1.csproj and ClassLibrary2.csproj.
- Add a new code generation file called CodeGenerator.tt to the first class library project.
- Modify contents of the new file to look like so:
<#@ template language="C#" hostspecific="True" debug="True" #> <#@ output extension="txt" #> <#@ include file="T4Toolbox.tt" #> <# SampleTemplate template = new SampleTemplate(); template.Output.File = @"SubFolder\SampleOutput.txt"; template.Output.Project = @"..\ClassLibrary2\ClassLibrary2.csproj"; template.Render(); #> <#+ public class SampleTemplate : Template { public override string TransformText() { this.WriteLine("Hello, World!"); return this.GenerationEnvironment.ToString(); } } #>
- Notice that a new output file called SampleOutput.txt was generated in a folder called SubFolder of the second class library project, ClassLibrary2.csproj.
- Also notice that a log file, CodeGenerator.tt.log was generated under CodeGenerator.tt. This file contains a list of all output files produced by last successful transformation and looks like so:
// <autogenerated> // This file contains the list of files generated by CodeGenerator.tt. // ... // </autogenerated> ..\ClassLibrary2\SubFolder\SampleOutput.txt .\CodeGenerator.tt.log
Analysis
The first thing we need to review is the new Output property of the Template base class. It gets an object of type OutputInfo which defines the output file that will be produced by the template. OutputInfo currently has three properties - File, Project and Encoding.
OutputInfo.Project
This property is of type String. You can set this property to specify the Visual Studio project where output file will be generated. Its value can be a relative path, in which case T4 Toolbox will resolve it based on the location of the TemplateFile being transformed, CodeGenerator.tt in the example above. The target project must be loaded in the current Visual Studio solution, or an error will be reported. By default, Project property is empty, which is interpreted as “the project that contains the TemplateFile being transformed” (ClassLibrary1 in the example above).
OutputInfo.File
This property is of type String. You can set this property to specify the file where output of the template will be stored. Its value can be a relative path, in which case T4 Toolbox will resolve it based on the location of the target project (see OutputInfo.Project above). By default, File property is empty, which means “standard output file of the TemplateFile being transformed” (CodeGenerator.txt in the example above). If File property contains only a file name without directory, the output file will be added as a nested project item under the main template file (CodeGenerator.tt in the example above).
OutputInfo.Encoding
This property is of type Encoding. You can set this property to specify encoding of the output file the template will produce. This is an equivalent of the encoding parameter of standard output directive provided by T4 which works for individual output files.
Template.Render()
Behavior of the Template.Render method now depends on the Output properties. By default, when Output.Project and Output.File are empty, Render adds output of the template to the standard output file, like it did in the previous versions of the toolbox.
Template.RenderToFile()
RenderToFile method has now become a convenience shortcut that sets Output.File and calls Render in a single statement.
Code generation log
Code generation log (CodeGenerator.tt.log) is created when at least one output file is generated in a different folder or a different project than the main TemplateFile. If all output files are generated in the same folder and the same project as the main TemplateFile, the list of generated output files is stored as a set of project items nested under it, like it did in the previous version.
Your feedback
Special thanks go to Jeff Odell, George Capnias and Billy McCaferty who provided their feedback on early versions of this functionality. Its design and implementation are based primarily on usage scenarios in the T4 Toolbox itself. It was a challenging process, requiring several tradeoffs. There is no feasible way for us to conduct usability studies, so your feedback will be very important in validating these decisions. Please give the new version a try and share your experience with us:
- Is the code easy to write?
- Does the code work as you would expect?
- Is the code intuitive?
Here are some design aspects we need your feedback on.
Template.Output property
Template.Output.File started as a simple Template.OutputFile property of type String. OutputInfo class and Template.Output property was created when we realized that there will be multiple attributes that will apply an output file. We felt that adding these additional attributes as properties to the Template class would pollute its interface and make concrete Template classes more difficult to use. As a result, we now have Output property which gets an object and have to write assignment statements with two periods in a chain just to assign output file or output project.
Relative Path Resolution
Relative path to Output.Project is resolved based on the location of the main TemplateFile. However, relative path to Output.File is resolved based on the location of the Output.Project. Initially, both paths were resolved relative to the location of the main TemplateFile. This feels logical and intuitive in a simple example where you have just one template. However, in a scenario with a complex directory structure and multiple templates such when generating SQL schema files in a VSTS Database project, the folder structure defined by Output.File doesn’t need to change based on Output.Project. In this case, it felt redundant having to write extra code for each Output.File in addition to simply changing Output.Project.
Code generation log
The initial goal was to continue storing the list of generated output files as nested items in the project that contains the main TemplateFile. Unfortunately, it appears to be impossible in the current Visual Studio project model to store links to files in other locations as nested project items under the main TemplateFile. The second choice was to store the log in standard output file of the main template and avoid having to create an extra file. However, Visual Studio overwrites the standard output file when there is a compilation error in the main template, which would obliterate your code generation log if you forget a semicolon at the end of the line and could result in dozens of orphaned output files in your solution. This is why we create code generation log as a separate file. Can you think of a better way to do this?



How do I fix this error.
Target project “path” does not belong to the solution.
I have already assigned the path to the template.Output.Project