none
DSL T4: Forcing new AppDomain for Template Generation

    Question

  • Is there any way to force the T4 VSHost to use a new AppDomain? It used to be the case that you could do this by changing the template code or by setting the "CacheAssemblies" registry key. But that doesn't seem to work now.

    To provide more context, what I'm trying to do is to have a non-GACed CodeGenerationLibrary.dll that is referenced by my templates. Of course the straightforward way to do this would be to use the "assembly" directive. However, the problem with this is that I cannot edit the CodeGenerationLibrary.dll source and compile it while a VS instance that is running (or has run) the T4 templates is still alive. Basically, the build fails while trying to copy the dll to the output directory because the T4 AppDomain is using the output dll.

    So I wrote a hacky DirectiveProcessor that copies the referenced dll into a temp path every time the dll changes and provides the temp path as the reference to load. However, I cannot get the AppDomain to refresh, so loading a newly compiled CodeGenerationLibrary.dll into the AppDomain doesn't work.

    If there is any way to force the AppDomain reload or to accomplish my primary goal (being able to edit and compile CodeGenerationLibrary.dll within a VS instance that is running the templates), I would love to know about it.

    Thanks,
    -George

    Saturday, October 31, 2009 12:14 AM

Answers

  • I do it from the DirectiveProcessor every time CodeGenerationLibrary.dll changes. Here's the code for the DP:

    public class DynamicAssemblyDirectiveProcessor : VSTextTemplating::DirectiveProcessor
    {
    public const string DirectiveProcessorName = "DynamicAssemblyDirectiveProcessor";

    private const string supportedDirectiveName = "DynamicAssembly";

    public DynamicAssemblyDirectiveProcessor()
    {
    //Empty
    }

    public override void FinishProcessingRun()
    {
    //Empty
    }

    public override void Initialize( Microsoft.VisualStudio.TextTemplating.ITextTemplatingEngineHost host )
    {
    _host = host;
    CleanTempDirectory();
    base.Initialize( host );
    }

    public override void StartProcessingRun( CodeDomProvider languageProvider, string templateContents, CompilerErrorCollection errors )
    {
    _appDomainUnloaded = false;
    base.StartProcessingRun( languageProvider, templateContents, errors );
    }

    public override string GetClassCodeForProcessingRun()
    {
    return String.Empty;
    }

    public override string[] GetImportsForProcessingRun()
    {
    return new string[] { };
    }

    public override string GetPostInitializationCodeForProcessingRun()
    {
    return String.Empty;
    }

    public override string GetPreInitializationCodeForProcessingRun()
    {
    return String.Empty;
    }

    public override string[] GetReferencesForProcessingRun()
    {
    return _tempAssemblies.ToArray();
    }

    public override bool IsDirectiveSupported( string directiveName )
    {
    return StringComparer.OrdinalIgnoreCase.Equals( directiveName, supportedDirectiveName );
    }

    public override void ProcessDirective( string directiveName, IDictionary<string, string> arguments )
    {
    if ( !arguments.ContainsKey( "name" ) ) throw new InvalidOperationException( "Required argument 'name' not provided" );
    if ( arguments.Count > 2 ) throw new InvalidOperationException( "More than 1 argument provided" );
    _tempAssemblies.Add( GetTempAssemblyName( arguments[ "name" ] ) );
    }

    private string GetTempPath()
    {
    string path = Path.Combine( Path.GetTempPath(), "DynamicAssemlies" );
    if ( !Directory.Exists( path ) )
    {
    Directory.CreateDirectory( path );
    }
    return path;
    }

    private void UnloadGenerationAppDomain()
    {
    //HACK!: Reflection hack for now to force the unloading of the code-gen AppDomain every time we need to load a new version
    //of the assembly
    if ( !_appDomainUnloaded )
    {
    MethodInfo method = _host.GetType().GetMethod( "UnloadGenerationAppDomain", BindingFlags.Instance | BindingFlags.NonPublic );
    if ( method != null )
    {
    method.Invoke( _host, new object[] { } );
    _appDomainUnloaded = true;
    }
    }
    }

    private string GetTempAssemblyName( string assemblyName )
    {
    string assemblyPath = _host.ResolveAssemblyReference( assemblyName );
    if ( !File.Exists( assemblyPath ) ) throw new Exception( String.Format( "The assembly '{0}' does not exist", assemblyPath ) );
    string existingCopy = GetLatestCopy( assemblyName );
    if ( String.IsNullOrEmpty( existingCopy ) || File.GetLastWriteTimeUtc( existingCopy ) < File.GetLastWriteTimeUtc( assemblyPath ) ) return CreateNewCopy( assemblyPath );
    return existingCopy;
    }

    private string GetLatestCopy( string assemblyName )
    {
    DateTime lastModified = DateTime.MinValue;
    string lastModifiedFile = string.Empty;
    foreach ( var file in Directory.GetFiles( GetTempPath(), Path.GetFileNameWithoutExtension( assemblyName ) + "*" ) )
    {
    DateTime currentFileLastModified;
    if ( ( currentFileLastModified = File.GetLastWriteTimeUtc( file ) ) > lastModified )
    {
    lastModified = currentFileLastModified;
    lastModifiedFile = file;
    }
    }
    return lastModifiedFile;
    }

    private string CreateNewCopy( string assemblyPath )
    {
    string copyPath = Path.Combine( GetTempPath(), Path.GetFileNameWithoutExtension( assemblyPath ) + Guid.NewGuid().ToString() + Path.GetExtension( assemblyPath ) );
    File.Copy( assemblyPath, copyPath );
    UnloadGenerationAppDomain();
    return copyPath;
    }

    private void CleanTempDirectory()
    {
    foreach ( var file in Directory.GetFiles( GetTempPath() ) )
    {
    try
    {
    File.Delete( file );
    }
    catch { }
    }
    }

    private VSTextTemplating::ITextTemplatingEngineHost _host;
    private List<string> _tempAssemblies = new List<string>();
    private bool _appDomainUnloaded = false;
    }

    -George
    Monday, November 02, 2009 11:27 AM
  • Thanks Oleg. I ended up using reflection to call the internal UnloadGenerationAppDomain() method on the TextTemplatingService (the class that implements the VS ITextTemplatingHost). Not the solution I hoped for, but it works perfectly. It's very nice being able to put most of the template generation code in a library - it makes it much cleaner and easier to maintain. 

    -George
    • Marked as answer by George Mathew Saturday, October 31, 2009 8:12 PM
    Saturday, October 31, 2009 8:11 PM

