2

I'm trying to automate my builds from the command line. Unfortunately, my project was not intended to be automated from the beginning and now I'm struggling with the T4 templates.

After finding several bugs (this, and this) when trying to transform T4 templates using MSBuild I decided to find all the *.tt files from the root of my project and apply TextTransform to them, one by one.

But... some of them are a bit problematic. When running TextTransform on those I receive the following message:

C:\Specifications.tt(0,0) : error : Running transformation: System.InvalidCastException: Cannot convert 'Microsoft.VisualStudio.TextTemplating.CommandLine.CommandLineHost' into 'System.IServiceProvider'. en Microsoft.VisualStudio.TextTemplating7ea01e60423f49d2871739be33c0c810.GeneratedTextTransformation.GetProject() in Microsoft.VisualStudio.TextTemplating7ea01e60423f49d2871739be33c0c810.GeneratedTextTransformation.FindClasses(String nameSpace, String className, String baseClassName) in Microsoft.VisualStudio.TextTemplating7ea01e60423f49d2871739be33c0c810.GeneratedTextTransformation.TransformText()

Opening the .tt and looking for IServiceProvider or CommandLineHost, I've found these two methods:

private Project GetProject()
{
    IServiceProvider serviceProvider = (IServiceProvider)this.Host;
    DTE dte = serviceProvider.GetService(typeof(DTE)) as DTE; 
    // Get DTE
    //var dte = (DTE)TransformationContext.Current.GetService(typeof(DTE));

    // Get ProjectItem representing the template file
    ProjectItem projectItem = dte.Solution.FindProjectItem(this.Host.TemplateFile);

    // Get the Project of the template file
    Project project = projectItem.ContainingProject;

    return project;
}

private string GetDefaultNamespace()
{
    IServiceProvider serviceProvider = (IServiceProvider)this.Host;
    DTE dte = serviceProvider.GetService(typeof(DTE)) as DTE; 
    // Get DTE
    //var dte = (DTE)TransformationContext.Current.GetService(typeof(DTE));

    // Get ProjectItem representing the template file
    ProjectItem projectItem = dte.Solution.FindProjectItem(this.Host.TemplateFile);

    // Get the Project of the template file
    Project project = projectItem.ContainingProject;

    var vsSolution = (IVsSolution)serviceProvider.GetService(typeof(SVsSolution));
    IVsHierarchy vsHierarchy;
    ErrorHandler.ThrowOnFailure(vsSolution.GetProjectOfUniqueName(project.FullName, out vsHierarchy));
    uint projectItemId;
    ErrorHandler.ThrowOnFailure(vsHierarchy.ParseCanonicalName(projectItem.FileNames[1], out projectItemId));
    object defaultNamespace;
    ErrorHandler.ThrowOnFailure(vsHierarchy.GetProperty(projectItemId, (int)VsHierarchyPropID.DefaultNamespace, out defaultNamespace));
    return ((string)defaultNamespace);
}

Is it possible to workaround this problem somehow? Changing the .tt code, using an alternative approach...

I just want to automate my builds from the command line!

UPDATE The following is the entire content of the template:

<#@ template language="C#" debug="false" hostspecific="true"#>
<#@ assembly name="EnvDTE" #>
<#@ assembly name="Microsoft.VisualStudio.Shell.11.0" #>
<#@ assembly name="Microsoft.VisualStudio.Shell.Interop" #>
<#@ import namespace="EnvDTE" #>
<#@ import namespace="Microsoft.VisualStudio.Shell" #>
<#@ import namespace="Microsoft.VisualStudio.Shell.Interop" #>
<#@ include file="EF.Utility.CS.ttinclude"#>
<#@ output extension="/"#>
<#
CodeGenerationTools code = new CodeGenerationTools(this);
MetadataLoader loader = new MetadataLoader(this);
CodeRegion region = new CodeRegion(this, 1);
MetadataTools ef = new MetadataTools(this);

string namespaceName =  code.VsNamespaceSuggestion();// @"ArcNet.Piloto.Domain.Repositories";
string filenameSuffix = "Repository.cs";

// include additional using statements here
List<string> usingList = new List<string>(){
            "IDB.MW.Domain.Entities",
            "EverNext.Domain.Contracts.Repositories",
            "System.ServiceModel",
            "System.CodeDom.Compiler",
            "EverNext.Domain.Contracts.Model"
//  ,"other namespace"
};

////////////////////////////////////////////////////////////////////////////// Don't edit from here

EntityFrameworkTemplateFileManager fileManager = EntityFrameworkTemplateFileManager.Create(this);

// Write out support code to primary template output file
WriteHeader(fileManager, usingList.ToArray());
BeginNamespace(namespaceName, code);
EndNamespace(namespaceName);

// Emit Entity Types

