Using T4 template to force PascalCase (TitleCase) for entities in Entity Framework 6

c# entity-framework entity-framework-6 t4 visual-studio

Question

Turns out, simply modifying the default T4 template was actually really easy. Inside GetTypeName there is a is StructuralType check which handles all non-primitive types. That pretty well fixed the majority of my issues. Then it was just a matter of Ctrl-F for keywords. My frustration was just getting the best of me when I originally posted this question.

--

I have been provided with a database that has all table names and column names in snake_case. The database cannot be changed. I am hoping to use the power of T4 templates to auto-generate all classes, members, properties, navigation properties, etc. in PascalCase (TitleCase).

So far, I am getting decently close, but I am starting to get stuck.

namespace PokeDB
{
    using System;
    using System.Collections.Generic;

    public partial class Ability
    {
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
        public ability()
        {
            this.ability_changelog = new HashSet<ability_changelog>();
            this.ability_flavor_text = new HashSet<ability_flavor_text>();
            this.ability_names = new HashSet<ability_names>();
            this.ability_prose = new HashSet<ability_prose>();
            this.conquest_pokemon_abilities = new HashSet<conquest_pokemon_abilities>();
            this.pokemon_abilities = new HashSet<pokemon_abilities>();
        }

        public long id { get; set; }
        public string identifier { get; set; }
        public long generation_id { get; set; }
        public bool is_main_series { get; set; }

        public virtual generation Generation { get; set; }
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
        public virtual ICollection<AbilityChangelog> AbilityChangelog { get; set; }
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
        public virtual ICollection<AbilityFlavorText> AbilityFlavorText { get; set; }
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
        public virtual ICollection<AbilityNames> AbilityNames { get; set; }
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
        public virtual ICollection<AbilityProse> AbilityProse { get; set; }
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
        public virtual ICollection<ConquestPokemonAbilities> ConquestPokemonAbilities { get; set; }
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
        public virtual ICollection<PokemonAbilities> PokemonAbilities { get; set; }
    }
}

As shown above, I have gotten the class name, the navigation property names, some of the navigation property return values, and the file name. Where I am getting stuck is the Constructor name, Constructor assignments, Properties, and the remainder of the navigation properties return values.

Right now, I am just manually replacing all references of the string with CultureInfo.CurrentCulture.TextInfo.ToTitleCase(string).Replace("_", "").

For Example, for the class name:

public string EntityClassOpening(EntityType entity)
    {
        return string.Format(
            CultureInfo.InvariantCulture,
            "{0} {1}partial class {2}{3}",
            Accessibility.ForType(entity),
            _code.SpaceAfter(_code.AbstractOption(entity)),
            _code.Escape(entity),
            _code.StringBefore(" : ", _typeMapper.GetTypeName(entity.BaseType)));
    }

Changed To:

public string EntityClassOpening(EntityType entity)
    {
        return string.Format(
            CultureInfo.InvariantCulture,
            "{0} {1}partial class {2}{3}",
            Accessibility.ForType(entity),
            _code.SpaceAfter(_code.AbstractOption(entity)),
            CultureInfo.CurrentCulture.TextInfo.ToTitleCase(_code.Escape(entity)).Replace("_", ""),
            _code.StringBefore(" : ", _typeMapper.GetTypeName(entity.BaseType)));
    }

It has been a long and tedious process, but it's better than absolutely nothing. I was hoping that you guys might have some further advice for me, or a bit of a cleaner solution? This is my first time using any type of custom T4 template. It feels like I should be able to knock some of these out with one fell swoop in some of the TypeManager methods. I'm just not entirely sure where.

I have found a couple StackOverflow questions and a couple blog posts about using T4 templates to do this, but unfortunately the templates were never shared.

Even if it isn't necessarily a perfect solution, I would appreciate any amount of guidance.


using System.Linq;

namespace PokeDB
{
    public class ConsoleDriver
    {
        public static readonly PokeDBContainer PokeDB = new PokeDBContainer();

        public static void Main(string[] args)
        {
            System.Diagnostics.Debug.WriteLine(PokeDB.Pokemon);
            var x = PokeDB.Pokemon.ToList();
        }
    }
}

Above is the current test driver that I have written to test out my DBContext. The very first line fails with a The following message: An unhandled exception of type 'System.InvalidOperationException' occurred in EntityFramework.dll Additional information: The entity type Pokemon is not part of the model for the current context.

