1

I have a search repository for EntityFramework 4.0 using LinqKit with the following search function:

public IQueryable<T> Search<T>(Expression<Func<T, bool>> predicate) 
    where T : EntityObject
{
    return _unitOfWork.ObjectSet<T>().AsExpandable().Where(predicate);
}

And another class which uses the IQueryable return value to subset the query in ways that are not possible using the Boolean LinqKit PredicateBuilder expressions:

public IQueryable<T> SubsetByUser<T>(IQueryable<T> set, User user) 
    where T : EntityObject
{
    return set.Join(_searcher.Search<Metadatum>((o) => o.UserGUID == user.GUID),
                    arc => arc.GUID,
                    meta => meta.ElementGUID,
                    (arc, meta) => arc);
}

The problem here is that 'T' as EntityObject does not define GUID, so I can't use this. The natural response to this is to actually define the SubsetByUser() method to use a constraint with a GUID property:

public IQueryable<T> SubsetByUser<T>(IQueryable<T> set, User user) 
    where T : IHaveMetadata
{
    return set.Join(_searcher.Search<Metadatum>((o) => o.UserGUID == user.GUID),
                    arc => arc.GUID,
                    meta => meta.ElementGUID,
                    (arc, meta) => arc);
}

But this doesn't work. I'm using LinqKit and the Expandable() method results in:

System.NotSupportedException: Unable to cast the type 'Oasis.DataModel.Arc' to
type 'Oasis.DataModel.Interfaces.IHaveMetadata'. LINQ to Entities only supports 
casting Entity Data Model primitive types

I need an IQueryable to be returned. I can do a fake like this:

public IQueryable<T> SubsetByUser<T>(IQueryable<T> set, User user) 
    where T : EntityObject
{
    return set.AsEnumerable()
              .Join(_searcher.Search<Metadatum>((o) => o.UserGUID == user.GUID),
                    arc => arc.GUID,
                    meta => meta.ElementGUID,
                    (arc, meta) => arc)
              .AsQueryable();
}

