다음을 통해 공유


Tip 36 – How to Construct by Query

While writing my tips series and writing EF controllers for MVC I found that I regularly wanted to create and attach a stub entity.

Unfortunately it isn’t quite that simple, you have to make sure the entity isn’t already attached first, otherwise you’ll see some nasty exceptions.

To avoid these exceptions I often found myself having to write code like this:

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

But that code is cumbersome, it pollutes my business logic with a whole heap of EF plumbing, which makes it hard to read and write.

I found myself wishing I could write code like this instead:

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

Now there is a lot of plumbing to make this possible, but the core problem is converting something like this:

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

which is a predicate or query into something like this

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

which is a LambdaExpression with a MemberInitExpression body.

Query By Example

Now those of you familiar with the history of ORMs might remember that in the ‘good old days’ a lot of ‘ORMs’ used a pattern called Query by Example:

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

With Query by Example you create an instance of the thing you want back from the database, fill in some fields, and the ORM uses this example object to create a query based on the values that have been set.

Construct By Query?

I bring this up, because the process of going from a Query to an Instance, looks like the exact opposite of going from an Instance to a Query (aka Query by Example).

Hence the title of this blog post: ‘Construct by Query’.

For me this analogy / comparison makes this idea all the more beautiful.

But hey that’s me!

Implementation

Anyway… so how do we actually do this:

Well first the plumbing, we need a method to look for an entity in the 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);
}

Then we actually write the GetOrCreateAndAttachStub(…) extension method:

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;
}

This looks in the ObjectStateManager for a match.

If nothing is found it converts the predicate expression into an LambdaExpression with a MemberInitExpression body, which is then compiled and invoked to create an instance of TEntity and attach it.

I’m not going to go into the AttachToDefaultSet method because I’ve shared the code for that previously in Tip 13.

So lets skip that and get right to…

The guts of the problem

The Create extension method, looks like this:

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);
}
}
}

 

The real work is done in VisitLambda.

Basically it throws if:

  1. The LambdaExpression isn’t a BinaryExpression.
  2. There is more than one parameter to the LambdaExpression. We can only construct one thing!

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.

Enjoy…

Comments

  • Anonymous
    September 25, 2009
    AlexThanks a lot for the real life issues you are covering in your blog!
  • Anonymous
    October 01, 2009
    I did something similar. For every entity in my model, I created a "CreateStub" method. This in turns call an extension method that attaches if it does not exist in the state manager.Friend Shared Function CreateStub(ByVal documentId As Integer, ByRef ctx As Entities) _       As Documents       Dim obj As Documents = New Documents() With {.DocumentId = documentId}       obj.EntityKey = ctx.CreateEntityKey(ctx.Documents.EntitySetName, obj)       Return ctx.AttachIfNotExists(obj)   End Function<Extension()> _   Public Function AttachIfNotExists(ByRef ctx As ObjectContext, ByVal entity As IEntityWithKey) _       As IEntityWithKey       Dim existingObj = ctx.GetObjectByKey(entity.EntityKey)       If existingObj IsNot Nothing Then           Return existingObj       Else           ctx.Attach(entity)           Return entity       End If   End Function
  • Anonymous
    October 03, 2009
    Here's a similar approach without hitting the db:       private static EntityKey GetKey<TEntity>(ObjectContext context, TEntity entity)       {           var fqName = String.Format("{0}.{1}", context.DefaultContainerName, entity.GetType().Name);           EntityKey key = context.CreateEntityKey(fqName, entity);           return key;       }       public static TEntity Attach<TEntity>(ObjectContext context, TEntity entity)       {           EntityKey key = GetKey(context, entity);           ObjectStateEntry ose;           bool res = context.ObjectStateManager.TryGetObjectStateEntry(key, out ose);           if (res)               return (TEntity)ose.Entity;           context.AttachTo(key.EntitySetName, entity);           return entity;       }
  • Anonymous
    October 04, 2009
    @Cankut,Your approach is cool. But I think you are missing something, because my approach isn't touching the database either...Alex
  • Anonymous
    October 04, 2009
    @AlexJ,Thats for sure, i forgot to mention that it was in reply to Osa's post.
  • Anonymous
    October 06, 2009
    @Cankut. I pointed out the problem. Thanks for pointing out. I did not notice it before.  GetObjectByKeyhttp://msdn.microsoft.com/en-us/library/system.data.objects.objectcontext.getobjectbykey.aspx