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





5.00/5 (4投票s)
一种反射方法,
引言
Microsoft 的 Entity Framework (EF) 长期以来提供了许多机会,可以使用概念模型中的多态性来构建可重用或可自定义的数据访问存储库,即 TPH、TPT 或 TPC 继承。使用这些技术,可以开发与数据访问层呈现特定实体还是其子类无关的业务逻辑,这种技术在创建可扩展 SDK 等方面非常有价值。然而,这些技术目前仅限于返回 IQueryable<T> 的操作,这包括表值函数操作,但不包括存储过程。存储过程操作而是返回 ObjectResult<T>。在一定程度上,这是一个合理的限制,因为存储过程可以建模为返回具有受限用法的 复杂类型。但是,EntityObjects 或 POCOs 并不是存储过程返回的类型的排除项。
在 Entity Framework 6.1 之前,在 Code First 实现中使用存储过程一直很繁琐,特别是当它们返回 多个结果集时。通过 6.1,可以开发 自定义约定,极大地简化了操作。其中一种约定 CodeFirstStoreFunctions,可作为开源或 .nuget 包使用,本文对此进行了广泛的利用。该约定中的 CodeFirstStoreFunctions.FunctionsConvention
由作者在一系列博客文章1,2,3 中进行了详细描述;以下是其操作的简要概述以及本文采用的使用模式。
子类使用 FunctionsConvention
的构造函数来提供存储过程适用的默认数据库架构以及一个包含 Entity Framework 用于调用存储过程的方法的 Type
。在本文中,该 Type
是一个类,它通过构造函数注入适用的 DbContext
。该 Type
中与存储过程相关的带有 DbFunctionAttribute
和 CodeFirstStoreFunctions.DbFunctionDetailsAttribute
装饰的方法。在模型创建时,约定提供的注册表会反射性地检查关联的 Type
中带有属性装饰的方法,并将每个方法的必要映射信息注入到 DbModel 中。
FunctionsConvention
极大地简化了调用存储过程的操作,并规范化了处理多个结果集的复杂性。然而,最终结果仍然受限于 ObjectResult<T>
。ObjectResult<T>
没有 public
构造函数,因此没有固有的重写机制,存储库开发人员可以返回 T
的子类。本文中的代码使用反射、动态类型和扩展方法提供了这种机制。此外,还提供了一种标准化地将多个结果集从底层流中卸载的机制。
模型
本文中用于演示技术的存储过程操作的是一个包含表示恐龙分类学的 Benthonian ‘生命之树’片段的层级表数据库。该数据库,如 Code First 默认实现通常那样,具有代理键,其关联的概念模型如图 1 所示。
存储过程将数据库转换为如图 2 所示的复合键模型;特化取决于存储过程是否返回与实体相关的注释(如果有)。
POCO 类层级结构允许表和基于存储过程的实体尽可能地共享通用性,同时允许它们各自被独立配置。由于不存在具有复合键的物理数据库实体,因此图 2 所示的对象图的填充部分由存储过程完成,部分由存储过程执行后填充导航属性的存储库代码完成。为了防止 Entity Framework 操作复合模型中的导航属性,每个映射都必须显式 `Ignore` 导航属性。请注意,在某些 POCO 中使用 NotMappedAttribute 是由于一个 EF 错误待解决,该错误导致 DbModelBuilder
绕过祖先类中的 Ignore
配置。此外,在运行时,必须将 DbContext
配置为仅与复合 POCOs 一起工作,而不是 EF 代理,即 DbContext.Configuration.LazyLoadingEnabled = DbContext.Configuration.ProxyCreationEnabled = false
。
使用 CodeFirstConventions
如引言中所述,每个存储过程集合都必须有一个 FunctionsConvention
的子类和一个包含存储过程调用代码的类。在本示例中,这些类分别是 DboConventions
和 DboFunctions
。
/// <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
提供了每个结果集的按顺序 Type
。DboRepositoryFunctions
具体类的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; }
:
}
构造函数确保指定方法的返回 Type
为 ObjectResult<T>
。返回类型的泛型参数被缓存到 ResultTypes
数组中。
此类中的辅助方法提供了大部分运行时功能。GetFirstResult
返回 EF 调用存储过程产生的第一个 QueryResult<T>
。通过使用动态类型,后续处理被简化。
public dynamic GetFirstResult(object target, params object[] arguments)
{
return ProcedureMethod.Invoke(target, arguments);
}
PopulateFromQueryResult
将一个查询的 resultset
卸载到一个支持 resultset Type
的 List
中。
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
方法。要使其这样做,需要更改源代码。
参考文献
- 1Pawel Kadluczka. Support for Store Functions (TVFs and Stored Procs) in Code First (Entity Framework 6.1). 2014 年 4 月 9 日。
- 2Pawel Kadluczka. The Beta Version of Store Functions for EntityFramework 6.1.1+ Code First Available. 2014 年 8 月 11 日。
- 3Pawel Kadluczka. The final version of the Store Functions for EntityFramework 6.1.1+ Code First convention released. 2014 年 10 月 18 日。