Avrohom Yisroel
Avrohom Yisroel

Reputation: 9440

GetType() in a T4 template always returns null

I'm trying to write a T4 template that will generate a partial class file for every model class in the folder. I may be doing this wrong, so please feel free to suggest improvements to the bits that seem to be working.

My test project has a Models folder that contains a couple of simple classes, eg Person.cs contains...

using System;

namespace WithTT.Models {
  public partial class Person {
    public int ID { get; set; }
    public string FirstName { get; set; }
    public string Surname { get; set; }
    // etc...
  }
}

My T4 template (in the same folder) uses the MultipleOutputHelper.ttinclude helper template. The the T4 file currently looks like this...

<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ include file="MultipleOutputHelper.ttinclude" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Reflection" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".log" #>

<# var manager = Manager.Create(Host, GenerationEnvironment); #>
<# manager.StartHeader(false); #>
<# manager.EndBlock(); #>

<#
  string folder = this.Host.ResolvePath("."); // get current folder
  folder = folder.Substring(0, folder.Length - 2); // knock the "\." off the end
  // Loop through each file and create an AbcExt.cs partial class
  foreach(string file in Directory.GetFiles(folder, "*.cs").Where(f => !f.EndsWith("Ext.cs")).Select(f => f.Replace(".cs", ""))) {
    manager.StartNewFile($"{file}Ext.cs"); 
    string className = Path.GetFileNameWithoutExtension(file);
    string ns = File.ReadAllLines(file + ".cs").Single(l => l.StartsWith("namespace")).Replace("namespace", "").Replace("{", "").Trim();
#>
// This code was generated from a template, do not modify!
namespace <#=ns#> {
  public partial class <#=className#> {
    // TODO AYS - Write the With() method...
  }
}
<#
manager.EndBlock();
}
#>

<# manager.Process(true); #>

This works fine so far, and produces basic code files that look like this

// This code was generated from a template, do not modify!
namespace WithTT.Models {
  public partial class Person {
    // TODO AYS - Write the With() method...
  }
}

Now, some of the helper methods I need to use to generate the code I want in here require the type of the class in question. Having got hold of the namespace (in the ns variable) and the class name (in the className variable), I expected to be able to do the following...

Type t = Assembly.GetExecutingAssembly().GetType(ns + "." + className);

...or...

Type t = Type.GetType(ns + "." + className);

However, whichever one I use, t is always null. The namespace and class name are fine, as you can see from the generated file, and according to the top two answers to this question, either should work.

If I try the same code in the Program.cs of the test project, I get the type without problem.

Anyone any idea how I can get the Type of the class in question? Thanks

Edit I realised why the type is always null. If I dump the assembly namespace into the output, say like this...

// <#=Assembly.GetExecutingAssembly().FullName#>

...then I see TemporaryT4Assembly, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null which is obviously not the same as the assembly that holds the classes. My problem now is finding out how to get hold of the assembly of the project, as opposed to the generated one from the T4 template

Upvotes: 2

Views: 550

Answers (1)

Avrohom Yisroel
Avrohom Yisroel

Reputation: 9440

Turned out to be pretty simple. This SO answer showed the following two lines...

var path = this.Host.ResolveAssemblyReference("$(TargetPath)");
var asm = Assembly.LoadFrom(path);

Once I had the assembly loaded, getting the type was pretty simple...

Type t = asm.GetType(fileNamespace + "." + className);

EDIT I discovered a major drawback with this approach, which is that Assembly.LoadFrom locks the assembly, meaning you can't rebuild the project without restarting Visual Studio.

An alternate method uses Assembly.Load(File.ReadAllBytes(path)), which avoids this problem (as it creates the Assembly from a copy of the actual assembly), but requires you to know the path and name of the assembly.

For my purposes, the following did the trick...

  IServiceProvider hostServiceProvider = (IServiceProvider)Host;
  EnvDTE.DTE dte = (EnvDTE.DTE)hostServiceProvider.GetService(typeof(EnvDTE.DTE));
  Array activeSolutionProjects = (Array)dte.ActiveSolutionProjects;
  EnvDTE.Project dteProject = (EnvDTE.Project)activeSolutionProjects.GetValue(0);
  string defaultNamespace = dteProject.Properties.Item("DefaultNamespace").Value.ToString();
  string templateDir = Path.GetDirectoryName(Host.TemplateFile);
  string fullPath = dteProject.Properties.Item("FullPath").Value.ToString();
  fullPath = fullPath.EndsWith("\\") ? fullPath.Substring(0, fullPath.Length-1) : fullPath;
  string exePath = fullPath + "\\bin\\Debug\\" + defaultNamespace + ".exe";
  Assembly asm = Assembly.Load(File.ReadAllBytes(exePath));

...which is quite a lot of code (although it may be possible to clean it up if you understand it better than I do right now). Under normal circumstances, Assembly.LoadFrom isn't recommended but here it works fine.

The main problem with this approach is that I assumed that the assembly is in the bin\Debug folder (not a bad assumption), is named the same as the project (not a great assumption, but probably true in general) and is an .exe (fairly bad assumption that will be wrong quite often). The last assumption could possibly be mitigated by poking around in the code above to find the project type, but I haven't had time to try that. I leave the comment here as a warning to anyone copying the code.

Upvotes: 1

Related Questions