简体   繁体   中英

The || (or) Operator in Linq with C#

I'm using linq to filter a selection of MessageItems. The method I've written accepts a bunch of parameters that might be null. If they are null, the criteria for the file should be ignored. If it is not null, use it to filter the results.

It's my understanding that when doing an || operation is C#, if the first expression is true, the second expression should not be evaluated.

eg

if(ExpressionOne() || ExpressionTwo())
{
     // only ExpressionOne was evaluated because it was true
}

now, in linq, I'm trying this:

var messages = (from msg in dc.MessageItems
where  String.IsNullOrEmpty(fromname) || (!String.IsNullOrEmpty(fromname) && msg.FromName.ToLower().Contains(fromname.ToLower()))
select msg);

I would have thought this would be sound, because String.IsNullOrEmpty(fromname) would equal true and the second part of the || wouldn't get run.

However it does get run, and the second part

msg.FromName.ToLower().Contains(fromname.ToLower()))

throws a null reference exception (because fromname is null)!! - I get a classic "Object reference not set to an instance of an object" exception.

Any help?

Have a read of this documentation which explains how linq and c# can experience a disconnect.

Since Linq expressions are expected to be reduced to something other than plain methods you may find that this code breaks if later it is used in some non Linq to Objects context.

That said

String.IsNullOrEmpty(fromname) || 
(   !String.IsNullOrEmpty(fromname) && 
    msg.FromName.ToLower().Contains(fromname.ToLower())
)

Is badly formed since it should really be

String.IsNullOrEmpty(fromname) || 
msg.FromName.ToLower().Contains(fromname.ToLower())

which makes it nice and clear that you are relying on msg and msg.FromName to both be non null as well.

To make your life easier in c# you could add the following string extension method

public static class ExtensionMethods
{
    public static bool Contains(
        this string self, string value, StringComparison comparison)
    {
        return self.IndexOf(value, comparison) >= 0;
    }

    public static bool ContainsOrNull(
        this string self, string value, StringComparison comparison)
    {
        if (value == null)
            return false;
        return self.IndexOf(value, comparison) >= 0;
    }
}

Then use:

var messages = (from msg in dc.MessageItems
where  msg.FromName.ContainsOrNull(
    fromname, StringComparison.InvariantCultureIgnoreCase)
select msg);

However this is not the problem. The problem is that the Linq to SQL aspects of the system are trying to use the fromname value to construct the query which is sent to the server.

Since fromname is a variable the translation mechanism goes off and does what is asked of it (producing a lower case representation of fromname even if it is null, which triggers the exception).

in this case you can either do what you have already discovered: keep the query as is but make sure you can always create a non null fromname value with the desired behaviour even if it is null.

Perhaps better would be:

IEnumerable<MessageItem> results;
if (string.IsNullOrEmpty(fromname))
{ 
    results = from msg in dc.MessageItems 
    select msg;    
}
else
{
    results = from msg in dc.MessageItems 
    where msg.FromName.ToLower().Contains(fromname) 
    select msg;    
}

This is not so great it the query contained other constraints and thus invovled more duplication but for the simple query actually should result in more readable/maintainable code. This is a pain if you are relying on anonymous types though but hopefully this is not an issue for you.

Okay. I found A solution.

I changed the offending line to:

where (String.IsNullOrEmpty(fromemail)  || (msg.FromEmail.ToLower().Contains((fromemail ?? String.Empty).ToLower())))

It works, but it feels like a hack. I'm sure if the first expression is true the second should not get evaluated.

Would be great if anyone could confirm or deny this for me...

Or if anyone has a better solution, please let me know!!!

If you are using LINQ to SQL, you cannot expect the same C# short-circuit behavior in SQL Server. See this question about short-circuit WHERE clauses (or lack thereof) in SQL Server.

Also, as I mentioned in a comment, I don't believe you are getting this exception in LINQ to SQL because:

  1. Method String.IsNullOrEmpty(String) has no supported translation to SQL, so you can't use it in LINQ to SQL.
  2. You wouldn't be getting the NullReferenceException. This is a managed exception, it would only happen client-side, not in SQL Server.

Are you sure this is not going through LINQ to Objects somewhere? Are you calling ToList() or ToArray() on your source or referencing it as a IEnumerable<T> before running this query?


Update: After reading your comments I tested this again and realized some things. I was wrong about you not using LINQ to SQL. You were not getting the "String.IsNullOrEmpty(String) has no supported translation to SQL" exception because IsNullOrEmpty() is being called on a local variable, not an SQL column, so it is running client-side, even though you are using LINQ to SQL (not LINQ to Objects). Since it is running client-side, you can get a NullReferenceException on that method call, because it is not translated to SQL, where you cannot get a NullReferenceException .

One way to make your solution seem less hacky is be resolving fromname 's "null-ness" outside the query:

string lowerfromname = String.IsNullOrEmpty(fromname) ? fromname : fromname.ToLower();

var messages = from msg in dc.MessageItems
               where String.IsNullOrEmpty(lowerfromname) || msg.Name.ToLower().Contains(lowerfromname)
               select msg.Name;

Note that this will not always be translated to something like (using your comments as example):

SELECT ... FROM ... WHERE @theValue IS NULL OR @theValue = theValue

Its translation will be decided at runtime depending on whether fromname is null or not. If it is null, it will translate without a WHERE clause. If it is not null, it will translate with a simple " WHERE @theValue = theValue ", without null check in T-SQL.

So in the end, the question of whether it will short-circuit in SQL or not is irrelevant in this case because the LINQ to SQL runtime will emit different T-SQL queries if fromname is null or not. In a sense, it is short-circuited client-side before querying the database.

你确定它的'fromname'是null而不是'msg.FromName'是null吗?

就像Brian说的那样,在执行ToLower()之前,我会查看msg.FromName是否为null。包含(fromname.ToLower()))

You are correct that the second conditional shouldn't get evaluated as you are using the short-circuit comparitors (see What is the best practice concerning C# short-circuit evaluation? ), however I'd suspect that the Linq might try to optimise your query before executing it and in doing so might change the execution order.

Wrapping the whole thing in brackets also, for me, makes for a clearer statement as the whole 'where' condition is contained within the parenthases.

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