Entity Framework 4.1 - POCO Generator: Function Import with empty return value doesn't generate

I'm in the middle of building a security model for one of my clients. They've specified that they want everything done in the ADO.Net Entity Framework 4.1, so I've been working with it quite a lot recently. Though it hasn't made a great first impression, I'll reserve my judgement until a later blog post.

One of my main concerns with EF is separation of concerns / layer abstraction. It offers a fairly robust model (with its limits), but in an enterprise environment, you wouldn't want to expose that model to your presentation code. After a bit of digging (and being pointed in the right direction by a colleague), I found the ADO.Net POCO Entity Generator Visual Studio Extension, which isn't available by default... you have to download it via Extension Manager.

I managed to get it to generate my POCO (Plain Ole' CLR Objects) in a relatively short amount of time, but because I'm concerned with the dynamic SQL generated by EF, I've been writing stored procedures to do most of my SELECT statements. Stored procedures are created as "Function Imports" in EF, and tag themselves directly on to the ObjectContext (which in itself is another one of my EF concerns). They give you a fairly flexible way of generating data from your stored procedures by allowing you to choose to place the resulting information in an Entity, a custom / Complex Type or scalars, but for some reason stored procedures that do not return a result set (maybe insert or update procedures, or procedures that return scalar output parameters) have limited support. They do work, but you have to generate a collection of only one value, which obviously creates additional keystrokes / a slight additional overhead.

Specifically, the POCO T4 generation files for the context ignore Function Imports with no return value completely. This makes me sad, so I fixed the Microsoft created .tt file for the ObjectContext generation. Here's my code, hopefully it'll help others. Bear in mind that the syntax highlighting will be a bit dodgy due to it being a .tt file:

<#
//*********************************************************
//
//    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);
MetadataTools ef = new MetadataTools(this);
MetadataLoader loader = new MetadataLoader(this);
CodeRegion region = new CodeRegion(this);

string inputFile = @"../PSL.Online.Security.DataModel/PSLOnlinePOC.edmx";
EdmItemCollection ItemCollection = loader.CreateEdmItemCollection(inputFile);
string namespaceName = code.VsNamespaceSuggestion();

EntityContainer container = ItemCollection.GetItems<EntityContainer>().FirstOrDefault();
if (container == null)
{
    return "// No EntityContainer exists in the model, so no code was generated";
}
#>
//------------------------------------------------------------------------------
// <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.Data.Objects;
using System.Data.EntityClient;

<#
if (!String.IsNullOrEmpty(namespaceName))
{
#>
namespace <#=code.EscapeNamespace(namespaceName)#>
{
<#
    PushIndent(CodeRegion.GetIndent(1));
}
#>
<#=Accessibility.ForType(container)#> partial class <#=code.Escape(container)#> : ObjectContext
{
    public const string ConnectionString = "name=<#=container.Name#>";
    public const string ContainerName = "<#=container.Name#>";

    #region Constructors

    public <#=code.Escape(container)#>()
        : base(ConnectionString, ContainerName)
    {
<#
        WriteLazyLoadingEnabled(container);
#>
    }

    public <#=code.Escape(container)#>(string connectionString)
        : base(connectionString, ContainerName)
    {
<#
        WriteLazyLoadingEnabled(container);
#>
    }

    public <#=code.Escape(container)#>(EntityConnection connection)
        : base(connection, ContainerName)
    {
<#
        WriteLazyLoadingEnabled(container);
#>
    }

    #endregion

<#
        region.Begin("ObjectSet Properties", 2);

        foreach (EntitySet entitySet in container.BaseEntitySets.OfType<EntitySet>())
        {
#>

    <#=Accessibility.ForReadOnlyProperty(entitySet)#> ObjectSet<<#=code.Escape(entitySet.ElementType)#>> <#=code.Escape(entitySet)#>
    {
        get { return <#=code.FieldName(entitySet) #>  ?? (<#=code.FieldName(entitySet)#> = CreateObjectSet<<#=code.Escape(entitySet.ElementType)#>>("<#=entitySet.Name#>")); }
    }
    private ObjectSet<<#=code.Escape(entitySet.ElementType)#>> <#=code.FieldName(entitySet)#>;
<#
        }

        region.End();

        region.Begin("Function Imports");

        foreach (EdmFunction edmFunction in container.FunctionImports)
        {
            object returnParameter = edmFunction.ReturnParameter; // CS Added 6th May 2011
            
            var parameters = FunctionImportParameter.Create(edmFunction.Parameters, code, ef);
            string paramList = String.Join(", ", parameters.Select(p => p.FunctionParameterType + " " + p.FunctionParameterName).ToArray());
            // CS commented 6th May 2011
            //if (edmFunction.ReturnParameter == null)
            //{
             //   continue;
            //}
            // CS Modified 6th May 2011
            string returnTypeElement = returnParameter == null ? string.Empty : code.Escape(ef.GetElementType(edmFunction.ReturnParameter.TypeUsage));
            string returnTypeString;
            string returnString;
            
            if(!string.IsNullOrEmpty(returnTypeElement)) {
                returnTypeString = string.Concat("ObjectResult<", returnTypeElement, ">");
                returnString = string.Concat("base.ExecuteFunction<", returnTypeElement, @">(""", edmFunction.Name, @"""", code.StringBefore(", ", String.Join(", ", parameters.Select(p => p.ExecuteParameterName).ToArray())), ");");
            } else {
                returnTypeString = "int";
                returnString = string.Concat(@"base.ExecuteFunction(""", edmFunction.Name, @"""", code.StringBefore(", ", String.Join(", ", parameters.Select(p => p.ExecuteParameterName).ToArray())), ");");
            }

#>
    <#=Accessibility.ForMethod(edmFunction)#> <#=returnTypeString#> <#=code.Escape(edmFunction)#>(<#=paramList#>)
    {
<#
            foreach (var parameter in parameters)
            {
                if (!parameter.NeedsLocalVariable)
                {
                    continue;
                }
#>

        ObjectParameter <#=parameter.LocalVariableName#>;

        if (<#=parameter.IsNullableOfT ? parameter.FunctionParameterName + ".HasValue" : parameter.FunctionParameterName + " != null"#>)
        {
            <#=parameter.LocalVariableName#> = new ObjectParameter("<#=parameter.EsqlParameterName#>", <#=parameter.FunctionParameterName#>);
        }
        else
        {
            <#=parameter.LocalVariableName#> = new ObjectParameter("<#=parameter.EsqlParameterName#>", typeof(<#=parameter.RawClrTypeName#>));
        }
<#
            }
#>
        return <#=returnString#>
    }
<#
        }

        region.End();

#>
}
<#
if (!String.IsNullOrEmpty(namespaceName))
{
    PopIndent();
#>
}
<#
}
#>
<#+

private void WriteLazyLoadingEnabled(EntityContainer container)
{
   string lazyLoadingAttributeValue = null;
   string lazyLoadingAttributeName = MetadataConstants.EDM_ANNOTATION_09_02 + ":LazyLoadingEnabled";
   if(MetadataTools.TryGetStringMetadataPropertySetting(container, lazyLoadingAttributeName, out lazyLoadingAttributeValue))
   {
       bool isLazyLoading = false;
       if(bool.TryParse(lazyLoadingAttributeValue, out isLazyLoading))
       {
#>
        this.ContextOptions.LazyLoadingEnabled = <#=isLazyLoading.ToString().ToLowerInvariant()#>;
<#+
       }
   }
}
#>

Happy coding! :)

Popular posts from this blog

Handling uploads with MVC4, JQuery, Plupload and CKEditor

Generating a self-signed SSL certificate for my QNAP NAS

TDD and Unit Testing with Moq