views:

120

answers:

4

I have a LINQ query running with multiple joins and I want to pass it around as an IQueryable<T> and apply additional filters in other methods.

The problem is that I can't work out how to pass around a var data type and keep it strongly typed, and if I try to put it in my own class (EG: .Select((a,b) => new MyClass(a,b))) I get errors when I try to add later Where clauses because my class has no translations into SQL. Is there any way I can do one of the following:

  1. Make my class map to SQL?
  2. Make the var data-type implement an interface (So I can pass it round as though it's that)?
  3. Something I haven't though of that'll solve my issue?

Example:

public void Main()
{
    using (DBDataContext context = new DBDataContext())
    {
      var result = context.TableAs.Join(
         context.TableBs,
         a => a.BID,
         b => b.ID,
        (a,b) => new {A = a, B = b}
      );
      result = addNeedValue(result, 4);
   }
}

private ???? addNeedValue(???? result, int value)
{
    return result.Where(r => r.A.Value == value);
}

PS: I know in my example I can flatten out the function easily, but in the real thing it'd be an absolute mess if I tried.

A: 

You are returning what's known as an 'anonymous type' from your join - it's not a specific database type, but is instead just an object containing A and B. Because the type is anonymous, I don't think you're going to be able to write a function that returns that type - or a collection of that type.

If it were a type that SQL / LINQ-to-SQL knew about then you'd probably be able to do this.

One way to get LINQ-to-SQL to recognize the type might be to create a stored procedure which performs the join and selects the relevant columns. If you add that stored procedure to your DBML file, LINQ-to-SQL will understand the type of each 'row' in the result. You should then be able to have your function return an IQueryable<ResultSetRow>.

Kirk Broadhurst
Hey Kirk, I grabbed the data type from an existing stored procedure and gave this a shot but no dice. I also tried checking it's designer and implementing some `System.Data.Linq.Mapping.Column` attributes on my own class based on it, but that didn't help either. Kept getting no supported translation errors. Nice idea but I suspect stored procedures aren't bound back to the database like a table is (And I can't copy a table cause I got fields from multiple DB tables in my data).
Tim Schneider
I'm surprised that the stored procedure method didn't work. My only other suggestion is similar - try creating a View. Views are treated very similarly to tables in LINQ-to-SQL and again there will be a generated class for each 'row'. Ultimately I think the (perhaps only?) way you'll achieve what you want is to 'trick' the designer into creating the mapping for you. Creating the mapping yourself is a real challenge.
Kirk Broadhurst
+2  A: 

All you have to do is de-anonymize your type that you're constructing as a result of the query - new { A = a, B = b } Just create a class with those properties and name it appropriately. Then your query will have a type of IQueryable<your type>

Like so:

public class MyClass 
{ 
    public int A { get; set; }
    public int B { get; set; }
}

public void Main()
{
    using (DBDataContext context = new DBDataContext())
    {
      var result = context.TableAs.Join(
         context.TableBs,
         a => a.BID,
         b => b.ID,
        (a,b) => new MyClass {A = a, B = b}
      );
      result = addNeedValue(result, 4);
   }
}