foreach (var classes in FindClasses("IDB.MW.Domain.Entities", "", "BaseEntity").Where(c=>c.ImplementedInterfaces.OfType<CodeInterface>()
                                                                                      .Any(d=>d.Name.Equals("IAggregateRoot"))))
{
    fileManager.StartNewFile(classes.Name + filenameSuffix);
    BeginNamespace(namespaceName, code);
#>

[ServiceContract]
<#="public"#> <#=code.SpaceAfter(classes.IsAbstract ? "abstract" : string.Empty)#>partial interface I<#=classes.Name#>Repository : IEntityRepository<<#=classes.Name#>>,IAggregateRoot
{
}
<#
    EndNamespace(namespaceName);
}

//
//if (!VerifyTypesAreCaseInsensitiveUnique(ItemCollection))
//{
    //return "";
//}

fileManager.Process();

#>
<#+
void WriteHeader(EntityFrameworkTemplateFileManager fileManager, params string[] extraUsings)
{
    fileManager.StartHeader();
#>
//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated from a template.
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

using System;
using System.Linq;
<#=String.Join(String.Empty, extraUsings.Select(u => "using " + u + ";" + Environment.NewLine).ToArray())#>
<#+
    fileManager.EndBlock();
}

void BeginNamespace(string namespaceName, CodeGenerationTools code)
{
    CodeRegion region = new CodeRegion(this);
    if (!String.IsNullOrEmpty(namespaceName))
    {
#>
namespace <#=code.EscapeNamespace(namespaceName)#>
{
<#+
        PushIndent(CodeRegion.GetIndent(1));
    }
}

void EndNamespace(string namespaceName)
{
    if (!String.IsNullOrEmpty(namespaceName))
    {
        PopIndent();
#>
}
<#+
    }
}

bool VerifyTypesAreCaseInsensitiveUnique(EdmItemCollection itemCollection)
{
    Dictionary<string, bool> alreadySeen = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
    foreach(StructuralType type in itemCollection.GetItems<StructuralType>())
    {
        if (!(type is EntityType || type is ComplexType))
        {
            continue;
        }

        if (alreadySeen.ContainsKey(type.FullName))
        {
            Error(String.Format(CultureInfo.CurrentCulture, "This template does not support types that differ only by case, the types {0} are not supported", type.FullName));
            return false;
        }
        else
        {
            alreadySeen.Add(type.FullName, true);
        }

    }

    return true;
}

        private System.Collections.Generic.List<CodeClass> FindClasses(string nameSpace, string className, string baseClassName)
    {
        System.Collections.Generic.List<CodeClass> result = new System.Collections.Generic.List<CodeClass>();
        FindClasses(GetProject().CodeModel.CodeElements, className, baseClassName, nameSpace, result, false);
        return result;

    }

    private void FindClasses(CodeElements elements, string className, string baseClassName, string searchNamespace, List<CodeClass> result, bool isNamespaceOk)
    {
        if (elements == null) return;
        foreach (CodeElement element in elements)
        {
            if (element is EnvDTE.CodeNamespace)
            {
                EnvDTE.CodeNamespace ns = element as EnvDTE.CodeNamespace;
                if (ns != null)
                {
                    if (ns.FullName == searchNamespace)
                        FindClasses(ns.Members, className, baseClassName, searchNamespace, result, true);
                    else
                        FindClasses(ns.Members, className, baseClassName, searchNamespace, result, false);
                }
            }
            else if (element is CodeClass && isNamespaceOk)
            {
                CodeClass c = element as CodeClass;
                if (c != null)
                {
                    if (c.FullName.Contains(className) && (baseClassName == null || (HasIt(c.Bases, baseClassName) && c.Name != baseClassName)))
                        result.Add(c);

                    FindClasses(c.Members, className, baseClassName, searchNamespace, result, true);
                }

            }
        }
    }

    private bool HasIt(CodeElements elements, string name)
    {
        foreach (CodeElement element in elements)
        {
            CodeClass c = element as CodeClass;
            if (c != null && c.Bases != null)
            {
                if (HasIt(c.Bases, name))
                {
                    return true;
                }
            }

            if (element.Name == name)
                return true;
        }
        return false;
    }

    private Project GetProject()
    {
        IServiceProvider serviceProvider = (IServiceProvider)this.Host;
        DTE dte = serviceProvider.GetService(typeof(DTE)) as DTE; 
        // Get DTE
        //var dte = (DTE)TransformationContext.Current.GetService(typeof(DTE));

        // Get ProjectItem representing the template file
        ProjectItem projectItem = dte.Solution.FindProjectItem(this.Host.TemplateFile);

        // Get the Project of the template file
        Project project = projectItem.ContainingProject;

        return project;
    }

    private string GetDefaultNamespace()
    {
        IServiceProvider serviceProvider = (IServiceProvider)this.Host;
        DTE dte = serviceProvider.GetService(typeof(DTE)) as DTE; 
        // Get DTE
        //var dte = (DTE)TransformationContext.Current.GetService(typeof(DTE));

        // Get ProjectItem representing the template file
        ProjectItem projectItem = dte.Solution.FindProjectItem(this.Host.TemplateFile);

        // Get the Project of the template file
        Project project = projectItem.ContainingProject;

        var vsSolution = (IVsSolution)serviceProvider.GetService(typeof(SVsSolution));
        IVsHierarchy vsHierarchy;
        ErrorHandler.ThrowOnFailure(vsSolution.GetProjectOfUniqueName(project.FullName, out vsHierarchy));
        uint projectItemId;
        ErrorHandler.ThrowOnFailure(vsHierarchy.ParseCanonicalName(projectItem.FileNames[1], out projectItemId));
        object defaultNamespace;
        ErrorHandler.ThrowOnFailure(vsHierarchy.GetProperty(projectItemId, (int)VsHierarchyPropID.DefaultNamespace, out defaultNamespace));
        return ((string)defaultNamespace);
    }

