views:

760

answers:

3

In a Linq Statement like the following (narrowed down to relevant parts for the question):

var sites = from s in DataContext.Sites
            join add in DataContext.Address 
              on s.PrimaryAddress equals add into sa
 from a in sa.DefaultIfEmpty()
            select new {
                        s.Id,
                        s.SiteName,
                        PrimaryAddress = a
                       };

The problem we have is that a control ultimately based on a GridView/LinqDataSource combination cannot sort correctly on the PrimaryAddress joined class (ignores it). We see this same behavior for all joined classes like this. Is there any way for GridView to handle this? Or alternatively, is there a way via Expressions that we can handle it in code in a dynamic OrderBy?

Additional notes: When adding .OrderBy(s => s.PrimaryAddress) we get Cannot order by type 'Address', when doing .OrderBy(gs => gs.PrimaryBusinessAddress.AddressLine1) we get Specified method is not supported. When doing the sort on the LinqDataSource.OrderBy itself, it's too late...it only sorts the records on the current page and not the overall set.

For clarity, this is the format of the Address in the grid:

public partial class Address
{
  public override string ToString()
  {
    return string.Format("{0} {1}{2} {3}",
    AddressLine1,
    City,
    !string.IsNullOrEmpty(State) && State != "None" ? ", " + State : string.Empty,
    !string.IsNullOrEmpty(Country) ? string.Format("({0})", Country) : string.Empty);
  }
}

If we could sort by AddressLine1 + City + State + Country, that would be good enough, but I'm unsure of how to do this via an expression tree...no matter what OrderBy we specify via expressions, it reverts to sorting by s.SiteName (the default sort on the grid). There are a very limited number of joined classes like this that show in our grid control, having an switch and Expression case for each would not be a problem at all. Any thoughts on a solution, or completely different approach?

A: 

When adding .OrderBy(s => s.PrimaryAddress) we get Cannot order by type 'Address'

That's because PrimaryAddress only exists in the code. The LINQ statement is going to be going out and getting all the rows to make an address, then merging them together in code. Have you tried some form of .OrderBy(a => a.Address) ? Namely, referencing the columns of the sub-table instead of your derived name.

JustLoren
We have, this results in the same behavior of the grid sorting only the current page. Since the actual generated SQL is a wrapped sub-query to restrict table rows returned, it's only ordering the already restricted subset. (Rows 1-25 the grid will display)
Nick Craver
So you're doing .Take(25) *before* appending the .OrderBy?
JustLoren
No, this is something the LinqDataSource does on it's own. The onSelecting event is where we tie this query to the LinqDataSource, if I have the above and .OrderBy(s => s.PrimaryAddress), the OrderBy still happens outside the Take(25), even though it's being called well before. The LinqDataSource.OnSelecting is this simple: e.Result = theLinqQueryAbove
Nick Craver
Ok, I see what you're saying now. Peronsally I don't work with LinqDataSources, so I was assuming some incorrect behavior. I'm guessing theres no way to sneak that OrderBy in prior to the Take(25)?
JustLoren
+3  A: 

I'm used to using Linq to Entities in code, but I use LINQ to SQL often enough in LINQPad that I've become fairly familiar with it. I'm not entirely sure I understand where you're running into difficulties, but I think the following should work:

var sites = from s in DataContext.Sites
            orderby s.PrimaryAddress.AddressLine1,
                     s.PrimaryAddress.City,
                     s.PrimaryAddress.State,
                     s.PrimaryAddress.Country
            select new 
            {
                s.Id,
                s.SiteName,
                s.PrimaryAddress
            };

Let me know if there's something that I'm not understanding.

Update

I'm not sure why this isn't working for you. I just did the following in LINQPad (LINQ to SQL mode):

from p in person
orderby p.clue_type.clue_type_id,
        p.clue_type.clue_type
select new
{
    p.person_id, p.clue_type
}

The results all had clue_type = null. LINQ to SQL just treats null references as values with all-null properties. Here's the generated SQL:

SELECT TOP (10) [t0].[person_id], [t2].[test], [t2].[clue_type_id], [t2].[clue_type]
FROM [person] AS [t0]
LEFT OUTER JOIN (
    SELECT 1 AS [test], [t1].[clue_type_id], [t1].[clue_type]
    FROM [clue_types] AS [t1]
    ) AS [t2] ON [t2].[clue_type_id] = [t0].[clue_type_id]
ORDER BY [t2].[clue_type_id], [t2].[clue_type]

Notice the LEFT OUTER JOIN. Will this not do what you're asking for?

Update 2

