海豚宝宝的代码生活

专注于.Net系统开发

导航

EF杂记36:如何实施Construct by Query

当我在位MVC编写EF控制器的时候,我发现我经常想要创建和加载一个Stub entity(一个包含了关键属性的不完整实体对象,用于简化数据库查询的实体对象),但很不幸的是,这样的动作并不容易实现,首先你必须要保证你所要加载的对象之前没有被加载过,同时你还要面对很多的错误。为了避免这些异常和错误,我经常要写如下的代码:

Person assignedTo = FindPersonInStateManager(ctx, p => p.ID == 5);
if (assignedTo == null)
{
     assignedTo = new Person{ID = 5};
     ctx.AttachTo(“People”, assignedTo); 
}
bug.AssignedTo = assignedTo;

但是上面的代码比较笨拙,同时它通过全局搜索等方式污染了我的业务逻辑,从而给代码的读写带来困难。我希望能够像这样的写这段代码:

bug.AssignedTo = ctx.People.GetOrCreateAndAttach(p => p.ID == 5);

现在已经有不少的方法能够使得上面的想法有可能实现,但是问题的关键就是如何转换下面这段表达式:

(Person p) => p.ID == 5;

这段代码将被定义为如下的形式:

() => new Person {ID = 5};

这段代码是一段包含了MemberInitExpression 内容的Lambda表达式。

Query By Example

我们记忆中,在ORM盛行的时代,它们经常使用一种叫做Query by Example的模式:

Person result = ORM.QueryByExample(new Person {ID = 5});

上面的这个查询(Query by Example)中创建了一个你希望从数据库中获取的实体对象,给它设置了部分的属性,ORM就利用这个只包含了部分属性的实体对象创建查询。

Construct By Query?

我在这里提到这点是因为从查询到实体的过程看上去很像从实体到查询的反向过程(就是我们前面所说的Query by Example)。因此这个帖子的标题也设置为“Construct by Query’”。

实现

我们如何实现上面的想法:

我们首先要探索的是,我们需要一个方法在ObjectStateManager中寻找一个实体。

public static IEnumerable<TEntity> Where<TEntity>(
    this ObjectStateManager manager,
    Func<TEntity, bool> predicate
) where TEntity: class
{
    return manager.GetObjectStateEntries(
        EntityState.Added |
        EntityState.Deleted |    
        EntityState.Modified |
        EntityState.Unchanged
    )
   .Where(entry => !entry.IsRelationship)
   .Select(entry => entry.Entity)
   .OfType<TEntity>()
   .Where(predicate);
}

然后我们编写GetOrCreateAndAttachStub(…) 这个扩展方法:

public static TEntity GetOrCreateAndAttachStub<TEntity>(
    this ObjectQuery<TEntity> query,
    Expression<Func<TEntity, bool>> expression
) where TEntity : class
{
    var context = query.Context;
    var osm = context.ObjectStateManager;
    TEntity entity = osm.Where(expression.Compile())
                        .SingleOrDefault();
    if (entity == null)
    {
        entity = expression.Create();
        context.AttachToDefaultSet(entity);
    }
    return entity;
}

这个函数将在 ObjectStateManager中搜寻匹配项。

如果什么也没有找到,它将把这段表达式转换成包含MemberInitExpression 内容的Lambda表达式,然后被编译和调用,获取一个TEntity实体,并且被附加。

我将不在这里提供AttachToDefaultSet方法的代码,请参考杂记13中的相关内容。

创建扩展方法:

public static T Create<T>(
    this Expression<Func<T, bool>> predicateExpr)
{
    var initializerExpression = PredicateToConstructorVisitor
                                    .Convert<T>(predicateExpr);
    var initializerFunction = initializerExpression.Compile();
    return initializerFunction();
}

Where PredicateToConstructorVisitor is a specialized ExpressionVisitor that just converts from a predicate expression to an MemberInitExpression.

public class PredicateToConstructorVisitor
{
    public static Expression<Func<T>> Convert<T>(
        Expression<Func<T, bool>> predicate)
    {
        PredicateToConstructorVisitor visitor = 
           new PredicateToConstructorVisitor();
        return visitor.Visit<T>(predicate);
    }
    protected Expression<Func<T>> Visit<T>(
        Expression<Func<T, bool>> predicate)
    {
        return VisitLambda(predicate as LambdaExpression) 
           as Expression<Func<T>>;
    }
    protected virtual Expression VisitLambda(
        LambdaExpression lambda)
    {
        if (lambda.Body is BinaryExpression)
        {
            // Create a new instance expression i.e.
            NewExpression newExpr =
               Expression.New(lambda.Parameters.Single().Type);
            BinaryExpression binary = 
               lambda.Body as BinaryExpression;
            return Expression.Lambda(
                    Expression.MemberInit(
                        newExpr,
                        GetMemberAssignments(binary).ToArray()
                    )
                );
        }
        throw new InvalidOperationException(
            string.Format(
               "OnlyBinary Expressions are supported.\n\n{0}",
               lambda.Body.ToString()
            )
        );
    }
    protected IEnumerable<MemberAssignment> GetMemberAssignments(
         BinaryExpression binary)
    {
        if (binary.NodeType == ExpressionType.Equal)
        {
            yield return GetMemberAssignment(binary);
        }
        else if (binary.NodeType == ExpressionType.AndAlso)
        {
            foreach (var assignment in 
              GetMemberAssignments(binary.Left as BinaryExpression).Concat(GetMemberAssignments(binary.Right as BinaryExpression)))
            {
                yield return assignment;
            }
        }
        else
            throw new NotSupportedException(binary.ToString());
    }
    protected MemberAssignment GetMemberAssignment(
        BinaryExpression binary)
    {
        if (binary.NodeType != ExpressionType.Equal)
            throw new InvalidOperationException(
               binary.ToString()
            );
        MemberExpression member = binary.Left as MemberExpression;
        ConstantExpression constant
           = GetConstantExpression(binary.Right);
        if (constant.Value == null)
            constant = Expression.Constant(null, member.Type);
        return Expression.Bind(member.Member, constant);
    }
    protected ConstantExpression GetConstantExpression(
        Expression expr)
    {
        if (expr.NodeType == ExpressionType.Constant)
        {
            return expr as ConstantExpression;
        }
        else
        {
            Type type = expr.Type;
            if (type.IsValueType)
            {
                expr = Expression.Convert(expr, typeof(object));
            }
            Expression<Func<object>> lambda
               = Expression.Lambda<Func<object>>(expr);
            Func<object> fn = lambda.Compile();
            return Expression.Constant(fn(), type);
        }
    }
}

真正的工作在 VisitLambda方法中完成。

如果出现下列情况将抛出异常:

  1. Lambda表达式不是BinaryExpression.
  2. Lambda表达式有超过一个的参数. 这里只能接受一个参数!

Then we go about the job of walking the BinaryExpression until we get to Equal nodes i.e. (p.ID == 5) which we convert to MemberAssignments (ID = 5) so we can construct a MemberInitExpression.

When creating the MemberAssignments we convert all Right hand-sides to constants too. i.e. so if the lambda looks like this:

(Person p) => p.ID == GetID();

we evaluate GetID(), so we can use the result in our MemberAssignment.

Summary

Again I’ve demonstrated that mixing EF Metadata and CLR Expressions makes it possible to write really useful helper methods that take a lot of the pain out writing your apps.

posted on 2010-08-31 17:43  Bruse  阅读(338)  评论(0)    收藏  举报