65.9K
CodeProject 正在变化。 阅读更多。
Home

多态地处理 Entity Framework 存储过程 ResultSet

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2014 年 11 月 25 日

CPOL

8分钟阅读

viewsIcon

37977

downloadIcon

300

一种反射方法,用于提供基于存储过程的数据访问方法,并基于作为 ObjectResult 中参数的多态类型使用重用模式。

引言

Microsoft 的 Entity Framework (EF) 长期以来提供了许多机会,可以使用概念模型中的多态性来构建可重用或可自定义的数据访问存储库,即 TPH、TPT 或 TPC 继承。使用这些技术,可以开发与数据访问层呈现特定实体还是其子类无关的业务逻辑,这种技术在创建可扩展 SDK 等方面非常有价值。然而,这些技术目前仅限于返回 IQueryable<T> 的操作,这包括表值函数操作,但不包括存储过程。存储过程操作而是返回 ObjectResult<T>。在一定程度上,这是一个合理的限制,因为存储过程可以建模为返回具有受限用法的 复杂类型。但是,EntityObjectsPOCOs 并不是存储过程返回的类型的排除项。

在 Entity Framework 6.1 之前,在 Code First 实现中使用存储过程一直很繁琐,特别是当它们返回 多个结果集时。通过 6.1,可以开发 自定义约定,极大地简化了操作。其中一种约定 CodeFirstStoreFunctions,可作为开源或 .nuget 包使用,本文对此进行了广泛的利用。该约定中的 CodeFirstStoreFunctions.FunctionsConvention 由作者在一系列博客文章1,2,3 中进行了详细描述;以下是其操作的简要概述以及本文采用的使用模式。

子类使用 FunctionsConvention 的构造函数来提供存储过程适用的默认数据库架构以及一个包含 Entity Framework 用于调用存储过程的方法的 Type。在本文中,该 Type 是一个类,它通过构造函数注入适用的 DbContext。该 Type 中与存储过程相关的带有 DbFunctionAttributeCodeFirstStoreFunctions.DbFunctionDetailsAttribute 装饰的方法。在模型创建时,约定提供的注册表会反射性地检查关联的 Type 中带有属性装饰的方法,并将每个方法的必要映射信息注入到 DbModel 中。

FunctionsConvention 极大地简化了调用存储过程的操作,并规范化了处理多个结果集的复杂性。然而,最终结果仍然受限于 ObjectResult<T>ObjectResult<T> 没有 public 构造函数,因此没有固有的重写机制,存储库开发人员可以返回 T 的子类。本文中的代码使用反射、动态类型和扩展方法提供了这种机制。此外,还提供了一种标准化地将多个结果集从底层流中卸载的机制。

模型

本文中用于演示技术的存储过程操作的是一个包含表示恐龙分类学的 Benthonian ‘生命之树’片段的层级表数据库。该数据库,如 Code First 默认实现通常那样,具有代理键,其关联的概念模型如图 1 所示。

Figure 1. Surrogate Key Conceptual Model

存储过程将数据库转换为如图 2 所示的复合键模型;特化取决于存储过程是否返回与实体相关的注释(如果有)。

Figure 2. Composite Key Conceptual Model

POCO 类层级结构允许表和基于存储过程的实体尽可能地共享通用性,同时允许它们各自被独立配置。由于不存在具有复合键的物理数据库实体,因此图 2 所示的对象图的填充部分由存储过程完成,部分由存储过程执行后填充导航属性的存储库代码完成。为了防止 Entity Framework 操作复合模型中的导航属性,每个映射都必须显式 `Ignore` 导航属性。请注意,在某些 POCO 中使用 NotMappedAttribute 是由于一个 EF 错误待解决,该错误导致 DbModelBuilder 绕过祖先类中的 Ignore 配置。此外,在运行时,必须将 DbContext 配置为仅与复合 POCOs 一起工作,而不是 EF 代理,即 DbContext.Configuration.LazyLoadingEnabled = DbContext.Configuration.ProxyCreationEnabled = false

使用 CodeFirstConventions

如引言中所述,每个存储过程集合都必须有一个 FunctionsConvention 的子类和一个包含存储过程调用代码的类。在本示例中,这些类分别是 DboConventionsDboFunctions

    /// <summary>
    ///     Registrar for stored procedure function calls in DbContext
    /// </summary>
    public class DboConventions : FunctionsConvention
    {
        public DboConventions() : base(DboFunctions.Schema, typeof (DboFunctions))
        {
        }
    }

    /// <summary>
    ///     Container for stored procedure invocation methods
    /// </summary>
    public abstract class DboFunctions : DbFunctionsBase
    {
        public const string Schema = "dbo";

        public DboFunctions(DbContext ctxt) : base(ctxt)
        {
        }

        [DbFunction(DefaultDbContextName, "usp_Superorder_Dinosauria")]
        [DbFunctionDetails(DatabaseSchema = Schema,
            ResultTypes = new[] {typeof (Order_sp), typeof (SubOrder_sp), typeof (InfraOrder_sp)})]
        public ObjectResult<Order_sp> GetDinosauriaByOrderId(int orderId)
        {
            return ((IObjectContextAdapter) Context).ObjectContext.ExecuteFunction<Order_sp>(
                ExtractFunctionName(MethodBase.GetCurrentMethod()), new[]
                {
                    new ObjectParameter("OrderId", orderId)
                });
        }
       
        [DbFunction(DefaultDbContextName, "usp_OrdersOfDinosauria")]
        [DbFunctionDetails(DatabaseSchema = Schema)]
        public ObjectResult<Order_sp> GetAllOrdersOfDinosauria()
        {
            return ((IObjectContextAdapter) Context).ObjectContext.ExecuteFunction<Order_sp>(
                ExtractFunctionName(MethodBase.GetCurrentMethod()), new ObjectParameter[0]);
        }
    }
	
    public class DboRepositoryFunctions : DboFunctions
    {
    :
    }

DbFunctionAttribute 提供了方法名与实际存储过程名之间的映射。这两个方法演示了返回单个和多个结果集的存储过程调用。在多个结果集的案例中,DbFunctionDetailsAttribute 提供了每个结果集的按顺序 TypeDboRepositoryFunctions 具体类的importance 稍后讨论。

DboFunctions 类继承自 DbFunctionsBase

    public abstract class DbFunctionsBase
    {
        // Default name of a DbContext in EF
        protected const string DefaultDbContextName = "CodeFirstContainer";

        // DbContext containing the EntityFramework Conventions on 
        // which the subclass depends
        protected readonly DbContext Context;

        /// <summary>
        ///     Constructor
        /// </summary>
        /// <param name="context">DbContext containing required Conventions</param>
        protected DbFunctionsBase(DbContext context)
        {
            Context = context;
        }

        /// <summary>
        ///     Creates an EntityFramework query string
        /// </summary>
        /// <param name="schema">database schema containing target function</param>
        /// <param name="method">
        ///     code method supporting function call. 
        ///     Actual TVF names is either method name or FunctionName property of
        ///     associated DbFunctionAttribute
        /// </param>
        /// <param name="arguments">Parameters (if any) of function 
        /// in order expected by function</param>
        /// <returns></returns>
        protected string ComposeTvfCall(MethodBase method, params ObjectParameter[] arguments)
        {
            var result = new StringBuilder("[");
            // The 'schema' position in an entity query is taken by the context name
            result.Append(((IObjectContextAdapter) 
                 Context).ObjectContext.DefaultContainerName).Append("].[");
            result.Append(ExtractFunctionName(method)).Append("](");

            IEnumerable<string> argnames = (from arg in arguments select "@" + arg.Name);
            result.Append(string.Join(", ", argnames));

            result.Append(")");
            return result.ToString();
        }

        /// <summary>
        ///     Returns name of method or of associated DbFunctionAttribute
        /// </summary>
        /// <param name="method">Method decorated with DbFunction</param>
        /// <returns>FunctionName, either that of DbFunction or of method</returns>
        protected string ExtractFunctionName(MethodBase method)
        {
            var attr = method.GetCustomAttribute(typeof (DbFunctionAttribute), 
                                                         false) as DbFunctionAttribute;
            return attr == null ? method.Name : attr.FunctionName;
        }
    }

该类为与存储过程关联的 DbContext 提供了一个占位符,并提供了两个帮助程序来协助实现子类。ExtractFunctionName 探查 DbFunctionAttribute 以获取 EF 的 ExecuteFunction 应调用的正确方法名。如前所述,FunctionsConvention 支持表值函数和存储过程,而 ComposeTvfCall 确保子类正确组合 EF 调用。ComposeTvfCall 是为了完整性而包含的,其使用在此文中并未演示。

此时,可以创建 DbContext 并对其进行动态配置。

    public class DinoContext : DbContext
    {
        public DinoContext() : base(Settings.Default.PolymorphicResultSetsConnectionString)
        {
        }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
            modelBuilder.Configurations.AddFromAssembly(Assembly.GetExecutingAssembly());
            modelBuilder.Conventions.AddFromAssembly(Assembly.GetExecutingAssembly());
        }
    }

该上下文可以注入到存储库实现中

    public class DinoRepository : IDinosauriaRepository
    {
        public DinoRepository(DbContext dbContext)
        {
            dbContext.Configuration.LazyLoadingEnabled = false;
            dbContext.Configuration.ProxyCreationEnabled = false;
            RepositoryFunctions = new DboRepositoryFunctions(dbContext);
        }

        public DboRepositoryFunctions RepositoryFunctions { get; protected set; }
        :
    }

使用辅助类 DinosauriaResults

    public class DinosauriaResults
    {
        public List<Order_sp> Orders { get; set; }
        public List<SubOrder_sp> SubOrders { get; set; }
        public List<InfraOrder_sp> InfraOrders { get; set; }
    }

存储库实现中的方法现在可以直接通过 DboRepositoryFunctions 中公开的方法调用存储过程。

        public virtual Order_sp GetDinosauriaGraphTheHardWay(int orderId)
        {
            var queryResult = new DinosauriaResults();
            var firstQuery = RepositoryFunctions.GetDinosauriaByOrderId(orderId);
            queryResult.Orders = (from o in firstQuery select o).ToList();
            var secondQuery = firstQuery.GetNextResult<SubOrder_sp>();
            queryResult.SubOrders = (from s in secondQuery select s).ToList();
            var thirdQuery = secondQuery.GetNextResult<InfraOrder_sp>();
            queryResult.InfraOrders = (from io in thirdQuery select io).ToList();
            var result = ReconnectGraph(queryResult);
            return result[0];
        }

尽管直接,但此代码展示了与引言中提到的 ObjectResult<T> 的紧密耦合。这种情况只有在引入 DboFunctions 的特化,该特化调用第二个存储过程返回第一个实体子类的情况下才会成为问题。

    public class PolymorphicConventions : FunctionsConvention
    {
        public PolymorphicConventions()
            : base(DboFunctions.Schema, typeof (PolymorphicResults))
        {
        }
    }

    public class PolymorphicResults : DboRepositoryFunctions
    {
        public PolymorphicResults(DbContext dbContext) : base(dbContext)
        {
            InvokeSuperorderDinosauria.Override(new MultiResultSetInvocation(GetType(),
                "GetDinosauriaByOrderIdWithComments"));
            InvokeOrdersOfDinosauria.Override(new SingleResultSetInvocation(GetType(),
                "GetAllOrdersOfDinosauriaWithComments"));
        }

        [DbFunction(DefaultDbContextName, "usp_Superorder_Dinosauria_Commented")]
        [DbFunctionDetails(DatabaseSchema = Schema,
            ResultTypes = new[] {typeof (Order_spWithComment), 
                   typeof (SubOrder_sp), typeof (InfraOrder_spWithComment)})
        ]
        public ObjectResult<Order_spWithComment> GetDinosauriaByOrderIdWithComments(int orderId)
        {
            return ((IObjectContextAdapter) Context).ObjectContext.ExecuteFunction<Order_spWithComment>(
                ExtractFunctionName(MethodBase.GetCurrentMethod()), new[]
                {
                    new ObjectParameter("OrderId", orderId)
                });
        }

        :
    }

