浅谈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实现的整体类结构图,为了简化,图中省略了对类成员的描述:

image

在上图中,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:

  1. SetIdGeneratorConvention:通过此调用设置ID字段是否需要自动产生,以及ID值的产生方式。此Convention继承于IIdGeneratorConvention接口
  2. 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,然后查看最新版本的代码。

posted @ 2012-07-23 22:41  dax.net  阅读(3518)  评论(16编辑  收藏  举报