Using project references as assembly paths in T4
Asked Answered
Y

1

13

I have a .tt script that needs to reference a couple of external assemblies.

Is it possible for the T4 host to automatically include the assemblies referenced in the project - rather than me manually adding an assembly directive for each one?

E.g. Referencing an assembly from a nuget is a moving target when using a path relative to $(ProjecDir).

Using assembly paths like $(Project)\bin\Debug\Example.dll also seems less than optimal - as it requires the build to have been successful previously - which is probably not the case if you have a .tt file generating the "ErrorGeneratingOutput" in a .cs file!?

Update 1:

So I have had a second stab at this but this time trying to tackle the issue around "TransformOnBuild" ( as a side note I can highly recommend @kzu's excellent project: https://github.com/clariuslabs/TransformOnBuild) and not having $(SolutionDir) available when not running TextTransform via direct from msbuild. Anyway - I came up with a 2-step solution.

  1. msbuild target uses WriteLinesToFile task to generates a .tt file with a fresh list of assembly directives based on the references found in the csproj file.

  2. Any other .tt files in the project can the include the auto-generated file to get project assemblies registered.

Here is an example of the target:

<Target Name="Write_AssemblyRefs_TT" BeforeTargets="TransformOnBuild">
  <!-- A message for all to enjoy! -->
  <WriteLinesToFile File="@(MyTextFile)" 
    Lines="&lt;# /* AUTOGENERATED BY MSBUILD and Kern Herskind Nightingale */ #&gt;" 
    Overwrite="true" 
    Encoding="Unicode" />
  <!-- Output all assembly references with a HintPath -->
  <WriteLinesToFile File="@(MyTextFile)" 
    Lines="&lt;#@ assembly name=&quot;$(ProjectDir)%(Reference.HintPath)&quot; #&gt;" 
    Overwrite="false"
    Encoding="Unicode"
    Condition="'%(Reference.HintPath)' != ''" />
  <!-- Output all project references - this could fail with custom nameing/build output dirs  -->
  <WriteLinesToFile File="@(MyTextFile)" 
    Lines="&lt;#@ assembly name=&quot;$(ProjectDir)%(ProjectReference.RelativeDir)bin\$(Configuration)\%(ProjectReference.Name).dll&quot; #&gt;" 
    Overwrite="false"
    Encoding="Unicode" />
</Target>
<ItemGroup>
  <MyTextFile Include="AssemblyRefs.tt" />
</ItemGroup>

And how to include it in the T4 file (trivial):

<#@ include file="AssemblyRefs.tt" #>

Code generation for the code generator :)

Update 2:

I have created a Nuget package to make it easy to add the above assembly directive generation build target: https://www.nuget.org/packages/AssemblyReferencesTT/1.0.12

Yah answered 23/9, 2014 at 8:14 Comment(7)
Wonder if the $(libDir) technique described here could be part of the solution: blogs.msdn.com/b/t4/archive/2013/08/29/…Yah
herskinduk, I couldn't find a way to do it, however wouldn't it be possible to generate another tt file with the correct path, then build this generated tt file?Workroom
@Workroom I would assume that that is possible. Basically this stems from the need to distribute a .tt file via nuget. It would be super slick if I could just use Nuget to manage the dependencies - this would require T4 to be able to load the project references though. Perhaps MEF is the answer?Yah
Here is what I have so far github.com/herskinduk/AutoWrapping/blob/… (see line 105). I still have two ugly assembly directives but the rest of the assemblies loaded dynamically and injected via MEF.Yah
I have been thinking about creating a custom Host that can facilitate this. Good/bad idea?Yah
After looking at both custom Host and Directive Processor I have more or less discarded them as being to cumbersome. Apparently there are some MEF features in T4 but I haven't found much documentation.Yah
This is a pretty serious hack... I think we need a proper solution, let's try and upvote this UserVoice ticket: visualstudio.uservoice.com/forums/121579-visual-studio-2015/…Fluxion
V
4

i would have posted this in comment if i could.

for the question: it is not possible to include the assemblies referenced in the project automatically, but you can limit the work you have to do.

if you see below link in suggestion number 1, you can use c# to define assembly code before it is read by the t4. which make it possible to read a directory with reflection and load each assembly there.so the question is where will your assembly be ?

List<Assembly> allAssemblies = new List<Assembly>();
string path = Assembly.GetExecutingAssembly().Location;

foreach (string dll in Directory.GetFiles(path, "*.dll"))
    allAssemblies.Add(Assembly.LoadFile(dll));
    <#@ assembly name=dll #>

this is untested but should get you started at lest. for reference -> how to load all assemblies from within your /bin directory

