Monday, January 20, 2014

T4 Generating Dummy Adapters

I needed to code up an adapter layer between the domain and the ui. For now, and until something more specific is needed, just expose out the public methods of the domain and their result types. There is more that the adapter is encapsulating but the premise remains.

Added my Nuget packages for code generation: T4EnvDte and T4MultiFile.

As it turns out some of the code that was useful for this operation required a modification of T4EnvDte. So I added an assembly reference to EnvDTE80. This was to finally write usable code to get all the projects in a solution recursing through solution folders.

Let's have a look at the actual T4 line by line, that uses the new code so we can see what is required for this task.
<#@ template debug="True" language="C#" hostspecific="True" #>
<#@ output extension=".txt"#>
<#@ include file="MultipleOutputHelper.ttinclude" #>
<#@ import namespace="EnvDTE"#>
<# EnvDTE.DTE Dte; #>
<#@ include file="EnvDteHelper.ttinclude" #>
<#  bool doMultiFile=true;#>

This is the basic start of the file, with 2 variables declared thus far. Dte and doMultiFile. One is the Visual studio com object, the other is a flag telling T4MultiFile if we want to generated multiple files or all into one.

Main file output
Last run at <#=DateTime.UtcNow.ToString()#>

This simple code is all that is going to the main file output, a simple date/time string for showing when the file was last run. The usefulness is debatable in any proper source controlled scenario.

<# var manager = Manager.Create(Host, GenerationEnvironment);
var targetNs=System.Runtime.Remoting.Messaging.CallContext.LogicalGetData("NamespaceHint"); //1
var projects= RecurseSolutionProjects(Dte); //2
var project = projects.First(p=>p.Name=="Company.Domain"); //3
var toGen= RecurseProjectItems(project).Where(pi=>pi.Name.StartsWith("Org")); //4

foreach(var p in toGen){
  var className=Before(p.Name,".")+"Adapter"; //1
  manager.StartNewFile(className+".generated.cs"); //2
  var fcModel= p.FileCodeModel; //3
  var ns=fcModel.CodeElements.OfType<EnvDTE.CodeNamespace>().First(); //4
  var codeClass= ns.Children.OfType<EnvDTE.CodeClass>().First(); //5
  var codeMethods= codeClass.Children.OfType<EnvDTE.CodeFunction>(); //6

This is all the setup and starting the file per type looping.

  1. targetNs asks what the proper namespace for our current file output location would be
  2. projects gets all the projects in the current solution using the new EnvDte helper code
  3. project the actual project name we are looking for
  4. toGen gets the projectItems in the source project we are interested in
Now we look over those items we will be generating
  1. className holds the name of the new adapter class
  2. tell the helper to start a new file for the class
  3. fcModel holds the FileCodeModel of the current source projectItem
  4. ns holds the namespace that wraps our target class and assumes there is only one namespace in the source file
  5. codeClassholds the reference to the target class and assumes there is only one class in the source file
  6. codeMethods gets all the methods of the source class

the meat of the individual file generation

//<#=p.Name#> <# /* 1 */ #>

using <#= ns.Name#>; <# /* 2 */ #>

namespace <#=targetNs#>{ <# /* 3 */ #>

  public class <#=className#>{ <# /* 4 */ #>
<# foreach (var m in codeMethods.Where(m=>m.Access==EnvDTE.vsCMAccess.vsCMAccessPublic)){ var parms= m.Parameters.OfType<CodeParameter>(); #> <# /* 5-5.1 */ #>
     public <#=  m.Type.AsFullName#> <#=m.Name#>(<#=parms.Any()? parms.Select(parm=>parm.Type.AsString+" "+parm.Name).Aggregate((s1,s2)=>s1+","+s2):string.Empty#>){ <# /* 5.2 */ #>
      return <#=Before(p.Name,".")#>.Instance.<#=m.Name#>(<#=parms.Any()?parms.Select(parm=>parm.Name).Aggregate((s1,s2)=>s1+","+s2):string.Empty#>); <# /* 5.3 */ #>
<#    } #>      
} //end foreach

  1. adds a comment of the source file name
  2. adds a using clause for the source file's namespace
  3. adds a namespace for our new file
  4. adds a class to our new adapter
  5. loop over the public methods in the class
    1. parms holds a reference to the parameters of the method if any
    2. Generate the method signature
    3. Generate the method return code (which includes the singleton-based Instance property requirement of this domain layer
  6. close up the single file generator and the foreach

the last little bits of code that complete the file


string Before(string text, string delimiter)
    return text.Substring(0,text.IndexOf(delimiter,StringComparison.CurrentCulture));

So there we see using T4EnvDte and T4MultiFile together to generate specific classes from another project in the solution. I welcome any and all questions or suggestions for improvement