namespace PokeDB
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations.Schema;

    [Table("pokemon")]
    public partial class Pokemon
    {
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
        public Pokemon()
        {
            this.Encounters = new HashSet<Encounter>();
            this.PokemonAbilities = new HashSet<PokemonAbilities>();
            this.PokemonForms = new HashSet<PokemonForms>();
            this.PokemonGameIndices = new HashSet<PokemonGameIndices>();
            this.PokemonItems = new HashSet<PokemonItems>();
            this.PokemonMoves = new HashSet<PokemonMoves>();
            this.PokemonStats = new HashSet<PokemonStats>();
            this.PokemonTypes = new HashSet<PokemonTypes>();
        }

        public long Id { get; set; }
        public string Identifier { get; set; }
        public Nullable<long> SpeciesId { get; set; }
        public long Height { get; set; }
        public long Weight { get; set; }
        public long BaseExperience { get; set; }
        public long Order { get; set; }
        public bool IsDefault { get; set; }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
        public virtual ICollection<Encounter> Encounters { get; set; }
        public virtual PokemonSpecies PokemonSpecies { get; set; }
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
        public virtual ICollection<PokemonAbilities> PokemonAbilities { get; set; }
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
        public virtual ICollection<PokemonForms> PokemonForms { get; set; }
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
        public virtual ICollection<PokemonGameIndices> PokemonGameIndices { get; set; }
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
        public virtual ICollection<PokemonItems> PokemonItems { get; set; }
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
        public virtual ICollection<PokemonMoves> PokemonMoves { get; set; }
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
        public virtual ICollection<PokemonStats> PokemonStats { get; set; }
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
        public virtual ICollection<PokemonTypes> PokemonTypes { get; set; }
    }
}

That is my current Pokemon.cs file that was generated via T4. And this is my DbSet declaration inside PokeDBContext.cs:

public virtual DbSet<Pokemon> Pokemon { get; set; }

I'm getting the correct Intellisense since the classes are set up properly. I'm just getting an instant InvalidOperationException any time I try to access the entities. Note that the true Database entity is pokemon.

1
1
1/11/2017 12:02:33 AM

Popular Answer

You're on the right track, but what you want is a simple snake case to PascalCase conversion. My concern is that EF would lose column mappings (if you're using model/database first) with these changes alone.

To do what you're asking, move your excellent CultureInfo.CurrentCulture.TextInfo.ToTitleCase(name).Replace("_", ""); to a reusable method in the CodeStringGenerator class

public string PascalCase(string name)
{
    return CultureInfo.CurrentCulture.TextInfo.ToTitleCase(name).Replace("_", "");
}

Then in the T4 template for the constructor change to:

public <#=code.Escape(codeStringGenerator.PascalCase(entity))#>()
{
<#
    foreach (var edmProperty in propertiesWithDefaultValues)
    {
#>
    this.<#=code.Escape(edmProperty)#> = <#=typeMapper.CreateLiteral(edmProperty.DefaultValue)#>;
<#
    }

    foreach (var navigationProperty in collectionNavigationProperties)
    {
        // for readability hold the type name in a variable
        var typeName = typeMapper.GetTypeName(navigationProperty.ToEndMember.GetEntityType());
#>
    // pascal case here
    this.<#=code.Escape(codeStringGenerator.PascalCase(navigationProperty))#> = new HashSet<<#=code.Escape(codeStringGenerator.PascalCase(typeName))#>>();
<#
    }

    foreach (var complexProperty in complexProperties)
    {
#>
    this.<#=code.Escape(complexProperty)#> = new <#=typeMapper.GetTypeName(complexProperty.TypeUsage)#>();
<#
    }
#>
}

Having done all this, your code will compile, but queries will likely fail due because the edmx still has the snake case naming, so no mappings to your actual database. Therefore, you'll also need to edit the edmx file. There are some examples here: How to force pascal case with Oracle's Entity Framework support?, but at this point you start wondering if it's worth the effort.

EDIT Modifying the EDMX instead (better option)

First revert your T4 changes, you don't need them anymore. Then use the example in the link. I've modified it slightly to use your title case. I will be using this for a similar task:

// In the main method get your edmx/designer 
//EDMX File location
string pathFile = @"c:\Path\To\DbModel.edmx";
//Designer location for EF 5.0
string designFile = @"c:\Path\To\DbModel.edmx.diagram";
// ...
// replace the PascalCase method with this
public static string PascalCase(string name, bool sanitizeName = true, bool pluralize = false)
{

    // if pascal case exists
    // exit function

    Regex rgx = new Regex(@"^[A-Z][a-z]+(?:[A-Z][a-z]+)*$");

    string pascalTest = name;

    if (name.Contains("."))
    {
        string[] test = new string[] { };
        test = name.Split('.');

        if (rgx.IsMatch(test[1].ToString()))
        {
            return name;
        }

    }
    else
    {

        if (rgx.IsMatch(name))
        {
            return name;
        }

    }

    //Check for dot notations in namespace
    string result;
    bool contains = false;
    string[] temp = new string[] { };
    var namespc = string.Empty;

    if (name.Contains("."))
    {
        contains = true;
        temp = name.Split('.');
        namespc = temp[0];

    }

    if (contains)
    {
        name = temp[1];
    }

    name = name.ToLowerInvariant(); // this may or may not be required
    // Here's the simplified snake to pascal case
    result = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(name).Replace("_", "");

    if (contains)
    {
        result = namespc.ToString() + "." + result;
    }

    if (pluralize)
    {
        result = Pluralize(result);
    }
    return result;
}
0
5/23/2017 12:33:51 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