All replies

  • CacheAssemblies option controls the way T4 host determines how long the templating AppDomain can be reused. When this option is "true" (the default), the T4 host will keep reusing the templating AppDomain until 15 or more template assemblies have not been used for 15 minutes or more. When this option is set to "false", the T4 host will reuse the templating AppDomain for 25 transformations. Either way, your CodeGenerationLibrary.dll will hang around for a while.

    Theoretically, you could force the T4 Host to unload the templating AppDomain by setting the CacheAssemblies option to false and calling the ITextTemplatingEngineHost.ProvideTemplatingAppDomain method 25 times. The trick would be to figure out when and how many times to call it so that it doesn't cause AppDomainUnloadedException errors in the host and the engine.


    Oleg
    Saturday, October 31, 2009 4:49 PM
  • Thanks Oleg. I ended up using reflection to call the internal UnloadGenerationAppDomain() method on the TextTemplatingService (the class that implements the VS ITextTemplatingHost). Not the solution I hoped for, but it works perfectly. It's very nice being able to put most of the template generation code in a library - it makes it much cleaner and easier to maintain. 

    -George
    • Marked as answer by George Mathew Saturday, October 31, 2009 8:12 PM
    Saturday, October 31, 2009 8:11 PM
  • I would be curious to find out how you are calling this method. Did you do it from the t4 template itself?

    Thanks,

    Oleg
    Sunday, November 01, 2009 12:56 PM
  • I do it from the DirectiveProcessor every time CodeGenerationLibrary.dll changes. Here's the code for the DP:

    public class DynamicAssemblyDirectiveProcessor : VSTextTemplating::DirectiveProcessor
    {
    public const string DirectiveProcessorName = "DynamicAssemblyDirectiveProcessor";

    private const string supportedDirectiveName = "DynamicAssembly";

    public DynamicAssemblyDirectiveProcessor()
    {
    //Empty
    }

    public override void FinishProcessingRun()
    {
    //Empty
    }

    public override void Initialize( Microsoft.VisualStudio.TextTemplating.ITextTemplatingEngineHost host )
    {
    _host = host;
    CleanTempDirectory();
    base.Initialize( host );
    }

    public override void StartProcessingRun( CodeDomProvider languageProvider, string templateContents, CompilerErrorCollection errors )
    {
    _appDomainUnloaded = false;
    base.StartProcessingRun( languageProvider, templateContents, errors );
    }

    public override string GetClassCodeForProcessingRun()
    {
    return String.Empty;
    }

    public override string[] GetImportsForProcessingRun()
    {
    return new string[] { };
    }

    public override string GetPostInitializationCodeForProcessingRun()
    {
    return String.Empty;
    }

    public override string GetPreInitializationCodeForProcessingRun()
    {
    return String.Empty;
    }

    public override string[] GetReferencesForProcessingRun()
    {
    return _tempAssemblies.ToArray();
    }

    public override bool IsDirectiveSupported( string directiveName )
    {
    return StringComparer.OrdinalIgnoreCase.Equals( directiveName, supportedDirectiveName );
    }

    public override void ProcessDirective( string directiveName, IDictionary<string, string> arguments )
    {
    if ( !arguments.ContainsKey( "name" ) ) throw new InvalidOperationException( "Required argument 'name' not provided" );
    if ( arguments.Count > 2 ) throw new InvalidOperationException( "More than 1 argument provided" );
    _tempAssemblies.Add( GetTempAssemblyName( arguments[ "name" ] ) );
    }

    private string GetTempPath()
    {
    string path = Path.Combine( Path.GetTempPath(), "DynamicAssemlies" );
    if ( !Directory.Exists( path ) )
    {
    Directory.CreateDirectory( path );
    }
    return path;
    }

    private void UnloadGenerationAppDomain()
    {
    //HACK!: Reflection hack for now to force the unloading of the code-gen AppDomain every time we need to load a new version
    //of the assembly
    if ( !_appDomainUnloaded )
    {
    MethodInfo method = _host.GetType().GetMethod( "UnloadGenerationAppDomain", BindingFlags.Instance | BindingFlags.NonPublic );
    if ( method != null )
    {
    method.Invoke( _host, new object[] { } );
    _appDomainUnloaded = true;
    }
    }
    }

    private string GetTempAssemblyName( string assemblyName )
    {
    string assemblyPath = _host.ResolveAssemblyReference( assemblyName );
    if ( !File.Exists( assemblyPath ) ) throw new Exception( String.Format( "The assembly '{0}' does not exist", assemblyPath ) );
    string existingCopy = GetLatestCopy( assemblyName );
    if ( String.IsNullOrEmpty( existingCopy ) || File.GetLastWriteTimeUtc( existingCopy ) < File.GetLastWriteTimeUtc( assemblyPath ) ) return CreateNewCopy( assemblyPath );
    return existingCopy;
    }

    private string GetLatestCopy( string assemblyName )
    {
    DateTime lastModified = DateTime.MinValue;
    string lastModifiedFile = string.Empty;
    foreach ( var file in Directory.GetFiles( GetTempPath(), Path.GetFileNameWithoutExtension( assemblyName ) + "*" ) )
    {
    DateTime currentFileLastModified;
    if ( ( currentFileLastModified = File.GetLastWriteTimeUtc( file ) ) > lastModified )
    {
    lastModified = currentFileLastModified;
    lastModifiedFile = file;
    }
    }
    return lastModifiedFile;
    }

    private string CreateNewCopy( string assemblyPath )
    {
    string copyPath = Path.Combine( GetTempPath(), Path.GetFileNameWithoutExtension( assemblyPath ) + Guid.NewGuid().ToString() + Path.GetExtension( assemblyPath ) );
    File.Copy( assemblyPath, copyPath );
    UnloadGenerationAppDomain();
    return copyPath;
    }

    private void CleanTempDirectory()
    {
    foreach ( var file in Directory.GetFiles( GetTempPath() ) )
    {
    try
    {
    File.Delete( file );
    }
    catch { }
    }
    }

    private VSTextTemplating::ITextTemplatingEngineHost _host;
    private List<string> _tempAssemblies = new List<string>();
    private bool _appDomainUnloaded = false;
    }

    -George
    Monday, November 02, 2009 11:27 AM
  • Ingenious! I would like to get your permission to use this code. How can I get in touch with you privately? Would you mind dropping an email to me here.

    Thanks!
    Oleg
    Monday, November 02, 2009 10:53 PM
  • A year after your post and I come across it.  Very useful, thanks George.

    I must now figure out how to register a custom directive processor.  Reading on MSDN now:

    http://msdn.microsoft.com/en-us/library/bb126315.aspx

     

    Cheers,

    Harvo

    Thursday, October 21, 2010 9:55 PM
  • Anyone stumbling on this thread, from VS2010 and higher there seems to be a really simple solution to reference a project in a T4 template as found in this SO question:

    http://stackoverflow.com/questions/3434713/cant-reference-an-assembly-in-a-t4-template

    just reference your local assembly like this

    <#@ assembly name="$(ProjectDir)$(OutDir)$(TargetFileName)" #>

    or reference another project like this:

    <#@ assembly name="$(ProjectDir)$(OutDir)MyProjectAssembly.dll" #>

    Thursday, October 25, 2012 10:15 AM