Reputation: 1087
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%'
Upvotes: 2
Views: 4776
Reputation: 101701
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);
Upvotes: 0
Reputation: 21887
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.
Upvotes: 3
Reputation: 3253
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
Upvotes: 1