浅谈Apworks对MongoDB的支持:设计与实现
概述
在企业级应用程序中,存储部分的技术选型是多样化的,开发人员可以根据应用的具体情况来选择合适的存储技术,比如关系型数据库或者文档数据库、对象数据库等。为此,Apworks也从框架级别对Repository的定制和二次开发进行支持,目前默认地提供三种Repository的实现:NHibernate Repository、Entity Framework Repository和MongoDB Repository。本文将对MongoDB的Repository设计与实现进行一些简要的讨论。
设计
Apworks为基于第三方框架的组件扩展提供了很好的支持。举例来说,分离接口(Separated Interface)模式使得基于第三方框架的组件扩展和升级能够独立于Apworks的核心组件,从而实现在不更改核心组件的情况下,能够非常方便地引入新的组件或者升级现有组件。就Repository的具体实现而言,Apworks目前已经能够支持三种不同的技术选型:NHibernate、Entity Framework以及MongoDB。前两者是基于关系型数据库和第三方ORM框架的,而后者则是一种较为流行的NoSQL数据存储方案。
以下是基于MongoDB的Repository实现的整体类结构图,为了简化,图中省略了对类成员的描述:
在上图中,IRepository接口、IRepositoryContext接口以及Repository抽象类都是定义在Apworks的核心部分(即Apworks.dll中),事实上,只要实现了IRepository和IRepositoryContext接口,那么这种Repository的具体实现就可以无缝地整合到Apworks框架中。通过查看Apworks的已有源代码不难发现,基于NHibernate和Entity Framework的Repository实现,都是遵循这样的规则。
MongoDBRepositoryContext类在初始化时,构造函数需要接受一个IMongoDBRepositoryContextSettings类型的参数,这个类型包含了MongoDB数据库服务器和数据库的设置信息,以及一个通过聚合根的类型来确定MongoDB中Collection名称的委托属性。在设计中引入这个接口的目的是:一方面能够让开发人员更多地掌握MongoDB的配置方式(比如对服务器和数据库的配置等),另一方面进一步降低框架与MongoDB之间的衔接关系(比如通过委托属性来获得聚合根类型与Collection名称之间的映射关系)。
另外,基于MongoDB的Repository实现,需要对MongoDB的文档序列化方式进行一些干预。比如在默认情况下,MongoDB会自动产生ObjectId以作为文档的主键,但Apworks框架中,实体将有自己的主键ID,此时就需要将实体的ID用作文档的主键。于是,在MongoDBRepositoryContext中提供了这样的静态函数:它允许调用者通过Convention Profile的方式,向MongoDB注册Convention,以便干预序列化方式。除了需要将实体ID用作文档主键之外,还需要通过以下调用来注册几个需要的(不一定是必须的)Convention:
- SetIdGeneratorConvention:通过此调用设置ID字段是否需要自动产生,以及ID值的产生方式。此Convention继承于IIdGeneratorConvention接口
- SetSerializationOptionsConvention:通过此调用设置是否使用本地时间来序列化System.DateTime类型。在MongoDB中,DateTime默认是采用UTC形式进行存储的。此Convention继承于ISerializationOptionsConvention接口
在实际应用中,开发人员可以通过可选参数来确定是否需要对以上两种Convention进行注册,也可以注册自定义的Convention Profile。由此可见,虽然Apworks对MongoDB的Repository实现进行了一些封装,但并不会代替开发人员决定些什么,各种面向MongoDB的设置和序列化方式都可以由开发人员完全掌控。
实现
Expression of type <A> cannot be used for return type <B>问题的解决
假设在实体Customer中有一个int类型的Sequence属性,那么对于下面的查询,在MongoDB上的执行是正确的:
var query = collection .AsQueryable<Customer>() .Where(p => true) .OrderByDescending(sort => sort.Sequence).ToList();
然而,如果使用下面的方式进行查询,就会抛出这样的ArgumentException: Expression of type <A> cannot be used for return type <B>:
Expression<Func<Customer, object>> sortPred = sort => sort.Sequence; var query = collection .AsQueryable<Customer>() .Where(p => true) .OrderByDescending(sortPred).ToList();
之所以我们需要保留第二种调用方式,是因为Apworks框架的Repository包含了接受Expression<Func<Customer, dynamic>>类型参数的函数重载,然而这种方式又会产生上面的问题。经过测试,发现NHibernate和Entity Framework的仓储实现均未出现上述问题。我想这里应该是MongoDB的LINQ Provider在使用ExpressionVisitor对Lambda Expression的处理方式与两者不同所致。
所以,对于MongoDBRepository的FindAll(Expression<Func<TAggregateRoot, dynamic>> sortPredicate, SortOrder sortOrder)方法,我们就不能简单地将sortPredicate参数直接传递给OrderByDescending扩展方法。
顺便说一下,在Apworks的Repository中,用于排序的表达式,我使用了dynamic关键字,类似于上面的Expression<Func<Customer, dynamic>>,事实上完全可以使用object。当初原以为可以借用dynamic来解决协变/逆变的问题,但后来发现不行,也就作罢了。
为了能够解决这个问题,我们需要对Lambda表达式进行修改。就上面的例子而言,虽然FindAll函数接受的是Expression<Func<TAggregateRoot, dynamic>>类型的参数,但OrderByDescending所需要的Lambda Expression应该是Expression<Func<TAggregateRoot, int>>类型,因为Sequence是int类型的。于是,可以使用Expression.Convert方法对sortPredicate中的Property Expression进行类型转换。比如:
ParameterExpression param = sortPred.Parameters[0]; Expression<Func<Customer, int>> expr = Expression.Lambda<Func<Customer, int>>( Expression.Convert( Expression.Property(param, "Sequence"), typeof(int)), param);
此时,再将生成的expr传递给OrderByDescending方法,就能够成功完成排序查询了。
但事情还没完,因为我们不能简单地将expr定义为Expression<Func<Customer, int>>类型,此处是因为Customer的Sequence类型是int型的,但在实际中用于排序的属性可以是任意类型。理想的做法是将expr定义为Expression<Func<Customer, object>>类型,事实上并没有办法将一个具体的基于某个属性类型的Lambda表达式转换成Expression<Func<Customer, object>>类型。
经过一段时间的研究,我采用了如下的方法:首先分析给定的sortPredicate所包含的属性的名称和类型,然后使用Expression.Lambda静态方法,将sortPredicate转换为弱类型的Lambda表达式(即Expression.Lambda调用返回的是一个LambdaExpression,而非Expression<Func<Customer, object>>这样的类型),然后使用反射来调用Queryable类型上的OrderBy和OrderByDescending静态方法从而获得IOrderedQueryable实例。接下来的处理方式就与正常情形一样了。采用反射来调用Queryable上的静态方法,是因为OrderBy和OrderByDescending方法无法接受弱类型的Lambda表达式作为传入参数。
最后,为了不变动已有代码,我在Apworks.Repositories.MongoDB的Assembly中针对IQueryable添加了两个扩展方法:OrderBy和OrderByDescending。完整代码如下:
/// <summary> /// Represents the helper (method extender) for the sorting lambda expressions. /// </summary> internal static class SortExpressionHelper { #region Private Static Methods private static IOrderedQueryable<TAggregateRoot> InvokeOrderBy<TAggregateRoot>(IQueryable<TAggregateRoot> query, Expression<Func<TAggregateRoot, dynamic>> sortPredicate, SortOrder sortOrder) where TAggregateRoot : class, IAggregateRoot { var param = sortPredicate.Parameters[0]; string propertyName = null; Type propertyType = null; Expression bodyExpression = null; if (sortPredicate.Body is UnaryExpression) { UnaryExpression unaryExpression = sortPredicate.Body as UnaryExpression; bodyExpression = unaryExpression.Operand; } else if (sortPredicate.Body is MemberExpression) { bodyExpression = sortPredicate.Body; } else throw new ArgumentException(@"The body of the sort predicate expression should be either UnaryExpression or MemberExpression.", "sortPredicate"); MemberExpression memberExpression = (MemberExpression)bodyExpression; propertyName = memberExpression.Member.Name; if (memberExpression.Member.MemberType == MemberTypes.Property) { PropertyInfo propertyInfo = memberExpression.Member as PropertyInfo; propertyType = propertyInfo.PropertyType; } else throw new InvalidOperationException(@"Cannot evaluate the type of property since the member expression represented by the sort predicate expression does not contain a PropertyInfo object."); Type funcType = typeof(Func<,>).MakeGenericType(typeof(TAggregateRoot), propertyType); LambdaExpression convertedExpression = Expression.Lambda(funcType, Expression.Convert(Expression.Property(param, propertyName), propertyType), param); var sortingMethods = typeof(Queryable).GetMethods(BindingFlags.Public | BindingFlags.Static); var sortingMethodName = GetSortingMethodName(sortOrder); var sortingMethod = sortingMethods.Where(sm => sm.Name == sortingMethodName && sm.GetParameters() != null && sm.GetParameters().Length == 2).First(); return (IOrderedQueryable<TAggregateRoot>)sortingMethod .MakeGenericMethod(typeof(TAggregateRoot), propertyType) .Invoke(null, new object[] { query, convertedExpression }); } private static string GetSortingMethodName(SortOrder sortOrder) { switch (sortOrder) { case SortOrder.Ascending: return "OrderBy"; case SortOrder.Descending: return "OrderByDescending"; default: throw new ArgumentException("Sort Order must be specified as either Ascending or Descending.", "sortOrder"); } } #endregion #region Internal Method Extensions /// <summary> /// Sorts the elements of a sequence in ascending order according to a lambda expression. /// </summary> /// <typeparam name="TAggregateRoot">The type of the aggregate root.</typeparam> /// <param name="query">A sequence of values to order.</param> /// <param name="sortPredicate">The lambda expression which indicates the property for sorting.</param> /// <returns>An <see cref="IOrderedQueryable[T]"/> whose elements are sorted according to the lambda expression.</returns> internal static IOrderedQueryable<TAggregateRoot> OrderBy<TAggregateRoot>(this IQueryable<TAggregateRoot> query, Expression<Func<TAggregateRoot, dynamic>> sortPredicate) where TAggregateRoot : class, IAggregateRoot { return InvokeOrderBy(query, sortPredicate, SortOrder.Ascending); } /// <summary> /// Sorts the elements of a sequence in descending order according to a lambda expression. /// </summary> /// <typeparam name="TAggregateRoot">The type of the aggregate root.</typeparam> /// <param name="query">A sequence of values to order.</param> /// <param name="sortPredicate">The lambda expression which indicates the property for sorting.</param> /// <returns>An <see cref="IOrderedQueryable[T]"/> whose elements are sorted according to the lambda expression.</returns> internal static IOrderedQueryable<TAggregateRoot> OrderByDescending<TAggregateRoot>(this IQueryable<TAggregateRoot> query, Expression<Func<TAggregateRoot, dynamic>> sortPredicate) where TAggregateRoot : class, IAggregateRoot { return InvokeOrderBy(query, sortPredicate, SortOrder.Descending); } #endregion }
在实际中应用基于MongoDB的Repository
首先我们需要新建一个实现IMongoDBRepositoryContextSettings接口的类,在类中对服务器、数据库进行设置,并指定聚合根类型与Collection之间的映射关系。比如:
public class MongoDBRepositoryContextSettings : IMongoDBRepositoryContextSettings { public MongoDBRepositoryContextSettings() { } #region IMongoDBRepositoryContextSettings Members public MapTypeToCollectionNameDelegate MapTypeToCollectionName { get { return null; // Null to use the type name as the collection name. } } public MongoServerSettings ServerSettings { get { MongoServerSettings serverSettings = new MongoServerSettings(); serverSettings.Server = new MongoServerAddress("localhost"); serverSettings.SafeMode = SafeMode.True; return serverSettings; } } public MongoDatabaseSettings GetDatabaseSettings(MongoServer server) { MongoDatabaseSettings databaseSettings = new MongoDatabaseSettings(server, "MyDatabaseName"); return databaseSettings; } #endregion }
之后,修改配置信息,在IoC容器中注册IMongoDBRepositoryContextSettings、IRepositoryContext以及IRepository类型,以使用MongoDB的实现类,比如对于Unity的IoC容器,可以在web.config/app.config中加入以下的配置信息:
<register type="Apworks.Repositories.MongoDB.IMongoDBRepositoryContextSettings, Apworks.Repositories.MongoDB" mapTo="ApworksStarterEF.Domain.Repositories.MongoDBRepositoryContextSettings, ApworksStarterEF.Domain.Repositories"> <lifetime type="ContainerControlledLifetimeManager"/> </register> <register type="Apworks.Repositories.IRepositoryContext, Apworks" mapTo="Apworks.Repositories.MongoDB.MongoDBRepositoryContext, Apworks.Repositories.MongoDB"> <lifetime type="Apworks.ObjectContainers.Unity.WcfPerRequestLifetimeManager, Apworks.ObjectContainers.Unity"/> <constructor> <param name="settings"> <dependency type="Apworks.Repositories.MongoDB.IMongoDBRepositoryContextSettings, Apworks.Repositories.MongoDB"/> </param> </constructor> </register>
接下来,别忘了在应用程序的BootStrapper中调用MongoDBRepositoryContext.RegisterConventions静态方法。
Apworks支持多种配置方式,我们完全可以不使用web.config/app.config对Apworks进行配置,我们可以使用写代码的方式,这种方式对于单体测试有很大的帮助。
最后,在使用时,可以直接使用ServiceLocator类来获取接口的具体实现。因此,我们可以无需修改任何源代码,就能实现对Repository的替换。
总结
本文简要地讨论了Apworks框架中,基于MongoDB的Repository的设计与实现。接下来我打算对三种不同的Repository做一些性能上的评估。相关的实现代码可以登录Apworks的站点http://apworks.codeplex.com,然后查看最新版本的代码。