概述
在企業級應用程序中,存儲部分的技術選型是多樣化的,開發人員可以根據應用的具體情況來選擇合適的存儲技術,比如關系型數據庫或者文檔數據庫、對象數據庫等。為此,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,然后查看最新版本的代碼。