然后,存储库的子类可以使用新过程函数覆盖实现。

    public class PolymorphicRepository : DinoRepository, IDinosauriaRepository
    {
        public PolymorphicRepository(DbContext dbContext) : base(dbContext)
        {
            RepositoryFunctions = new PolymorphicResults(dbContext);
        }

        public override Order_sp GetDinosauriaGraphTheHardWay(int orderId)
        {
            var queryResult = new DinosauriaResults();
            var firstQuery = (RepositoryFunctions as PolymorphicResults).
                                    GetDinosauriaByOrderIdWithComments(orderId);
            queryResult.Orders = new List<Order_sp>((from o in firstQuery select o));
            var secondQuery = firstQuery.GetNextResult<SubOrder_sp>();
            queryResult.SubOrders = (from s in secondQuery select s).ToList();
            var thirdQuery = secondQuery.GetNextResult<InfraOrder_spWithComment>();
            queryResult.InfraOrders = new List<InfraOrder_sp>((from io in thirdQuery select io));
            var result = ReconnectGraph(queryResult);
            return result[0];
        }
    }

FunctionsConvention 要求存储过程调用代码返回 ObjectResult<T>GetNextResult<T> 在处理后续结果集时也是如此。由于 ObjectResult<T> 没有 public 构造函数,因此子类必须重新实现基类功能。这种代码重复以及处理多个结果集的繁琐是下一步的目标。

结果集调用类

