Parallelisation in .NET 4.0 – Part 2 Throwing Exceptions

With more threads running simultaneously in an application there is increasing complexity when it comes to debugging. When exceptions are thrown you usually catch them somewhere and handle them. But what happens if you throw an exception inside a thread?

Naturally, if you can handle the exception within the thread then that makes life much easier. But what if an exception bubbles up and out into code that created the thread?

In the example in my previous post on Parallelisation in .NET 4.0 had the calls to a third party service happening in separate threads. So, what happens if somewhere in the call an exception is raised.

In the service call GetAvailability, I’ve simulated some error conditions to throw exceptions based on the input to illustrate the examples. This is what it looks like:

public HotelAvail GetAvailability(string hotelCode, DateTime startDate, int nights)
{
    // Throw some exceptions depending on the input.
    if (hotelCode == null)
        throw new ArgumentNullException("hotelCode");

    if ((hotelCode.Length > 10) || (hotelCode.Length == 0))
        throw new ArgumentOutOfRangeException(
            "Hotel Codes are 1 to 10 chars in length. Got code which was " +
            hotelCode.Length + " chars.");

    if (hotelCode.StartsWith("Z"))
        throw new AvailabilityException("Hotel code '" + hotelCode +
                                        "' does not exist"); // A custom exception type
    // ... etc. ...
}

The calling code, from the previous example, looks like this:

public IEnumerable<HotelAvail> GetAvailability(IEnumerable<string> codes,
        DateTime startDate, int numNights)
{
        return codes.AsParallel().Select(code =>
            new AvailService().GetAvailability(code, startDate, numNights))
            .ToList();
}

If we provide incorrect input into the service such that it causes exceptions to be raised then Visual Studio responds in the normal way by breaking the debugging session at the point closest to where the exception is thrown.

If we were to wrap the call to the service in a try catch block (as in the following code sample) then we’d except that Visual Studio wouldn’t break the debugging session as there is a handler (the catch block) for the exception.

public IEnumerable<HotelAvail> GetAvailabilityPlinqException(IEnumerable<string> codes,
        DateTime startDate, int numNights)
{
    try
    {
        return codes.AsParallel().Select(code =>
            new AvailService().GetAvailability(code, startDate, numNights))
            .ToList();
    }
    catch (Exception ex)
    {
        // Do stuff to handle the exception.
    }
    return null;
}

Normally, that would be the case, however if the handler is outside the thread that threw the exception, as in the above example, the situation is somewhat different. In this case the Exception Assistant will appear and highlight the exception (or the code nearest the exception if it can’t highlight the throw statement itself*)

AvailabilityException in Exception Assistant

This happens because the exception is not caught within the thread in which it was originally thrown.

The AggregateException

If you just tell the debugger to continue executing the application it will continue, but the code that created the threads will have to handle an AggregateException. This is a special exception class that contains an InnerExceptions (note the plural) property that contains all the exceptions thrown from each of the threads.

AggregateException.InnerExceptions

You can enumerate over each of the inner exceptions to find out what happened in each of the threads.

Be aware, however, that an Aggregate exception can, itself, contain an AggregateException. So simply calling InnerExceptions may yet yield another AggregateException. For example if the hierarchy of exceptions looks like this:

AggregateException Hierarchy

Then the results of iterating over the InnerExceptions will be:

foreach(Exception ex in aggregateException.InnerExceptions)
{
    // ... do stuff ...
}
  • AggregateException
  • ApplicationException

You can flatten the hierarchy into a single AggregateException object that doesn’t contain InnerExceptions with any additional AggregateException objects. To do this call Flatten() on the original AggregateException. This returns a new AggregateException which you can then call InnerExceptions on and not have to worry about any hierarchy.

For example:

foreach(Exception ex in aggregateException.Flatten().InnerExceptions)
{
    // ... do stuff ...
}

Which results in the following exceptions being enumerated by the loop:

  • ApplicationException
  • NullReferenceException
  • ArgumentException
  • DivideByZeroException

But it’s broken, why doesn’t it just stop?

Well, it does. Once a thread has thrown an exception that bubbles up and out then no new tasks are started, so no new threads are created, and no new work gets done. However, remember that there will be other threads running as well and if one breaks, maybe others will break too, or maybe they will complete successfully. We won’t know unless they are allowed to finish what they are doing.

