object88
object88

Reputation: 760

Filtering OData request with inherited entities causes casting exception

I feel like I'm asking a lot of questions, but I keep getting stuck. I am developing an OData service, and I want an entity that can have several user-designated name-value pairs associated, which can then be searched against. I am using EF4.3, DataServiceVersion 3.0. I am using a custom Metadata, Query, and Update provider.

So let's say that I have an entity Person:

public class Person : EntityBase
{
    public virtual IList<Property> PropertySet { get; set; }
}

(EntityBase is a common POCO that all my entities come from; it has only a Guid ID property.) Now let's define our Property:

public abstract class Property : EntityBase
{
    public string Name { get; set; }
    public Person Person { get; set; }
    public Guid PersonId { get; set; }
}

public class IntProperty : Property
{
    public int? IValue { get; set; }
}

public class StringProperty : Property
{
    public string SValue { get; set; }
}

So far, so good. In my configuration, I am using Table Per Hierarchy for inheritance.

Now, I am able to add a Property to my Person, and when I make a request like this:

GET /Service/People(guid'THE_ID')?$expand=PropertySet

It works:

{"d": {
  "__metadata": {...},
  "PropertySet": {
    "results": [{
      "__metadata": {...},
      "Id": "PROP_1_ID",
      "Name": "Number",
      "IValue": 1234
    },{
      "__metadata": {...},
      "Id": "PROP_2_ID",
      "Name": "EmailAddress",
      "SValue": "AAAA"
    }]
  },
  "Id": "THE_ID",
  }
}

If I query for a Person that has a property named 'EmailAddress', that works:

GET /Service/People?$expand=PropertySet&$filter=PropertySet/any(x: x/Name eq 'EmailAddress')

But even for that, I had to pull a few tricks. I implemented an expression visitor, and whacked out a few comparisons that Linq To Entities didn't seem to like:

protected override Expression VisitBinary(BinaryExpression node)
{
  if (node.NodeType == ExpressionType.Equal)
  {
    Expression left = Visit(node.Left);
    Expression right = Visit(node.Right);

    ConstantExpression rightConstant = right as ConstantExpression;
    if (null != rightConstant && rightConstant.Value == null)
    {
      if (left.Type == typeof(IList<Property>))
      {
        return Expression.Constant(false, typeof(bool));
      }
    }
  }
  return base.VisitBinary(node);
}

protected override Expression VisitConditional(ConditionalExpression node)
{
  Expression visitedTest = Visit(node.Test);
  Expression visitedIfTrue = Visit(node.IfTrue);
  Expression visitedIfFalse = Visit(node.IfFalse);
  ConstantExpression constantTest = visitedTest as ConstantExpression;
  if (null != constantTest && constantTest.Value is bool)
  {
    return ((bool)constantTest.Value) ? visitedIfTrue : visitedIfFalse;
  }
  return Expression.Condition(visitedTest, visitedIfTrue, visitedIfFalse);
}

The gist is with the first override my query gets expressions such as "it.PropertySet == null", which I know will always be untrue. (In my spike, the only thing that has PropertySet is a Person, and a Person always has a PropertySet.) In the second override, I'm looking at expressions such as "IIF((it.PropertySet == null), Empty(), it.PropertySet)", and I know that "it" will always have a PropertySet. This prevents errors comparing an IList against null.

Now, the problem.

Merely searching for the presence of a Property isn't enough. I would like to check it's value:

GET /Service/People?$expand=PropertySet&$filter=PropertySet/any(x: x/Name eq 'EmailAddress' and cast(x, 'InheritedPropertyTest.Entities.StringProperty')/SValue eq 'AAAA')

And this is the resulting query:

value(System.Data.Objects.ObjectQuery`1[InheritedPropertyTest.Entities.Person])
.MergeAs(AppendOnly)
.Where(it => it.PropertySet.Any(x => ((x.Name == "EmailAddress") AndAlso (IIF((Convert(x) == null), null, Convert(x).SValue) == "AAAA"))))
.OrderBy(p => p.Id)
.Take(100)
.Select(p => new ExpandedWrapper`2() {ExpandedElement = p, Description = "PropertySet", ReferenceDescription = "", ProjectedProperty0 = p.PropertySet.OrderBy(p => p.Id)    .Take(100)})

But I get this error: "Unable to cast the type 'InheritedPropertyTest.Entities.Property' to type 'InheritedPropertyTest.Entities.StringProperty'. LINQ to Entities only supports casting Entity Data Model primitive types." So... now I'm stuck beating my head against the wall again. Perhaps my inheritance isn't set up correctly? Do I need to overload some other Expression Visitor method to make the Convert work? How do I convince Linq To Entities to work with inherited properties?

Thanks!

Upvotes: 2

Views: 1985

Answers (1)

Vitek Karas MSFT
Vitek Karas MSFT

Reputation: 13320

Instead of cast, use the type segment. So for example x/InheritedPropertyTest.Entities.StringProperty/SValue That should be translated to a TypeOf which EF should be able to handle.

Note though that custom provider over EF will have lot of problems anyway. One way to simplify the expressions is to turn of null propagation (IDataServiceQueryProvider.IsNullPropagationRequired = false). That should get rid of the IFF(i==null, null, something). But you will still run into problems especially when you start using projections ($select).

Upvotes: 1

Related Questions