Entity Framework, Template T4 and data annotations
[French version of this post can be found here]
For the new version of my personal website using ASP.Net MVC, I decided to implement the repository pattern with Entity Framework 4.0, helped by the use of T4 templates. Let’s skip the explanation about how to declare the repository interfaces and services to use this pattern. What we really want to do now, is to be able to use data annotations, these data contracts which allow to define our validation rules within our model. But how to do that the “easyiest” way?
The main interest of using T4 templates in my case, is to automatically generate data annotations (= attributes) in order to take benefit of the ASP.Net MVC validation mechanism. This way, the validation is centralized and every view using my objects will be able to use the same rules of validation.
The task is a little more complicated that it seems. Indeed, you have :
- to analyse SSDL (Storage Schema Definition Langage) to read metadata and fields from each entity
- to dynamically generate attributes
- to be able to update your model layer without loosing manual modifications (for instance, if your add a regex attribute to your class, the next generation will overwrite it)
The first difficulty is to create attributes automatically without any action from the developper. It gives limits to the feasibility and some attributes like Range or regular expressions will not be available (since they cannot be inferred from the SSDL).
First we have to download the ADO.NET C# POCO Entity Generator template to generate our POCO classes. This template will help us to regenerate our classes each time our CSDL (Conceptual Schema Definition Langage) changes. Sadly, the behavior of T4 template is to rewrite all files and our classes are detroyed and recreated, deleting all manual modifications.
It is then necessary to place these attributes in a different file from each class but then, how to link the attributes to the members of our classes? Partial classes? Impossible, since we would have to redefine the member and a compilation error would occur. The solution is to use metadata, a mechanism allowing to define a class, containing all metadata for another class.
Here is a simple example :
[MetadataType(typeof(ArticleMetaData))]
public partial class Article
{
string Author{ get; set; }
string Title { get; set; }
}
public class ArticleMetaData
{
[Required]
[StringLength(255)]
private string Author;
}
We have our POCO class on which we specify, with the MetaDataType attribute, the class which will contain the metadata. The second one contains only members on which we have to add annotations and for each member, we can add differents attributes.
For our issue of regeneration, it is necessary to generate these metadata classes in different files than our POCO classes and to be more precise, it will be necessary to have a specific T4 template to generate POCO classes and a second one to generate metadata classes. This way, we will be able to regenerate POCO classes while keeping our previously created metadata classes. Of course, one day, you will may have to regenerate metadata classes too but then, you will have to create the new classes manually or by “processing” the template. But it is the best way to give your project a quick start!
The first task consists of editing the default T4 template (the one which generates POCO Classes) and to add (line 42 :), just before the class declaration), the following line :
[MetadataType(typeof(<#=code.Escape(entity)#>MetaData))]
The second step is to create a secondary template, copy of the first one (without the line previously added) within you will have to remove all code to generate navigation properties and association fixups (you can use #region to detect what to remove and what to keep). In the WriteHeader method, add the next line :
using System.ComponentModel.DataAnnotations;
Then, line 52, just before the creation of properties, add the following code block which allows to add specific atttributes (Required and StringLength) when necessary :
<# // begin max length attribute
if (code.Escape(edmProperty.TypeUsage) == "string")
{
int maxLength = 0;
if (edmProperty.TypeUsage.Facets["MaxLength"].Value != null && Int32.TryParse(
edmProperty.TypeUsage.Facets["MaxLength"].Value.ToString(),
out maxLength))
{
#>
[StringLength(<#=code.CreateLiteral(maxLength)#>,
ErrorMessage="This field cannot be longer than
<#=code.CreateLiteral(maxLength)#> characters")]
<#
}
}
// begin required attribute
if (edmProperty.TypeUsage.Facets["Nullable"].Value.ToString() =="False")
{
#>
[Required]
<#
}
#>
and voila! For each entity, one metadata class will be created and each time that the property must be “not null” or that its length si defined in the database then the corresponding attribute is added. To finish, you just have to add complementary attributes (range, regex, etc.)
Comments
Anonymous
January 17, 2011
This is a great idea - well done!Anonymous
January 18, 2011
Un grand merci pour cet article, il a fait gagner au moins 1 semaine de travail pour une base d'une centaine de table....Anonymous
January 29, 2011
Nice! But,how can I refine the MetadataPoco to have the get and set like this: public string Title { get { return _title; } set { this._title=value.ToUpper(); } }Anonymous
January 29, 2011
@Bogdan, you just have to look in the T4 file (the POCO one) where the propertie is created. Then you can just add "ToUpper()" BUT:
- it will be present for EVERY property
- it will get you a NullReferenceException if you don't test for null values imho, it does not seem to be a good idea
Anonymous
February 10, 2011
Louis - Please help. I have an idea to try to make this more extensible. Would it be possible to extend the generated class via a partial class declaration? For example, suppose the metadata generator t4 will generate a partial class PersonEntityMetadata.cs and contain the attributes for standard, well-known, and common annotations-- such as Required, StringLength, Etc -- annotations that can and should be generated every time the t4 is run to update, for instance, a string-length change on a column in the database, etc. Then, by hand, create another partial class that PersonEntityMetadata.cs, in another folder of course, and in THAT file add the stuff that might change based on business requirements such as displaying "PersonName" and "Name Of Person" and then, next week per a manager's request, changing it to "Person's Name" and then next week, per another manager's request, changing it to "Name" and etc, where we have a very custom annotation that we do not want overwritten when the t4 is run again and that we do want to manage by hand manually BUT we also want the luxury of being able to run the t4 again to the latest metadata from the actual database/model . Anyway, that is the general idea. Could it work? What do you think? Please advise. Thank you. - Mark KamoskiAnonymous
February 10, 2011
1- Louis-Guillaume! ;) in France, we love hyphenate firstnames :-) 2- your solution is possible and the default POCO generator creates partial classes ESPECIALLY to do what you want to do :) I use this "strategy" often not just for business "needs" but also just to extend my class with properties and/or helpers such as Person.FullName where FullName = String.Format("{0} {1} {2}", Title, FirstName, LastName). In fact, I see one problem. If you generate automatic dataannotations for some fields, you will not be able to add annotations for the same fields in another file since the property/member would be declared twice in the MyClassMetaData.cs file and a compilation error will occur.Anonymous
February 10, 2011
Louis - Please help. I am now facing a problem because the data-annotations are not being used by the DataForm in my project. I am using Silverlight 4, .NET Framework 4, Visual Web Developer 2010 Express. I have 1 WebApp project, which contains my DataService.svc and Model.edmx and my Self-Tracking-Entities generator Model.TT file, and the Default.aspx that hosts the Silverlight front-end. I have a custom Silverlight Class Library project that has a linked-file pointing to the Model.TT file to expose the classes in a Silverlight-consumable way. I have 1 Silverlight project that has a reference to my custom Silverlight Class Libary project (to be able to reference the entity types) and it has a reference to the DataService.svc and so on. The problem is, I think, is that the data-annotations are not being "carried over" to the Silverlight side-- but, I am not sure why. Must one tell the DataForm to use them in some way? What could be missing? Any help that you can provide is appreciated. Thank you. - Mark KamoskiAnonymous
January 06, 2012
Excelente! Muchas gracias por el aporte! Saludos de UruguayAnonymous
August 01, 2012
We have a database table that has a text field in the EF it is a string but the dataannotations want to use this [StringLength(Max)] which fails at compile time because Max is unknown (am I missing a reference or a conversion?). Any clue on how to resolve? I liked your instruction very easy to follow, thanksAnonymous
August 03, 2012
I'm not sure to understand. From where comes your "Max" string. Is it inferred by the t4 Template or did you specified it?