Going back to the room availability example if the input hotel codes contain invalid codes then it will throw an exception that is not caught within the thread. What if a selection of good and bad hotel codes are passed:

1, 2, 3, Z123, 4, 5, 6, 1234567890ABC, 7, 8, 9

Of the above list “Z123” and “1234567890ABC” are both invalid and produce different exceptions. However, when running tests the AggregateException only contains one of the exceptions.

To show what happens, I’ve modified my “service” like this and run it through a console applications. Here’s the full code:

The service class

public class AvailService
{
    // ...

    public HotelAvail GetAvailability(string hotelCode, DateTime startDate, int nights)
    {
        Console.WriteLine("Start @ {0:HH-mm-ss.fff}: {1}", DateTime.Now, hotelCode);

        ValidateInput(hotelCode);

        // ... do stuff to process the request ...

        Console.WriteLine("  End @ {0:HH-mm-ss.fff}: {1}", DateTime.Now, hotelCode);
        return result;
    }

    private void ValidateInput(string hotelCode)
    {
        if (hotelCode == null)
        {
            Console.WriteLine("Error @ {0:HH-mm-ss.fff}: hotelCode is null", DateTime.Now);
            throw new ArgumentNullException("hotelCode");
        }

        if ((hotelCode.Length > 10) || (hotelCode.Length == 0))
        {
            Console.WriteLine("Error @ {0:HH-mm-ss.fff}: hotelCode is {1}", DateTime.Now, hotelCode);
            throw new ArgumentOutOfRangeException(
                "Hotel Codes are 1 to 10 chars in length. Got code which was " +
                hotelCode.Length + " chars.");
        }

        if (hotelCode.StartsWith("Z"))
        {
            Console.WriteLine("Error @ {0:HH-mm-ss.fff}: hotelCode is {1}", DateTime.Now, hotelCode);
            throw new AvailabilityException("Hotel code '" + hotelCode +
                                            "' does not exist");
        }
    }
}

The method on the controller class

public IEnumerable<HotelAvail> GetAvailability(IEnumerable<string> codes,
        DateTime startDate, int numNights)
{
    return codes.AsParallel().Select(code =>
        new AvailService().GetAvailability(code, startDate, numNights))
        .ToList();
}

The Main method on the Program class

static void Main(string[] args)
{
    string[] codes = "1,2,3,Z123,4,5,6,1234567890ABC,,7,8,9".Split(',');
    AvailController ctrl = new AvailController();

    DateTime start = DateTime.Now;
    try
    {
        var result = ctrl.GetAvailability(codes,
            DateTime.Today.AddDays(7.0), 2);
    }
    catch (AggregateException aex)
    {
        Console.WriteLine(aex.Message);

        foreach (Exception ex in aex.InnerExceptions)
            Console.WriteLine(" -- {0}", ex.Message);

    }
    finally
    {
        DateTime end = DateTime.Now;
        Console.WriteLine("Total time in ms: {0}",
                            (end - start).TotalMilliseconds);

    }
}

And the console output is:

Start @ 16-36-36.518: 7
Start @ 16-36-36.518: Z123
Start @ 16-36-36.518: 6
Start @ 16-36-36.518: 1
Error @ 16-36-36.526: hotelCode is Z123
  End @ 16-36-42.438: 1
  End @ 16-36-42.654: 6
  End @ 16-36-42.900: 7
One or more errors occurred.
 -- Hotel code 'Z123' does not exist
Total time in ms: 6400

As you can see only 4 items got started out of an initial input collection of 11 items. The error occurred 8ms after these items started. Those items that did not cause an error were allowed to continue to completion. The result variable in the Main method will never have anything because of the exception so we never get the results of the three items that did succeed.

Naturally, the best course of action is not to let the exception bubble up and out of the thread in which the code is executing.

 

 

* Note, there appears to be a bug in Visual Studio with the Exception Assistant not always highlighting the correct line of code.

Tip of the day #20: Don't spam your own email while developing apps that send email

When we develop applications, often there will be a requirement for that application to send out emails. While this is going on we usually end up with lots of emails being sent to our own email address for test purposes.

I got this fantastic tip from a colleague of mine, Andy Gibson, so here it is:

