Usar una tupla o algún otro tipo complejo en una expresión de consulta Linq-to-Entities

So I want to search within a table of customers all of the customers which in each has its name, email address, or phone numbers match all of the query keywords.

... Which is probably easier to understand in code than in English:

public IQueryable<Contact> SearchCustomers(string query)
{
    var ws = from w in query.Split()
                where !String.IsNullOrWhiteSpace(w)
                select w;

    var q =
        from c in Customers
        where ws.All(w =>
                c.FirstName == w
                || c.LastName == w
                || c.EmailAddress == w
                || c.HomePhone == PhoneNumber.Pack(w)
                || c.CellPhone == PhoneNumber.Pack(w))
        select c;

    return q;
}

But I can't call PhoneNumber.Pack on the database, so I need to make w a format which will store both the raw value of w así como la Packed value, and I have to do that on the client's side. The problem is that Linq doesn't like having tuples or arrays in the expression arguments, and it doesn't support String.IndexOf, so I can't throw two strings in one and then take substrings.

Any other ways to get around this? Or maybe a restatement of the query?

Edit: The generated SQL looks like this:

SELECT 
[Extent1].[ID] AS [ID], 
[Extent1].[FirstName] AS [FirstName], 
[Extent1].[LastName] AS [LastName], 
(etc)
FROM [dbo].[Contacts] AS [Extent1]
WHERE ( NOT EXISTS (SELECT 
    1 AS [C1]
    FROM  ( SELECT 1 AS X ) AS [SingleRowTable1]
    WHERE ( NOT ([Extent1].[FirstName] = N'rei' OR [Extent1].[LastName] = N'rei' OR [Extent1].[EmailAddress] = N'rei' OR [Extent1].[HomePhone] = N'rei' OR [Extent1].[CellPhone] = N'rei')) OR (CASE WHEN ([Extent1].[FirstName] = N'rei' OR [Extent1].[LastName] = N'rei' OR [Extent1].[EmailAddress] = N'rei' OR [Extent1].[HomePhone] = N'rei' OR [Extent1].[CellPhone] = N'rei') THEN cast(1 as bit) WHEN ( NOT ([Extent1].[FirstName] = N'rei' OR [Extent1].[LastName] = N'rei' OR [Extent1].[EmailAddress] = N'rei' OR [Extent1].[HomePhone] = N'rei' OR [Extent1].[CellPhone] = N'rei')) THEN cast(0 as bit) END IS NULL)
))

preguntado el 08 de enero de 11 a las 21:01

BTW, how did you get the generated SQL? Profiler? -

4 Respuestas

public IQueryable<Contact> SearchCustomers(string query)
{
    var ws = from w in query.Split()
                where !String.IsNullOrWhiteSpace(w)
                select new { Unpacked = w , Packed = PhoneNumber.Pack(w) };

    var q = Customers;
    foreach(var x in ws)
    {
        string ux = x.Unpacked;
        string px = x.Packed;
        q = q.Where(
               c=> 
                c.FirstName == ux
                || c.LastName == ux
                || c.EmailAddress == ux
                || c.HomePhone == px
                || c.CellPhone == px
            );
    }
    return q;
}

This will produce the desired result, and temp variable inside foreach will resolve your issue.

contestado el 27 de mayo de 11 a las 14:05

Well, that's a lot easier than I was making it. Thanks! Wish I could split the bounty. - Rei Miyasaka

I'd create a private struct:

private struct UnpackedAndPacked
{
    public string Unpacked {get;set;}
    public string Packed {get;set;}
}

var ws = from w in query.Split()
         where !String.IsNullOrWhiteSpace(w)
         select new UnpackedAndPacked
                    {
                        Unpacked=w, 
                        Packed=PhoneNumber.Pack(w)
                    };  

Then change the condition:

    where ws.All(w => 
                 c.FirstName == w.Unpacked
                  || c.LastName == w.Unpacked
                  || c.EmailAddress == w.Unpacked
                  || c.HomePhone == w.Packed
                  || c.CellPhone == w.Packed)
    select c;

I looked into this further, and I think you're not going to get this done as-is. The problem is that, because of the ws.All, it wants to create a set of SQL clauses once for each value in the ws sequence. It needs that to be a sequence of primitive types, like string.

