[原创].NET 业务框架开发实战之十 第一阶段总结,深入浅出,水到渠成(后篇)
.NET 业务框架开发实战之十 第一阶段总结,深入浅出,水到渠成(后篇)
前言:接着上篇来。
系列文章链接:
[原创].NET 分布式架构开发实战之三 数据访问深入一点的思考
[原创].NET 分布式架构开发实战之四 构建从理想和实现之间的桥梁(前篇)
[原创].NET 分布式架构开发实战五 Framework改进篇
[原创].NET 业务框架开发实战之八 业务层Mapping的选择策略
[原创].NET 业务框架开发实战之九 Mapping属性原理和验证规则的实现策略
3. 再次借鉴.NET Framework设计思想
自己实现其实不难,关键看怎么做了。在实现的时候,如果自己单独去搞一套方式,如果设计的不好,可能到后来别人不好理解,甚至连自己也忘记当初自己为什么这样设计。所以,要充分的借鉴已有的好的实现思想。分析了IQueryable,其实最大的区别就是,我们不希望去立刻操作数据源,但是在实现IQueryable过程中是操作数据源的。除此之外,如记录对于实现了IQueryable接口的类的上的操作,这是我们需要的,也就是说我们自己的实现的条件对象也要记录对它对象的操作,然后把这些操作在服务器那边解析执行。
所以,条件对象的接口实现如下:
/// 所有的查询对象都要从这个接口继承
/// </summary>
public interface ICriteria
{
#region Property
Type ObjectType { get; }
ICriteriaProvider Provider { get; }
#endregion
}
/// 泛型版的条件对象
/// </summary>
/// <typeparam name="TSource"></typeparam>
public interface ICriteria<T> : ICriteria
{
Dictionary<string, List<Expression>> ExpressionDictionary { get; }
ICriteria<T> Where(Expression<Func<T, bool>> predicate);
ICriteria<T> OrderBy<K>(Expression<Func<T, K>> predicate);
ICriteria<T> OrderByDescending<K>(Expression<Func<T, K>> predicate);
ICriteria<T> ThenBy<K>(Expression<Func<T, K>> predicate);
ICriteria<T> ThenByDescending<K>(Expression<Func<T, K>> predicate);
ICriteria<T> Skip(int count);
ICriteria<T> Take(int count);
ICriteria<T> First();
ICriteria<T> First(Expression<Func<T, bool>> predicate);
ICriteria<T> Distinct<K>(Expression<Func<T, K>> predicate);
ICriteria<T> All(Expression<Func<T, bool>> predicate);
ICriteria<T> Any(Expression<Func<T, bool>> predicate);
ICriteria<T> GroupBy<K>(Expression<Func<T, K>> predicate);
ICriteria<T> Max<K>(Expression<Func<T, K>> predicate);
ICriteria<T> Min<K>(Expression<Func<T, K>> predicate);
}
public interface ICriteriaProvider
{
object Execute<T>(ICriteria<T> condition);
}
大家可以看到,上面的接口的声明和IQueryable合QueryProvider的声明很相似。
大家看到了,在上面的ICritera接口声明中,有一些方法,如Where,First等等,其实这些方法在IQueryable也是有的,只不过是以扩展方法的形式出现了。我们这里就直接写在这里了,如果以后要加入更多的方法,我们也可以利用扩展方法的方式。
在IQueryable中,使用 Expression Tree表达式树来记录操作的,那么我们这里也采用这种方式。
还有一点比较重要:在上面的自己的实现linq to sql的例子中,或者微软官方实现的linq to sql中,操作的都是数据实体和ADO.NET对象之间的关系,例如下面的代码:
if (this.reader.Read()) {
if (this.fieldLookup == null) {
this.InitFieldLookup();
}
T instance = new T();
for (int i = 0, n = this.fields.Length; i < n; i++) {
int index = this.fieldLookup[i];
if (index >= 0) {
FieldInfo fi = this.fields[i];
if (this.reader.IsDBNull(index)) {
fi.SetValue(instance, null);
}
else {
fi.SetValue(instance, this.reader.GetValue(index));
}
}
}
this.current = instance;
return true;
}
return false;
}
从代码中可以看出,在解析表达式树后,执行返回的数据实体的属性的名字和查询出的表的字段名字是相同的。
但是:我们在客户端构造出来的条件对象中使用的字段是业务类的属性,业务类中的属性名字和数据库表字段的名字可能不是一样的,甚至有可能业务类中的一个属性的值是几个表中字段计算而来的。如Age>3,其中在Age属性的值,可能来自己数据库中UserDetail表中age字段。所以,对于在业务类上生成的条件对象,通过解析最后要生成对应表的查询,例如在User类上生成的条件对象,最后生成的sql语句要是:select * from UserDetail where age>3 ,而不是select * from User where Age>3. 也就是说,User业务类要清楚的知道自己的数据来自哪个数据库表,而且User类中的属性也要清楚的知道自己的数据到底来自数据表中的哪个字段。
下面就来讲述如何解决上面提到的问题。
Mapping属性
new PropertyInfo<int>("UserId",typeof(M_Product)","Id"));
public string UserId
{
get { return ReadProperty(UserIdProperty ); }
set { LoadProperty(UserIdProperty , value); }
}
从代码中可以看出,业务类的属性的声明和之前有点不一样了。可能一直以来,业务类中的属性都是像下面这样声明的:
但是,为了解决上面的问题:即每个属性都要清楚的知道自己对应数据表中的那个字段,那么每个属性就要保存一些数据库中表的字段的信息,这样也便于条件对象最后生成正确的sql语句。
在框架中,有一个接口的声明:
{
/// <summary>
/// Gets the property name value.
/// </summary>
string Name { get; }
/// <summary>
/// Gets the type of the property.
/// </summary>
Type Type { get; }
/// <summary>
/// Gets the friendly display name
/// for the property.
/// </summary>
string FriendlyName { get; }
/// <summary>
/// Mapping data entity type
/// </summary>
Type DataEntityTyoe { get; set; }
/// <summary>
/// Mapping to data entity name
/// </summary>
string DataEntityPropertyName { get; set; }
/// <summary>
/// Gets the default initial value for the property.
/// </summary>
/// <remarks>
/// This value is used to initialize the property's
/// value, and is returned from a property get
/// if the user is not authorized to
/// read the property.
/// </remarks>
object DefaultValue { get; }
}
这个接口就为是为用来描述业务类中每一个属性的信息的,例如,属性对应哪个数据表中哪个字段。
框架中存在一个全局的字典,用来保存所有业务类的一些属性的信息:如下
Dictionary<Type,List<IPropertyInfo>> propertyInfomationDictionary;
在条件对象中的表达式树遍历解析的时候,就会使用这个字典中保存的信息来生成正确的sql语句。看看下面条件对象解释器的代码:大家只看Translate方法就行了。
{
#region Fields
StringBuilder sb;
Dictionary<string, string> resultDictionary = null;
private Type objectType;
#endregion
#region Constructor
internal CriteriaTranslator(Type type)
{
objectType = type;
}
#endregion
#region Main Methods
internal Dictionary<string, string> Translate(Dictionary<string, List<Expression>> expressionDictionary)
{
resultDictionary = new Dictionary<string, string>();
foreach (var expressionKeyValePair in expressionDictionary)
{
this.sb = new StringBuilder();
foreach (Expression expression in expressionKeyValePair.Value)
{
this.Visit(Evaluator.PartialEval(expression));
}
AddExpressionResult(expressionKeyValePair.Key, sb.ToString());
}
return resultDictionary;
}
#endregion
#region Override Methods
protected override Expression VisitMethodCall(MethodCallExpression m)
{
if (m.Method.DeclaringType == typeof(string))
{
switch (m.Method.Name)
{
case "StartsWith":
sb.Append("(");
this.Visit(m.Object);
return m;
case "Contains":
sb.Append("(");
this.Visit(m.Object);
sb.Append(" LIKE '%' + ");
this.Visit(m.Arguments[0]);
sb.Append(" + '%')");
return m;
}
}
throw new NotSupportedException(string.Format("The method '{0}' is not supported", m.Method.Name));
}
protected override Expression VisitUnary(UnaryExpression u)
{
switch (u.NodeType)
{
case ExpressionType.Not:
sb.Append(" NOT ");
this.Visit(u.Operand);
break;
default:
throw new NotSupportedException(string.Format("The unary operator '{0}' is not supported", u.NodeType));
}
return u;
}
protected override Expression VisitMemberAccess(MemberExpression m)
{
if (m.Expression != null && m.Expression.NodeType == ExpressionType.Parameter)
{
if (this.objectType.IsAssignableFrom(typeof(BusinessBase)))
{
List<IPropertyInfo> propertyInfoList = PropertyInfoManager.GetRegisteredProperties(this.objectType);
var property = propertyInfoList.Where(u => u.Name == m.Member.Name).SingleOrDefault();
if (property != null)
{
var dataEntityName = property.DataEntityPropertyName;
sb.Append(dataEntityName);
AddExpressionResult("Type", property.DataEntityTyoe.Name);
}
}
return m;
}
else
throw new NotSupportedException(string.Format("The member '{0}' is not supported", m.Member.Name));
}
protected override Expression VisitBinary(BinaryExpression b)
{
sb.Append("(");
this.Visit(b.Left);
switch (b.NodeType)
{
case ExpressionType.And:
case ExpressionType.AndAlso:
sb.Append(" AND ");
break;
case ExpressionType.Or:
case ExpressionType.OrElse:
sb.Append(" OR");
break;
case ExpressionType.Equal:
sb.Append(" = ");
break;
case ExpressionType.NotEqual:
sb.Append(" <> ");
break;
case ExpressionType.LessThan:
sb.Append(" < ");
break;
case ExpressionType.LessThanOrEqual:
sb.Append(" <= ");
break;
case ExpressionType.GreaterThan:
sb.Append(" > ");
break;
case ExpressionType.GreaterThanOrEqual:
sb.Append(" >= ");
break;
default:
throw new NotSupportedException(string.Format("The binary operator '{0}' is not supported", b.NodeType));
}
this.Visit(b.Right);
sb.Append(")");
return b;
}
protected override Expression VisitConstant(ConstantExpression c)
{
IQueryable q = c.Value as IQueryable;
if (q != null)
{
// assume constant nodes w/ IQueryables are table references
sb.Append("SELECT * FROM ");
sb.Append(q.ElementType.Name);
}
else if (c.Value == null)
{
sb.Append("NULL");
}
else
{
switch (Type.GetTypeCode(c.Value.GetType()))
{
case TypeCode.Boolean:
sb.Append(((bool)c.Value) ? 1 : 0);
break;
case TypeCode.String:
sb.Append("'");
sb.Append(c.Value);
sb.Append("'");
break;
case TypeCode.Object:
throw new NotSupportedException(string.Format("The constant for '{0}' is not supported", c.Value));
default:
sb.Append(c.Value);
break;
}
}
return c;
}
#endregion
#region Assistant Methods
private static Expression StripQuotes(Expression e)
{
while (e.NodeType == ExpressionType.Quote)
{
e = ((UnaryExpression)e).Operand;
}
return e;
}
private void AddExpressionResult(string key, string value)
{
string tempValue = string.Empty;
if (!resultDictionary.TryGetValue(key, out tempValue))
{
lock (resultDictionary)
{
if (!resultDictionary.TryGetValue(key, out tempValue))
{
resultDictionary.Add(key, value);
}
}
}
}
#endregion
}
VisitMemberAccess方法就是用来把业务属性的名字换成对应数据库中表字段的名字的。
{
if (m.Expression != null && m.Expression.NodeType == ExpressionType.Parameter)
{
if (this.objectType.IsAssignableFrom(typeof(BusinessBase)))
{
List<IPropertyInfo> propertyInfoList = PropertyInfoManager.GetRegisteredProperties(this.objectType);
var property = propertyInfoList.Where(u => u.Name == m.Member.Name).SingleOrDefault();
if (property != null)
{
var dataEntityName = property.DataEntityPropertyName;
sb.Append(dataEntityName);
AddExpressionResult("Type", property.DataEntityTyoe.Name);
}
}
return m;
}
else
throw new NotSupportedException(string.Format("The member '{0}' is not supported", m.Member.Name));
}
上面的代码中,并没有立刻就把所有操作拼接成sql语句,而且把操作分类的记录下来在一个字典中,这个字典最后会被数据层那里使用。例如,字典中可能保存的值如下:
操作 |
生成的sql语句 |
Where |
age>18 and name like ’%xiaoyang%’ |
OrderBy |
age |
GroupBy |
age |
............... |
......................... |
最后这个字典会在数据层那边被使用:ICriteria包含CriteriaProvider,然后DAL代码调用Icriteria.CriteriaProvider.Execute(),获得字典,然后执行真正的数据库操作。
下面就看看数据层的一些接口声明就清楚了。
/// <summary>
/// 数据提供者要实现的借口
/// </summary>
public interface IDataProvider
{
DataResult<TEntity> Add<TEntity>(TEntity entity) where TEntity : IDataEntity;
DataResult<TEntity> Add<TEntity>(List<TEntity> entityList) where TEntity : IDataEntity;
DataResult<TEntity> Update<TEntity>(TEntity entity) where TEntity : IDataEntity;
DataResult<TEntity> Update<TEntity>(List<TEntity> entityList) where TEntity : IDataEntity;
bool Update(ICriteria condiftion, object value);
DataResult<TEntity> Delete<TEntity>(TEntity entity) where TEntity : IDataEntity;
DataResult<TEntity> Delete<TEntity>(List<TEntity> entityList) where TEntity : IDataEntity;
bool Delete(ICriteria condiftion);
int GetCount(ICriteria condition);
DataResult<TEntity> GetOne<TEntity>(ICriteria condition) where TEntity : IDataEntity;
DataResult<TEntity> GetList<TEntity>(ICriteria condition) where TEntity : IDataEntity;
DataResult<TEntity> GetPageData<TEntity>(ICriteria condition, int pageIndex, int pageSize, ref int entityCount) where TEntity : IDataEntity;
List<object> GetCustomData(ICriteria condiftion);
}
4. 水到渠成
到这里,一切就很清楚了。如果有不明白的地方或者需要讨论的,大家可以给我留言。我会及时的解答。
5. 代码版本的说明
大家之后可以下载到代码的 V1.0版本,下载代码之后可以看到,这个V1.0的版本的代码中存在着CSLA的影子,确实是这样的。本框架在V1.0中确实融合了很多的开源框架,所以不要奇怪。对CSLA不懂,也是没有问题的。
下面我就讲述一下以后版本的一些特性和本系列文章的走向:
V1.0: 存在CLSA的影子。
V2.0:去掉CLSA的影子,重构,并且使用VS开发出DSL工具,图形化的业务类开发和代码的生成
V3.0: 设计和开发出一种基于领域的语言和这个语言的编译器。
V4.0: 设计和开发领域语言的代码IDE。
V5.0: 使用领域语言开发项目
其中使用领域的语言开发时 目标:例如,可能最后的代码是这样的写的:
Define a use who name is “xiaoyang”and email is “yangyang4502@yahoo.com.cn”
然后我们的领域语言编译器把上面的语句解释为代码,然后进行操作,最终的结果就是在数据库中的UserDetail表中添加了一条记录。
也就是我们梦寐以求的那种“自然语言编程”,是的业务人员来写程序。
文章以后的难度和深度会越来越大,我们一起加油,也希望大家及时的反馈。
呵呵,现在比较的兴奋,说句”胡话”:咱们搞开发的,不仅仅只是懂得使用别人的开源框架和开源技术,咱们自己也可以成为技术的创建者。共勉!
版权为小洋和博客园所有,欢迎转载,转载请标明出处给作者。
http://www.cnblogs.com/yanyangtian