If you want to test the email an application sends out without spamming your inbox you can modify your web.config with the following code so that it will save the emails to your machine as flat files rather than sending them through the SMTP client. If you combine this with ASP.NET 4 build configurations (web.config.release, web.config.debug, etc,) then this becomes even niftier.

<system.net>
  <mailSettings>
    <smtp deliveryMethod="SpecifiedPickupDirectory">
      <specifiedPickupDirectory pickupDirectoryLocation="D:Email"/>
      <network host="localhost"/> <!-- Required for .NET 4.0! -->
    </smtp>
  </mailSettings>
</system.net>

There is an added benefit to this, .NET saves it as a .eml file so your default mail client (in my case Outlook 2007) will open it on double click, or if you need to see the raw email including headers, you can open it in notepad.

Parallelisation in .NET 4.0 – Part 1 looping

In an upcoming project we have a need for using some parallelisation features. There are two aspects to this, one is that we have to make multiple calls out to a web service that can take some time to return and in the meantime we have to get data out of the CMS to match up to the data coming back from the web service.

I’ll be writing a series (just for the irony of it) of posts on these new features in .NET and how we will be implementing them.

The problem

We have a web service that we have to call to get data back to our system. Calls to the web service take in the region of 4 to 7 seconds each to return data to us. The performance of the web service does not degrade significantly if we make multiple calls to it.

The Solution (first attempt)

Since the calls to the web service are not altering state we can safely make those calls in parallel. There is a class called AvailService that calls the web service and gets the results back to us. If you’re interested the web service checks on the availability of rooms in a hotel based on the the hotel code you pass and the stay date range (expressed as a start date and number of nights).

What we could have done in a serial implementation is this:

public IEnumerable<hotelAvail> GetAvailabilitySerial(
    IEnumerable<string> codes, DateTime startDate, int numNights)
{
    List result = new List<hotelAvail>();

    foreach (string code in codes)
    {
        AvailService service = new AvailService();
        HotelAvail singleResult = service.
                GetAvailability(code, startDate, numNights);
        result.Add(singleResult);
    }

    return result;
}

This simply goes around each item and gets the availability of rooms in that hotel for the stay date range. It could be refactored into a LINQ expression, but I’m going to leave it as a fuller loop just to show more clearly what’s going on.

Using Parallel.For

When we change this to a parallel it doesn’t really change much. I’ve changed the list to an array which is set up with the correct size of the result set and each parallel iteration only interacts with one slot in the array.

public IEnumerable<HotelAvail> GetAvailability (
    IList<string> codes, DateTime startDate, int numNights)
{
    HotelAvail[] result = new HotelAvail[codes.Count];

    Parallel.For(0, codes.Count, i =>
        {
            string code = codes[i];
            result[i] = new AvailService().
                GetAvailability(
                    code, startDate, numNights);
        });

    return result;
}

The code in the lambda expression is the part that is parallelised. I’ve had to make some concessions here as well. The IEnumerable of codes is now an IList, this is because I need to be able to access specific indexes into the list in order to align it with the array. That way the there is no accidental overwriting of elements in the result set.

If the ordering of the output is important, (e.g. must be the same order as the input) then this will maintain that ordering. Many of the remaining solutions do not maintain the order.

However, there is a better way to do this that doesn’t involve setting up and maintaining structures in this way and it closer to our serial code.

Using a Parallel.ForEach and a ConcurrentBag

public IEnumerable<HotelAvail> GetAvailabilityConcurrentCollection(
    IEnumerable<string> codes,
    DateTime startDate, int numNights)
{
    ConcurrentBag<HotelAvail> result = new ConcurrentBag();

    Parallel.ForEach(codes, code => result.Add(
        new AvailService().
            GetAvailability(code, startDate, numNights)));

    return result;
}

The content of the Parallel.ForEach here is almost identical to the serial foreach version, except that the code is slightly more terse. However, this time I’m using a ConcurrentBag for the result collection. As the method has always returned an IEnumerable this will not change the return type of the method, meaning that a serial method can be made parallel (assuming other considerations necessary for parallelism are taken into account) fairly easily.

The order of the results may be quite different from the order of the input.

Using PLINQ

Finally, I’ve refactored the code using PLINQ. This example is really quite terse, but if you understand LINQ then it should be very easy to pick up.

public IEnumerable<HotelAvail> GetAvailabilityPlinq(
    IEnumerable<string> codes,
    DateTime startDate, int numNights)
{

    return codes.AsParallel().Select(code =>
        new AvailService().GetAvailability(code, startDate, numNights))
        .ToList();
}

Again, the order of the result may be quite different from the order of the input. If ordering is important to you you can instruct PLINQ to maintain the ordering by using .AsOrdered(). This ensures that the output from the PLINQ expression is in the same order as the input.

e.g.

 

public IEnumerable<HotelAvail> GetAvailabilityPlinq(
    IEnumerable<string> codes,
    DateTime startDate, int numNights)
{

    return codes.AsParallel().AsOrdered().Select(code =>
        new AvailService().GetAvailability(code, startDate, numNights))
        .ToList();
}

The important part to all this is the call to the service, which has remained the same throughout all the samples. It is really just the infrastructure around that call that has changed over these examples.

The results

This table and graph show the results of some tests I ran between the serial and parallel versions. The numbers across the top row and X-axis represent the number of calls to the service. The numbers in the table and Y-Axis represent the time (in seconds) to complete the calls.

 

1 2 3 4 5 6 7 8 9
Serial 5.52 11.22 16.72 22.60 28.59 34.32 40.32 45.67 51.64
Parallel 5.52 6.20 6.35 6.45 11.24 12.35 12.45 12.86 16.66

Parallel vs Serial processing

 

Finally, a warning about using parallelised code from Alex Mackey’s book Introducing .NET 4.0:

“Although parallelization enhancements make writing code to run in parallel much easier, don’t underestimate the increase in complexity that parallelizing an application can bring. Parallelization shares many of the same issues you might have experiences when creating multithreaded applications. You must take care when developing parallel applications to isolate code that can be parallelized.”

Tip of the Day #19: Create a list of objects instead of many lists of values

I?ve been reviewing some code and I came across something that jars. What is wrong with this is many-fold. Essentially, instead of encapsulating an related data into an entity that describes the whole the developer had created silos of data values, and you’d better hope that nothing went awry with any of it.

It looked like this:

List<string> addOnNames = new List<string>();
List<string> addOnDescriptions = new List<string>();
List<string> addOnCodes = new List<string>();
List<decimal> addOnBasePrice = new List<decimal>();

Okay, so if you don’t yet find it jarring, here are some of the things that are wrong with this structure.

To iterate through and operate on a single “add on” you have to do something like this:

for(int i = 0; i < addOnNames.Count; i++)
{
    string name = addOnNames[i];
    string description = addOnDescriptions[i];
    string code = addOnCodes[i];
    decimal basePrice = addOnCode[i];
    // Do stuff that operates on an "Add On" here
}

That?s quite a bit of work just to get at the values you need in a particular loop iteration.

If you need to pass the lists on to methods, you have to pass each in its own parameter. Method signatures start to become needlessly large. In the simplified example above (yes, the real code had a lot more in it) there are three extra parameters that need to be passed around to each method call that needs to act on a collection of add-ons.

private void DoSomethingToACollectionOfAddOns(
                     List<string> addOnNames,
                      List<string> addOnDescriptions,
                      List<string> addOnCodes,
                     List<decimal> addOnBasePrice)
{
    // Do something.
}

It is also quite difficult to use LINQ to query what is effectively an AddOn entity.

Unless you are very careful, your lists can become out of sync, and when that happens all sorts of strange and difficult to trace bugs enter into the system. In the code I was reviewing there was an edge case where by one of the lists didn?t get updated when it was initially populated. Because all the lists are assumed to be the same length the first time that they were iterated over, what should have been the final entity couldn?t be retrieved on one of the lists because it didn?t exist. As this happened well after the creation of the lists and in a method called several levels deep it was quite a job working out where the original bug was coming from.

Solution

What needs to happen here is that an entity class is created for an add on. Each of these lists indicates a field or property in an entity class. The class might simply look something like this:

public class AddOn{
    public string Name { get; set; }
    public string Description { get; set; }
    public string Code { get; set; }
    public decimal BasePrice { get; set; }
}

This encapsulates everything about a single add on entity in one place. If you want a collection of these objects you can do something like this:

    List<AddOn> addOns = new List<AddOn>();

If you want to loop over them you don?t have to write lots of code to get all the elements out of a variety of lists a simple foreach will suffice (unless you need to also know the index)

    foreach(AddOn addOn in addOns){}