If you could change your code to have two query parameters, then I think it might work. You'd need one set of parameters for the things that don't need packing, and one for those that do. You would then change this into a LINQ methods chain and do a Union between the two. Example to follow.


It worked. My code is below. Note that I used the AdventureWorks2008R2 database, so mine is a bit more complicated than yours - I have a collection of email addresses and of phones to deal with; a match on either of those is accepted:

public static IQueryable<Person> SearchCustomers(
    AdventureWorksEntities entities, string nameQuery, string phoneQuery)
{
    var wsu = from w in nameQuery.Split()
        where !String.IsNullOrWhiteSpace(w)
        select w;
    var wsp = from w in phoneQuery.Split()
        where !String.IsNullOrWhiteSpace(w)
        select Pack(w);
    return
        entities.People.Where(
            c => wsu.All(w => c.FirstName == w || c.LastName == w)).
            Union(
                entities.People.Where(
                    c =>
                    wsp.All(
                        w =>
                        c.PersonPhones.Any(p => p.PhoneNumber == w) ||
                        c.EmailAddresses.Any(a => a.EmailAddress1 == w))));
}

Note also that I found another way to get trace output:

IQueryable<Person> query = SearchCustomers(entities, "w1 w2",
                                           "(602) (408)");
var oc = (ObjectQuery<Person>) query;
Console.WriteLine(oc.ToTraceString());

Respondido el 10 de enero de 11 a las 04:01

Bleh, same error: System.NotSupportedException: Unable to create a constant value of type 'SearchWord'. Only primitive types ('such as Int32, String, and Guid') are supported in this context. - Rei Miyasaka

@Rei: you know, that kinda makes sense. Ask yourself how you would do this in just plain SQL. Wouldn't you have to pass the word list as a table, even if you didn't have the issue with Pack? In fact, does the query work if you compare the phone numbers directly to w? - John Saunders

Yeah, it does make sense, but there must be a query that'll do the same thing. Pack just gets rid of non-alphanumeric characters so that the numbers are compared consistently. It's sort of necessary. - Rei Miyasaka

@Rei: but what happens if you remove the packing and just use w? Does it work? And I wonder what SQL it generates for that? - John Saunders

Oh, yeah, it works fine, to the extent that it'll compile, run, and return the right entries so long as I give it a packed phone number. Of course the problem then is that it screws up the other queries. - Rei Miyasaka

Tenga en cuenta que query.Where(a).Where(b) es el mismo que query.Where(a & b), and qry.All() is essentially taking a range of conditions and chaining together AND statements, something like (word 1 is found) && (word 2 is found) && (word 3 is found)...

You can use that to do the following (I'm using extension methods, so that I can chain this onto the end of any other IQueryable<Customer>).

    [System.Runtime.CompilerServices.Extension()]
    public static IQueryable<Customer> Search(this IQueryable<Customer> query, string searchTerm)
    {
        string[] queryWords = searchTerm.Split(" ");

        foreach (string w in queryWords) {
            string word = w;
            string packedWord = Pack(word);

            query = query.Where(c => c.FirstName == word || c.LastName == word || c.HomePhone == packedWord || c.CellPhone == packedWord);
        }
        return query;
    }

Or VB equivalent

<System.Runtime.CompilerServices.Extension()>
Public Function Search(query As IQueryable(Of Customer), searchTerm As String) As IQueryable(Of Customer)
    Dim queryWords = searchTerm.Split(" ")

    For Each w In queryWords
        Dim word = w
        Dim packedWord = Pack(word)

        query = query.Where(Function(c) c.FirstName = word OrElse
                                c.LastName = word OrElse
                                c.HomePhone = packedWord OrElse
                                c.CellPhone = packedWord)
    Next
    Return query
End Function

contestado el 24 de mayo de 11 a las 04:05

I would split it into 2 methods:

  • SearchCustomer
  • SearchCustomerPhoneNumber

In SearchCustomerPhoneNumber you convert the parameter to packed before doing the query.

Since the phone number will not contain letters and the others will, it is possible to check which of the methods should be run. The splitting will actually reduce the load on the database.

contestado el 24 de mayo de 11 a las 00:05

Phone numbers can contain letters, e.g. 1-800-flowers - Rei Miyasaka

Then you would probably have to run both methods each time, spilting would still give you a much simpler program - Shiraz Bhaiji

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.