The second issue is linked to the
Assembly directive. You use it somewhat like this:
<#@ Assembly Name="System.Core.dll" #>. Observe the lack of a path for the dll. You don't need any if the assembly you want to use is loaded in the GAC. However, if you want to use your own library from somewhere, only rooted paths will work. Relative paths would be resolved relative to the T4 templating engine executable, not the folder in which the
.tt file is placed. The solution here would be to use the
Host.ResolvePath method in your code and dynamically load your assembly. Obviously that brings along its own world of hurt.
The third and most uncomprehensible for me is the issue of assembly file lock. You see, when you use an assembly, the file is locked as being in use. That also happends for the T4 engine. The issue here is that the text generation is executed in the same
AppDomain as Visual Studio. So if you want to use a library that is part of your project in a T4 template, you have to either restart Visual Studio or rename the dll before recompiling that library lest you get an ugly error that the file could not be saved. The sad thing is that the T4 engine is very customizable and one can easily create their own engine host that inherits from the default one and only override the
ProvideTemplatingAppDomain method and create another AppDomain so that there would be no issues, but then you have to load your templating engine somewhere, specify it in your project, etc. Give your project to someone else and they have to have the same templating engine, just for this little crap.
I've spent the day before trying to solve the problems above. Loading a library from my solution dynamically was ugly, but I only needed a type to reflect from it, so it was ok.
var path=Host.ResolvePath("../Assemblies/Work/Siderite.Contract.dll");
In order to get rid of the issue of assembly lock I just added a postbuild event to copy the dll to
Assemblies/Work and took it from there. It is easier to rename it when the lock error appears.
I have actually tried other solutions:
- I tried to load the code in another AppDomain by creating a new one and executing a callback function in it, with AppDomain.DoCallBack(delegate). Since everything in a T4 file is inside a method already, all I could do is provide it with an inline anonymous delegate. It worked, but then it would throw a SerializationException. Apparently, when trying to send a method to another AppDomain, not even the string type is serializable!
- I saved all the values I needed as parameters with AppDomain.SetData and then got them inside the delegate method using AppDomain.GetData. Then I got a FileNotFoundException
- I googled a bit to find out that the exception was thrown whenever I would try to execute a method in a different AppDomain. Apparently it is only supposed to be used from inside the AppDomain! Imagine that. This led me to the solution to use a custom library in which to create a class that inherits from MarshalByRefObject so that I can load it dynamically using AppDomain.Load(assemblyName) and then make a MarshalByRefObject instance execute the code I wanted.
- This brings me back full circle to using a custom library
- Later still, I find out that the blog entry about that FileNotFoundException was bullshit. What happened is that the AppDomain needed to be created with an AppDomainSetup parameter which specifies the ApplicationBase property as the folder in which the library you want to load is found. I still needed a way to load the class from somewhere.
A solution is to run the T4 engine from the command line only at compile time, as a prebuild task or something like that. But that means partially discarding the Visual Studio integration. At this point, creating my own app to generate files seemed not only better, but also cleaner and faster. Then I learned about the
T4 class feature blocks that allow adding helper classes and methods. The only difference was the starting tag, not
<#, but
<#+!!
This allowed me to create a class to load the library I wanted to in another app domain, get the Type, unload the domain and then do my thing! Even better, I got to use the
<#@ include #> directive to load in my templates the
.ttinclude file that contained my
AssemblyLoader object.
Phew! Done. If you had the same problems like me, you probably want the code. It will fill the last part of the blog post. Before you do that, you'd better read
this link collection from Scott Hanselman. There are mostly from
Oleg Sych's blog about T4, and some of them are slightly outdated, but you need to read them in order to understand what this is about. One of the gotchas there is that the class
Template is now
TextTransformation so when you get the error
Compiling transformation: The type or namespace name 'Template' could not be found remember to use TextTransformation instead.
Ok, now for the code:
<#@ template debug="true" hostSpecific="true" #>
<#@ output extension=".cs" #>
<#@ Assembly Name="System.Core.dll" #>
<#@ Assembly Name="System.Windows.Forms.dll" #>
<#@ import namespace="System" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Diagnostics" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Collections" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Reflection" #>
<#@ include file="AssemblyLoader.ttinclude" #>
<#
// resolve the relative path for the Contract assembly
string path=Host.ResolvePath("../Assemblies/Work/Siderite.Contract.dll");
// create a loader in another appdomain
AssemblyLoader loader=AssemblyLoader.
CreateInNewDomain("Template generation domain",Path.GetDirectoryName(path));
// load the Contract assembly in the appdomain
loader.LoadAssembly(path);
// get the type we need to use reflection on in order to generate code
Type type=loader.GetType("Siderite.Contract.IDataServiceFacade");
// unload the appdomain and exit.
AssemblyLoader.Unload(loader);
#>
using System;
using System.ServiceModel;
using System.Reflection;
using System.ServiceModel.Channels;
using Siderite.Configuration;
namespace Siderite.Proxy
{
// code of the generated class comes here
// with <# code #> and <#= value #> tags in it
// using the type variable from the Contract library to get the information I want
}
As you can notice, right above the code block and below the Assembly and import directives there is an include directive that loads AssemblyLoader.ttinclude. Here is the content of the file. Notice the slightly different begin tags in it and the absence of template or output directives:
<#+
class AssemblyLoader : MarshalByRefObject
{
private Assembly _assembly;
public AppDomain Domain
{
get;
private set;
}
public override object InitializeLifetimeService()
{
return null;
}
public void LoadAssembly(string path)
{
_assembly = Assembly.Load(AssemblyName.GetAssemblyName(path));
}
public Type GetType(string typeName)
{
return _assembly.GetType(typeName);
}
public static AssemblyLoader CreateInNewDomain(string domainName, string applicationBase)
{
AppDomainSetup setup=new AppDomainSetup{ApplicationBase=applicationBase};
var ad = AppDomain.CreateDomain(domainName,null,setup);
// Loader lives in another AppDomain
AssemblyLoader loader = (AssemblyLoader)ad.CreateInstanceAndUnwrap(
typeof(AssemblyLoader).Assembly.FullName,
typeof(AssemblyLoader).FullName);
loader.Domain = ad;
return loader;
}
public static void Unload(AssemblyLoader loader)
{
AppDomain.Unload(loader.Domain);
}
}
#>
I will probably work on a custom tranformation engine that will also be encapsulated in a
.ttinclude file and that would provide AppDomain separation and assembly path resolving. Later, though.