If you need to pass the entity around, or a collection of the entities around then you only need to pass one parameter into a method.

    private void DoSomethingToACollectionOfAddOns(List<AddOn> addOns) {}

Using LINQ becomes much easier because now you have everything encapsulated in the one place. I can?t even imagine the convoluted joins that would be needed to process the individual values in a LINQ expression otherwise.

When creating the initial collection, if any particular property is not needed then it can be simply ignored, if need be a default value can be set in the constructor. Then you never have an issue with one collection being out of sync with another. You no longer have to worry about synchronising collections, everything to do with a single instance of the entity is in one place.

Tip of the Day #18: Storing User Input in XML

If you are going to dump user generate input into XML please remember to escape appropriately. For example, the ampersand symbol has special meaning in XML and you must escape it. e.g. & becomes &amp;

Lambda cheat sheet

A while ago I wrote about all the new language features of C# 3.0 and it occurred to me that I’d left out a chunk about Lambdas. With LINQ being such an important part of C#3.0 that seems like a terrible omission, so I’m going to make up for it now.

The easiest way to think about a lambda is that it is a short form of anonymous methods that were introduced in C#2. However, you can also use lambdas to create expression trees (I’ll come to those in more detail in another post, for the moment, I’ll be concentrate on using lambdas for creating anonymous methods).

Basic Structure

A lambda that looks like this

(i) => 123 + i
^^^ ^^ ^^^^^^^
(1) (2)  (3)

is compiled to an anonymous method

delegate (int i) { return 123+i; }
         ^^^^^^^   ^^^^^^^^^^^^^
           (1)          (3)

The first part of the lambda (1), in the brackets, declares the parameters. The brackets are, incidentally, optional if there is only one parameter. The type is inferred so you don’t have to explicitly declare it as you would have to do with an anonymous method. However, if the compiler cannot infer the parameter type then you will have to declare it explicitly:

(int i) => 123 + i

The second part (2) is pronounced “goes to” and does not really have an equivalent with an anonymous method, although you could argue that it is the equivalent of the delegate keyword.

The third part (3) is the expression or statement. In a lambda expression the return is implicit so it does not need to be declared. It can also contain a number of statements enclosed in separated by semi-colon, but in that case it cannot be used to create expression trees and you must explicitly have a return statement if there is something being returned.

delegate void DisplayAdditionDelegate(int i, int j);

DisplayAdditionDelegate add = (i, j) =>
    { Console.WriteLine("{0} + {1} = {2}", i, j, i+j); };
add(2, 5);
// Output is: 2 + 5 = 7

Framework assistance

The .NET Framework provides a number of predefined generic delegate types that can be used with lambdas in order that is is easy to refer to them and pass them around.

Each of these contains a generic list of parameter types and finally the return type, or if there are no parameters just the return type.

The delegates with a return type are defined as:

public delegate TResult Func<TResult>()
public delegate TResult Func<T, TResult>(T arg)
public delegate TResult Func<T1, T2, TResult>(T1 arg1, T2 arg2)
public delegate TResult Func<T1, T2, T3, TResult>(T1 arg1, T2 arg2, T3 arg3)
public delegate TResult Func<T1, T2, T3, T4, TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4)

The delegates without a return type are defined as:

public delegate void Action<T>(T obj)
public delegate void Action<T1, T2>(T arg1, T2 arg2)
public delegate void Action<T1, T2, T3>(T arg1, T2 arg2, T3 arg3)
public delegate void Action<T1, T2, T3, T4>(T arg1, T2 arg2, T3 arg3, T4 arg4)

You can then uses these delegates to represent an appropriate method without having to create a custom delegate type – for most purposes they are quite sufficient.

Outer variables

Outer variables are variables that the lambda can use that are defined in the method that defines the lambda.

int j = 25;
Func<int, int> function = i => 100 + i + j;
j = 75;
int result = function(50);
// result == 225

As you can see in the above example the value of j is not evaluated at the point the lambda is declared, but at the point it is invoked. The lambda is able to keep a reference to j even although it was declared in a different scope. The same is true if the lambda is eventually invoked from a different scope block altogether. For example, if it has been passed out of the method in which it is declared.

The lambda can also change the value of the outer variable. For example:

int j = 6;
Func<int, int> function = i => { j = j * j; return 100 + i + j; };
int result = function(50);
// result == 186
// j == 36