Making the query dynamic might be fairly difficult, depending on how dynamic you're making it. Here's one solution if you want to be able to order by any of the properties that you're returning, based on a string value that gets passed into your method:

public class SiteDisplayInfo
{
    public int Id {get;set;}
    public string SiteName {get;set;}
    public string PrimaryAddress {get;set;}

    public static readonly Dictionary<string, Func<IQueryable<Site>, IOrderedQueryable<Site>>> OrderByFuncs = 
    new Dictionary<string, Func<IQueryable<Site>, IOrderedQueryable<Site>>>
    {
        {"Id", q => q.OrderBy(s => s.Id)},
        {"SiteName", q => q.OrderBy(s => s.SiteName)},
        {"PrimaryAddress", 
        q => q.OrderBy(s => s.PrimaryAddress.AddressLine1)
                             .ThenBy(s => s.PrimaryAddress.City)}
    };
}

...

public IEnumerable<SiteDisplayInfo> GetSites(string orderByString)
{
    IQueryable<Site> sites = DataBase.Sites;
    if (orderByString != null && SiteDisplayInfo.OrderByFuncs.ContainsKey(orderByString))
    {
        sites = SiteDisplayInfo.OrderByFuncs[orderByString](sites);
    }
    var query = from s in sites
                select new SiteDisplayInfo
                {
                    Id = s.Id,
                    SiteName = s.SiteName,
                    PrimaryAddress = s.PrimaryAddress.AddressLine1 + s.PrimaryAddress.City
                };
    return query.ToList();
}

There are a few other ways to do something similar, but this gives you a general idea.

StriplingWarrior
The problem is with the joined tables, if we sort on the original table, it would be in the form you say .OrderBy(s => s.PrimaryAddress.AddressLine1). However, since it needs PrimaryAddress in that case, it restricts the result set to only those where PrimaryAddress is not null, leaving off 90% of the sites in our case. That was my first thought to solve the problem, but it doesn't...also it results in an extra left join (non-outer) just to do the OrderBy...which is why it leaves off all the null PrimaryAddress sites. Perhaps it acts different in L2E but the left join kills it for us.
Nick Craver
I don't know if we're working on different versions of .NET or what, but LINQ to SQL is not behaving that way for me. See my update.
StriplingWarrior
What does it look like when you join the child object? The problem doesn't arise here when we did it your style, however the SQL output does differ greatly, instead of the expected left join what we actually see is only the sort table is left joined, the other children are queried 1 by 1 based on ID since they're first referenced in the anonymous type. This means we have (Children-1)n+1 queries instead of 1....can't go that route for sure. Aside from that, we need a *dynamic* solution, depending on what the user clicked, hopefully without a hand coded linq query for ever column possibility
Nick Craver
I wonder if when you narrowed down your example to its relevant parts, you may have narrowed it down beyond its relevant parts? Your sample LINQ statement led me to believe that you're just dealing with Sites and Addresses, but it sounds like you're actually dealing with other tables as well. Can you give additional information regarding your schema and the query that you're actually trying to do? Regarding a dynamic solution, I will update the answer shortly to show how you can do that.
StriplingWarrior
When LINQ does a query with a join, it uses the join for the sake of the query itself, but does not necessarily pull all the data in the join across the wire. It is your select statement that determines which data will actually get returned from the database, which is why I have my select statement include all the data I'm looking to use. If you have additional data that you want to have included in your LINQ query, make sure you add that data in your select statement, or LINQ to SQL will wait until you try to access that object and then query for it, resulting in (n) database roundtrips.
StriplingWarrior
+2  A: 

I just tried a similar setup to what you have, this should work:

var sites = from s in dc.Sites
    join addr in dc.Addresses
        on s.PrimaryAddress equals addr into sa
    from a in sa.DefaultIfEmpty()
    orderby a.AddressLine1, a.City, a.State, a.Country /* insert orderby here */
    select new
        {
            s.Id,
            s.SiteName,
            PrimaryAddress = a
        };

If you want to change the orientation of the orderby, just add descending to every column:

orderby a.AddressLine1 descending, a.City descending, a.State descending, a.Country descending

Although this approach works, it's not exactly dynamic. You would have to rewrite the query to sort in another orientation or column order.

If this is something you want, I would advise using either the method approach with some additional plumbing, or the fully dynamic approach using a dynamic OrderBy that allows you to specify the column name as a literal. For the latter check this topic and this blogpost.

Yannick M.
Same result as StriplingWarrior posted...the result is only the 25 returned (first 25 sites) are sorted, and unfortunately it's issuing a query every row to fetch the address resulting in the children*n+1 performance problem.
Nick Craver