Customizing T4 Templates

 


The information in this post is out of date.

Visit msdn.com/data/ef for the latest information on current and past releases of EF.


 

 

The whole point of us using T4 for code-generation is that it makes it easy to customize the generated entities.

A good example might be to support some sort of validation when setting string properties.

To illustrate lets re-use the code from the recent POCO template walkthrough post.

In that project there is a Person entity which has an EmailAddress property. If we want this property to only accept valid EmailAddresses, we need to modify the generated code to do some validation, probably using a Regular Expression.

One way to do this is to use Structural Annotations in conjunction with a customized T4 template. Structural Annotations, if you aren’t familiar with them, allow you to put custom annotations in the model and retrieve them using the Entity Frameworks metadata APIs. So we can use Structural Annotations to embed our Regular Expressions right in the model, and access them from inside the T4 template, which already uses the Entity Framework’s metadata APIs.

To do this, the first step is to register a custom namespace in the schema element of the CSDL portion of the EDMX file:

 <edmx:ConceptualModels>
      <Schema xmlns="https://schemas.microsoft.com/ado/2008/09/edm" xmlns:store="https://schemas.microsoft.com/ado/2007/12/edm/EntityStoreSchemaGenerator" 
              xmlns:Regex="https://Regex"
              Namespace="Blogging" Alias="Self">
        <EntityContainer Name="BloggingContainer" >
          <EntitySet Name="Blogs" EntityType="Blogging.Blog" />
          <EntitySet Name="People" EntityType="Blogging.Person" />
          <EntitySet Name="Entrys" EntityType="Blogging.Entry" />

With that in place we can put the Regular Expression on our EmailAddress property by putting some custom XML, in the namespace we just registered, inside the corresponding Property.

Something like this:

 <EntityType Name="Person">
          <Key>
            <PropertyRef Name="ID" /></Key>
          <Property Type="Int32" Name="ID" Nullable="false" store:StoreGeneratedPattern="Identity" />
          <Property Type="String" Name="Firstname" Nullable="false" MaxLength="50" />
          <Property Type="String" Name="Surname" Nullable="false" MaxLength="50" />
          <Property Type="String" Name="EmailAddress" Nullable="false" MaxLength="100">
            <Regex:Expression ErrorMessage="A valid emailaddress is required">^(([A-Za-z0-9]+_+)|([A-Za-z0-9]+\-+)|([A-Za-z0-9]+\.+)|([A-Za-z0-9]+\++))*[A-Za-z0-9]+@((\w+\-+)|(\w+\.))*\w{1,63}\.[a-zA-Z]{2,6}$</Regex:Expression>
          </Property>
          <NavigationProperty Name="Entries" Relationship="Blogging.PostPerson" FromRole="Person" ToRole="Post" />
          <NavigationProperty Name="Blogs" Relationship="Blogging.PersonBlog" FromRole="Person" ToRole="Blog" />
        </EntityType>

I’m using this regular expression: “ ^(([A-Za-z0-9]+_+)|([A-Za-z0-9]+\-+)|([A-Za-z0-9]+\.+)|([A-Za-z0-9]+\++))*[A-Za-z0-9] +@((\w+\-+)|(\w+\.))*\w{1,63}\.[a-zA-Z]{2,6}$ ” but the of course any Regular expression would work.

The ErrorMessage attribute is there so it can be used as the Message of an Exception in the event of a non-match.

Once you have this Regular Expression in your model, you can modify your T4 template (in this case the template is the POCO Types template that ships with the Microsoft Entity Framework Feature CTP 1), so that it looks for and handles Regular Expressions.

So rather than this:

 public string EmailAddress { 

get

 ; 

set

 ; }

We want to produce something like this:

 private string _EmailAddress; 
        private 

Regex

  _EmailAddressRegex = new 

Regex

 (@"^(([A-Za-z0-9]+_+)|([A-Za-z0-9]+\-+)|([A-Za-z0-9]+\.+)|([A-Za-z0-9]+\++))*[A-Za-z0-9]+@((\w+\-+)|(\w+\.))*\w{1,63}\.[a-zA-Z]{2,6}$");
        public string EmailAddress 
        { 
             

get

  { return _EmailAddress; }
             

set

  
             { 
                if (value != null && !_EmailAddressRegex.IsMatch(value))
                   throw new Exception("A valid emailaddress is required");
                _EmailAddress = value; 
             }
        }

