none
Dynamic expression building RRS feed

  • Question

  • I am constructing some dynamic linq functionality for my clients, who would like to be able to select patients based on multiple selection criteria such as health riscs, diagnosis, vaccination, age..... In this context it is neccessary for to be able to dynamically build queries with joins.

    A query that returns patients with one or more diagnosis of "flu" whould require a join on the table containing the diagnosises.
    If I would write the linq expression at design time it would look like this:
    var patients = (from patient in patientDataSource.AsExpandable()
                          let diagnosises in diagnosisDataSource.Where(p=>p.DiagnosisPatient == patient.ID)
                          where diagnosised.Any(<<insert diagnosis selection expression>>)
                          select patient);

    I would like the join expression (the argument for the .Where() invocation to be dynamic and generic for all entities for wich an association is defined. Thus i can use the AssociationAttribute as information source that denotes the join relation.

    internal static AssociationAttribute DiscoverAssociationAttribute<TParentEntity, TChildEntity>()
        {
          // todo : add specific exception & type info to exception message
          Type desiredPropertyType = typeof (EntitySet<>).MakeGenericType (typeof (TChildEntity));
          PropertyInfo entitySetProperty = typeof (TParentEntity).GetProperties ().FirstOrDefault (propertyInfo => propertyInfo.PropertyType == desiredPropertyType);
    
          if( entitySetProperty == null )
          {
            throw new Exception (
              "The subquery is invalid because there is no EntitySet of the desired type for the child entities in the parent entity type.");
          }
    
          var obj = entitySetProperty.GetCustomAttributes (typeof (AssociationAttribute), false).FirstOrDefault ();
          if( obj == null )
          {
            throw new Exception (
              "The subquery is invalid because the EntitySet property of the parent entity is not associated (Attribute not found).");
          }
    
          return (AssociationAttribute)obj;
        }
    

    The static method above returns the AssociationAttribute between two entities, or throws an exception when none was found.

    The second step entails building an expression wich i would be able to use in the following code (as the join expression):
    var patients = (from patient in patientSource.AsExpandable() let diagnosises = diagnosisDataSource.Where(<<insert join expression>>) where diagnosises.Any(<<insert diagnosis expression>>) select patient)

    Thus far i have the following:

    using System;
    using System.Collections.Generic;
    using System.Data.Linq;
    using System.Data.Linq.Mapping;
    using System.Linq;
    using System.Linq.Expressions;
    using System.Reflection;
    using Accrimed.Datacontext;
    
    namespace Accrimed.QueryBuilder
    {
      internal static class QueryHelper
      {
        internal static AssociationAttribute DiscoverAssociationAttribute<TParentEntity, TChildEntity>()
        {
          // todo : add specific exception & type info to exception message
          Type desiredPropertyType = typeof (EntitySet<>).MakeGenericType(typeof (TChildEntity));
          PropertyInfo entitySetProperty =
            typeof (TParentEntity).GetProperties().FirstOrDefault(
              propertyInfo => propertyInfo.PropertyType == desiredPropertyType);
    
          if (entitySetProperty == null)
          {
            throw new Exception(
              "The subquery is invalid because there is no EntitySet of the desired type for the child entities in the parent entity type.");
          }
    
          object obj = entitySetProperty.GetCustomAttributes(typeof (AssociationAttribute), false).FirstOrDefault();
          if (obj == null)
          {
            throw new Exception(
              "The subquery is invalid because the EntitySet property of the parent entity is not associated (Attribute not found).");
          }
    
          return (AssociationAttribute) obj;
        }
    
        internal static Expression<Func<TParentEntity, TChildEntity, bool>> GetJoinExpression
          <TParentEntity, TChildEntity>()
        {
          // TODO: Add unit test for following use case: the association is defined between properties of type Nullabe<T> and typeof(T)
          // TODO: what if the value of Guid? == null   will the conversion fail?
          // desired behavior: the child entity whith associated property value equal to null should be skipped. 
          // maybe a conditional expression must be added?
    
          AssociationAttribute associationAttribute = DiscoverAssociationAttribute<TParentEntity, TChildEntity>();
    
          PropertyInfo parentKeyPropertyInfo = typeof (TParentEntity).GetProperty(associationAttribute.ThisKey);
          PropertyInfo childKeyPropertyInfo = typeof (TChildEntity).GetProperty(associationAttribute.OtherKey);
    
          ParameterExpression childParam = Expression.Parameter(typeof (TChildEntity), "child");
          ParameterExpression parentParam = Expression.Parameter(typeof (TParentEntity), "parent");
    
          Expression parentPropertyExpression = Expression.Property(parentParam, associationAttribute.ThisKey);
          Expression childPropertyExpression = Expression.Property(childParam, associationAttribute.OtherKey);
    
          if (childKeyPropertyInfo.PropertyType != parentKeyPropertyInfo.PropertyType)
          {
            // TODO: what if situation is reversed (parent propety is Nullable<T> and child propety == typeof(T) )
            // TODO: what if the property types are incomparable?
            // some more code is needed!!
            Type nullableParentType = typeof (Nullable<>).MakeGenericType(parentKeyPropertyInfo.PropertyType);
            if (childKeyPropertyInfo.PropertyType == nullableParentType)
            {
              childPropertyExpression = Expression.Convert(childPropertyExpression,
                                     parentKeyPropertyInfo.PropertyType);
            }
          }
          LambdaExpression parentKeySelector = Expression.Lambda(parentPropertyExpression, parentParam);
          LambdaExpression childKeySelector = Expression.Lambda(childPropertyExpression, childParam);
    
          Expression<Func<TParentEntity, TChildEntity, bool>> expression =
            Expression.Lambda<Func<TParentEntity, TChildEntity, bool>>(
              Expression.Equal(childKeySelector, parentKeySelector), new[] {childParam, parentParam});
    
          return expression;
        }
      }
    }
    

    The method GetJoinExpression<,> is the one that causes me loss of sleep :-)
    On execution an exception is thrown:

    System.InvalidOperationException: The binary operator Equal is not defined for the types 'System.Func`2[<<TParentEntity>>,System.Guid]' and 'System.
    Func`2[<<TChildEntity>>,System.Guid]'.

    It seems that the expression i have defined checks the equality of Func<TParentEntity,System.Guid> and Func<TChildEntity, System.Guid>.
    While the intended behavior is that the equality of the respective property values on instances of TParentEntity and TChildEntity.

    Is there someone out there who can kindly explain what i am doing wrong?

    Note: I use the LinqKit extension methods (PredicateBuilder, ...)

     

    Saturday, January 29, 2011 12:49 PM

Answers

  • Found it!

    The second to last statement of the GetJoinExpression<,>()  is now replaced by:

    Expression<Func<TParentEntity, TChildEntity, bool>> expression =
            Expression.Lambda<Func<TParentEntity, TChildEntity, bool>>(
              Expression.Equal(Expression.Invoke(parentKeySelector, parentParam), Expression.Invoke(childKeySelector, childParam)), new[] {parentParam, childParam });
    
    (notice the InvocationExpressions)
    • Marked as answer by c0nundrum Saturday, January 29, 2011 1:52 PM
    Saturday, January 29, 2011 1:52 PM

All replies

  • Found it!

    The second to last statement of the GetJoinExpression<,>()  is now replaced by:

    Expression<Func<TParentEntity, TChildEntity, bool>> expression =
            Expression.Lambda<Func<TParentEntity, TChildEntity, bool>>(
              Expression.Equal(Expression.Invoke(parentKeySelector, parentParam), Expression.Invoke(childKeySelector, childParam)), new[] {parentParam, childParam });
    
    (notice the InvocationExpressions)
    • Marked as answer by c0nundrum Saturday, January 29, 2011 1:52 PM
    Saturday, January 29, 2011 1:52 PM
  • Hi c0nundrum,

    I’m glad to hear that you got it working. Thank you for sharing your experience here.

    It will be very beneficial for other community members having the similar questions.

     

    Have a nice day.


    Alan Chen[MSFT]
    MSDN Community Support | Feedback to us
    Get or Request Code Sample from Microsoft
    Please remember to mark the replies as answers if they help and unmark them if they provide no help.

    Monday, January 31, 2011 12:59 AM
    Moderator