Compartilhar via


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

Comments

  • Anonymous
    July 22, 2009
    Can you guys put your EF 4.0 T4 templates on CodePlex? That way new features like this can be added to them, not only by you guys, but by other community members? That would be fantastic! Thanks a bunch! -Robert

  • Anonymous
    July 23, 2009
    You know what I'd like to see more than anything? I'd like to see the generated code have the [GeneratedCode] attribute, so that FxCop doesn't barf on it. Sure, I could do this by customizing the T4 but I'd be happy if it shipped that way.

  • Anonymous
    July 24, 2009
    Oh, and that's especially true for view generation, which we can't customize.

  • Anonymous
    July 24, 2009
    @Craig I'll take the [GeneratedCode] idea back to the team. Seems like a good idea. Alex

  • Anonymous
    July 24, 2009
    Thank you! I have a huge and difficult-to-maintain file of FxCop exceptions I could delete if this were implemented.

  • Anonymous
    July 25, 2009
    @Robert, The tt file is now attached (it was meant to be all along) so feel free to put it or some derivative of it up on CodePlex. Alex

  • Anonymous
    December 10, 2009
    Hi! I'm trying to accomplich DataAnnotations as suggested in the article but I'm afraid I can't figure out how to get to the MaxLength attribute of a Property. I would like to be able to automatically limit the number of chars in a text field. Any suggestions? Thanks Fredrik

  • Anonymous
    April 30, 2010
    The comment has been removed

  • Anonymous
    January 13, 2011
    The T4 templates are sitting on your machine so you can modify them.  You can open them up and add your attributes easily.