简体   繁体   中英

LINQ to SQL - Select where starts with any of list

Working on a Linq-to-SQL project and observing some odd behavior with the generated SQL. Basically I have an array of strings, and I need to select all rows where a column starts with one of those strings.

using (SqlConnection sqlConn = new SqlConnection(connString))
{
    using (IdsSqlDataContext context = new IdsSqlDataContext(sqlConn))
    {
        //generated results should start with one of these.
        //in real code base they are obviously not hardcoded and list is variable length
        string[] args = new string[] { "abc", "def", "hig" };

        IQueryable<string> queryable = null;

        //loop through the array, the first time through create an iqueryable<>, and subsequent passes union results onto original
        foreach (string arg in args)
        {
            if (queryable == null)
            {
                queryable = context.IdsForms.Where(f => f.MatterNumber.StartsWith(arg)).Select(f => f.MatterNumber);
            }
            else
            {
                queryable = queryable.Union(context.IdsForms.Where(f => f.MatterNumber.StartsWith(arg)).Select(f => f.MatterNumber));
            }
        }

        //actually execute the query.
        var result = queryable.ToArray();
    }
}

I would expect the sql generated to be functionally equivalent to the following.

select MatterNumber 
from IdsForm 
where MatterNumber like 'abc%' or MatterNumber like 'def%' or MatterNumber like 'hig%'

But the actual SQL generated is below, notice 'hig%' is the argument for all three like clauses.

exec sp_executesql N'SELECT [t4].[MatterNumber]
FROM (
    SELECT [t2].[MatterNumber]
    FROM (
        SELECT [t0].[MatterNumber]
        FROM [dbo].[IdsForm] AS [t0]
        WHERE [t0].[MatterNumber] LIKE @p0
        UNION
        SELECT [t1].[MatterNumber]
        FROM [dbo].[IdsForm] AS [t1]
        WHERE [t1].[MatterNumber] LIKE @p1
        ) AS [t2]
    UNION
    SELECT [t3].[MatterNumber]
    FROM [dbo].[IdsForm] AS [t3]
    WHERE [t3].[MatterNumber] LIKE @p2
    ) AS [t4]',N'@p0 varchar(4),@p1 varchar(4),@p2 varchar(4)',@p0='hig%',@p1='hig%',@p2='hig%'

Looks like you're closing over the loop variable. This is a common gotcha in C#. What happens is that the value of arg is evaluated when the query is run , not when it is created.

Create a temp variable to hold the value:

foreach (string arg in args)
{
    var temp = arg;
    if (queryable == null)
    {
        queryable = context.IdsForms.Where(f => f.MatterNumber.StartsWith(temp)).Select(f => f.MatterNumber);
    }
    else
    {
        queryable = queryable.Union(context.IdsForms.Where(f => f.MatterNumber.StartsWith(temp)).Select(f => f.MatterNumber));
    }
}

You can read this Eric Lippert post about closing over a loop variable. As Eric notes at the top of the article, and as @Magus points out in a comment, this has changed in C# 5 so that the foreach variable is a new copy on each iteration. Creating a temp variable, like above, is forward compatible though.

The union is correct, due to you using union in your linq to sql query. The reason they are all hig% is because the lambda f => f.MatterNumber.StartsWith(arg) creates a closure around the loop parameter. To fix, declare a local variable in the loop

    foreach (string arg in args)
    {
        var _arg = arg;
        if (queryable == null)
        {
            queryable = context.IdsForms.Where(f => f.MatterNumber.StartsWith(_arg)).Select(f => f.MatterNumber);
        }
        else
        {
            queryable = queryable.Union(context.IdsForms.Where(f => f.MatterNumber.StartsWith(_arg)).Select(f => f.MatterNumber));
        }
    }

But I agree the union seems unnecessary. If the array of strings to check against is not going to change, then you can just use a standard where clause. Otherwise you could take a look at predicate builder! Check here

How about this ?

queryable = context..IdsForms.Where(f =>
            {
               foreach (var arg in args)
               {
                   if (f.MatterNumber.StartsWith(arg))
                      return true;
               }
               return false;
            }).Select(f => f.MatterNumber);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM