Duck typing Entity Framework classes using T4 Templates

Duck typing is an interesting concept, and alien to C# generally. But using the techniques of my previous post about T4 and Entity Framework, it is possible to have your entities implement interfaces if they have the required properties, resulting in behaviour similar to duck typing. Please read the previous blog post before reading this one.

The previous blog post gives us code to implement interfaces for each entity in an object model. In order to provide “duck typing”, we will extend this to only implement the interface for an entity if that entity has the properties of the interface.

Fortunately System.Data.Metadata.Edm.EntityType gives us the ability to inspect the properties of an entity. For my purposes, I only check for properties by name, as I control my database and would never have the same column name with two different data types. Extension of this code to check property types as well as names is left as an exercise for the reader.

IEnumerable<EntityType> GetEntitiesWithPropertyOrRelationship(
    EdmItemCollection itemCollection, params string[] requiredProperties)
{
    return itemCollection.GetItems<EntityType>().
        Where(entity => EntityHasPropertyOrRelationship(entity, requiredProperties));
}

bool EntityHasPropertyOrRelationship(
    EntityType entity, params string[] requiredProperties)
{
    return requiredProperties.All(
        requiredProperty => entity.Properties.Any(property => property.Name == requiredProperty)
        || entity.NavigationProperties.Any(property => property.Name == requiredProperty));
}

Pretty simple stuff. EntityHasPropertyOrRelationship checks both the Properties (properties relating to simply database columns), and NavigationProperties (properties relating to foreign key relationships) for properties with the required names. If our entity has all the required properties, it’s a match.

GetEntitiesWithPropertyOrRelationship uses EntityHasPropertyOrRelationship to retrieve all the entities that have the required properies from our itemCollection.

I’ve blogged about further extending the template to handle multiple interfaces, with one file per interface.

Here’s the full code of the example from the last blog post, updated so entities only implement IUserNameStamped if they actually have a column called UserName.

<#@ template language="C#" debug="false" hostspecific="true"#>
<#@ include file="EF.Utility.CS.ttinclude"#>
<#@ output extension=".cs"#>
<#
string inputFile = @"Entities.edmx";
EdmItemCollection itemCollection = new MetadataLoader(this).
    CreateEdmItemCollection(inputFile);

CodeGenerationTools code = new CodeGenerationTools(this);
string namespaceName = code.VsNamespaceSuggestion();

WriteHeader();
BeginNamespace(namespaceName, code);
WriteIUserNameStamped();
WriteEntitiesWithInterface(itemCollection);
EndNamespace(namespaceName);
#>
<#+
void WriteHeader(params string[] extraUsings)
{
#>
//------------------------------------------------------------------------------
// <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>
//------------------------------------------------------------------------------

<#=String.Join(String.Empty, extraUsings.
    Select(u => "using " + u + ";" + Environment.NewLine).ToArray())#>
<#+
}

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();
#>
}
<#+
    }
}

void WriteIUserNameStamped()
{
#>
/// <summary>
/// An entity that is stamped with the Username that created it
/// </summary>
/// <remarks>
/// All OTICRS entities should have a username. If any entity fails to implement
/// this interface, it means the table needs the UserName column added to it.
/// </remarks>
public interface IUserNameStamped
{
    string UserName { get; set; }
}

<#+
}

void WriteEntitiesWithInterface(
    EdmItemCollection itemCollection)
{
    foreach (EntityType entity in 
        GetEntitiesWithPropertyOrRelationship(itemCollection, "UserName").
        OrderBy(e => e.Name))
    {
        WriteEntityWithInterface(entity.Name);
    }
}

IEnumerable<EntityType> GetEntitiesWithPropertyOrRelationship(
    EdmItemCollection itemCollection, params string[] requiredProperties)
{
    return itemCollection.GetItems<EntityType>().Where(
        entity => EntityHasPropertyOrRelationship(entity, requiredProperties));
}

bool EntityHasPropertyOrRelationship(
    EntityType entity, params string[] requiredProperties)
{
    return requiredProperties.All(
        requiredProperty => entity.Properties.Any(property => property.Name == requiredProperty)
        || entity.NavigationProperties.All(property => property.Name == requiredProperty));
}

void WriteEntityWithInterface(string entityName)
{
#>
public partial class <#=entityName#> : IUserNameStamped
{
}

<#+
}

#>