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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<#@ 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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<#@ 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 ...
/// skip ...
▋T4 for Services
Same process on generating CRUD Service class.
Notice the relative path of .edmx is updated to
“../Models”
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<#@ 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