private IQueryable<MyClass> addNeedValue(IQueryable<MyClass> result, int value)
{
    return result.Where(r => r.A.Value == value);
}
Fyodor Soikin
Hey Fyodor, that was my first though, but as soon as I do this I get `The member 'Test.MyClass.A' has no supported translation to SQL.` as soon as I try to execute the query (EG: Call `result.ToList()`). I've tried trawling over the dynamic type that's created with reflection and I can't see any reason it'd work when mine doesn't, but yeah...
Tim Schneider
The runtime won't know how your type relates to the SQL - just because it has the same properties doesn't mean that it will be able to use them the same way. There may be some interface or attribute you can use but I doubt it; you need a class that contains all the LINQ-to-SQL stuff.
Kirk Broadhurst
Kirk, how can it work with the var keyword if it's not possible to create a class that will work? Is the anonymous class just treated specially by LINQ or? Also do you have a link to somewhere that'd give an overview of how one would go about putting in all the LINQ-to-SQL stuff for a class that's the result of a join? (Only things I can find all map back to the same DB table)
Tim Schneider
It won't work with the var keyword! You can't have parameters that accept 'var' parameters or return 'var' - you need to explicitly provide the type. I'm not convinced you can do what you want to do with LINQ-to-SQL (looks like a job for Entity Framework), and I have never tried manually creating those DBML classes. Very messy.
Kirk Broadhurst
Hey Kirk, yeah I know I can't pass a var to a function, but with the above if I use `result = result.Where(r => r.A.Value == 4);` in the place of the function call it works, but if I use my own class it wont even if I don't use a function call. That's why I was hoping to find some magic in the dynamic class I could steal but I can't find anything :P
Tim Schneider
I messed up, this is correct. Thanks to Michael's comment above I have realized this does in fact work. I had indeed been using a constructor rather than the member initialization. Once I fixed that up this did indeed work and the end where clause is definitely translated to SQL and not ran locally, so this is 100% what I'm after.
Tim Schneider
Oh... Sorry I didn't reply, was too busy in the recent days. Glad it finally worked out. :-)
Fyodor Soikin
A: 

I definitely don't recommend it, but you can probably use dynamic here, if you're on .NET 4.0.

Joe Enos
Interesting idea. I'm not on .NET 4.0, only 3.0, but I still don't think it'd work for the same reasons reflection wouldn't (That is, I'm pretty sure once I use reflection or dynamics there's very little chance it'll convert down to SQL properly)
Tim Schneider
+1  A: 

Here's two different approaches; the first applies the filter before doing the Join, since the joined queries don't have to just be basic tables. The second approach applies the filter after the join, using an intermediary projection (and changing it to return the predicate, rather than applying it internally).

This has been tested successfully on .NET 3.5 and .NET 4; note that in 3.5 (SP1) the Expression.Invoke (for the second example) won't work on EF, but is fine for LINQ-to-SQL.

If you want to run the example, I've used Northwind (just because that is what I had locally):

using System;
using System.Linq;
using System.Linq.Expressions;
using ConsoleApplication1; // my data-context's namespace
static class Program
{
    public static void Main()
    {
        using (var context = new TestDataContext())
        {
            context.Log = Console.Out; // to check it has worked
            IQueryable<Order> lhs = context.Orders;
            IQueryable<Order_Detail> rhs = context.Order_Details;
            // how ever many predicates etc here
            rhs = addBeforeJoin(rhs, 4);

            var result = lhs.Join(rhs,
                   a => a.OrderID,
                   b => b.OrderID,
                  (a, b) => new { A = a, B = b }
            );
            // or add after
            result = result.Where(row => row.B, addAfterJoin(100));
            Console.WriteLine(result.Count());
        }
    }

    private static IQueryable<Order_Detail> addBeforeJoin(IQueryable<Order_Detail> query, int value)
    {
        return query.Where(r => r.Quantity >= value);
    }
    private static Expression<Func<Order_Detail, bool>> addAfterJoin(int value)
    {
        return r => r.Quantity <= value;
    }
    private static IQueryable<TSource> Where<TSource, TProjection>(
        this IQueryable<TSource> source,
        Expression<Func<TSource, TProjection>> selector,
        Expression<Func<TProjection, bool>> predicate)
    {
        return source.Where(
            Expression.Lambda<Func<TSource, bool>>(
            Expression.Invoke(predicate, selector.Body),
            selector.Parameters));
    }

}
Marc Gravell
Nice idea. I like the point that you can indeed filter prior to the join, and very cool to see you can traverse an object using an extension method like that so you can make your where clause on a child class. Ended up going with Fyodor's answer cause it worked once I took Michael's advice and seems slightly more elegant (Though not as powerful).
Tim Schneider