Updated chart of .NET version numbers

Way back when, I published a table detailing the version numbers of the various parts that make up a .NET application: The Tools, the C# language, the Framework itself and the Engine (CLR) that it all runs on. With the latest version about to be released I thought it was time to update that table.

 

Year 2002 2003 2005 2006 2007 2010
Tool VS.NET 2002 VS.NET 2003 VS 2005 VS 2005
+ Extension
VS 2008 VS 2010
Language (C#) v1.0 v1.1 v2.0 v2.0 v3.0 v4.0
Framework v1.0 v1.1 v2.0 v3.0 v3.5 v4.0
Engine (CLR) v1.0 v1.1 v2.0 v2.0 v2.0 v4.0

 

The main new thing is that finally someone has had the good sense to re-sync the version numbers. It all got a bit silly with the release of Visual Studio 2008.

Tip of the Day #17: Duplicate input fields

Don’t allow duplicate input fields into your form.

The other day I was trying to debug a bug in an application that I maintain. The code created a set of pagination buttons at the top of the page with previous and next buttons. At some point a request had come in that the buttons needed to be replicated at the bottom of the page. Since the HTML was being built up in a string and dumped in a literal control in the first place the developer that was tasked with making the change just dumped the string into two literal controls, the original at the top of the page, and the new one at the bottom of the page. The previous and next buttons use hidden input field to tell the application which actual page number the buttons correspond to. And these were now duplicated and as a result the previous and next buttons ceased to work.

Here is an example of something similar:

<input id="first-hidden-field" value="123" type="hidden" name="some-name" />
<input id="submit-button" value="Submit" type="submit" />
<input id="second-hidden-field" value="456" type="hidden" name="some-name" />

When the form fields are returned to the application and the field “some-name” is queried the result back is a combination of the two fields with the duplicate name. In this case:

string someName = Request.Form["some-name"];

will result in the value of “123,456” being stored in the string. Basically, it is the comma separated form of all the input fields with the given name.

Tip of the Day #16: NaN (Not a Number)

The Issue

If you want to detect if a double (System.Double) or float (System.Single) is ?not a number? or NaN you cannot use something like this:

if (myDouble == double.NaN)
{
   /* do something */
}

It will always be false.

Sounds crazy? Try this:

double myDouble = double.NaN;
Console.WriteLine("myDouble == double.NaN : {0}", myDouble == double.NaN);

The result is:

myDouble == double.NaN : False

You can see that myDouble was explicitly set the value of double.NaN, yet in the next line it is returning false.

The Solution

If you want to test for a floating point value being Not a Number you to use IsNan() which is a static method on System.Double and System.Single. Here is the first example re-written to use the static method. It will now work correctly:

if (double.IsNan(myDouble) { /* do something */ }

If we re-write our other example:

double myDouble = double.NaN;
Console.WriteLine("double.IsNaN(myDouble) : {0}", double.IsNaN(myDouble));

We get the expected result too:

double.IsNaN(myDouble) : True

The Reason

According to Wikipedia: In computing, NaN, which stands for Not a Number, is a value or symbol that is usually produced as the result of an operation on invalid input operands, especially in floating-point calculations. For example, most floating-point units are unable to explicitly calculate the square root of negative numbers, and will instead indicate that the operation was invalid and return a NaN result. NaNs may also be used to represent missing values in computations.

It goes on to say: A NaN does not compare equal to any floating-point number or NaN, even if the latter has an identical representation. One can therefore test whether a variable has a NaN value by comparing it to itself, thus if x = x gives false then x is a NaN code.

This is why (double.NaN == double.NaN) always results in false. And it is also how the .NET framework detects the NaN value in the IsNan() method.

public static bool IsNaN(double d)
{
     return (d != d);
}

Tip of the day #13 (String Equality)

When comparing two strings in a case insensitive manner, use:

myFirstString.Equals(mySecondString, StringComparison.InvariantCultureIgnoreCase)

or, if cultural rules are to be ignored completely* then use:

myFirstString.Equals(mySecondString, StringComparison.OrdinalIgnoreCase)

over:

myFirstString.ToLower() == mySecondString.ToLower()

* The invariant culture is actually a non-region specific English language culture. The ordinal comparison is faster than any culture specific comparison as it uses a much simpler comparison algorithm.