反射提供了一种可以模拟 ObjectResult<T> 函数重载的机制。解决方案首先将存储过程函数(ProcedureInfo)的 MethodInfo 封装在 SingleResultSetInvocation 类中。

    public class SingleResultSetInvocation
    {
        public static BindingFlags BindingFlags = BindingFlags.Instance | 
                                  BindingFlags.NonPublic | BindingFlags.Public;

        public SingleResultSetInvocation(MethodInfo method)
        {
            if (method == null) throw new ArgumentNullException
                        ("method", @"Missing ProcedureMethod specification");
            if (method.ReturnType.IsGenericType &&
                ProcedureMethod = method;
            else
            {
                throw new ArgumentException("Specified method does not derived from ObjectResult<T>.");
            }
            ResultTypes = ProcedureMethod.ReturnType.GetGenericArguments();
            GenericPopulateResultSet = GetType().GetMethod("PopulateFromQueryResult");
        }

        public SingleResultSetInvocation(Type t, string methodName) : 
                                     this(t.GetMethod(methodName, BindingFlags))
        {
        }

        public Type[] ResultTypes { get; protected set; }

        /// <summary>
        ///     Initialized at construction this property is used to construct
        /// </summary>
        public MethodInfo GenericPopulateResultSet { get; private set; }

        /// <summary>
        ///     Method to call at runtime
        /// </summary>
        public MethodInfo ProcedureMethod { get; private set; }
            :
    }

构造函数确保指定方法的返回 TypeObjectResult<T>。返回类型的泛型参数被缓存到 ResultTypes 数组中。

此类中的辅助方法提供了大部分运行时功能。GetFirstResult 返回 EF 调用存储过程产生的第一个 QueryResult<T>。通过使用动态类型,后续处理被简化。

        public dynamic GetFirstResult(object target, params object[] arguments)
        {
            return ProcedureMethod.Invoke(target, arguments);
        }

PopulateFromQueryResult 将一个查询的 resultset 卸载到一个支持 resultset TypeList 中。

        public List<T> PopulateFromQueryResult<T>(dynamic queryResult) where T : class
        {
            var list = new List<T>();
            foreach (dynamic r in queryResult)
            {
                list.Add(r as T);
            }
            return list;
        }

SingleResultSetContainer 使用的约定是,resultset 被卸载到一个 container 类中,该类类似于上面显示的 DinosauriaResults,即一个具有 List<T> 属性的类,该属性对应于一个 resultset Type 或其祖先之一。GetResultSetPropertyFromContainer 方法在容器对象中查找特定 Type 的此类属性。

        public PropertyInfo GetResultSetPropertyFromContainer(Type propertyType, object container)
        {
            PropertyInfo result =
                (from prop in container.GetType().GetProperties
                        (BindingFlags.Public | BindingFlags.Instance)
                    where prop.PropertyType.IsGenericType
                          && prop.PropertyType.GetGenericTypeDefinition() == typeof (List<>)
                          && prop.PropertyType.GetGenericArguments().First().
                                               IsAssignableFrom(propertyType)
                    select prop).FirstOrDefault();
            if (result == null)
            {
                throw new ArgumentException(
                    string.Format("Specified result container does not contain List<{0}> property.",
                        propertyType.FullName));
            }
            return result;
        }

Override 方法允许 `ProcedureMethod` 从 SingleResultSetContainer 的另一个实例以编程方式更改。但是,模拟的重写必须具有与原始 ProcedureMethod 相同的参数数量和 Type,并且返回 Type 必须在同一 Type 层次结构中。

        public void Override(SingleResultSetInvocation from)
        {
            if (ProcedureMethod != null && !IsAssignableFrom(from))
            {
                throw new MethodAssignmentException();
            }
            ProcedureMethod = from.ProcedureMethod;
            ResultTypes = from.ResultTypes;
        }

        public bool IsAssignableFrom(SingleResultSetInvocation to)
        {
            if (to.ResultTypes.Count() != ResultTypes.Count()) return false;
            ParameterInfo[] toParms = to.ProcedureMethod.GetParameters();
            ParameterInfo[] myParms = ProcedureMethod.GetParameters();
            if (myParms.Count() != toParms.Count()) return false;
            if (myParms.Any())
            {
                if (!myParms.Select((p, i) => 
                    ReferenceEquals(p.ParameterType, toParms[i].ParameterType))
                    .Aggregate((bResult, t) => bResult &= t)) return false;
            }
            return ResultTypes.Select((t, i) => t.IsAssignableFrom(to.ResultTypes[i]))
                .Aggregate((bResult, next) => bResult &= next);
        }

MultiResultSetInvocation 为返回多个 resultset 的过程添加了一个特化。其构造函数使用 DbFunctionDetailsAttribute 中的 ResultType[] 来初始化类。

    public class MultiResultSetInvocation : SingleResultSetInvocation
    {
        /// <summary>
        ///     Constructor acquires ResultTypes from DbFunctionDetailsAttribute
        /// </summary>
        /// <param name="method"></param>
        public MultiResultSetInvocation(MethodInfo method)
            : base(method)
        {
            var details =
                method.GetCustomAttribute(typeof (DbFunctionDetailsAttribute)) 
                             as DbFunctionDetailsAttribute;
            if (details == null)
                throw new ArgumentNullException("method",
                    string.Format("Missing DbFunctionDetailsAttribute on specified method {0}.{1}.",
                        method.DeclaringType.FullName, method.Name));
            ResultTypes = details.ResultTypes;
        }

        public MultiResultSetInvocation(Type t, string method) : this(t.GetMethod(method))
        {
        }
        :
    }

MultiResultSetInvocation 还提供了 GetNextResult 来反射性地调用 QueryResult<T>.GetNextResult<T’>()。同样,动态类型可以规避直接使用 QueryResult<T> 的问题。

        public dynamic GetNextResult(dynamic currentResult, Type nextResultType)
        {
            const string getNextResult = "GetNextResult";
            MethodInfo queryMeth = currentResult.GetType().GetMethod(getNextResult);
            MethodInfo genericRestrictions = queryMeth.MakeGenericMethod(nextResultType);
            return genericRestrictions.Invoke(currentResult, null);
        }

有了调用类,现在可以为 DbFunctionsBase 创建一个扩展方法,该方法可以完成调用过程和卸载结果集流的所有工作。扩展方法的使用确保使用正确的 DbFunctionsBase `this` 来启动查询过程。

        public static void Load(this DbFunctionsBase f, 
                SingleResultSetInvocation invoker, object resultSetContainer,
            object[] parameters)
        {
            dynamic query = null;

            Array.ForEach(invoker.ResultTypes, t =>
            {
                // find the property in the result container that will hold the returned data
                PropertyInfo propInfo = 
                     invoker.GetResultSetPropertyFromContainer(t, resultSetContainer);
                // find the appropriate query object
                query = query == null
                    ? invoker.GetFirstResult(f, parameters)
                    : ((MultiResultSetInvocation) invoker).GetNextResult(query, t);
                // create the generic method that unloads the resultset from the database stream
                MethodInfo method =
                    invoker.GenericPopulateResultSet.MakeGenericMethod(new[]
                    {propInfo.PropertyType.GetGenericArguments().First()});
                // unload the data and save it in the container
                propInfo.SetValue(resultSetContainer, method.Invoke(invoker, new object[] {query}));
            });
        }

Using the Code

在示例中,在 DboRepositoryFunctions 类中添加了两个 protected 调用类属性。这些属性在构造函数中绑定到 DboFunctions 类中的特定存储过程。

    public class DboRepositoryFunctions : DboFunctions
    {
        public DboRepositoryFunctions(DbContext dbContext) : base(dbContext)
        {
            InvokeSuperorderDinosauria = 
             new MultiResultSetInvocation(GetType(), "GetDinosauriaByOrderId");

            InvokeOrdersOfDinosauria = 
              new SingleResultSetInvocation(GetType(), "GetAllOrdersOfDinosauria");
        }

        protected MultiResultSetInvocation InvokeSuperorderDinosauria { get; private set; }
        protected SingleResultSetInvocation InvokeOrdersOfDinosauria { get; private set; }

        :
    }

然后向 DboRepositoryFunctions 添加了两个方法来反射性地调用过程并返回结果。

        public DinosauriaResults GetDinosauriaOrder(int orderId)
        {
            var result = new DinosauriaResults();
            this.Load(InvokeSuperorderDinosauria, result, new object[] {orderId});
            return result;
        }

        public DinosauriaResults GetAllDinosauria()
        {
            var result = new DinosauriaResults();
            this.Load(InvokeOrdersOfDinosauria, result, new object[0]);
            return result;
        }

然后修改存储库实现,使用这些新函数来实现其 interface

    public class DinoRepository : IDinosauriaRepository
    {
        :

        public DinosauriaResults GetDinosauriaResultsByOrderId(int orderId)
        {
            return RepositoryFunctions.GetDinosauriaOrder(orderId);
        }

        public Order_sp GetDinosauriaGraph(int orderId)
        {
            var results = GetDinosauriaResultsByOrderId(orderId);
            return ReconnectGraph(results)[0];
        }
        :
    }

现在,可以通过重写构造函数中的存储过程属性,在 DboRepositoryFunctions 的特化中轻松更改存储过程。

    public class PolymorphicResults : DboRepositoryFunctions
    {
        public PolymorphicResults(DbContext dbContext) : base(dbContext)
        {
            InvokeSuperorderDinosauria.Override(new MultiResultSetInvocation(GetType(),
                "GetDinosauriaByOrderIdWithComments"));
            InvokeOrdersOfDinosauria.Override(new SingleResultSetInvocation(GetType(),
                "GetAllOrdersOfDinosauriaWithComments"));
        }
        :
    }

DboRepositoryFunctions 的子类或使用它的存储库不需要实现。子类实体现在无需进行大量或重复的开发即可实现。

运行示例

提供的示例包含一系列 Visual Studio 2013 单元测试,这些测试用于测试本文中的代码。关联的数据库作为 mdf 文件提供在解决方案目录中。mdf 文件附加到 localDb,并通过项目 app.config 中的连接字符串进行引用。

打包说明

DbFunctionsBase 派生的类层次结构很大程度上是出于希望将 CodeFirstStoreFunctions 相关方法与调用过程中的方法分开的考虑。该层次结构也允许 `abstract` 类在其他上下文中重用。理想情况下,CodeFirstStoreFunctions 方法应该位于 protected 作用域,但截至目前,FunctionsConvention 注册表在探测函数时不会考虑 BindingFlags.NonPublic 方法。要使其这样做,需要更改源代码。

参考文献

© . All rights reserved.