To make this change to the generated code there are three parts of the template that need to change:

    1. A change to the part of the template that emits primitive properties:
    2. <#=Accessibility.ForType(entity)#> <#=code.SpaceAfter(code.AbstractOption(entity))#>partial class <#=code.Escape(entity)#><#=code.StringBefore(" : ", code.Escape(entity.BaseType))#>

 

    1. {

 

    1. <#

 

    1.     region.Begin("Primitive Properties");

 

    1.     foreach (EdmProperty edmProperty in entity.Properties.Where(p => p.TypeUsage.EdmType is PrimitiveType

 

    1.         && p.DeclaringType == entity))

 

    1.     { #>  

 

    1. <# if (HasRegex(edmProperty)) { #>

 

    1.       private <#=code.Escape(edmProperty.TypeUsage)#> _<#=code.Escape(edmProperty)#>;

 

    1.     private Regex _<#=code.Escape(edmProperty)#>Regex = new Regex(@"<#= GetRegexElement(edmProperty).Value #>");

 

    1.     <#=Accessibility.ForProperty(edmProperty)#> <#=code.Escape(edmProperty.TypeUsage)#> <#=code.Escape(edmProperty)#>

 

    1.     {

 

    1.          <#=code.SpaceAfter(Accessibility.ForGetter(edmProperty))#>get { return _<#=code.Escape(edmProperty)#>; }

 

    1.          <#=code.SpaceAfter(Accessibility.ForSetter(edmProperty))#>set

 

    1.          {

 

    1.             if (value != null && !_<#=code.Escape(edmProperty)#>Regex.IsMatch(value))

 

    1.                throw new Exception("<#=GetRegexErrorMessage(edmProperty)#>");

 

    1.             _<#=code.Escape(edmProperty)#> = value;

 

    1.          }

 

    1.     }

 

    1. <# } else { #>

 

    1.     <#=Accessibility.ForProperty(edmProperty)#> <#=code.Escape(edmProperty.TypeUsage)#> <#=code.Escape(edmProperty)#> {

 

    1.         <#=code.SpaceAfter(Accessibility.ForGetter(edmProperty))#>get; <#=code.SpaceAfter(Accessibility.ForSetter(edmProperty))#>set;

 

    1.     }

 

    1. <# }

 

    1.     }

 

    1.     region.End();

 

    1. #>
    1. Some utility functions (used above), that use the Entity Framework Metadata APIs to access the Regular Expression:

MetadataProperty GetRegexProperty(EdmProperty property)

{

return property.MetadataProperties.FirstOrDefault(mp => mp.Name == "https://Regex:Expression");

}

bool HasRegex(EdmProperty property)

{

return GetRegexProperty(property) != null;

}

System.Xml.Linq.XElement GetRegexElement(EdmProperty property)

{

var prop = GetRegexProperty(property);

if (prop == null) throw new Exception("No Regex found");

var node = prop.Value as System.Xml.Linq.XElement;

if (node == null) throw new Exception("No Regex found");

return node;

}

string GetRegexErrorMessage(EdmProperty property)

{

var node = GetRegexElement(property);

var messageAttr = node.Attribute("ErrorMessage");

if (messageAttr == null)

return "Regular Expression check for " + property.Name + " failed.";

else

return messageAttr.Value;

}

    1. And something to make the generated classes use System.Text.RegularExpressions:

<#

void WriteHeader(string namespaceName, CodeGenerationTools code, params string[] extraUsings)

{

CodeRegion region = new CodeRegion(this);

#>

//------------------------------------------------------------------------------

// <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.Collections.Generic;

using System.Text.RegularExpressions;

Now if you replace your POCO Types T4 template with the template attached below, the generated classes will enforce any regular expressions you add to your model. To find the changes in the T4 template just find ‘Regex’ and ‘RegularExpression’.

This is just scratching the surface though, you could really run with this idea, perhaps producing DataAnnotations directly from the EDM so that they can be used by frameworks like Dynamic Data?

Your options are almost unlimited.

Have fun!
Alex James
Entity Framework Team

Blogging.Types.tt