Which, of course, works, but which is also, of course, a bat-shit crazy thing to do. (The whole reason I want IQueryable is to not execute the query until we're final.

I've even tried this:

public IQueryable<T> SubsetByUser<T>(IQueryable<T> set, User user) 
    where T : EntityObject
{
    return set.Join(_searcher.Search<Metadatum>((o) => o.UserGUID == user.GUID),
                    arc => arc.GetType().GetProperty("GUID").GetValue(arc,null),
                    meta => meta.ElementGUID,
                    (arc, meta) => arc);
}

Which uses reflection to get the Runs collection– working around the compiler error. I thought that was rather clever, but it results in the LINQ exception:

System.NotSupportedException: LINQ to Entities does not recognize the 
method 'System.Object GetValue(System.Object, System.Object[])' method, 
and this method cannot be translated into a store expression.

I could also try to change the search method:

public IQueryable<T> Search<T>(Expression<Func<T, bool>> predicate) 
    where T : IRunElement
{
    return _unitOfWork.ObjectSet<T>().AsExpandable().Where(predicate);
}

But this won't compile of course because IRunElement is not an EntityObject, and ObjectSet constrains T as a class.

The final possibility is simply making all the parameters and return values IEnumerable:

public IEnumerable<T> SubsetByUser<T>(IEnumerable<T> set, User user) 
    where T : EntityObject
{
    return set.Join(_searcher.Search<Metadatum>((o) => o.UserGUID == user.GUID),
                    arc => arc.GetType().GetProperty("GUID").GetValue(arc,null),
                    meta => meta.ElementGUID,
                    (arc, meta) => arc);
}

Which also works, but which, again, doesn't allow us to delay instantiation until the end.

So, it seems like there's little I can do to make this work without instantiating everything as an IEnumerable and then returning it using AsQueryable(). Is there some way I can put this together that I'm missing?

JohnMetta
  • 18,782
  • 5
  • 31
  • 57
  • Would it be possible to show a little bit more code on how you used this SubsetByUser function(where you would call it from your query). Trying to do something similar. – AaronLS May 31 '12 at 21:07
  • @AaronLS, I'm not on this project anymore, but basically– IIRC, the set argument is the returnset from another query. So it's just _subsetContainer.SubsetByUser(_container.Search(predicate), someUser); – JohnMetta Jun 15 '12 at 16:56

1 Answers1

2
The natural response to this is to actually define the SubsetByUser() method to use a constraint with a GUID property: ... But this doesn't work. I'm using LinqKit and the Expandable() method results in: System.NotSupportedException: Unable to cast the type 'Oasis.DataModel.Arc' to type 'Oasis.DataModel.Interfaces.IHaveMetadata'. LINQ to Entities only supports casting Entity Data Model primitive types

You're very close with that. You can make this work if you use an ExpressionVisitor that removes all unnecessary casts (casts to a base type or an implemented interface) that are automatically generated.

public IQueryable<T> SubsetByUser<T>(IQueryable<T> set, User user) 
    where T : IHaveMetadata
{
    Expression<Func<T, Guid>> GetGUID = arc => arc.GUID;
    GetGUID = (Expression<Func<T, Guid>>)RemoveUnnecessaryConversions.Instance.Visit(GetGUID);
    return set.Join(_searcher.Search<Metadatum>((o) => o.UserGUID == user.GUID),
        GetGUID,
        meta => meta.ElementGUID,
        (arc, meta) => arc);
}

public class RemoveUnnecessaryConversions : ExpressionVisitor
{
    public static readonly RemoveUnnecessaryConversions Instance = new RemoveUnnecessaryConversions();

    protected RemoveUnnecessaryConversions() { }

    protected override Expression VisitUnary(UnaryExpression node)
    {
        if (node.NodeType == ExpressionType.Convert
            && node.Type.IsAssignableFrom(node.Operand.Type))
        {
            return base.Visit(node.Operand);
        }
        return base.VisitUnary(node);
    }
}

Alternatively, create the expression tree manually using the Expression.* functions, so that you can avoid including the cast in the first place.

  • This is perfect. Exactly what I wanted. Thank you so much! – JohnMetta Feb 02 '12 at 23:38
  • @hvd I think I'm trying to do something similar. Can you show an example of how you'd call SubsetByUser in a query? I'm confused since it's not an extension method. I'm trying to create a method that can be used by linq2entities: http://stackoverflow.com/questions/10826275/iqueryable-extension-method-for-linq2entities – AaronLS May 31 '12 at 15:30
  • @AaronLS You'd use `from x in SubsetByUser(...) ... select x` -- it's not what you're trying to do. This is a function which returns a query. You're trying to call a function from within a query. (That said, I've since found out that my answer is needlessly complicated, and will update when I can.) –  May 31 '12 at 19:46
  • @hvd Where does SubsetByUser get declared? In a static class? The method is not static? I have only figured out how to use linqkit to declare a local expression variable called asOfCurrent so I can do `.Where(asOfCurrent)`. This is a step in the right direction. So I want to make it a little more globally available, and also be able to take a parameter(a DateTime), similar to how the above takes a User parameter. – AaronLS May 31 '12 at 20:13
  • @AaronLS It could have been a static function, except for the `_searcher` that this function uses that (as far as I can tell) is specific to this question. I'd guess that it's an instance method on the context. In your case, it could well be a static method in any class, static or nonstatic, that you want. –  May 31 '12 at 20:20
  • @hvd Errors about linq 2 entities not recognizing the method. uusually not a problem because it'd be chained onto an AsExpandable call or with .Invoke, but can't figure out how to get it to work here, or what to pass to the `set` parameter since it's already `from x` but since it's not an extension method the first parameter is not a `this` and so doesn't automatically get the current query. – AaronLS May 31 '12 at 21:07