In this post I’m going to talk about extending EF entities to encapsulate common functionality. For this particular case I will expand upon the concept in this blog post EF4 Part 2: EDM Inheritance Pains. In it Paul talks about putting common properties into a complex type, this post will focus on extending each entity to implement an interface that exposes these common properties.
In our model we have a situation like so:
Every entity in our model has ‘Audit’ information. The reason each piece of audit information is individually named is for inheritance reasons – Each level of inheritance in our model also has audit information and individual naming prevents the properties from clashing. The audit complex type in this instance looks like the following:
I know every entity has an Audit complex type in it but it is not neatly defined by an interface… or even by commonly defined properties. We can change the entity generators to do this for us however. The first step I took was to copy the default generator (In this case I used the POCO generator) and strip down all the actual code writing parts of it – This is what I’m left with:
<#
//*********************************************************
//
// Copyright (c) Microsoft. All rights reserved.
// This code is licensed under the Microsoft Public License.
// THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
// ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
// IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
// PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************
#>
<#@ template language="C#" debug="false" hostspecific="true"#>
<#@ include file="EF.Utility.CS.ttinclude"#><#@
output extension=".cs"#><#
CodeGenerationTools code = new CodeGenerationTools(this);
MetadataLoader loader = new MetadataLoader(this);
CodeRegion region = new CodeRegion(this, 1);
MetadataTools ef = new MetadataTools(this);
string inputFile = @"..\Planet.Sonia.SoniaModel\SoniaModel.edmx";
EdmItemCollection ItemCollection = loader.CreateEdmItemCollection(inputFile);
string namespaceName = code.VsNamespaceSuggestion();
EntityFrameworkTemplateFileManager fileManager = EntityFrameworkTemplateFileManager.Create(this);
// Write out support code to primary template output file
WriteHeader(fileManager);
// Emit Entity Types
foreach (EntityType entity in ItemCollection.GetItems<EntityType>().OrderBy(e => e.Name))
{
fileManager.StartNewFile(entity.Name + "Extension.cs");
BeginNamespace(namespaceName, code);
bool entityHasNullableFKs = entity.NavigationProperties.Any(np => np.GetDependentProperties().Any(p=>ef.IsNullable(p)));
#>
// Generated stuff goes here
<#
EndNamespace(namespaceName);
}
fileManager.Process();
#>
<#+
void WriteHeader(EntityFrameworkTemplateFileManager fileManager, params string[] extraUsings)
{
fileManager.StartHeader();
#>
//------------------------------------------------------------------------------
// <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;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
<#=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();
#>
}
<#+
}
}
#>
I should also note in this code there are two small changes made to the default generator. The first is the following line that tells the generator where the defining model is:
string inputFile = @"..\Planet.Sonia.SoniaModel\SoniaModel.edmx";
The next is to avoid file name clashes with my base generated entities. I added ‘Extension’ to the end of the generated file name:
fileManager.StartNewFile(entity.Name + "Extension.cs");
The generated classes look like the following:
//------------------------------------------------------------------------------
// <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;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
namespace Planet.Sonia.SoniaModel.POCOEntities
{
// Generated stuff goes here
}
The next step is to define an interface that all our entities will use, the idea is to expose the complex type properties using virtual so if we have inheritance we can call the same base properties to chain the audit information.
public interface IBusinessEntity
{
byte[] Concurrency { get; }
DateTime DateCreated { get; }
DateTime DateUpdated { get; set; }
string CreatedBy { get; }
string UpdatedBy { get; set; }
}
Because we have the framework in place it is easy enough to implement this interface for every entity. The first step is to define the entity class header that includes the interface. I made sure to use the same inbuilt functions that are used to create the base entities – for example code.Escape() creates a safe string from the entity.
<#=Accessibility.ForType(entity)#> <#=code.SpaceAfter(code.AbstractOption(entity))#>partial class <#=code.Escape(entity)#> : IBusinessEntity
{
}
Inside the brackets comes our implementation:
<#= entity.BaseType == null ? "virtual" : "override" #> public byte[] Concurrency
{
get { return <#= code.Escape(entity) #>Audit.Concurrency; }
}
<#= entity.BaseType == null ? "virtual" : "override" #> public DateTime DateCreated
{
get { return <#= code.Escape(entity) #>Audit.DateCreated; }
}
<#= entity.BaseType == null ? "virtual" : "override" #> public DateTime DateUpdated
{
get { return <#= code.Escape(entity) #>Audit.DateUpdated; }
set
{
<#= entity.BaseType == null ? string.Empty : "base.DateUpdated = value; "#>
<#= code.Escape(entity) #>Audit.DateUpdated = value;
if(Concurrency == null)
{
<#= code.Escape(entity) #>Audit.DateCreated = value;
}
}
}
<#= entity.BaseType == null ? "virtual" : "override" #> public string CreatedBy
{
get { return <#= code.Escape(entity) #>Audit.CreatedBy; }
}
<#= entity.BaseType == null ? "virtual" : "override" #> public string UpdatedBy
{
get { return <#= code.Escape(entity) #>Audit.UpdatedBy; }
set
{
<#= entity.BaseType == null ? string.Empty : "base.UpdatedBy = value; "#>
<#= code.Escape(entity) #>Audit.UpdatedBy = value;
if(Concurrency == null)
{
<#= code.Escape(entity) #>Audit.CreatedBy = value;
}
}
}
entity.BaseType lets us know whether the current entity is a derived type. From this we know whether we need to create virtual or overridden properties – and also whether we want to call the base implementation. The generated code for a base class will look like:
public partial class Address : IBusinessEntity
{
virtual public byte[] Concurrency
{
get { return AddressAudit.Concurrency; }
}
virtual public DateTime DateCreated
{
get { return AddressAudit.DateCreated; }
}
virtual public DateTime DateUpdated
{
get { return AddressAudit.DateUpdated; }
set
{
AddressAudit.DateUpdated = value;
if(Concurrency == null)
{
AddressAudit.DateCreated = value;
}
}
}
virtual public string CreatedBy
{
get { return AddressAudit.CreatedBy; }
}
virtual public string UpdatedBy
{
get { return AddressAudit.UpdatedBy; }
set
{
AddressAudit.UpdatedBy = value;
if(Concurrency == null)
{
AddressAudit.CreatedBy = value;
}
}
}
}
And for a derived class:
public partial class Address : IBusinessEntity
{
override public byte[] Concurrency
{
get { return AddressAudit.Concurrency; }
}
override public DateTime DateCreated
{
get { return AddressAudit.DateCreated; }
}
override public DateTime DateUpdated
{
get { return AddressAudit.DateUpdated; }
set
{
base.DateUpdated = value;
AddressAudit.DateUpdated = value;
if(Concurrency == null)
{
AddressAudit.DateCreated = value;
}
}
}
override public string CreatedBy
{
get { return AddressAudit.CreatedBy; }
}
override public string UpdatedBy
{
get { return AddressAudit.UpdatedBy; }
set
{
base.UpdatedBy = value;
AddressAudit.UpdatedBy = value;
if(Concurrency == null)
{
AddressAudit.CreatedBy = value;
}
}
}
}
I hope this post has illustrated the ways that T4 generation can really help when trying to encapsulate common features you may have in an Entity Framework model.
Print | posted on Sunday, 15 August 2010 4:16 PM