for the second part:

  1. using $(SolutionDir) but this is the same as the $(Project) one except one level lower. -> How do I use custom library/project in T4 text template?
  2. use c# path utilities to navigate to the wanted path at runtime, but again this might require the assembly to compile first.
  3. Registering the External Library in GAC. this seams to resolve your problem the most since you will not have to set a path at all. see -> How to register a .Net dll in GAC?

Edit: here is a working dynamic include. just reference the result of the .ttinclude generated by this in any other .tt file

i tested it with the debugger and it seems to work.

and change assembly localisation to point where you need it to.

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

List<Assembly> allAssemblies = new List<Assembly>();
string file = Assembly.GetExecutingAssembly().Location;
if(file!= "")
{
    string path = Path.GetDirectoryName(file).TrimEnd();
    if(path != "")
        foreach (string dll in Directory.GetFiles(path, "*.dll"))
        {
            if(dll != "")
            {
                allAssemblies.Add(Assembly.LoadFile(dll));
                #>\<#<#= "@ assembly name=\""+ dll +"\" "#>\#><#="\n"#><#
            }
        }
}

#>

output:

<#@ assembly name="C:\TEMP\3mo0m0mq.dll" #> 
 <#@ assembly name="C:\TEMP\4ybsqre3.dll" #> 
 <#@ assembly name="C:\TEMP\ao0bzedf.dll" #> 
 <#@ assembly name="C:\TEMP\bo2w102t.dll" #> 
 <#@ assembly name="C:\TEMP\c5o2syvv.dll" #> 
 <#@ assembly name="C:\TEMP\dz1fin10.dll" #> 
 <#@ assembly name="C:\TEMP\giym0gef.dll" #> 
 <#@ assembly name="C:\TEMP\hjfgqkov.dll" #> 
 <#@ assembly name="C:\TEMP\ibuz4wvb.dll" #> 
 <#@ assembly name="C:\TEMP\ilrcwa2y.dll" #> 
 <#@ assembly name="C:\TEMP\k0yeumhb.dll" #> 
 <#@ assembly name="C:\TEMP\kirzdsqp.dll" #> 
 <#@ assembly name="C:\TEMP\ksxl4f2z.dll" #> 
 <#@ assembly name="C:\TEMP\l4kja4ts.dll" #> 
 <#@ assembly name="C:\TEMP\ljgxkpo0.dll" #> 
 <#@ assembly name="C:\TEMP\lkvkmlct.dll" #> 
 <#@ assembly name="C:\TEMP\lnofhhlq.dll" #> 
 <#@ assembly name="C:\TEMP\nbqhmjqd.dll" #> 
 <#@ assembly name="C:\TEMP\oc3pxhmq.dll" #> 
 <#@ assembly name="C:\TEMP\qb43ntcu.dll" #> 
 <#@ assembly name="C:\TEMP\qlyoyhyr.dll" #> 
 <#@ assembly name="C:\TEMP\snwvtb00.dll" #> 
 <#@ assembly name="C:\TEMP\umhhb2wb.dll" #> 
 <#@ assembly name="C:\TEMP\xsyfel0b.dll" #> 
 <#@ assembly name="C:\TEMP\z1weyhko.dll" #> 

you can escape the <# character with \<# see this.

Violaceous answered 6/10, 2014 at 17:16 Comment(5)
I don't think this is the correct answer. Finding the project reference paths can be done like this: gist.github.com/herskinduk/4528231150d13439e325 But the <#@ assembly #> cannot be output dynamically in the way you suggest. That is the big issue.Yah
On your three points: 1 + 2) See my previous comment for an effective way to locate project references. Finding the assemblies is not the issue. Having the T4 host adding them as assembly references is the real killer. 3) Adding the assemblies to the GAC may have some milage but I don't think it constitutes a solution to the problem originally described.Yah
@Yah yes the GAC way is far to problematic since you need to remember to do it on each machine that will build. ive come up with a working model there might be a way to make it work directly, im still trying to figure this one out.Violaceous
Looking at your edit - remember that the problem it is not outputting the string "<#@ assembly ... #>"... The problem is making the T4 host load the assemblies so you can use them in the T4 code.Yah
@Yah Then the '#>\<#<#= "@ assembly name=\""+ dll +"\" "#>\#><#="\n"#><#' line is useless in your case, but this code still does the Assembly.Load(); so you can start using the loaded assembly right away. you will have to resolve the types you want to use once first, but that's it. i don't get what your trying to achieve that this code doesn't do ? if you need to use the type why not try this on top of my method ? -> #465988Violaceous

© 2022 - 2024 — McMap. All rights reserved.