Search code examples
c#entity-frameworklinqkit

LinqKit Predicate Or with Contains evaluates to equals


I am implementing a search for my application. The search works except for when the user's search cascades by searching related entities. I have debugged the code and tested the SQL generated by Entity Framework. I found the problem is Contains() translates to a '=' in SQL when it should be 'LIKE'. Contains() works as intended for my initial predicate on FirstName, MiddleName etc. but not in the if (cascade) code block.

My C# search logic:

public IList<Individual> Find(string search, bool cascade, bool includeInactive)
{
    _context.Database.Log = s => System.Diagnostics.Debug.WriteLine(s);
    IQueryable<Individual> query = _context.Individuals;
    if (!string.IsNullOrWhiteSpace(search))
    {
        search = search.Trim();
        string[] searchParts = search.Split(' ');
        ExpressionStarter<Individual> predicate = PredicateBuilder.New<Individual>(false);
        foreach (string searchPart in searchParts)
        {
            predicate = predicate.Or(c =>
                c.FirstName.Contains(searchPart) || c.MiddleNames.Contains(searchPart) ||
                c.LastName.Contains(searchPart) || c.PreferredName.Contains(searchPart));
            if (cascade)
            {
                predicate = predicate.Or(c =>
                    c.IndividualOrganisationGroups.Select(og => og.OrganisationGroup).Select(g => g.Group.Name)
                        .Contains(searchPart));
            }
        }
        query = query.Where(predicate);
    }
    if (!includeInactive)
    {
        query = query.Where(c => c.Active);
    }
    return query.ToList();
}

The SQL generated from EF:

SELECT 
    [Extent1].[ID] AS [ID], 
    [Extent1].[RegistrationTypeID] AS [RegistrationTypeID], 
    [Extent1].[Title] AS [Title], 
    [Extent1].[FirstName] AS [FirstName], 
    [Extent1].[MiddleNames] AS [MiddleNames], 
    [Extent1].[LastName] AS [LastName], 
    [Extent1].[PreferredName] AS [PreferredName], 
    [Extent1].[RegistrationNumber] AS [RegistrationNumber], 
    [Extent1].[Username] AS [Username], 
    [Extent1].[AzureID] AS [AzureID], 
    [Extent1].[Notes] AS [Notes], 
    [Extent1].[Active] AS [Active], 
    [Extent1].[CreatedDate] AS [CreatedDate], 
    [Extent1].[CreatedBy] AS [CreatedBy], 
    [Extent1].[UpdatedDate] AS [UpdatedDate], 
    [Extent1].[UpdatedBy] AS [UpdatedBy]
    FROM [dbo].[Individual] AS [Extent1]
    WHERE ([Extent1].[FirstName] LIKE @p__linq__0 ESCAPE '~') OR ([Extent1].[MiddleNames] LIKE @p__linq__1 ESCAPE '~') OR ([Extent1].[LastName] LIKE @p__linq__2 ESCAPE '~') OR ([Extent1].[PreferredName] LIKE @p__linq__3 ESCAPE '~') OR ( EXISTS (SELECT 
        1 AS [C1]
        FROM   [dbo].[IndividualOrganisationGroup] AS [Extent2]
        INNER JOIN [dbo].[OrganisationGroup] AS [Extent3] ON [Extent2].[OrganisationGroupID] = [Extent3].[ID]
        INNER JOIN [dbo].[Group] AS [Extent4] ON [Extent3].[GroupID] = [Extent4].[ID]
        WHERE ([Extent1].[ID] = [Extent2].[IndividualID]) AND (([Extent4].[Name] = @p__linq__4) OR (1 = 0))
    ))
-- p__linq__0: '%screen%' (Type = AnsiString, Size = 8000)

-- p__linq__1: '%screen%' (Type = AnsiString, Size = 8000)

-- p__linq__2: '%screen%' (Type = AnsiString, Size = 8000)

-- p__linq__3: '%screen%' (Type = AnsiString, Size = 8000)

-- p__linq__4: 'screen' (Type = AnsiString, Size = 8000)

The problematic SQL (last WHERE clause):

WHERE ([Extent1].[ID] = [Extent2].[IndividualID]) AND (([Extent4].[Name] = @p__linq__4) OR (1 = 0))

What it should look like:

WHERE ([Extent1].[ID] = [Extent2].[IndividualID]) AND (([Extent4].[Name] LIKE @p__linq__4) OR (1 = 0))

So my question is, how do I make this code translate to a LIKE in my Entity Framework SQL?

predicate = predicate.Or(c =>
                    c.IndividualOrganisationGroups.Select(og => og.OrganisationGroup).Select(g => g.Group.Name)
                        .Contains(searchPart));

Solution

  • It has nothing to do with LINQKit PredicateBuilder. The reason is that the result of

    c.IndividualOrganisationGroups.Select(og => og.OrganisationGroup).Select(g => g.Group.Name)
    

    is IEnumerable<string> (or IQueryable<string>), so your are hitting the Enumerable (or Queryable) Contains method instead of the string.Contains as in the other places.

    Instead of Select + Contains, what you really need is the Any extension method.

    The most concise syntax with your sample is:

    c => c.IndividualOrganisationGroups
        .Any(og => og.OrganisationGroup.Group.Name.Contains(searchPart))
    

    Of course if you need to check more properties of the Group (or even w/o that), then use Select + Any:

    c => c.IndividualOrganisationGroups
        .Select(og => og.OrganisationGroup.Group)
        .Any(g => g.Name.Contains(searchPart))
    

    or the most natural way of turning collection with filter to single bool by writing the query in a "normal" way. e.g. Select, Where etc. and putting parameterless Any at the end:

    c => c.IndividualOrganisationGroups
        .Select(og => og.OrganisationGroup.Group)
        .Where(g => g.Name.Contains(searchPart))
        .Any())