Introduction
In Code First approach, all POCO classes are written manually with required data annotation but with database first approach, classes are generated based on EDMX file which already has a certain amount of information to populate data annotation attributes.
In this tutorial, we are going to look into how we can generate metadata classes based on EDMX file and associating generated classes with actual models. These could be used directly as view model or having validation on any layers.
T4 Template for Metadata Generation
Let's start with creating new T4 template file as DbModelMetadata.tt. I am putting up a lot of comments in code itself to make it self explanatory.
<#@ template language="C#" debug="true" hostspecific="true"#>
<#@ import namespace="System.Text.RegularExpressions" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Diagnostics" #>
<#@ include file="EF.Utility.CS.ttinclude"#><#@
output extension=".cs"#><#
CodeGenerationTools code = new CodeGenerationTools(this);
MetadataLoader loader = new MetadataLoader(this);
string inputFile = @"EDMX file location";
string suffix = "Metadata";
EdmItemCollection ItemCollection = loader.CreateEdmItemCollection(inputFile);
string namespaceName = code.VsNamespaceSuggestion();
EntityFrameworkTemplateFileManager fileManager =
EntityFrameworkTemplateFileManager.Create(this);
foreach (EntityType entity in
ItemCollection.GetItems<EntityType>().OrderBy(e => e.Name))
{
string fileName = entity.Name + suffix + ".cs";
if (!DoesFileExist(fileName))
{
WriteHeader(fileManager);
fileManager.StartNewFile(fileName);
BeginNamespace(namespaceName, code);
#>
<#= Accessibility.ForType(entity)#>
<#=code.SpaceAfter(code.AbstractOption(entity))#>partial class <#=code.Escape(entity) + suffix#>
{
<#
foreach (EdmProperty edmProperty in entity.Properties.Where(p =>
p.TypeUsage.EdmType is PrimitiveType && p.DeclaringType == entity))
{
#>
<#
WriteDisplayName(edmProperty);
WriteRequiredAttribute(edmProperty);
WriteStringLengthAttribute(edmProperty);
#>
<#=Accessibility.ForProperty(edmProperty)#> <#=code.Escape(edmProperty.TypeUsage)#>
<#=code.Escape(edmProperty)#> { <#=Accessibility.ForGetter(edmProperty)#>get;
<#=Accessibility.ForSetter(edmProperty)#>set; }
<#
}
#>
}
<#
EndNamespace(namespaceName);
}
else
{
fileManager.StartNewFile(fileName);
this.Write(OutputFile(fileName));
}
}
fileManager.Process();
#>
<#+
void WriteDisplayName(EdmProperty edmProperty) {
string displayName = edmProperty.Name;
if (!string.IsNullOrEmpty(displayName))
{
displayName = GetFriendlyName(edmProperty.Name);
string displayAttribute =
string.Format("[DisplayName(\"{0}\")]", displayName);
#>
<#=displayAttribute#>
<#+
}
}
void WriteRequiredAttribute(EdmProperty edmProperty) {
if (!edmProperty.Nullable)
{
WriteLine("{0}[Required(ErrorMessage = \"{1} is required\")]",
CodeRegion.GetIndent(1),GetFriendlyName(edmProperty.Name));
}
}
void WriteStringLengthAttribute(EdmProperty edmProperty) {
Facet maxLengthfacet;
if (edmProperty.TypeUsage.Facets.TryGetValue("MaxLength", true, out maxLengthfacet))
{
double lengthAttribute;
if (double.TryParse(maxLengthfacet.Value.ToString(), out lengthAttribute))
{
WriteLine("{0}[MaxLength({1}, ErrorMessage = \"{2} cannot " +
"be longer than {1} characters\")]",
CodeRegion.GetIndent(1),lengthAttribute,GetFriendlyName(edmProperty.Name));
}
}
}
void WriteHeader(EntityFrameworkTemplateFileManager fileManager, params string[] extraUsings)
{
fileManager.StartHeader();
#>
using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
<#=String.Join(String.Empty, extraUsings.Select(u => "using " + u +
";" + Environment.NewLine).ToArray())#>
<#+
fileManager.EndBlock();
}
void BeginNamespace(string namespaceName, CodeGenerationTools code)
{
CodeRegion region = new CodeRegion(this);
if (!String.IsNullOrEmpty(namespaceName))
{
#>
namespace <#=code.EscapeNamespace(namespaceName)#>
{
<#+
PushIndent(CodeRegion.GetIndent(1));
}
}
void EndNamespace(string namespaceName)
{
if (!String.IsNullOrEmpty(namespaceName))
{
PopIndent();
#>
}
<#+
}
}
#>
<#+
bool DoesFileExist(string filename)
{
return File.Exists(Path.Combine(GetCurrentDirectory(),filename));
}
string GetCurrentDirectory()
{
return System.IO.Path.GetDirectoryName(Host.TemplateFile);
}
string OutputFile(string filename)
{
using(StreamReader sr =
new StreamReader(Path.Combine(GetCurrentDirectory(),filename)))
{
return sr.ReadToEnd();
}
}
string GetFriendlyName(string value)
{
return Regex.Replace(value,
"([A-Z]+)", " $1",
RegexOptions.Compiled).Trim();
}
#>
This is going to generate files based upon number of models associated with EDMX. All those class members will be having data annotation like DisplayName
, Required
, MaxLength
according to model. After first time generation, those files will not be overwritten. So, we can happily modify data annotation according to our need without worrying about loosing any newly added code under generated classes from TT file.
Associating Generated Metadata Classes to Actual Models
Now we need to associate metadata classes to actual classes. These could be done by setting MetaDataType
attribute to our models. Example:
[MetadataType(typeof(EmployeeTypeMetadata))]
EmployeeTypeMetadata
is the name of generated metadata class from the above TT file.
To set these attributes, we need to modify Model TT (Ex: EmployeeModel.tt) file found under EDMX file. This will apply metadata attribute to all models present under EDMX.
Just above:
<#=codeStringGenerator.EntityClassOpening(entity)#>
put:
[MetadataType(typeof(<#=code.Escape(entity)#>Metadata))]
The above code will set metadata classes to actual models from EDMX.
You might be thinking why we need to separate up metadata classes with actual model classes. We could have directly modified Model TT (Ex: EmployeeModel.tt) file to generate data annotation for all classes. But with separation, we will be having control on setting attributes manually whereas whenever EDMX gets updated, all changes to classes get lost.
Note: I have not tried to test it with complex types.