T4 template to generate Code First MetaData buddy classes

ef-code-first entity-framework-6 t4

Question

Setup:

I create many MVC applications from scratch, but they all use pre-existing databases.

I retrieve the Code First classes, DbContext class, and mapping classes from the database using the Entity Framework -> Reverse Engineer Code First context menu option.

What I Need:

To include my customized DisplayName characteristics, for example, I would also like to create MetaData classes.

To avoid clogging up the Models directory, the MetaData Classes would be in a separate directory called MetaData.

Question:

Anyone familiar with a T4 template that accomplishes this? If I am the only one with this criterion, that would be strange...

My Possibilities

Since I'm new to T4, any template that pulls files from a specific directory, reads each one in a loop, modifies it a little (ideally by adding an attribute to a property! ), and then writes to a new file in a different directory would be acceptable because from there on, I'll be able to figure out how to use it for my particular need.

Note:

I don't want to overwrite my MetaData classes, hence I don't want the files to be generated at the same time as the Reverse Engineered Code First files. If the file already exists in the directory when I DO execute the template, I would modify or write the template to prevent this from happening.MetaData directory, the template skips that item and does not generate a fresh MetaData file to replace the old one.

Model first and database first have both been discussed, but not code first. It's possible that I could convert one of those to code first by just reading in the previously created files, retrieving the properties, and adding the DisplayName attribute to them in place of the EDMX bits.

I hope I made sense.

Edited to remove:

I removed the initial edit because I've since advanced. Refer to EDIT 2 below.

EDIT 2:

I also deleted EDIT 2 because I found the answers to all of my issues. The response is below.

1
2
5/19/2014 2:50:18 PM

Accepted Answer

Blood, sweat, and tears as well as TemplateFileManagerV2.1.ttinclude and VisualStudioAutomationHelper.ttinclude from Tangible T4 were used to solve my issue, however with the adjustment advised by Tangible T4 support in the following post:

Editing their Visual Studio Automation Helper to enable creating files that are not encased in a.txt4 file is advised by Tangible T4 support.

It was a little painful because I don't have Tangible T4's Pro edition. Hey, I'm not looking down my nose at freebies.

I can't tell if a property in the source file is virtual, therefore I get the navigation properties in my friend metadata classes as well, which I didn't want. This is the last remaining issue. I'm going to live to fight another day.

Additionally, even though I can produce the files, the project does not have them. The code to include them is straightforward, however I was unable to get it to function in a single file and had to separate it into other files as follows:

T4_1_GenerateCodeFirstBuddies.tt T4_2_GenerateCodeFirstBuddies.tt

The two Tangible T4 helper.ttincludes that are used by T4 1 GenerateCodeFirstBuddies.tt, one of which leaves a residual error, are separated, which has a side advantage. Running my second file eliminates the error and the irritating red wavy lines in the solution explorer.

So, here is the code for my files:

T4_1_GenerateCodeFirstBuddies.tt

<#@ template debug="true" hostSpecific="true" language="C#" #>
<#@ output extension=".cs" #>
<#@ Assembly Name="System.Core" #>
<#@ import namespace="System" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Diagnostics" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Collections" #>
<#@ import namespace="System.Collections.Generic" #> 
<#@ import namespace="System.Text.RegularExpressions" #>
<#@ import namespace="EnvDTE" #>
<#@ include file="VisualStudioAutomationHelper.ttinclude" #>
<#@ include file="TemplateFileManagerV2.1.ttinclude" #><#
    var modelFileDirectory = this.Host.ResolvePath("Models");
    var metaDataFilesDirectory = this.Host.ResolvePath("MetaData"); 
    var nspace = "";
    var manager = TemplateFileManager.Create(this);
    foreach(var file in System.IO.Directory.GetFiles(modelFileDirectory, "*.cs"))
    {
        var projectItem = this.VisualStudioHelper.FindProjectItem(file);
        foreach(EnvDTE.CodeClass classInFile in this.VisualStudioHelper.CodeModel.GetAllCodeElementsOfType(projectItem.FileCodeModel.CodeElements, EnvDTE.vsCMElement.vsCMElementClass, false))
        {
            var name = classInFile.Name;
            if(nspace == "") nspace = classInFile.Namespace.Name;
            // Danger: Beware if a table name includes the string "Context" or "AspNet"!!
            // These files are removed because they are either the DbContext, or the sysdiagram file, or else the AspNet.Identity tables
            if(name != "sysdiagram" && name.IndexOf("Context") == -1 && name.IndexOf("AspNet") == -1)
            {                               
                if(!FileExists(metaDataFilesDirectory, classInFile.Name + "MetaData.cs")) 
                {
                    manager.StartNewFile(name +"MetaData.cs", "", "MetaData"); #>
using System;
using System.Collections.Generic;
using System.ComponentModel;
//using System.ComponentModel.DataAnnotations;
//using Wingspan.Web.Mvc.Extensions;
using Wingspan.Web.Mvc.Crud;

namespace <#= nspace #>
{
    public class <#= name + "MetaData" #>
    {
        <# foreach (CodeElement mem in classInFile.Members)
        { 
            if (mem.Kind == vsCMElement.vsCMElementProperty) // && "[condition to show that mem is not marked as virtual]") 
            {
                PushIndent("        ");
                WriteLineDisplayName(mem); 
                WriteLineProperty(mem);
                WriteLine("");
                PopIndent();
            }
        } #>    
    }

    public partial class <#= name #> : IInjectItemSL
    {
        public ItemSL ItemSL
        {
            get
            {
                return new ItemSL
                {
                    ItemId = <#= name #>Id, ItemText = Name
                };
            } 
        }   
    }
}<#
                } 
            }
        }       
    }
    manager.Process();  
