2018年9月27日 星期四

[Entity Framework 6] DB First - Create Metadata class and services with T4



 C#   Entity Framework   Code-first


Introduction


This articles was inspired by WASICHRIS’ article:


Since I have a UOW (Unit of work) pattern on my Entity Framework 6 library, every POCO entity should inherits the base entity class and has its own CRUD service class as following,


PS. We need to create a Partial class which inherits UowEntity in DB-first.




However the time for creating the Partial classes and Service classes for a whole new DB-first data model (ADO.NET Entity Data Model) is expensive, so that we will use T4 to generate them automatically.




Related articles



Environment


Visual Studio 2017 community
Entity Framework 6.1.3


Implement


T4 for Models

OK, the code is complex and I won’t give too much explanation on it.
The flow is,

1.  Load .edmx and get all entity types (EntityType) from it
2.  For every EntityType, generate a .cs file with same class name (if not exists)
3.  Write namespaces and partial class structure into the .cs file


<#@ template language="C#" debug="true" hostspecific="true"#>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Diagnostics" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Text.RegularExpressions" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ include file="EF.Utility.CS.ttinclude"#>
<#@ output extension=".cs" #>
<#
// Formatting helper for code
CodeGenerationTools code = new CodeGenerationTools(this);
// object for creating entity information
MetadataLoader loader = new MetadataLoader(this);
// TODO: NEED TO PROVIDE EDMX FILE LOCATION
string inputFile = string.Empty;
var currentPath = this.Host.ResolvePath("");
string[] files = System.IO.Directory.GetFiles(currentPath, "*.edmx");
if(files==null || files.Length <=0)
return string.Empty;
else
inputFile = files[0];
// File generation suffix
string suffix = "Metadata";
// Meta data information for the conceptual model
EdmItemCollection ItemCollection = loader.CreateEdmItemCollection(inputFile);
// Suggested namespace
string namespaceName = code.VsNamespaceSuggestion();// + suffix;
// File generator according to different section
EntityFrameworkTemplateFileManager fileManager =
EntityFrameworkTemplateFileManager.Create(this);
// Loop through each entity type
foreach (EntityType entity in
ItemCollection.GetItems<EntityType>().OrderBy(e => e.Name))
{
// File name for data annotation file
string fileName = entity.Name + suffix + ".cs";
// Check for file existence, If it does not
// exist create new file for data annotation
if (!DoesFileExist(fileName))
{
// Header for file
writeHeader(fileManager);
// Create new file
fileManager.StartNewFile(fileName);
// Add namespaces into file
BeginNamespace(namespaceName, code);
#>
/// <summary>
/// <#=code.Escape(entity)#> class
/// </summary>
[Description("<#=GetFriendlyName(code.Escape(entity))#>")]
[MetadataType(typeof(<#=code.Escape(entity) + suffix#>))]
<#= Accessibility.ForType(entity)#> <#=code.SpaceAfter(code.AbstractOption(entity))#> partial class <#=code.Escape(entity)#> : UowEntity
{
}
/// <summary>
/// <#=code.Escape(entity)#> Metadata class
/// </summary>
internal <#=code.SpaceAfter(code.AbstractOption(entity))#> class <#=code.Escape(entity) + suffix#>
{
<#
// Loop through each primitive property of entity
foreach (EdmProperty edmProperty in entity.Properties.Where(p =>
p.TypeUsage.EdmType is PrimitiveType && p.DeclaringType == entity))
{
#>
<#= CodeRegion.GetIndent(1) #>
/// <summary>
/// <#=GetFriendlyName(code.Escape(edmProperty))#>
/// </summary>
<#
// Write display name data annotation
writeDisplayName(edmProperty);
// Write description data annotation
writeDescriptionAttribute(edmProperty);
// Write required field data annotation
writeRequiredAttribute(edmProperty);
// Write string length annotation
writeStringLengthAttribute(edmProperty);
#>
<#=Accessibility.ForProperty(edmProperty)#> <#=code.Escape(edmProperty.TypeUsage)#> <#=code.Escape(edmProperty)#> { <#=Accessibility.ForGetter(edmProperty)#>get; <#=Accessibility.ForSetter(edmProperty)#>set; }
<#
}
#>
<#= CodeRegion.GetIndent(1) #>
}
<#
// End namespace
EndNamespace(namespaceName);
}
else
{
// Write with original file
fileManager.StartNewFile(fileName);
this.Write(OutputFile(fileName));
}
}
fileManager.Process();
#>
<#+
// Write display name data annotation
void writeDisplayName(EdmProperty edmProperty) {
string displayName = edmProperty.Name;
// Check for property name
if (!string.IsNullOrEmpty(displayName))
{
// Generate user friendly name
displayName = GetFriendlyName(edmProperty.Name);
// Populate actual string to be written
WriteLine("{0}[DisplayName(\"{1}\")]", CodeRegion.GetIndent(1), displayName);
}
}
//Write description attribute
void writeDescriptionAttribute(EdmProperty edmProperty) {
string displayName = edmProperty.Name; //Use Name as description
// Check for property name
if (!string.IsNullOrEmpty(displayName))
{
// Generate user friendly name
displayName = GetFriendlyName(edmProperty.Name);
// Populate actual string to be written
WriteLine("{0}[Description(\"{1}\")]", CodeRegion.GetIndent(1), displayName);
}
}
//Write required attribute
void writeRequiredAttribute(EdmProperty edmProperty) {
// Check for required property
if (!edmProperty.Nullable)
{
WriteLine("{0}[Required(ErrorMessage = \"{1} is required\")]",
CodeRegion.GetIndent(2),GetFriendlyName(edmProperty.Name));
}
}
//Write max string length
void writeStringLengthAttribute(EdmProperty edmProperty) {
// Object for retrieving additional information from property
Facet maxLengthfacet;
// Try to get max length from property
if (edmProperty.TypeUsage.Facets.TryGetValue("MaxLength", true, out maxLengthfacet))
{
// Max length for property
double lengthAttribute;
// Try to parse max length value
if (double.TryParse(maxLengthfacet.Value.ToString(), out lengthAttribute))
{
// Generate actual string for attribute
WriteLine("{0}[MaxLength({1}, ErrorMessage = \"{2} cannot " +
"be longer than {1} characters\")]",
CodeRegion.GetIndent(2),lengthAttribute,GetFriendlyName(edmProperty.Name));
}
}
}
// Initialize header
void writeHeader(EntityFrameworkTemplateFileManager fileManager, params string[] extraUsings)
{
fileManager.StartHeader();
#>
using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using JB.Infra.Util.EF.Entity;
<#=String.Join(String.Empty, extraUsings.Select(u => "using " + u + ";" + Environment.NewLine).ToArray())#>
<#+
fileManager.EndBlock();
}
// Add namespace
void BeginNamespace(string namespaceName, CodeGenerationTools code)
{
// Generate region
CodeRegion region = new CodeRegion(this);
// Check for namespace value
if (!String.IsNullOrEmpty(namespaceName))
{
#>
namespace <#=code.EscapeNamespace(namespaceName)#>
{
<#+
// Add indent
PushIndent(CodeRegion.GetIndent(1));
}
}
// End namespace
void EndNamespace(string namespaceName)
{
if (!String.IsNullOrEmpty(namespaceName))
{
PopIndent();
#>
}
<#+
}
}
#>
<#+
// Check for file existence
bool DoesFileExist(string filename)
{
return File.Exists(Path.Combine(GetCurrentDirectory(),filename));
}
// Get current folder directory
string GetCurrentDirectory()
{
return System.IO.Path.GetDirectoryName(this.Host.TemplateFile);
}
// Get content of file name
string OutputFile(string filename)
{
using(StreamReader sr =
new StreamReader(Path.Combine(GetCurrentDirectory(),filename)))
{
return sr.ReadToEnd();
}
}
// Get friendly name for property names
string GetFriendlyName(string value)
{
return Regex.Replace(value,
"([A-Z]+)", " $1",
RegexOptions.Compiled).Trim();
}
#>





Gist for EdmxModel-simple.tt

<#@ template language="C#" debug="true" hostspecific="true"#>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Diagnostics" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Text.RegularExpressions" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ include file="EF.Utility.CS.ttinclude"#>
<#@ output extension=".cs" #>
<#
// Formatting helper for code
CodeGenerationTools code = new CodeGenerationTools(this);
// object for creating entity information
MetadataLoader loader = new MetadataLoader(this);
// TODO: NEED TO PROVIDE EDMX FILE LOCATION
string inputFile = string.Empty;
var currentPath = this.Host.ResolvePath("");
string[] files = System.IO.Directory.GetFiles(currentPath, "*.edmx");
if(files==null || files.Length <=0)
return string.Empty;
else
inputFile = files[0];
// File generation suffix
string suffix = "Metadata";
// Meta data information for the conceptual model
EdmItemCollection ItemCollection = loader.CreateEdmItemCollection(inputFile);
// Suggested namespace
string namespaceName = code.VsNamespaceSuggestion();// + suffix;
// File generator according to different section
EntityFrameworkTemplateFileManager fileManager =
EntityFrameworkTemplateFileManager.Create(this);
// Loop through each entity type
foreach (EntityType entity in
ItemCollection.GetItems<EntityType>().OrderBy(e => e.Name))
{
// File name for data annotation file
string fileName = entity.Name + suffix + ".cs";
// Check for file existence, If it does not
// exist create new file for data annotation
if (!DoesFileExist(fileName))
{
// Header for file
writeHeader(fileManager);
// Create new file
fileManager.StartNewFile(fileName);
// Add namespaces into file
BeginNamespace(namespaceName, code);
#>
/// <summary>
/// <#=code.Escape(entity)#> class
/// </summary>
[Description("<#=GetFriendlyName(code.Escape(entity))#>")]
<#= Accessibility.ForType(entity)#> <#=code.SpaceAfter(code.AbstractOption(entity))#> partial class <#=code.Escape(entity)#> : UowEntity
{
}
<#
// End namespace
EndNamespace(namespaceName);
}
else
{
// Write with original file
fileManager.StartNewFile(fileName);
this.Write(OutputFile(fileName));
}
}
fileManager.Process();
#>
<#+
// Write display name data annotation
void writeDisplayName(EdmProperty edmProperty) {
string displayName = edmProperty.Name;
// Check for property name
if (!string.IsNullOrEmpty(displayName))
{
// Generate user friendly name
displayName = GetFriendlyName(edmProperty.Name);
// Populate actual string to be written
WriteLine("{0}[DisplayName(\"{1}\")]", CodeRegion.GetIndent(1), displayName);
}
}
//Write description attribute
void writeDescriptionAttribute(EdmProperty edmProperty) {
string displayName = edmProperty.Name; //Use Name as description
// Check for property name
if (!string.IsNullOrEmpty(displayName))
{
// Generate user friendly name
displayName = GetFriendlyName(edmProperty.Name);
// Populate actual string to be written
WriteLine("{0}[Description(\"{1}\")]", CodeRegion.GetIndent(1), displayName);
}
}
//Write required attribute
void writeRequiredAttribute(EdmProperty edmProperty) {
// Check for required property
if (!edmProperty.Nullable)
{
WriteLine("{0}[Required(ErrorMessage = \"{1} is required\")]",
CodeRegion.GetIndent(2),GetFriendlyName(edmProperty.Name));
}
}
//Write max string length
void writeStringLengthAttribute(EdmProperty edmProperty) {
// Object for retrieving additional information from property
Facet maxLengthfacet;
// Try to get max length from property
if (edmProperty.TypeUsage.Facets.TryGetValue("MaxLength", true, out maxLengthfacet))
{
// Max length for property
double lengthAttribute;
// Try to parse max length value
if (double.TryParse(maxLengthfacet.Value.ToString(), out lengthAttribute))
{
// Generate actual string for attribute
WriteLine("{0}[MaxLength({1}, ErrorMessage = \"{2} cannot " +
"be longer than {1} characters\")]",
CodeRegion.GetIndent(2),lengthAttribute,GetFriendlyName(edmProperty.Name));
}
}
}
// Initialize header
void writeHeader(EntityFrameworkTemplateFileManager fileManager, params string[] extraUsings)
{
fileManager.StartHeader();
#>
using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using JB.Infra.Util.EF.Entity;
<#=String.Join(String.Empty, extraUsings.Select(u => "using " + u + ";" + Environment.NewLine).ToArray())#>
<#+
fileManager.EndBlock();
}
// Add namespace
void BeginNamespace(string namespaceName, CodeGenerationTools code)
{
// Generate region
CodeRegion region = new CodeRegion(this);
// Check for namespace value
if (!String.IsNullOrEmpty(namespaceName))
{
#>
namespace <#=code.EscapeNamespace(namespaceName)#>
{
<#+
// Add indent
PushIndent(CodeRegion.GetIndent(1));
}
}
// End namespace
void EndNamespace(string namespaceName)
{
if (!String.IsNullOrEmpty(namespaceName))
{
PopIndent();
#>
}
<#+
}
}
#>
<#+
// Check for file existence
bool DoesFileExist(string filename)
{
return File.Exists(Path.Combine(GetCurrentDirectory(),filename));
}
// Get current folder directory
string GetCurrentDirectory()
{
return System.IO.Path.GetDirectoryName(this.Host.TemplateFile);
}
// Get content of file name
string OutputFile(string filename)
{
using(StreamReader sr =
new StreamReader(Path.Combine(GetCurrentDirectory(),filename)))
{
return sr.ReadToEnd();
}
}
// Get friendly name for property names
string GetFriendlyName(string value)
{
return Regex.Replace(value,
"([A-Z]+)", " $1",
RegexOptions.Compiled).Trim();
}
#>




The difference between EdmxModel-metadata.tt and EdmxModel-simple.tt:

1.   The partial class created from EdmxModel-metadata.tt inherits UowEntity and defines the MetadataType
2.   The partial class created from EdmxModel-simple.tt only inherits UowEntity

Assume that there is a POCO: MyModel in EDMX, the above T4 will generate:
1.  MyModels.cs (by EdmxModel-simple.tt)
2.  MyModelsMetadata.cs (by EdmxModel-metadata.tt)



And their contents:

MyModels.cs
namespace JB.Infra.Util.EF.Models
{
    using System;
    using System.Collections.Generic;
   
    public partial class MyModels
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Department { get; set; }
    }
}

MyModelsMetadata.cs
using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using JB.Infra.Util.EF.Entity;

namespace JB.Infra.Utility.EF.DbFirst.UnitTest.Models
{

    /// <summary>
    /// MyModels class
    /// </summary>
    [Description("My Models")]
    [MetadataType(typeof(MyModelsMetadata))]
    public partial class MyModels : UowEntity
    {

    }

    /// <summary>
    /// MyModels Metadata class
    /// </summary>
    internal class MyModelsMetadata
    {

        /// <summary>
        /// Id
        /// </summary>       
        [DisplayName("Id")]
        [Description("Id")]
        [Required(ErrorMessage = "Id is required")]
        public int Id { get; set; }

       
/// skip ...


T4 for Services

Same process on generating CRUD Service class.
Notice the relative path of .edmx is updated to “../Models”


<#@ template language="C#" debug="true" hostspecific="true"#>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Diagnostics" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Text.RegularExpressions" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ include file="EF.Utility.CS.ttinclude"#>
<#@ output extension=".cs" #>
<#
// Formatting helper for code
CodeGenerationTools code = new CodeGenerationTools(this);
// object for creating entity information
MetadataLoader loader = new MetadataLoader(this);
// TODO: NEED TO PROVIDE EDMX FILE LOCATION
string inputFile = string.Empty;
var currentPath = this.Host.ResolvePath("../Models");
string[] files = System.IO.Directory.GetFiles(currentPath, "*.edmx");
if(files==null || files.Length <=0)
return string.Empty;
else
inputFile = files[0];
// File generation suffix
string suffix = "Service";
// Meta data information for the conceptual model
EdmItemCollection ItemCollection = loader.CreateEdmItemCollection(inputFile);
// Suggested namespace
string namespaceName = code.VsNamespaceSuggestion();// + suffix;
// File generator according to different section
EntityFrameworkTemplateFileManager fileManager =
EntityFrameworkTemplateFileManager.Create(this);
// Loop through each entity type
foreach (EntityType entity in
ItemCollection.GetItems<EntityType>().OrderBy(e => e.Name))
{
// File name for data annotation file
string fileName = entity.Name + suffix + ".cs";
// Check for file existence, If it does not
// exist create new file for data annotation
if (!DoesFileExist(fileName))
{
// Header for file
string modelNamespace= string.Concat(code.EscapeNamespace(namespaceName).ToString().Replace(".Service",".Models"));
writeHeader(fileManager, modelNamespace);
// Create new file
fileManager.StartNewFile(fileName);
// Add namespaces into file
BeginNamespace(namespaceName, code);
#>
/// <summary>
/// <#=code.Escape(entity)#> Service
/// </summary>
<#=Accessibility.ForType(entity)#> class <#=code.Escape(entity)#>Service<T> : JB.Infra.Util.EF.Service.BaseDalService<T> where T : <#=code.Escape(entity)#>
{
public <#=code.Escape(entity)#>Service(DbContext dbContext):base(dbContext)
{
}
}
<#
// End namespace
EndNamespace(namespaceName);
}
else
{
// Write with original file
fileManager.StartNewFile(fileName);
this.Write(OutputFile(fileName));
}
}
fileManager.Process();
#>
<#+
// Initialize header
void writeHeader(EntityFrameworkTemplateFileManager fileManager, params string[] extraUsings)
{
fileManager.StartHeader();
#>
using System.Data.Entity;
<#=String.Join(String.Empty, extraUsings.Select(u => "using " + u + ";" + Environment.NewLine).ToArray())#>
<#+
fileManager.EndBlock();
}
// Add namespace
void BeginNamespace(string namespaceName, CodeGenerationTools code)
{
// Generate region
CodeRegion region = new CodeRegion(this);
// Check for namespace value
if (!String.IsNullOrEmpty(namespaceName))
{
#>
namespace <#=code.EscapeNamespace(namespaceName)#>
{
<#+
// Add indent
PushIndent(CodeRegion.GetIndent(1));
}
}
// End namespace
void EndNamespace(string namespaceName)
{
if (!String.IsNullOrEmpty(namespaceName))
{
PopIndent();
#>
}
<#+
}
}
#>
<#+
// Check for file existence
bool DoesFileExist(string filename)
{
return File.Exists(Path.Combine(GetCurrentDirectory(),filename));
}
// Get current folder directory
string GetCurrentDirectory()
{
return System.IO.Path.GetDirectoryName(this.Host.TemplateFile);
}
// Get content of file name
string OutputFile(string filename)
{
using(StreamReader sr =
new StreamReader(Path.Combine(GetCurrentDirectory(),filename)))
{
return sr.ReadToEnd();
}
}
// Get friendly name for property names
string GetFriendlyName(string value)
{
return Regex.Replace(value,
"([A-Z]+)", " $1",
RegexOptions.Compiled).Trim();
}
#>

Result:





Reference




沒有留言:

張貼留言