#>
Luis
  • 2,833
  • 3
  • 27
  • 41
  • Why not use TFS build server for automating the build? – Kar May 08 '15 at 20:41
  • Because the whole deployment process involves several, more complicated steps. – Luis May 08 '15 at 20:45
  • 1
    Because we don't have TFS. Because I want a simple, easy to configure, flexible mechanism, based on command line preferably. So, TFS build server is not an option here. – Luis May 08 '15 at 20:50
  • This is still quite abstract. What is the "transformation"? How are these CodeElements used in the rest of the template? – Michael Baker May 14 '15 at 08:47
  • Ok, i'll include the whole file – Luis May 14 '15 at 10:08
  • Unfortunately your template is driven from DTE so will only ever work inside Visual Studio. You have two options - you can run your transformation manually when it requires updating, check in the results and move on or you can change your abstraction and use a different model to generate code. – Michael Baker May 15 '15 at 08:43
  • I see... Ok, Michael, thanks for your great support! – Luis May 15 '15 at 20:54

1 Answers1

0

We don't know what your template looks like - what includes it uses, what other technologies or templates it depends on but it's probably very likely you don't need any DTE (Visual Studio) features inside the template.

This template is attempting to read the "Namespace" field from the project item using the DTE which isn't possible as part of the command line build. To work around this simply change your template and hard code the namespace string in the template.

As a navie example like this

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

namespace Packaging {

    internal partial class FileSystem { } 
}

Or you can have your template import another template (in this example MyInclude.ttinclude) and call a method defined in that import (Generate) which takes the namespace as a parameter which is then used in the code generation.

<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ output extension="generated.cs" #>

<#@ include file="..\MyInclude.ttinclude"#>

<#@ Generate("MyNamespace", false); #> 
Michael Baker
  • 3,338
  • 26
  • 29
  • Bad news are that the template uses CodeClass, CodeElement and Project, defined in DTE (https://msdn.microsoft.com/en-us/library/aa300737%28v=vs.71%29.aspx). How could I workaround that? – Luis May 11 '15 at 14:40
  • I've tried the -u "C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\PublicAssemblies\EnvDTE.dll" option in TextTransform to specify namespaces, but now I get a strange error: error CS1056: Compiling transformation: Unexpected character '\' – Luis May 11 '15 at 17:31
  • Use a different type of abstraction, forgo the DTE interfaces and roll some alternative approach. If the text template engine isn't being hosted by Visual Studio there isn't much you can do. How much code do you have which uses the DTE interfaces? – Michael Baker May 11 '15 at 20:02
  • I have about 10 templates not working for this reason and I'm new to T4, so I don't find a simple way to workaround what they do manually (it seems that DTE is used to find projects, classes and so forth to decide which should apply the template). I found that I can run msbuild with /t:TransformAll, but currently is not working for me. I created a new thread here: http://stackoverflow.com/questions/30176352/the-target-transformall-does-not-exist-in-the-project-t4-transformation-msbuil – Luis May 11 '15 at 20:12
  • Can you reveal anymore detail about exactly what from the DTE is being extracted and used? For code generation (T4) there should not really be any need to access the project system. Templates should have any external model data supplied by reading data from the file system (e.g. XML files or whatever) or by template parameters. Perhaps an alternative can be suggested if you can explain what exactly you need the DTE for and what role it is playing in your T4 implementation. In short builds invoked from MS Build simply do not have anyway to access the DTE. – Michael Baker May 13 '15 at 08:42
  • Sure, I added more detail in the question, above. Thanks for your support! – Luis May 13 '15 at 14:35