#>
<#+
// Check for file existence
bool FileExists(string directory, string filename)
{            
    return File.Exists(Path.Combine(directory, filename));    
}

// Get current  folder directory
string GetCurrentDirectory()
{
    return System.IO.Path.GetDirectoryName(Host.TemplateFile);
}

string GetRootDirectory()
{
    return this.Host.ResolvePath("");
}

// Get content of file name
string xOutputFile(string filename)
{
    using(StreamReader sr = 
      new StreamReader(Path.Combine(GetCurrentDirectory(),filename)))
    {
        return sr.ReadToEnd();
    }
}

// Get friendly name for property names
string GetFriendlyName(string value)
{
return Regex.Replace(value,
            "([A-Z]+)", " $1",
            RegexOptions.Compiled).Trim();
}

void WriteLineProperty(CodeElement ce)
{
    var access = ((CodeProperty) ce).Access == vsCMAccess.vsCMAccessPublic ? "public" : "";
    WriteLine(access + " " + (((CodeProperty) ce).Type).AsFullName + " " + ce.Name + " { get; set; }");
}

void WriteLineDisplayName(CodeElement ce) 
{
    var name = ce.Name;
    if (!string.IsNullOrEmpty(name)) 
    {
        name = GetFriendlyName(name);
        WriteLine(string.Format("[DisplayName(\"{0}\")]", name));
    }
}
#>

T4_2_GenerateCodeFirstBuddies.tt:

<#@ template debug="true" hostSpecific="true" language="C#" #>
<#@ output extension=".cs" #>
<#@ Assembly Name="System.Core" #>
<#@ import namespace="System" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Diagnostics" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Collections" #>
<#@ import namespace="System.Collections.Generic" #> 
<#@ import namespace="System.Text.RegularExpressions" #>
<#@ import namespace="EnvDTE" #>
<#@ include file="VisualStudioAutomationHelper.ttinclude" #>
<#@ include file="TemplateFileManagerV2.1.ttinclude" #><#

    var metaDataFilesDirectory = this.Host.ResolvePath("MetaData"); 

    var metaDataFiles = System.IO.Directory.GetFiles(metaDataFilesDirectory, "*.cs");
    var project = VisualStudioHelper.CurrentProject;
    var projectItems = project.ProjectItems;
    foreach( var f in metaDataFiles)
    {
        projectItems.AddFromFile(f);
    }

#>

The output files produced are adequate in my opinion and resemble:

using System;
using System.Collections.Generic;
using System.ComponentModel;
//using System.ComponentModel.DataAnnotations;
//using Wingspan.Web.Mvc.Extensions;
using Wingspan.Web.Mvc.Crud;

namespace BuddyClassGenerator.Models
{
    public class ChemicalMetaData
    {
        [DisplayName("Chemical Id")]
        public System.Guid ChemicalId { get; set; }

        [DisplayName("Active Ingredient")]
        public System.String ActiveIngredient { get; set; }

        [DisplayName("Type")]
        public System.String Type { get; set; }

        [DisplayName("LERAP")]
        public System.String LERAP { get; set; }

        [DisplayName("Hazard Classification")]
        public System.String HazardClassification { get; set; }

        [DisplayName("MAPP")]
        public System.Int32 MAPP { get; set; }

        [DisplayName("Hygiene Practice")]
        public System.String HygienePractice { get; set; }

        [DisplayName("Medical Advice")]
        public System.String MedicalAdvice { get; set; }

        [DisplayName("Label")]
        public  System.String Label { get; set; }

        [DisplayName("PPE")]
        public System.String PPE { get; set; }

        [DisplayName("Warnings")]
        public System.String Warnings { get; set; }

        [DisplayName("Products")]
        public System.Collections.Generic.ICollection<BuddyClassGenerator.Models.Product> Products { get; set; }

    }

    public partial class Chemical : IInjectItemSL
    {
        public ItemSL ItemSL
        {
            get
            {
                return new ItemSL
                {
                    ItemId = ChemicalId, ItemText = Name
                };
            } 
        }   
    }

You'll probably see that I combined two classes into one file. It may not be the greatest practice, but since it saves me time and reduces visual clutter in the folders, I have the right to use it.

To-do items include removing the namespace names from the property types and not including the navigation properties in the buddy class.

I sincerely hope that this is helpful to someone, but keep in mind that the Tangible T4 ttincludes described above are required for it to function.

2
5/19/2014 3:11:10 PM


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow