Chapter 4. Language Integrated Query (LINQ) and Lambda Expressions
4.0 Introduction
Language Integrated Query (LINQ) is a great way to access data from many different sources. LINQ provides a single querying model that can operate against different data domains individually or all together in a single query. LINQ brings the ability to query data to .NET languages, and some of the languages have provided extensions to make its use even more intuitive. One of these languages is C#; there are a number of extensions to the language in C# that help to facilitate querying in a rich and intuitive manner.
Traditional object-oriented programming is based on an imperative style wherein developers describe in detail not only what they want to happen, but also exactly how it should be performed through code. LINQ helps to take coding down a more declarative path that facilitates describing what the developer wants to do instead of detailing how to accomplish the goal. LINQ also enables a more functional style of programming. These changes can dramatically shorten the amount of code it takes to perform some tasks. That said, object-oriented programming is still very much alive and well in C# and .NET, but for the first time the language is offering you the chance to choose the style of programming based on your needs. Note, however, that LINQ will not fit into every scenario and is not a replacement for good design or practice. You can write bad code using LINQ just as you can write bad object-oriented or procedural code. The trick, as it always has been, is to figure out when it is appropriate to use which technique.
The initial version of LINQ encompasses a number of data domains:
-
LINQ to Objects
-
LINQ to XML
-
LINQ to ADO.NET
-
LINQ to SQL
-
LINQ to DataSet
-
LINQ to Entities
As you begin your examination of LINQ, it is easy to think of it as a new object relational mapping layer, or some neat new widget on IEnumerable<T>
, or a new XML API, or even just an excuse to not write SQL directly anymore. You can use it as any of these things, but we would encourage you to instead think of LINQ as how your program asks for, calculates, or transforms sets of data from both single and disparate sources. It takes a bit of time and playing with LINQ for its functionality to click, but once it does, you will be surprised at what you can do with it. This chapter begins to show some of what is possible with LINQ and will hopefully get you thinking of which of your scenarios are applicable to this new capability in C#.
To write the LINQ query expressions to specify criteria and select data, we use lambda expressions. They are a convenient way to represent the delegate passed to LINQ queries like System.Func<T, TResult>
when the Enumerable.Where
method is called as part of narrowing down a result set. Lambda expressions are functions with a different syntax that enables them to be used in an expression context instead of the usual object-oriented method of being a member of a class. This means that with a single syntax, we can express a method definition, declaration, and the invocation of delegate to execute it, just as anonymous methods can, but with a more terse syntax. A projection is a lambda expression that translates one type into another.
A lambda expression looks like this:
j => j * 42
This means “using j
as the parameter to the function, j
goes to the result of j * 42
.” The =>
can be read as “goes to” for both this and a projection declared like so:
j => new { Number = j*42 };
If you think about it, in C# 1.0 you could do the same thing:
public delegate int IncreaseByANumber(int j); public delegate int MultipleIncreaseByANumber(int j, int k, int l); static public int MultiplyByANumber(int j) { return j * 42; } public static void ExecuteCSharp1_0() { IncreaseByANumber increase = new IncreaseByANumber( DelegatesEventsLambdaExpressions.MultiplyByANumber); Console.WriteLine(increase(10)); }
In C# 2.0 with anonymous methods, the C# 1.0 syntax could be reduced to the following example, as it is no longer necessary to provide the name for the delegate since all we want is the result of the operation:
public delegate int IncreaseByANumber(int j); public static void ExecuteCSharp2_0() { IncreaseByANumber increase = new IncreaseByANumber( delegate(int j) { return j * 42; }); Console.WriteLine(increase(10)); }
This brings us back to C# today and lambda expressions, where we can now just write:
public static void ExecuteCSharp6_0() { // declare the lambda expression IncreaseByANumber increase = j => j * 42; // invoke the method and print 420 to the console Console.WriteLine(increase(10)); MultipleIncreaseByANumber multiple = (j, k, l) => ((j * 42) / k) % l; Console.WriteLine(multiple(10, 11, 12)); }
Type inference helps the compiler to infer the type of j
from the declaration of the IncreaseByANumber
delegate type. If there were multiple arguments, then the lambda expression could look like this:
MultipleIncreaseByANumber multiple = (j, k, l) => ((j * 42) / k) % l; Console.WriteLine(multiple(10, 11, 12));
This chapter’s recipes make use of delegates, events, and lambda expressions. Among other topics, these recipes cover:
-
Handling each method invoked in a multicast delegate separately
-
Synchronous delegate invocation versus asynchronous delegate invocation
-
Enhancing an existing class with events
-
Various uses of lambda expressions, closures, and functors
If you are not familiar with delegates, events, or lambda expressions, you should read the MSDN documentation on these topics. There are also good tutorials and example code showing you how to set them up and use them in a basic fashion.
4.1 Querying a Message Queue
Solution
Use the EnumerableMessageQueue
class to write a LINQ query to retrieve messages using the System.Messaging.MessageQueue
type:
string queuePath = @".\private$\LINQMQ"; EnumerableMessageQueue messageQueue = null; if (!EnumerableMessageQueue.Exists(queuePath)) messageQueue = EnumerableMessageQueue.Create(queuePath); else messageQueue = new EnumerableMessageQueue(queuePath); using (messageQueue) { BinaryMessageFormatter messageFormatter = new BinaryMessageFormatter(); // Query the message queue for specific messages with the following criteria: // 1) the label must be less than 5 // 2) the name of the type in the message body must contain 'CSharpRecipes.D' // 3) the results should be in descending order by type name (from the body) var query = from Message msg in messageQueue // The first assignment to msg.Formatter is so that we can touch the // Message object. It assigns the BinaryMessageFormatter to each message // instance so that it can be read to determine if it matches the // criteria. This is done and then checks that the formatter was // correctly assigned by performing an equality check which satisfies the // where clause's need for a Boolean result while still executing the // assignment of the formatter. where ((msg.Formatter = messageFormatter) == messageFormatter) && int.Parse(msg.Label) < 5 && msg.Body.ToString().Contains("CSharpRecipes.D") orderby msg.Body.ToString() descending select msg; // check our results for messages with a label > 5 and containing // a 'D' in the name foreach (var msg in query) Console.WriteLine($"Label: {msg.Label}" + $" Body: {msg.Body}"); }
The query retrieves the data from the MessageQueue
by selecting the messages where the Label
is a number greater than 5 and the message body contains the text “CSharpRecipes.D”. These messages are then returned, sorted by the message body in descending order.
Discussion
There are a number of keywords in this LINQ code that were not previously used to access a message queue:
var
var
instructs the compiler to infer the variable type from the right side of the statement. In essence, the variable type is determined by what is on the right side of the operator, separating thevar
keyword and the expression. This allows for implicitly typed local variables.from
- The
from
keyword sets out the source collection to query against and a range variable to represent a single element from that collection. It is always the first clause in a query operation. This may seem counterintuitive if you are used to SQL and expectselect
to be first, but if you consider that we need to know what to work on before we determine what to return, it makes sense. In fact, if we weren’t already used to how SQL works, it would be SQL that seems counterintuitive. where
- The
where
keyword specifies the constraints by which the elements to return are filtered. Each condition must evaluate to a Boolean result, and when all expressions evaluate totrue
, the element of the collection is allowed to be selected. orderby
orderby
indicates that the result set should be sorted according to the criteria specified. The default order is ascending, and elements use the default comparer.select
select
allows the projection of an entire element from the collection, the construction of a new type with parts of that element and other calculated values, or a subcollection of items into the result.
The messageQueue
collection is of type System.Messaging.MessageQueue
, which implements the IEnumerable
interface. This is important, as the LINQ methods provided need a set or collection to implement at least IEnumerable
in order to work with that set or collection. It is possible to implement a set of extension methods that do not need IEnumerable
, but most people will not have the need to do so. It is even better when the set or collection implements IEnumerable<T>
, as LINQ then knows the type of element in the set or collection with which it is working.
Even though MessageQueue
implements the IEnumerable
interface (but not IEnumerable<T>
), the original implementation of IEnumerable
had some problems, so now if you try to use it, it doesn’t actually enumerate any results. You will also get a deprecation warning reading This method returns a MessageEnumerator that implements RemoveCurrent family of methods incorrectly. Please use GetMessageEnumerator2 instead.
if you try to use GetEnumerator
on MessageQueue
.
To address this, we have created the EnumerableMessageQueue
, which derives from MessageQueue
but uses the suggested GetMessageEnumerator2
method to implement both IEnumerable
and IEnumerable<Message>
. So we can just use the EnumerableMessageQueue
instance with LINQ:
public class EnumerableMessageQueue : MessageQueue, IEnumerable<Message> { public EnumerableMessageQueue() : base() { } public EnumerableMessageQueue(string path) : base(path) { } public EnumerableMessageQueue(string path, bool sharedModeDenyReceive) : base (path, sharedModeDenyReceive) { } public EnumerableMessageQueue(string path, QueueAccessMode accessMode) : base (path, accessMode) { } public EnumerableMessageQueue(string path, bool sharedModeDenyReceive, bool enableCache) : base (path, sharedModeDenyReceive, enableCache) { } public EnumerableMessageQueue(string path, bool sharedModeDenyReceive, bool enableCache, QueueAccessMode accessMode) : base (path, sharedModeDenyReceive, enableCache, accessMode) { } public static new EnumerableMessageQueue Create(string path) => Create(path, false); public static new EnumerableMessageQueue Create(string path, bool transactional) { // Use MessageQueue directly to make sure the queue exists if (!MessageQueue.Exists(path)) MessageQueue.Create(path, transactional); // create the enumerable queue once we know it is there return new EnumerableMessageQueue(path); } public new MessageEnumerator GetMessageEnumerator() { throw new NotSupportedException("Please use GetEnumerator"); } public new MessageEnumerator GetMessageEnumerator2() { throw new NotSupportedException("Please use GetEnumerator"); } IEnumerator<Message> IEnumerable<Message>.GetEnumerator() { //NOTE: In .NET 3.5, you used to be able to call "GetEnumerator" on //MessageQueue via normal LINQ semantics and have it work. Now we //have to call GetMessageEnumerator2, as GetEnumerator has been //deprecated. Now we use EnumerableMessageQueue which deals with //this for us... MessageEnumerator messageEnumerator = base.GetMessageEnumerator2(); while (messageEnumerator.MoveNext()) { yield return messageEnumerator.Current; } } IEnumerator IEnumerable.GetEnumerator() { //NOTE: In .NET 3.5, you used to be able to call "GetEnumerator" on //MessageQueue via normal LINQ semantics and have it work. Now we have // to call GetMessageEnumerator2, as GetEnumerator has been deprecated. //Now we use EnumerableMessageQueue which deals with this for us... MessageEnumerator messageEnumerator = base.GetMessageEnumerator2(); while (messageEnumerator.MoveNext()) { yield return messageEnumerator.Current; } } }
Now the query provides the element type Message
, as shown in the from
line in the LINQ query:
var query = from Message msg in messageQueue
In the Solution, the messages in the queue have been sent with BinaryFormatter
. To be able to query against them correctly, the Formatter
property must be set on each Message
before it is examined as part of the where
clause:
// The first assignment to msg.Formatter is so that we can touch the // Message object. It assigns the BinaryMessageFormatter to each message // instance so that it can be read to determine if it matches the criteria. // This is done, and then it checks that the formatter was correctly assigned // by performing an equality check, which satisfies the where clause's need // for a boolean result, while still executing the assignment of the formatter. where ((msg.Formatter = messageFormatter) == messageFormatter) &&
There are two uses of the var
keyword in the Solution code:
var query = from Message msg in messageQueue ... foreach (var msg in query) ...
The first usage infers that an IEnumerable<Message>
will be returned and assigned to the query
variable. The second usage infers that the type of msg
is Message
because the query
variable is of type IEnumerable<Message>
and the msg
variable is an element from that IEnumerable
.
It is also worth noting that when performing operations in a query, you can use actual C# code to determine the conditions, and there is more than just the predetermined set of operators. In the where
clause of this query, both int.Parse
and string
. Contains
are used to help filter messages:
int.Parse(msg.Label) > 5 && msg.Body.ToString().Contains('CSharpRecipes.D')
Finally, the orderby
is used to sort the results in descending order:
orderby msg.Body.ToString() descending
See Also
Recipe 4.9, and the “MessageQueue class,” “Implicitly typed local variable,” “from keyword,” “where keyword,” “orderby keyword,” and “select keyword” topics in the MSDN documentation.
4.2 Using Set Semantics with Data
Solution
Use the set operators provided as part of the standard query operators to perform those operations.
Distinct
:
IEnumerable<string> whoLoggedIn = dailySecurityLog.Where( logEntry => logEntry.Contains("logged in")).Distinct();
Union
:
// Union Console.WriteLine("Employees for all projects"); var allProjectEmployees = project1.Union(project2.Union(project3));
Intersect
:
// Intersect Console.WriteLine("Employees on every project"); var everyProjectEmployees = project1.Intersect(project2.Intersect(project3));
Except
:
Console.WriteLine("Employees on only one project"); var onlyProjectEmployees = allProjectEmployees.Except(unionIntersect);
Discussion
The standard query operators are the set of methods that represent the LINQ pattern. This set includes operators to perform many different types of operations, such as filtering, projection, sorting, grouping, and many others, including set operations.
The set operations for the standard query operators are:
-
Distinct
-
Union
-
Intersect
-
Except
The Distinct
operator extracts all nonduplicate items from the collection or result set being worked with. Say, for example, that we had a set of strings representing today’s login and logout behavior for a virtual machine in a common use development environment:
string[] dailySecurityLog = { "Rakshit logged in", "Aaron logged in", "Rakshit logged out", "Ken logged in", "Rakshit logged in", "Mahesh logged in", "Jesse logged in", "Jason logged in", "Josh logged in", "Melissa logged in", "Rakshit logged out", "Mary-Ellen logged out", "Mahesh logged in", "Alex logged in", "Scott logged in", "Aaron logged out", "Jesse logged out", "Scott logged out", "Dave logged in", "Ken logged out", "Alex logged out", "Rakshit logged in", "Dave logged out", "Josh logged out", "Jason logged out"};
From that collection, we would like to determine the list of people who logged in to the virtual machine today. Since people can log in and log out many times during the course of a day or remain logged in for the whole day, we need to eliminate the duplicate login entries. Distinct
is an extension method on the System.Linq.Enumerable
class (which implements the standard query operators) that we can call on the string array (which supports IEnumerable
) in order to get the distinct set of items from the collection. (For more information on extension methods, see Recipe 4.4.) To get the set, we use another of the standard query operators, where
, which takes a lambda expression that determines the filter criteria for the set and examines each string in the IEnumerable<string>
to determine if the string has “logged in.” Lambda expressions are inline statements (similar to anonymous methods) that can be used in place of a delegate. (See Recipe 4.12 for more on lambda expressions.) If the strings have logged in, then they are selected. Distinct
narrows down the set of strings further to eliminate duplicate “logged in” records, leaving only one per user:
IEnumerable<string> whoLoggedIn = dailySecurityLog.Where( logEntry => logEntry.Contains("logged in")).Distinct(); Console.WriteLine("Everyone who logged in today:"); foreach (string who in whoLoggedIn) Console.WriteLine(who);
To make things a bit more interesting, for the rest of the operators we will work with sets of employees on various projects in a company. An Employee
is a pretty simple class with a Name
and overrides for ToString
, Equals
, and GetHashCode
, as shown here:
public class Employee { public string Name { get; set; } public override string ToString() => this.Name; public override bool Equals(object obj) => this.GetHashCode().Equals(obj.GetHashCode()); public override int GetHashCode() => this.Name.GetHashCode(); }
You might wonder why Equals
and GetHashCode
are overloaded for such a simple class. The reason is that when LINQ compares elements in the sets or collections, it uses the default comparison, which in turn uses Equals
and GetHashCode
to determine if one instance of a reference type is the same as another. If you do not include the semantics in the reference type class to provide the same hash code or equals value when the data for two instances of the object is the same, then by default the instances will be different, as two reference types have different hash codes by default. We override that so that if the Name
is the same for each Employee
, the hash code and the equals value will both correctly identify the instances as the same. There are also overloads for the set operators that take a custom comparer, which would also allow you to make this determination even for classes for which you can’t make the changes to Equals
and GetHashCode
.
Having done this, we can now assign Employee
s to projects like so:
Employee[] project1 = { new Employee(){ Name = "Rakshit" }, new Employee(){ Name = "Jason" }, new Employee(){ Name = "Josh" }, new Employee(){ Name = "Melissa" }, new Employee(){ Name = "Aaron" }, new Employee() { Name = "Dave" }, new Employee() {Name = "Alex" } }; Employee[] project2 = { new Employee(){ Name = "Mahesh" }, new Employee() {Name = "Ken" }, new Employee() {Name = "Jesse" }, new Employee(){ Name = "Melissa" }, new Employee(){ Name = "Aaron" }, new Employee(){ Name = "Alex" }, new Employee(){ Name = "Mary-Ellen" } }; Employee[] project3 = { new Employee(){ Name = "Mike" }, new Employee(){ Name = "Scott" }, new Employee(){ Name = "Melissa" }, new Employee(){ Name = "Aaron" }, new Employee(){ Name = "Alex" }, new Employee(){ Name = "Jon" } };
To find all Employee
s on all projects, we can use Union
to get all nonduplicate Employee
s in all three projects and write them out, as Union
will give you all distinct Employee
s of all three projects:
// Union Console.WriteLine("Employees for all projects"); var allProjectEmployees = project1.Union(project2.Union(project3)); foreach (Employee employee in allProjectEmployees) Console.WriteLine(employee);
We can then use Intersect
to get the Employee
s on every project, as Intersect
will determine the common Employee
s from each project and return those:
// Intersect Console.WriteLine("Employees on every project"); var everyProjectEmployees = project1.Intersect(project2.Intersect(project3)); foreach (Employee employee in everyProjectEmployees) Console.WriteLine(employee);
Finally, we can use a combination of Union
and Except
to find Employee
s that are on only one project, as Except
filters out all Employee
s on more than one project:
// Except var intersect1_3 = project1.Intersect(project3); var intersect1_2 = project1.Intersect(project2); var intersect2_3 = project2.Intersect(project3); var unionIntersect = intersect1_2.Union(intersect1_3).Union(intersect2_3); Console.WriteLine("Employees on only one project"); var onlyProjectEmployees = allProjectEmployees.Except(unionIntersect); foreach (Employee employee in onlyProjectEmployees) Console.WriteLine(employee);
Output for the code shown is:
Everyone who logged in today: Rakshit logged in Aaron logged in Ken logged in Mahesh logged in Jesse logged in Jason logged in Josh logged in Melissa logged in Alex logged in Scott logged in Dave logged in Employees for all projects Rakshit Jason Josh Melissa Aaron Dave Alex Mahesh Ken Jesse Mary-Ellen Mike Scott Jon Employees on every project Melissa Aaron Alex Employees on only one project Rakshit Jason Josh Dave Mahesh Ken Jesse Mary-Ellen Mike Scott Jon
See Also
The “Standard Query Operators,” “Distinct method,” “Union method,” “Intersect method,” and “Except method” topics in the MSDN documentation.
4.3 Reusing Parameterized Queries with LINQ to SQL
Solution
Use the CompiledQuery.Compile
method to build an expression tree that will not have to be parsed each time the query is executed with new parameters:
var GetEmployees = CompiledQuery.Compile((NorthwindLinq2Sql.NorthwindLinq2SqlDataContext nwdc, string ac, string ttl) => from employee in nwdc.Employees where employee.HomePhone.Contains(ac) && employee.Title == ttl select employee); var northwindDataContext = new NorthwindLinq2Sql.NorthwindLinq2SqlDataContext();
The first time the query executes is when it actually compiles (where GetEmployees
is called the first time in the foreach
loop). Every other iteration in this loop and in the next loop uses the compiled version, avoiding the expression tree parsing:
foreach (var employee in GetEmployees(northwindDataContext, "(206)", "Sales Representative")) Console.WriteLine($"{employee.FirstName} {employee.LastName}"); foreach (var employee in GetEmployees(northwindDataContext, "(71)", "Sales Manager")) Console.WriteLine($"{employee.FirstName} {employee.LastName}");
Discussion
We used var
for the query declaration because it was cleaner, but in this case var
is actually:
System.Func<NorthwindLinq2Sql.NorthwindLinq2SqlDataContext, string, string, System.Linq.IQueryable<NorthwindLinq2Sql.Employee>>
which is the delegate signature for the lambda expression we created that contains the query. That’s right—all this crazy query stuff, and we just instantiated a delegate. To be fair, the Func
delegate was brought about in the System
namespace as part of LINQ, so do not despair: we are still doing cool stuff!
This illustrates that we are not returning an IEnumerable
- or IQueryable
-based result set from Compile
, but rather an expression tree that represents the potential for a query rather than the query itself. Once we have that tree, LINQ to SQL then has to convert it to actual SQL that can run against the database. Interestingly enough, if we had put in a call to string.Format
as part of detecting the area code in the employee’s home phone number, we would get a NotSupportedException
informing us that string.Format
can’t be translated to SQL:
where employee.HomePhone.Contains(string.Format($"({ac})")) && System.NotSupportedException: Method 'System.String Format(System.String,System.Object)' has no supported translation to SQL.
This is understandable, as SQL has no concept of .NET Framework methods for performing actions, but keep in mind as you design your queries that this is a limitation of using LINQ to SQL.
After the first execution, the query is compiled, and for every iteration after that, we do not pay the transformation cost for turning the expression tree into the parameterized SQL.
Compiling your queries is recommended for parameterized queries that get a lot of traffic, but if a query is infrequently used, it may not be worth the effort. As always, profile your code to see the areas where doing so could be useful.
Note that in the templates for Entity Framework 5 and up, you could not use CompiledQuery
with the context that is generated, because those templates were redone to use DbContext
, not ObjectContext
, and CompiledQuery.Compile
requires an ObjectContext
. The good news is that if you are using Entity Framework 5 and up, DbContext
does precompilation of queries for you! You can still use CompiledQuery
with the LINQ to SQL data context.
Microsoft recommends using a DbContext
in new development, but if you have existing code on prior data access mechanisms, CompiledQuery
can still help!
See Also
The “CompiledQuery.Compile method” and “Expression Trees” topics in the MSDN documentation.
4.4 Sorting Results in a Culture-Sensitive Manner
Solution
Use the overload of the OrderBy
query operator, which accepts a custom comparer, in order to specify the culture in which to perform comparisons:
// Create CultureInfo for Danish in Denmark. CultureInfo danish = new CultureInfo("da-DK"); // Create CultureInfo for English in the U.S. CultureInfo american = new CultureInfo("en-US"); CultureStringComparer comparer = new CultureStringComparer(danish,CompareOptions.None); var query = names.OrderBy(n => n, comparer);
Discussion
Handling localization issues such as sorting for a specific culture is a relatively trivial task in .NET if the current culture of the thread is the one you want to use. To access the framework classes that assist in handling culture issues in C, you include the System.Globalization
namespace. You’d include this namespace in order to make the code in the Solution run. One example of overriding the thread’s current culture would be an application that needs to display a sorted list of Danish words on a version of Windows that is set for US English. This functionality might also be useful if you are working with a multitenant web service or website with global clients.
The current thread in the application may have a CultureInfo
for “en-US” and, by default, the sort order for OrderBy
will use the current culture’s sort settings. To specify that this list should sort according to Danish rules instead, you must do a bit of work in the form of a custom comparer:
CultureStringComparer comparer = new CultureStringComparer(danish,CompareOptions.None);
The comparer variable is an instance of the custom comparer class CultureStringComparer
, which is defined to implement the IComparer<T>
interface specialized for strings. This class is used to provide the culture settings for the sort order:
public class CultureStringComparer : IComparer<string> { private CultureStringComparer() { } public CultureStringComparer(CultureInfo cultureInfo, CompareOptions options) { if (cultureInfo == null) throw new ArgumentNullException(nameof(cultureInfo)); CurrentCultureInfo = cultureInfo; Options = options; } public int Compare(string x, string y) => CurrentCultureInfo.CompareInfo.Compare(x, y, Options); public CultureInfo CurrentCultureInfo { get; set; } public CompareOptions Options { get; set; } }
To demonstrate how this could be used, first we compile a list of words to order by. Since the Danish language treats the character Æ as an individual letter, sorting it after Z in the alphabet, and the English language treats the character Æ as a special symbol, sorting it before the letter A in the alphabet, this example will demonstrate the sort difference:
string[] names = { "Jello", "Apple", "Bar", "Æble", "Forsooth", "Orange", "Zanzibar" };
Now, we can set up the CultureInfo
s for both Danish and US English and call OrderBy
with the comparer specific to each culture. This query does not use the query expression syntax, but rather uses the functional style of IEnumerable<string>.OrderBy()
:
// Create CultureInfo for Danish in Denmark. CultureInfo danish = new CultureInfo("da-DK"); // Create CultureInfo for English in the U.S. CultureInfo american = new CultureInfo("en-US"); CultureStringComparer comparer = new CultureStringComparer(danish,CompareOptions.None); var query = names.OrderBy(n => n, comparer); Console.WriteLine($"Ordered by specific culture : " + $"{comparer.CurrentCultureInfo.Name}"); foreach (string name in query) Console.WriteLine(name); comparer.CurrentCultureInfo = american; query = names.OrderBy(n => n, comparer); Console.WriteLine($"Ordered by specific culture : " + $"{comparer.CurrentCultureInfo.Name}"); foreach (string name in query) Console.WriteLine(name); query = from n in names orderby n select n; Console.WriteLine("Ordered by Thread.CurrentThread.CurrentCulture : " + $"{ Thread.CurrentThread.CurrentCulture.Name}"); foreach (string name in query) Console.WriteLine(name); // Create CultureInfo for Danish in Denmark. CultureInfo danish = new CultureInfo("da-DK"); // Create CultureInfo for English in the U.S. CultureInfo american = new CultureInfo("en-US"); CultureStringComparer comparer = new CultureStringComparer(danish, CompareOptions.None); var query = names.OrderBy(n => n, comparer); Console.WriteLine("Ordered by specific culture : " + comparer.CurrentCultureInfo.Name); foreach (string name in query) { Console.WriteLine(name); } comparer.CurrentCultureInfo = american; query = names.OrderBy(n => n, comparer); Console.WriteLine("Ordered by specific culture : " + comparer.CurrentCultureInfo.Name); foreach (string name in query) { Console.WriteLine(name); }
These output results show that the word Æble is last in the Danish list and first in the US English list:
Ordered by specific culture : da-DK Apple Bar Forsooth Jello Orange Zanzibar Æble Ordered by specific culture : en-US Æble Apple Bar Forsooth Jello Orange Zanzibar
See Also
The “OrderBy,” “CultureInfo,” and “IComparer<T>” topics in the MSDN documentation.
4.5 Adding Functional Extensions for Use with LINQ
Solution
Use extension methods to help achieve a more functional style of programming for your collection operations. For example, to add a weighted moving average calculation operation to numeric collections, implement a set of WeightedMovingAverage
extension methods in a static class and then call them as part of those collections:
decimal[] prices = new decimal[10] { 13.5M, 17.8M, 92.3M, 0.1M, 15.7M, 19.99M, 9.08M, 6.33M, 2.1M, 14.88M }; Console.WriteLine(prices.WeightedMovingAverage()); double[] dprices = new double[10] { 13.5, 17.8, 92.3, 0.1, 15.7, 19.99, 9.08, 6.33, 2.1, 14.88 }; Console.WriteLine(dprices.WeightedMovingAverage()); float[] fprices = new float[10] { 13.5F, 17.8F, 92.3F, 0.1F, 15.7F, 19.99F, 9.08F, 6.33F, 2.1F, 14.88F }; Console.WriteLine(fprices.WeightedMovingAverage()); int[] iprices = new int[10] { 13, 17, 92, 0, 15, 19, 9, 6, 2, 14 }; Console.WriteLine(iprices.WeightedMovingAverage()); long[] lprices = new long[10] { 13, 17, 92, 0, 15, 19, 9, 6, 2, 14 }; Console.WriteLine(lprices.WeightedMovingAverage());
To provide WeightedMovingAverage
for the full range of numeric types, methods for both the nullable
and non-nullable
numeric types are included in the LinqExtensions
class:
public static class LinqExtensions { public static decimal? WeightedMovingAverage( this IEnumerable<decimal?> source) { if (source == null) throw new ArgumentNullException(nameof(source)); decimal aggregate = 0.0M; decimal weight; int item = 1; // count how many items are not null and use that // as the weighting factor int count = source.Count(val => val.HasValue); foreach (var nullable in source) { if (nullable.HasValue) { weight = item / count; aggregate += nullable.GetValueOrDefault() * weight; count++; } } if (count > 0) return new decimal?(aggregate / count); return null; } // The same method pattern as above is followed for each of the other // types and its nullable counterparts (double / double?, int / int?, etc.) }
Discussion
Extension methods allow you to create operations that appear to be part of a collection. They are static methods that can be called as if they were instance methods, allowing you to extend existing types. Extension methods must also be declared in static classes that are not nested. Once a static class is defined with extension methods, the using
directive for the namespace of the class makes those extensions available in the source file.
Note
If an instance method exists with the same signature as the extension method, the extension method will never be called. Conflicting extension method declarations will resolve to the method in the closest enclosing namespace.
You cannot use extension methods to create:
-
Properties (get and set methods)
-
Operators (+, -, = , etc.)
-
Events
To declare an extension method, you specify the this
keyword in front of the first parameter of a method declaration, and the type of that parameter is the type being extended. For example, in the Nullable<decimal>
version of the WeightedMovingAverage
method, collections that support IEnumerable<decimal?>
(or IEnumerable<Nullable<decimal>>
) are supported:
public static decimal? WeightedMovingAverage(this IEnumerable<decimal?> source) { if (source == null) throw new ArgumentNullException(nameof(source)); decimal aggregate = 0.0M; decimal weight; int item = 1; // count how many items are not null and use that // as the weighting factor int count = source.Count(val => val.HasValue); foreach (var nullable in source) { if (nullable.HasValue) { weight = item / count; aggregate += nullable.GetValueOrDefault() * weight; count++; } } if (count > 0) return new decimal?(aggregate / count); return null; }
The extension methods that support much of the LINQ functionality are on the System.Linq.Extensions
class, including an Average
method. The Average
method has most of the numeric types but does not provide an overload for short
(Int16
). We can easily rectify that by adding one ourselves for short
and Nullable<short>
:
public static double? Average(this IEnumerable<short?> source) { if (source == null) throw new ArgumentNullException(nameof(source)); double aggregate = 0.0; int count = 0; foreach (var nullable in source) { if (nullable.HasValue) { aggregate += nullable.GetValueOrDefault(); count++; } } if (count > 0) return new double?(aggregate / count); return null; } public static double Average(this IEnumerable<short> source) { if (source == null) throw new ArgumentNullException(nameof(source)); double aggregate = 0.0; // use the count of the items from the source int count = source.Count(); foreach (var value in source) { aggregate += value; } if (count > 0) return aggregate / count; else return 0.0; } public static double? Average<TSource>(this IEnumerable<TSource> source, Func<TSource, short?> selector) => source.Select<TSource, short?>(selector).Average(); public static double Average<TSource>(this IEnumerable<TSource> source, Func<TSource, short> selector) => source.Select<TSource, short>(selector).Average(); #endregion // Extend Average
We can then call Average
on short-based collections just like WeightedMovingAverage
:
short[] sprices = new short[10] { 13, 17, 92, 0, 15, 19, 9, 6, 2, 14 }; Console.WriteLine(sprices.WeightedMovingAverage()); // System.Linq.Extensions doesn't implement Average for short but we do for them! Console.WriteLine(sprices.Average());
See Also
The “Extension Methods” topic in the MSDN documentation.
4.6 Querying and Joining Across Data Repositories
Solution
Use LINQ to bridge across the disparate data domains. LINQ is intended to be used in the same manner across different data domains and supports combining those sets of data with join syntax.
To demonstrate this, we will join an XML file full of categories with the data from a database (Northwind) of products to create a new set of data for product information that holds the product name, the category description, and the category name:
Northwind dataContext = new Northwind(Settings.Default.NorthwindConnectionString); ProductsTableAdapter adapter = new ProductsTableAdapter(); Products products = new Products(); adapter.Fill(products._Products); XElement xmlCategories = XElement.Load("Categories.xml"); var expr = from product in products._Products where product.Units_In_Stock > 100 join xc in xmlCategories.Elements("Category") on product.Category_ID equals int.Parse( xc.Attribute("CategoryID").Value) select new { ProductName = product.Product_Name, Category = xc.Attribute("CategoryName").Value, CategoryDescription = xc.Attribute("Description").Value }; foreach (var productInfo in expr) { Console.WriteLine("ProductName: " + productInfo.ProductName + " Category: " + productInfo.Category + " Category Description: " + productInfo.CategoryDescription); }
The new set of data is printed to the console, but this could easily have been rerouted to another method, transformed in another query, or written out to a third data format:
ProductName: Grandma's Boysenberry Spread Category: Condiments Category Description: Sweet and savory sauces, relishes, spreads, and seasonings ProductName: Gustaf's Knäckebröd Category: Grains/Cereals Category Description: Breads, crackers, pasta, and cereal ProductName: Geitost Category: Dairy Products Category Description: Cheeses ProductName: Sasquatch Ale Category: Beverages Category Description: Soft drinks, coffees, teas, beer, and ale ProductName: Inlagd Sill Category: Seafood Category Description: Seaweed and fish ProductName: Boston Crab Meat Category: Seafood Category Description: Seaweed and fish ProductName: Pâté chinois Category: Meat/Poultry Category Description: Prepared meats ProductName: Sirop d'érable Category: Condiments Category Description: Sweet and savory sauces, relishes, spreads, and seasonings ProductName: Röd Kaviar Category: Seafood Category Description: Seaweed and fish ProductName: Rhönbräu Klosterbier Category: Beverages Category Description: Soft drinks, coffees, teas, beer, and ale
Discussion
The Solution combines data from two different data domains: XML and a SQL database. Before LINQ, to do this you would have had to not only create a third data repository by hand to hold the result, but also write the specific code for each domain to query that domain for its part of the data (XPath for XML; SQL for database) and then manually transform the result sets from each domain into the new data repository. LINQ enables you to write the query to combine the two sets of data, automatically constructs a type via projecting a new anonymous type, and places the pertinent data in the new type, all in the same syntax. Not only does this simplify the code, but it also allows you to concentrate more on getting the data you want and less on determining exactly how to read both data domains.
This example uses both LINQ to DataSet and LINQ to XML to access the multiple data domains:
var dataContext = new NorthwindLinq2Sql.NorthwindLinq2SqlDataContext(); ProductsTableAdapter adapter = new ProductsTableAdapter(); Products products = new Products(); adapter.Fill(products._Products); XElement xmlCategories = XElement.Load("Categories.xml");
NorthwindLinq2SqlDataContext
is a DataContext
class. A DataContext
is analogous to an ADO.NET Connection
and Command
object rolled into one. You use it to establish your connection, execute queries, or access tables directly via entity classes. You can generate a DataContext
directly from the database through Visual Studio by adding a new “LINQ to SQL Classes” item. This provides access to the local Northwind.mdf database for the query. A Products DataSet
is loaded from the Products table in the Northwind.mdf database for use in the query.
XElement
is one of the main classes in LINQ to XML. It enables the loading of existing XML, creation of new XML, or retrieval of the XML text for the element via ToString
. Example 4-1 shows the Categories.xml file that will be loaded. For more on XElement
and LINQ to XML, see Chapter 10.
Example 4-1. Categories.xml
<?xml version="1.0" encoding="utf-8"?> <Categories> <Category Id="1" Name="Beverages" Description="Soft drinks, coffees, teas, beers, and ales" /> <Category Id="2" Name="Condiments" Description="Sweet and savory sauces, relishes, spreads, and seasonings" /> <Category Id="3" Name="Confections" Description="Desserts, candies, and sweet breads" /> <Category Id="4" Name="Dairy Products" Description="Cheeses" /> <Category Id="5" Name="Grains/Cereals" Description="Breads, crackers, pasta, and cereal" /> <Category Id="6" Name="Meat/Poultry" Description="Prepared meats" /> <Category Id="7" Name="Produce" Description="Dried fruit and bean curd" /> <Category Id="8" Name="Seafood" Description="Seaweed and fish" /> </Categories>
The two sets of data are joined via LINQ and, in particular, the join
keyword. We join the data by matching the category ID in the Products table with the category ID in the XML file to combine the data. In SQL terms, the join
keyword represents an inner join:
var expr = from product in products._Products where product.UnitsInStock > 100 join xc in xmlCategories.Elements("Category") on product.CategoryID equals int.Parse(xc.Attribute("Id").Value)
Once the join result is complete, we project a new type using the select
keyword:
select new { ProductName = product.ProductName, Category = xc.Attribute("Name").Value, CategoryDescription = xc.Attribute("Description").Value };
This allows us to combine different data elements from the two sets of data to make a third set that can look completely different than either of the original two.
Doing joins on two sets of database data would be a bad idea, as the database can do this much faster for those sets, but when you need to join disparate data sets, LINQ can lend a helping hand.
See Also
The “join keyword,” “System.Data.Linq.DataContext,” and “XElement” topics in the MSDN documentation.
4.7 Querying Configuration Files with LINQ
Solution
Use LINQ to query against the configuration sections. In the following example, we do this by retrieving all chapter titles with even numbers and the word and in the title from the custom configuration section containing chapter information:
CSharpRecipesConfigurationSection recipeConfig = ConfigurationManager.GetSection("CSharpRecipesConfiguration") as CSharpRecipesConfigurationSection; var expr = from ChapterConfigurationElement chapter in recipeConfig.Chapters.OfType<ChapterConfigurationElement>() where (chapter.Title.Contains("and")) && ((int.Parse(chapter.Number) % 2) == 0) select new { ChapterNumber = $"Chapter {chapter.Number}", chapter.Title }; foreach (var chapterInfo in expr) Console.WriteLine($"{chapterInfo.ChapterNumber} : {chapterInfo.Title}");
The configuration section being queried looks like this:
<CSharpRecipesConfiguration CurrentEdition="4"> <Chapters> <add Number="1" Title="Classes and Generics" /> <add Number="2" Title="Collections, Enumerators, and Iterators" /> <add Number="3" Title="Data Types" /> <add Number="4" Title="LINQ & Lambda Expressions" /> <add Number="5" Title="Debugging and Exception Handling" /> <add Number="6" Title="Reflection and Dynamic Programming" /> <add Number="7" Title="Regular Expressions" /> <add Number="8" Title="Filesystem I/O" /> <add Number="9" Title="Networking and Web" /> <add Number="10" Title="XML" /> <add Number="11" Title="Security" /> <add Number="12" Title="Threading, Synchronization, and Concurrency" /> <add Number="13" Title="Toolbox" /> </Chapters> <Editions> <add Number="1" PublicationYear="2004" /> <add Number="2" PublicationYear="2006" /> <add Number="3" PublicationYear="2007" /> <add Number="4" PublicationYear="2015" /> </Editions> </CSharpRecipesConfiguration>
The output from the query is:
Chapter 2 : Collections, Enumerators, and Iterators Chapter 6 : Reflection and Dynamic Programming Chapter 12 : Threading, Synchronization, and Concurrency
Discussion
Configuration files in .NET play a significant role in achieving manageability and ease of deployment for .NET-based applications. It can be challenging to get all of the various settings right in the hierarchy of configuration files that can affect an application, so understanding how to write utilities to programmatically check configuration file settings is of great use during development, testing, deployment, and ongoing management of an application.
Note
To access the configuration types, you will need to reference the System.Configuration
assembly.
Even though the ConfigurationElementCollection
class (the base of data sets in configuration files) supports only IEnumerable
and not IEnumerable<T>
, we can still use it to get the elements we need by using the OfType<ChapterConfigurationElement>
method on the collection, which selects elements of that type from the collection:
var expr = from ChapterConfigurationElement chapter in recipeConfig.Chapters.OfType<ChapterConfigurationElement>()
ChapterConfigurationElement
is a custom configuration section class that holds the chapter number and title:
/// <summary> /// Holds the information about a chapter in the configuration file /// </summary> public class ChapterConfigurationElement : ConfigurationElement { /// <summary> /// Default constructor /// </summary> public ChapterConfigurationElement() { } /// <summary> /// The number of the Chapter /// </summary> [ConfigurationProperty("Number", IsRequired=true)] public string Number { get { return (string)this["Number"]; } set { this["Number"] = value; } } /// <summary> /// The title of the Chapter /// </summary> [ConfigurationProperty("Title", IsRequired=true)] public string Title { get { return (string)this["Title"]; } set { this["Title"] = value; } } }
This technique can be used on the standard configuration files, such as machine.config, as well. This example determines which sections in machine.config require access permissions. For this collection, OfType<ConfigurationSection>
is used, as this is a standard section:
System.Configuration.Configuration machineConfig = ConfigurationManager.OpenMachineConfiguration(); var query = from ConfigurationSection section in machineConfig.Sections.OfType<ConfigurationSection>() where section.SectionInformation.RequirePermission select section; foreach (ConfigurationSection section in query) Console.WriteLine(section.SectionInformation.Name);
The sections detected will look something like this:
configProtectedData satelliteassemblies assemblyBinding system.codedom system.data.dataset system.data.odbc system.data system.data.oracleclient system.data.oledb uri system.windows.forms system.runtime.remoting runtime system.diagnostics windows mscorlib system.webServer system.data.sqlclient startup
See Also
The “Enumerable.OfType method,” “ConfigurationSectionCollection class” and “ConfigurationElementCollection class” topics in the MSDN documentation.
4.8 Creating XML Straight from a Database
Solution
Use LINQ to Entities and LINQ to XML to retrieve and transform the data all in one query. In this case, we will select the top five customers in the Northwind database whose contact is the owner and those owners who placed orders totaling more than $10,000, then create XML containing the company name, contact name, phone number, and total amount of the orders. Finally, the results are written out to the BigSpenders.xml file:
NorthwindEntities dataContext = new NorthwindEntities(); // Log the generated SQL to the console dataContext.Database.Log = Console.WriteLine; // select the top 5 customers whose contact is the owner and // those owners placed orders spending more than $10000 this year var bigSpenders = new XElement("BigSpenders", from top5 in ( (from customer in ( from c in dataContext.Customers // get the customers where the contact is the // owner and they placed orders where c.ContactTitle.Contains("Owner") && c.Orders.Count > 0 join orderData in ( from c in dataContext.Customers // get the customers where the contact is the // owner and they placed orders where c.ContactTitle.Contains("Owner") && c.Orders.Count > 0 from o in c.Orders // get the order details join od in dataContext.Order_Details on o.OrderID equals od.OrderID select new { c.CompanyName, c.CustomerID, o.OrderID, // have to calc order value from orderdetails //(UnitPrice*Quantity as Total)- // (Total*Discount) as NetOrderTotal NetOrderTotal = ( (((double)od.UnitPrice) * od.Quantity) - ((((double)od.UnitPrice) * od.Quantity) * od.Discount)) } ) on c.CustomerID equals orderData.CustomerID into customerOrders select new { c.CompanyName, c.ContactName, c.Phone, // Get the total amount spent by the customer TotalSpend = customerOrders.Sum(order => order.NetOrderTotal) } ) // only worry about customers that spent > 10000 where customer.TotalSpend > 10000 orderby customer.TotalSpend descending // only take the top 5 spenders select new { CompanyName = customer.CompanyName, ContactName = customer.ContactName, Phone = customer.Phone, TotalSpend = customer.TotalSpend }).Take(5) ).ToList() // format the data as XML select new XElement("Customer", new XAttribute("companyName", top5.CompanyName), new XAttribute("contactName", top5.ContactName), new XAttribute("phoneNumber", top5.Phone), new XAttribute("amountSpent", top5.TotalSpend))); using (XmlWriter writer = XmlWriter.Create("BigSpenders.xml")) { bigSpenders.WriteTo(writer); }
Note
When building larger queries, you may find it is sometimes easier to use the functional approach (.Join()
) to build up the query instead of the query expression manner (join x on y equals z
) if you have done more C# than SQL.
Discussion
LINQ to SQL is the part of LINQ to ADO.NET that facilitates rapid database development. It is targeted at the scenarios where you want to program almost directly against the database schema. Most of these scenarios have one-to-one correlations between strongly typed classes and database tables. If you are in more of an enterprise development scenario with lots of stored procedures and databases that have moved away from “one table equals one entity” scenarios, you would want to look into LINQ to Entities.
You can access the LINQ to SQL visual designer by adding a new or opening an existing “LINQ to SQL Classes” item (*.dbml file) to the project, which opens the designer. This helps you to build out the DataContext
and entity classes for your database, which can then be used with LINQ (or other programming constructs if you wish). A DataContext
is analogous to an ADO.NET Connection
and Command
object rolled into one. You use it to establish your connection, execute queries, or access tables directly via entity classes. The NorthwindLinq2Sql
data context is a strongly typed instance of a DataContext
and is partially shown here:
public partial class NorthwindLinq2SqlDataContext : System.Data.Linq.DataContext { private static System.Data.Linq.Mapping.MappingSource mappingSource = new AttributeMappingSource(); #region Extensibility Method Definitions partial void OnCreated(); partial void InsertCategory(Category instance); partial void UpdateCategory(Category instance); partial void DeleteCategory(Category instance); partial void InsertTerritory(Territory instance); partial void UpdateTerritory(Territory instance); partial void DeleteTerritory(Territory instance); partial void InsertCustomerCustomerDemo(CustomerCustomerDemo instance); partial void UpdateCustomerCustomerDemo(CustomerCustomerDemo instance); partial void DeleteCustomerCustomerDemo(CustomerCustomerDemo instance); partial void InsertCustomerDemographic(CustomerDemographic instance); partial void UpdateCustomerDemographic(CustomerDemographic instance); partial void DeleteCustomerDemographic(CustomerDemographic instance); partial void InsertCustomer(Customer instance); partial void UpdateCustomer(Customer instance); partial void DeleteCustomer(Customer instance); partial void InsertEmployee(Employee instance); partial void UpdateEmployee(Employee instance); partial void DeleteEmployee(Employee instance); partial void InsertEmployeeTerritory(EmployeeTerritory instance); partial void UpdateEmployeeTerritory(EmployeeTerritory instance); partial void DeleteEmployeeTerritory(EmployeeTerritory instance); partial void InsertOrder_Detail(Order_Detail instance); partial void UpdateOrder_Detail(Order_Detail instance); partial void DeleteOrder_Detail(Order_Detail instance); partial void InsertOrder(Order instance); partial void UpdateOrder(Order instance); partial void DeleteOrder(Order instance); partial void InsertProduct(Product instance); partial void UpdateProduct(Product instance); partial void DeleteProduct(Product instance); partial void InsertRegion(Region instance); partial void UpdateRegion(Region instance); partial void DeleteRegion(Region instance); partial void InsertShipper(Shipper instance); partial void UpdateShipper(Shipper instance); partial void DeleteShipper(Shipper instance); partial void InsertSupplier(Supplier instance); partial void UpdateSupplier(Supplier instance); partial void DeleteSupplier(Supplier instance); #endregion public NorthwindLinq2SqlDataContext() : base( global::NorthwindLinq2Sql.Properties.Settings.Default.NorthwindConnectionString, mappingSource) { OnCreated(); } public NorthwindLinq2SqlDataContext(string connection) : base(connection, mappingSource) { OnCreated(); } public NorthwindLinq2SqlDataContext(System.Data.IDbConnection connection) : base(connection, mappingSource) { OnCreated(); } public NorthwindLinq2SqlDataContext(string connection, System.Data.Linq.Mapping.MappingSource mappingSource) : base(connection, mappingSource) { OnCreated(); } public NorthwindLinq2SqlDataContext(System.Data.IDbConnection connection, System.Data.Linq.Mapping.MappingSource mappingSource) : base(connection, mappingSource) { OnCreated(); } public System.Data.Linq.Table<Category> Categories { get { return this.GetTable<Category>(); } } public System.Data.Linq.Table<Territory> Territories { get { return this.GetTable<Territory>(); } } public System.Data.Linq.Table<CustomerCustomerDemo> CustomerCustomerDemos { get { return this.GetTable<CustomerCustomerDemo>(); } } public System.Data.Linq.Table<CustomerDemographic> CustomerDemographics { get { return this.GetTable<CustomerDemographic>(); } } public System.Data.Linq.Table<Customer> Customers { get { return this.GetTable<Customer>(); } } public System.Data.Linq.Table<Employee> Employees { get { return this.GetTable<Employee>(); } } public System.Data.Linq.Table<EmployeeTerritory> EmployeeTerritories { get { return this.GetTable<EmployeeTerritory>(); } } public System.Data.Linq.Table<Order_Detail> Order_Details { get { return this.GetTable<Order_Detail>(); } } public System.Data.Linq.Table<Order> Orders { get { return this.GetTable<Order>(); } } public System.Data.Linq.Table<Product> Products { get { return this.GetTable<Product>(); } } public System.Data.Linq.Table<Region> Regions { get { return this.GetTable<Region>(); } } public System.Data.Linq.Table<Shipper> Shippers { get { return this.GetTable<Shipper>(); } } public System.Data.Linq.Table<Supplier> Suppliers { get { return this.GetTable<Supplier>(); } } }
The entity class definitions for the Northwind database are all present in the generated code as well, with each table having an entity class defined for it. The entity classes are indicated by the Table
attribute with no parameters. This means that the name of the entity class matches the table name:
[global::System.Data.Linq.Mapping.TableAttribute(Name="dbo.Customers")] public partial class Customer : INotifyPropertyChanging, INotifyPropertyChanged { private static PropertyChangingEventArgs emptyChangingEventArgs = new PropertyChangingEventArgs(String.Empty); private string _CustomerID; private string _CompanyName; private string _ContactName; private string _ContactTitle; private string _Address; private string _City; private string _Region; private string _PostalCode; private string _Country; private string _Phone; private string _Fax; private EntitySet<CustomerCustomerDemo> _CustomerCustomerDemos; private EntitySet<Order> _Orders; #region Extensibility Method Definitions partial void OnLoaded(); partial void OnValidate(System.Data.Linq.ChangeAction action); partial void OnCreated(); partial void OnCustomerIDChanging(string value); partial void OnCustomerIDChanged(); partial void OnCompanyNameChanging(string value); partial void OnCompanyNameChanged(); partial void OnContactNameChanging(string value); partial void OnContactNameChanged(); partial void OnContactTitleChanging(string value); partial void OnContactTitleChanged(); partial void OnAddressChanging(string value); partial void OnAddressChanged(); partial void OnCityChanging(string value); partial void OnCityChanged(); partial void OnRegionChanging(string value); partial void OnRegionChanged(); partial void OnPostalCodeChanging(string value); partial void OnPostalCodeChanged(); partial void OnCountryChanging(string value); partial void OnCountryChanged(); partial void OnPhoneChanging(string value); partial void OnPhoneChanged(); partial void OnFaxChanging(string value); partial void OnFaxChanged(); #endregion public Customer() { this._CustomerCustomerDemos = new EntitySet<CustomerCustomerDemo>(new Action<CustomerCustomerDemo>(this.attach_CustomerCustomerDemos), new Action<CustomerCustomerDemo>(this.detach_CustomerCustomerDemos)); this._Orders = new EntitySet<Order>( new Action<Order>(this.attach_Orders), new Action<Order>(this.detach_Orders)); OnCreated(); } public event PropertyChangingEventHandler PropertyChanging; public event PropertyChangedEventHandler PropertyChanged; protected virtual void SendPropertyChanging() { if ((this.PropertyChanging != null)) { this.PropertyChanging(this, emptyChangingEventArgs); } } protected virtual void SendPropertyChanged(String propertyName) { if ((this.PropertyChanged != null)) { this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } }
The standard property change notifications are implemented via INotifyPropertyChanging
and INotifyPropertyChanged
and have PropertyChanging
and PropertyChanged
events for conveying the change to a property. There is also a set of partial methods that will report when a specific property is modified on this entity class if they are implemented in another partial class definition for the entity class.
Note
Many of the classes generated by Microsoft .NET are generated as partial classes. This is so that you can extend them in your own partial class and add methods and properties to the class without being in danger of the code generator stomping on your code the next time it is regenerated.
In this case, if no other partial class definition is found, the compiler will remove those notifications. Partial methods enable the declaration of a method signature in one file of a partial class declaration and the implementation of the method in another. If the signature is found but the implementation is not, the signature is removed by the compiler.
The properties in the entity class match up to the columns in the database via the Column
attribute, where the Name
value is the database column name and the Storage
value is the internal storage for the class of the data. Events for the property changes are wired into the setter for the property:
[global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_CompanyName", DbType="NVarChar(40) NOT NULL", CanBeNull=false)] public string CompanyName { get { return this._CompanyName; } set { if ((this._CompanyName != value)) { this.OnCompanyNameChanging(value); this.SendPropertyChanging(); this._CompanyName = value; this.SendPropertyChanged("CompanyName"); this.OnCompanyNameChanged(); } } }
For a one-to-many child relationship, an EntitySet<T>
of the child entity class is declared with an Association
attribute. The Association
attribute specifies the relationship information between the parent and child entity classes, as shown here for the Orders
property on Customer
:
[global::System.Data.Linq.Mapping.AssociationAttribute(Name="Customer_Order", Storage="_Orders", ThisKey="CustomerID", OtherKey="CustomerID")] public EntitySet<Order> Orders { get { return this._Orders; } set { this._Orders.Assign(value); } }
LINQ to SQL covers much more than what has been shown here, and we encourage you to investigate it more. Now, however, let’s move on to the other data domain we are dealing with: LINQ to XML.
LINQ to XML is not only how you perform queries against XML, it is also a more developer-friendly way to work with XML. One of the main classes in LINQ to XML is XElement
, which allows you to create XML in a manner that more closely resembles the structure of the XML itself. This may not seem like a big deal, but when you can see the XML taking shape in your code, it’s easier to know where you are. (Ever forget which XmlWriter.WriteEndElement
you were on? We have!) You can get more details and examples about using XElement
in Chapter 10, so we won’t go much further into it here, but as you can see, it is very easy to build up XML in a query.
The first part of the query deals with setting up the main XML element, BigSpenders
, and getting the initial set of customers where the contact is the owner:
var bigSpenders = new XElement("BigSpenders", from top5 in ( (from customer in ( from c in dataContext.Customers // get the customers where the contact is the owner // and they placed orders where c.ContactTitle.Contains("Owner") && c.Orders.Count > 0
The middle of the query deals with joining the order and order detail information with the customer information to get the NetOrderTotal
for the order. It also creates order data containing that value, the customer and order IDs, and the company name. We need the NetOrderTotal
in the last part of the query, so stay tuned!
join orderData in ( from c in dataContext.Customers // get the customers where the contact is the owner // and they placed orders where c.ContactTitle.Contains("Owner") && c.Orders.Count > 0 from o in c.Orders // get the order details join od in dataContext.OrderDetails on o.OrderID equals od.OrderID select new { c.CompanyName, c.CustomerID, o.OrderID, // have to calc order value from orderdetails //(UnitPrice*Quantity as Total) (Total*Discount) // as NetOrderTotal NetOrderTotal = ( (((double)od.UnitPrice) * od.Quantity) - ((((double)od.UnitPrice) * od.Quantity) * od.Discount)) } ) on c.CustomerID equals orderData.CustomerID into customerOrders
The last part of the query determines the TotalSpend
for that customer across all orders using the Sum
function on NetOrderTotal
for the generated customerOrders
collection. Finally, the query selects only the top five customers with a TotalSpend
value greater than 10,000 by using the Take
function. (Take
is the equivalent to TOP
in SQL.) We then use those records to construct one inner Customer
element with attributes that nest inside the BigSpenders
root element we set up in the first part of the query:
select new { c.CompanyName, c.ContactName, c.Phone, // Get the total amount spent by the customer TotalSpend = customerOrders.Sum(order => order. NetOrderTotal) } ) // only worry about customers that spent > 10000 where customer.TotalSpend > 10000 orderby customer.TotalSpend descending // only take the top 5 spenders select customer).Take(5) ) // format the data as XML select new XElement("Customer", new XAttribute("companyName", top5.CompanyName), new XAttribute("contactName", top5.ContactName), new XAttribute("phoneNumber", top5.Phone), new XAttribute("amountSpent", top5.TotalSpend)));
Note
It is much easier to build large-nested queries as individual queries first and then put them together once you are sure the inner query is working.
At this point, for all of the code here, nothing has happened yet. That’s right: until the query is accessed, nothing happens because of the magic of deferred execution. LINQ has constructed a query expression, but nothing has talked to the database; there is no XML in memory, nada. Once the WriteTo
method is called on the bigSpenders
query expression, the query is evaluated by LINQ to SQL, and the XML is constructed. The WriteTo
method writes out the constructed XML to the XmlWriter
provided, and we are done:
using (XmlWriter writer = XmlWriter.Create("BigSpenders.xml")) { bigSpenders.WriteTo(writer); }
If you are interested in what that SQL will look like, connect the DataContext.Log
property to a TextWriter
(like the console):
// Log the generated SQL to the console dataContext.Log = Console.Out;
This query generates SQL that looks like this:
Generated SQL for query - output via DataContext.Log SELECT [t10].[CompanyName], [t10].[ContactName], [t10].[Phone], [t10].[TotalSpend] FROM ( SELECT TOP (5) [t0].[Company Name] AS [CompanyName], [t0].[Contact Name] AS [ContactName], [t0].[Phone], [t9].[value] AS [TotalSpend] FROM [Customers] AS [t0] OUTER APPLY ( SELECT COUNT(*) AS [value] FROM [Orders] AS [t1] WHERE [t1].[Customer ID] = [t0].[Customer ID] ) AS [t2] OUTER APPLY ( SELECT SUM([t8].[value]) AS [value] FROM ( SELECT [t3].[Customer ID], [t6].[Order ID], ([t7].[Unit Price] * (CONVERT(Decimal(29,4),[t7].[Quantity]))) - ([t7].[Unit Price] * (CONVERT(Decimal(29,4),[t7].[Quantity])) * (CONVERT(Decimal(29,4),[t7].[Discount]))) AS [value], [t7].[Order ID] AS [Order ID2], [t3].[Contact Title] AS [ContactTitle], [t5].[value] AS [value2], [t6].[Customer ID] AS [CustomerID] FROM [Customers] AS [t3] OUTER APPLY ( SELECT COUNT(*) AS [value] FROM [Orders] AS [t4] WHERE [t4].[Customer ID] = [t3].[Customer ID] ) AS [t5] CROSS JOIN [Orders] AS [t6] CROSS JOIN [Order Details] AS [t7] ) AS [t8] WHERE ([t0].[Customer ID] = [t8].[Customer ID]) AND ([t8].[Order ID] = [ t8].[Order ID2]) AND ([t8].[ContactTitle] LIKE @p0) AND ([t8].[value2] > @p1) AN D ([t8].[CustomerID] = [t8].[Customer ID]) ) AS [t9] WHERE ([t9].[value] > @p2) AND ([t0].[Contact Title] LIKE @p3) AND ([t2].[va lue] > @p4) ORDER BY [t9].[value] DESC ) AS [t10] ORDER BY [t10].[TotalSpend] DESC -- @p0: Input String (Size = 0; Prec = 0; Scale = 0) [%Owner%] -- @p1: Input Int32 (Size = 0; Prec = 0; Scale = 0) [0] -- @p2: Input Decimal (Size = 0; Prec = 29; Scale = 4) [10000] -- @p3: Input String (Size = 0; Prec = 0; Scale = 0) [%Owner%] -- @p4: Input Int32 (Size = 0; Prec = 0; Scale = 0) [0] -- Context: SqlProvider(SqlCE) Model: AttributedMetaModel Build: 3.5.20706.1
Here is the final XML:
<BigSpenders> <Customer companyName="Folk och fa HB" contactName="Maria Larsson" phoneNumber="0695-34 67 21" amountSpent="39805.162472039461" /> <Customer companyName="White Clover Markets" contactName="Karl Jablonski" phoneNumber="(206) 555-4112" amountSpent="35957.604972146451" /> <Customer companyName="Bon app'" contactName="Laurence Lebihan" phoneNumber="91.24.45.40" amountSpent="22311.577472746558" /> <Customer companyName="LINO-Delicateses" contactName="Felipe Izquierdo" phoneNumber="(8) 34-56-12" amountSpent="20458.544984650609" /> <Customer companyName="Simons bistro" contactName="Jytte Petersen" phoneNumber="31 12 34 56" amountSpent="18978.777493602414" /> </BigSpenders>
See Also
The “The Three Parts of a LINQ Query,” “DataContext.Log, property,” “DataContext class,” “XElement class,” and “LINQ to SQL” topics in the MSDN documentation.
4.9 Being Selective About Your Query Results
Solution
Use the TakeWhile
extension method to retrieve all results until the criteria are matched:
NorthwindEntities dataContext = new NorthwindEntities(); // find the products for all suppliers var query = dataContext.Suppliers.GroupJoin(dataContext.Products, s => s.SupplierID, p => p.SupplierID, (s, products) => new { s.CompanyName, s.ContactName, s.Phone, Products = products }).OrderByDescending(supplierData => supplierData.Products.Count()); var results = query.AsEnumerable().TakeWhile(supplierData => supplierData.Products.Count() > 3); Console.WriteLine($"Suppliers that provide more than three products: " + $"{results.Count()}"); foreach (var supplierData in results) { Console.WriteLine($" Company Name : {supplierData.CompanyName}"); Console.WriteLine($" Contact Name : {supplierData.ContactName}"); Console.WriteLine($" Contact Phone : {supplierData.Phone}"); Console.WriteLine($" Products Supplied : {supplierData.Products.Count()}"); foreach (var productData in supplierData.Products) Console.WriteLine($" Product: {productData.ProductName}"); }
You can also use the SkipWhile
extension method to retrieve all results once the criteria are matched:
NorthwindEntities dataContext = new NorthwindEntities(); // find the products for all suppliers var query = dataContext.Suppliers.GroupJoin(dataContext.Products, s => s.SupplierID, p => p.SupplierID, (s, products) => new { s.CompanyName, s.ContactName, s.Phone, Products = products }).OrderByDescending(supplierData => supplierData.Products.Count()); var results = query.AsEnumerable().SkipWhile(supplierData => supplierData.Products.Count() > 3); Console.WriteLine($"Suppliers that provide more than three products: " + $"{results.Count()}"); foreach (var supplierData in results) { Console.WriteLine($" Company Name : {supplierData.CompanyName}"); Console.WriteLine($" Contact Name : {supplierData.ContactName}"); Console.WriteLine($" Contact Phone : {supplierData.Phone}"); Console.WriteLine($" Products Supplied : {supplierData.Products.Count()}"); foreach (var productData in supplierData.Products) Console.WriteLine($" Product: {productData.ProductName}"); }
Discussion
In this example using LINQ to Entities, we determine the number of products each supplier provides, and sort the result set in descending order by product count:
var query = dataContext.Suppliers.GroupJoin(dataContext.Products, s => s.SupplierID, p => p.SupplierID, (s, products) => new { s.CompanyName, s.ContactName, s.Phone, Products = products }).OrderByDescending(supplierData => supplierData.Products.Count());
From that result, the supplier data is accepted into the final result set only if the supplier provides more than three products and the results are displayed. TakeWhile
is used with a lambda expression to determine if the product count is greater than 3
, and if so, the supplier is accepted into the result set:
var results = query.AsEnumerable().TakeWhile(supplierData => supplierData.Products.Count() > 3);
If SkipWhile
were used instead, all of the suppliers that provide three or fewer products would be returned:
var results = query.AsEnumerable().SkipWhile(supplierData => supplierData.Products.Count() > 3);
Being able to write code-based conditions allows for more flexibility than the regular Take
and Skip
methods, which are based on absolute record count, but keep in mind that once the condition is hit for either TakeWhile
or SkipWhile
, you get all records after that, which is why it’s important to sort the result set before using them.
The query also uses GroupJoin
, which is comparable to a SQL LEFT
or RIGHT OUTER JOIN
, but the result is not flattened. GroupJoin
produces a hierarchical result set instead of a tabular one, which is used to get the collection of products by supplier in this example:
dataContext.Suppliers.GroupJoin(dataContext.Products, s => s.SupplierID, p => p.SupplierID,
This is the output for the TakeWhile
:
Suppliers that provide more than three products: 4 Company Name : Pavlova, Ltd. Contact Name : Ian Devling Contact Phone : (03) 444-2343 Products Supplied : 5 Product: Pavlova Product: Alice Mutton Product: Carnarvon Tigers Product: Vegie-spread Product: Outback Lager Company Name : Plutzer Lebensmittelgroßmärkte AG Contact Name : Martin Bein Contact Phone : (069) 992755 Products Supplied : 5 Product: Rössle Sauerkraut Product: Thüringer Rostbratwurst Product: Wimmers gute Semmelknödel Product: Rhönbräu Klosterbier Product: Original Frankfurter grüne Soße Company Name : New Orleans Cajun Delights Contact Name : Shelley Burke Contact Phone : (100) 555-4822 Products Supplied : 4 Product: Chef Anton's Cajun Seasoning Product: Chef Anton's Gumbo Mix Product: Louisiana Fiery Hot Pepper Sauce Product: Louisiana Hot Spiced Okra Company Name : Specialty Biscuits, Ltd. Contact Name : Peter Wilson Contact Phone : (161) 555-4448 Products Supplied : 4 Product: Teatime Chocolate Biscuits Product: Sir Rodney's Marmalade Product: Sir Rodney's Scones Product: Scottish Longbreads
See Also
The “Enumerable.TakeWhile method,” “Enumerable.SkipWhile method,” and “Enumerable.GroupJoin method” topics in the MSDN documentation.
4.10 Using LINQ with Collections That Don’t Support IEnumerable<T>
Solution
The type cannot be inferred from the original IEnumeration
or ICollection
interfaces, so you must provide it using either the OfType<T>
or Cast<T>
extension methods or by specifying the type in the from
clause, which inserts a Cast<T>
for you. The first example uses Cast<XmlNode>
to let LINQ know that the elements in the XmlNodeList
returned from XmlDocument.SelectNodes
are of type XmlNode
. For an example of how to use the OfType<T>
extension method, see the Discussion section:
// Make some XML with some types that you can use with LINQ // that don't support IEnumerable<T> directly XElement xmlFragment = new XElement("NonGenericLinqableTypes", new XElement("IEnumerable", new XElement("System.Collections", new XElement("ArrayList"), new XElement("BitArray"), new XElement("Hashtable"), new XElement("Queue"), new XElement("SortedList"), new XElement("Stack")), new XElement("System.Net", new XElement("CredentialCache")), new XElement("System.Xml", new XElement("XmlNodeList")), new XElement("System.Xml.XPath", new XElement("XPathNodeIterator"))), new XElement("ICollection", new XElement("System.Diagnostics", new XElement("EventLogEntryCollection")), new XElement("System.Net", new XElement("CookieCollection")), new XElement("System.Security.AccessControl", new XElement("GenericAcl")), new XElement("System.Security", new XElement("PermissionSet")))); XmlDocument doc = new XmlDocument(); doc.LoadXml(xmlFragment.ToString()); // Select the names of the nodes under IEnumerable that have children and are // named System.Collections and contain a capital S and return that list in // descending order var query = from node in doc.SelectNodes("/NonGenericLinqableTypes/IEnumerable/*").Cast<XmlNode>() where node.HasChildNodes && node.Name == "System.Collections" from XmlNode xmlNode in node.ChildNodes where xmlNode.Name.Contains('S') orderby xmlNode.Name descending select xmlNode.Name; foreach (string name in query) Console.WriteLine(name);
The second example works against the application event log and retrieves the errors that occurred in the last six hours. The type of the element in the collection (EventLogEntry
) is provided next to the from
keyword, which allows LINQ to infer the rest of the information it needs about the collection element type:
EventLog log = new EventLog("Application"); query = from EventLogEntry entry in log.Entries where entry.EntryType == EventLogEntryType.Error && entry.TimeGenerated > DateTime.Now.Subtract(new TimeSpan(6, 0, 0)) select entry.Message; Console.WriteLine($"There were {query.Count<string>()}" + " Application Event Log error messages in the last 6 hours!"); foreach (string message in query) Console.WriteLine(message);
Discussion
Cast<T>
will transform the IEnumerable
into IEnumerable<T>
so that LINQ can access each item in the collection in a strongly typed manner. Before you use Cast<T>
, it would behoove you to check that all elements of the collection really are of type T
; otherwise, you will get an InvalidCastException
if the type of the element is not convertible to the type T
specified, because all elements will be cast using that type. Placing the type of the element next to the from
keyword acts just like a Cast<T>
:
ArrayList stuff = new ArrayList(); stuff.Add(DateTime.Now); stuff.Add(DateTime.Now); stuff.Add(1); stuff.Add(DateTime.Now); var expr = from item in stuff.Cast<DateTime>() select item; foreach (DateTime item in expr) Console.WriteLine(item);
Note
Because of the deferred execution semantics, the exception that occurs with Cast<T>
or from
happens only once that element has been iterated to.
Another way to approach this issue is to use OfType<T>
, as it will return only the elements of a specific type and not try to cast elements from one type to another:
var expr = from item in stuff.OfType<DateTime>() select item; // only three elements, all DateTime returned. No exceptions foreach (DateTime item in expr) Console.WriteLine(item);
See Also
The “OfType<TResult> method” and “Cast<TResult> method” topics in the MSDN documentation.
4.11 Performing an Advanced Interface Search
Solution
Use LINQ to query the type interface information and perform rich searches. The method shown in Example 4-2 demonstrates one complex search that can be performed with LINQ.
Example 4-2. Performing complex searches of interfaces on a type
// set up the interfaces to search for Type[] interfaces = { typeof(System.ICloneable), typeof(System.Collections.ICollection), typeof(System.IAppDomainSetup) }; // set up the type to examine Type searchType = typeof(System.Collections.ArrayList); var matches = from t in searchType.GetInterfaces() join s in interfaces on t equals s select s; Console.WriteLine("Matches found:"); foreach (Type match in matches) Console.WriteLine(match.ToString());
The code in Example 4-2 searches for any of the three interface types contained in the Names
array that are implemented by the System.Collections.ArrayList
type. It does this by using LINQ to query if the type is an instance of any of the set of interfaces.
The GetInterface
method searches for an interface only by name (using a case-sensitive or case-insensitive search), and the GetInterfaces
method returns an array of all the interfaces implemented on a particular type. To execute a more focused search—for example, searching for interfaces that define a method with a specific signature, or implemented interfaces that are loaded from the Global Assembly Cache (GAC) where common assemblies are stored—you need to use a different mechanism, like LINQ. LINQ gives you a more flexible and more advanced searching capability for interfaces without requiring you to create your own interface search engine. You might use this capability to load assemblies with a specific interface, to generate code from existing assemblies, or even as a reverse-engineering tool!
Discussion
There are many ways to use LINQ to search for interfaces implemented on a type. Here are just a few other searches that can be performed:
-
A search for all implemented interfaces that are defined within a particular namespace (in this case, the
System.Collections
namespace):var collectionsInterfaces = from type in searchType.GetInterfaces() where type.Namespace == "System.Collections" select type;
-
A search for all implemented interfaces that contain a method called
Add
, which returns anInt32
value:var addInterfaces = from type in searchType.GetInterfaces() from method in type.GetMethods() where (method.Name == "Add") && (method.ReturnType == typeof(int)) select type;
-
A search for all implemented interfaces that are loaded from the GAC:
var gacInterfaces = from type in searchType.GetInterfaces() where type.Assembly.GlobalAssemblyCache select type;
-
A search for all implemented interfaces that are defined within an assembly with the version number 4.0.0.0:
var versionInterfaces = from type in searchType.GetInterfaces() where type.Assembly.GlobalAssemblyCache && type.Assembly.GetName().Version.Major == 4 && type.Assembly.GetName().Version.Minor == 0 && type.Assembly.GetName().Version.Build == 0 && type.Assembly.GetName().Version.Revision == 0 select type;
See Also
The “Lambda Expressions (C# Programming Guide)” and “where keyword [LINQ] (C#)” topics in the MSDN documentation.
4.12 Using Lambda Expressions
Problem
C# includes a feature called lambda expressions. While you can view lambda expressions as syntactic sugar for making anonymous method definition less difficult, you also want to understand all of the different ways that you can use them to help you in your daily programming chores as well as the ramifications of those uses.
Solution
Lambda expressions can be implemented by the compiler from methods created by the developer. There are two orthogonal characteristics that lambda expressions may have:
-
Parameter lists may have explicit or implicit types.
-
Bodies may be expressions or statement blocks.
Let’s start with the original way to use delegates. First, you would declare a delegate type—DoWork
in this case—and then create an instance of it (as shown here in the WorkItOut
method). Declaring the instance of the delegate requires that you specify a method to execute when the delegate is invoked, and here the DoWorkMethodImpl
method has been connected. The delegate is invoked, and the text is written to the console via the DoWorkMethodImpl
method:
class OldWay { // declare delegate delegate int DoWork(string work); // have a method to create an instance of and call the delegate public void WorkItOut() { // declare instance DoWork dw = new DoWork(DoWorkMethodImpl); // invoke delegate int i = dw("Do work the old way"); } // Have a method that the delegate is tied to with a matching signature // so that it is invoked when the delegate is called public int DoWorkMethodImpl(string s) { Console.WriteLine(s); return s.GetHashCode(); } }
Lambda expressions allow you to set up code to run when a delegate is invoked, but you do not need to give a named formal method declaration to the delegate. The method thus declared is nameless and closed over the scope of the outer method. For example, you could have written the preceding code using a lambda expression such as this:
class LambdaWay { // declare delegate delegate int DoWork(string work); // have a method to create an instance of and call the delegate public void WorkItOut() { // declare instance DoWork dw = s => { Console.WriteLine(s); return s.GetHashCode(); }; // invoke delegate int i = dw("Do some inline work"); } }
Notice that instead of having a method called DoWorkMethodImpl
, you use the =>
operator to directly assign the code from that method inline to the DoWork
delegate. The assignment looks like this:
DoWork dw = s => { Console.WriteLine(s); return s.GetHashCode(); };
You also provide the parameter required by the DoWork
delegate (string
), and your code returns an int
(s.GetHashCode()
) as the delegate requires. When you’re setting up a lambda expression, the code must match the delegate signature, or you will get a compiler error.
By “match,” we mean:
-
If explicitly typed, the lambda parameters must exactly match the delegate parameters. If implicitly typed, the lambda parameters get the delegate parameter types.
-
The body of the lambda must be a legal expression or statement block given the parameter types.
-
The return type of the lambda must be implicitly convertible to the return type of the delegate. It need not match exactly.
There is yet another way you can set up the delegate: through the magic of delegate inference. Delegate inference allows you to assign the method name directly to the delegate instance without having to write the code to create a new delegate object. Under the covers, C# actually writes the IL for creating the delegate object, but you don’t have to do it explicitly here. Using delegate inference instead of writing out new [Delegate Type]([Method Name])
everywhere helps to unclutter the code involved in delegate use, as shown here:
class DirectAssignmentWay { // declare delegate delegate int DoWork(string work); // have a method to create an instance of and call the delegate public void WorkItOut() { // declare instance and assign method DoWork dw = DoWorkMethodImpl; // invoke delegate int i = dw("Do some direct assignment work"); } // Have a method that the delegate is tied to with a matching signature // so that it is invoked when the delegate is called public int DoWorkMethodImpl(string s) { Console.WriteLine(s); return s.GetHashCode(); } }
Notice that all that is assigned to the DoWork
delegate instance dw
is the method name DoWorkMethodImpl
. There is no new DoWork(DoWorkMethodImpl)
call as there was in older C# code.
Note
Remember, the underlying delegate wrapper does not go away; delegate inference just simplifies the syntax a bit by hiding some of it.
Alternatively, you can also set up lambda expressions that take generic type parameters to enable working with generic delegates, as you see here in the GenericWay
class:
class GenericWay { // have a method to create two instances of and call the delegates public void WorkItOut() { Func<string, string> dwString = s => { Console.WriteLine(s); return s; }; // invoke string delegate string retStr = dwString("Do some generic work"); Func<int, int> dwInt = i => { Console.WriteLine(i); return i; }; // invoke int delegate int j = dwInt(5); } }
Discussion
One of the useful things about lambda expressions is the concept of outer variables. The official definition of outer variable is any local variable, value parameter, or parameter array with a scope that contains the lambda expression.
This means that, inside the code of the lambda expression, you can touch variables outside the scope of that method. This introduces the concept of “capturing” the variables, which occurs when a lambda expression actually makes reference to one of the outer variables. In the following example, the count
variable is captured and incremented by the lambda expression. The count
variable is not part of the original scope of the lambda expression but rather part of the outer scope. It is incremented, and then the incremented value is returned and totaled:
public void SeeOuterWork() { int count = 0; int total = 0; Func<int> countUp = () => count++; for (int i = 0; i < 10; i++) total += countUp(); Debug.WriteLine($"Total = {total}"); }
What capturing actually does is extend the lifetime of the outer variable to coincide with the lifetime of the underlying delegate instance that represents the lambda expression. This should encourage you to be careful about what you touch from inside a lambda expression. You could be causing things to hang around a lot longer than you originally planned. The garbage collector won’t get a chance to clean up those outer variables until later, when they are used in the lambda expression. Capturing outer variables has another garbage-collector effect: when locals or value parameters are captured, they are no longer considered to be fixed but are now movable, so any unsafe code must now fix that variable—via the fixed
keyword—before the variable is used.
Outer variables can affect how the compiler generates the internal IL for the lambda expression. If the lambda expression uses outer variables, the lambda expression is generated as a private method of a nested class. If the lambda expression does not use outer variables, it would be generated as another private method of the class in which it is declared. If the outer method is static, then the lambda expression cannot access instance members via the this
keyword, as the nested class will also be generated as static.
There are two types of lambda expressions: expression lambdas and statement lambdas. This expression lambda has no parameters and simply increments the count
variable in an expression:
int count = 0; Func<int> countUp = () => count++;
Statement lambdas have the body enclosed in curly braces and can contain any number of statements like this:
Func<int, int> dwInt = i => { Console.WriteLine(i); return i; };
Note
A few last things to remember about lambda expressions:
-
They can’t use
break
,goto
, orcontinue
to jump from the lambda expression to a target outside the lambda expression block. -
No unsafe code can be executed inside a lambda expression.
-
Lambda expressions cannot be used on the left side of the
is
operator. -
Since lambda expressions are a superset of anonymous methods, all restrictions that apply to anonymous methods also apply to lambda expressions.
See Also
The “Lambda Expressions (C# Programming Guide)” topic in the MSDN documentation.
4.13 Using Different Parameter Modifiers in Lambda Expressions
Solution
Lambda expressions can use out
and ref
parameter modifiers but not the params
modifier in their parameter list. However, this does not prevent the creation of delegates with any of these modifiers, as shown here:
// declare out delegate delegate int DoOutWork(out string work); // declare ref delegate delegate int DoRefWork(ref string work); // declare params delegate delegate int DoParamsWork(params object[] workItems);
Even though the DoParamsWork
delegate is defined with the params
keyword on the parameter, it can still be used as a type for a lambda expression, as you’ll see in a bit. To use the DoOutWork
delegate, create a lambda expression inline using the out
keyword and assign it to the DoOutWork
delegate instance. Inside the lambda expression body, the out
variable s
is assigned a value first (as it doesn’t have one by definition as an out
parameter), writes it to the console, and returns the string hash code. Note that in the parameter list, you must provide the type of s
(i.e., string
), as type is not inferred for variables marked with the out
or ref
keywords. It is not inferred for out
or ref
variables to preserve the representation at the call site and the parameter declaration site to help the developer clearly reason about the possible assignment to these variables:
// declare instance and assign method DoOutWork dow = (out string s) => { s = "WorkFinished"; Console.WriteLine(s); return s.GetHashCode(); };
To run the lambda expression code, invoke the delegate with an out
parameter, and then print out the result to the console:
// invoke delegate string work; int i = dow(out work); Console.WriteLine(work);
To use the ref
parameter modifier in a lambda expression, create an inline method to hook up to the DoRefWork
delegate with a ref
parameter. In the method, you write the original value out
, reassign the value, and get the hash code of the new value. Remember that, as with the out
keyword, you must provide the type of s
(string
) in the parameter list, as type cannot be inferred for a variable marked with the ref
keyword:
// declare instance and assign method DoRefWork drw = (ref string s) => { Console.WriteLine(s); s = "WorkFinished"; return s.GetHashCode(); };
To run the lambda expression, assign a value to the string work
and then pass it as a ref
parameter to the DoRefWork
delegate that is instantiated. Upon the return from the delegate call, write out the new value for the work
string:
// invoke delegate work = "WorkStarted"; i = drw(ref work); Console.WriteLine(work);
While it is possible to declare a delegate with the params
modifier, you cannot hook up the delegate using a lambda expression with the params
keyword in the parameter list. If you try this, the compiler displays the CS1670 params is not valid in this context
compiler error on the DoParamsWork
line:
////Done as an lambda expression you also get ////CS1670 "params is not valid in this context" //DoParamsWork dpwl = (params object[] workItems) => //{ // foreach (object o in workItems) // { // Console.WriteLine(o.ToString()); // } // return workItems.GetHashCode(); //};
Even if you attempt this using an anonymous method instead of a lambda expression, you still cannot hook up this delegate with the params
keyword in the parameter list. If you try, the compiler still displays the CS1670 params is not valid in this context
compiler error on the DoParamsWork
line:
//Done as an anonymous method you get CS1670 "params is not valid in this context" //DoParamsWork dpwa = delegate (params object[] workItems) //{ // foreach (object o in workItems) // { // Console.WriteLine(o.ToString()); // } // return workItems.GetHashCode(); //};
You can, however, omit the params
keyword and still set up the lambda expression for the delegate, as shown here:
// All we have to do is omit the params keyword. DoParamsWork dpw = workItems => { foreach (object o in workItems) Console.WriteLine(o.ToString()); return workItems.GetHashCode(); };
Notice that although you’ve removed the params
keyword from the lambda expression, this doesn’t stop you from using the same syntax. The params
keyword is present on the delegate type, so you can invoke it thusly:
int i = dpw("Hello", "42", "bar");
So this illustrates that you can bind a lambda expression to a delegate declared using params
, and once you’ve done that, you can invoke the lambda expression, passing in any number of parameters you like, just as you’d expect.
Discussion
Lambda expressions cannot access the ref
or out
parameters of an outer scope. This means any out
or ref
variables that were defined as part of the containing method are off-limits for use inside the body of the lambda expression:
public void TestOut(out string outStr) { // declare instance DoWork dw = s => { Console.WriteLine(s); // Causes error CS1628: // "Cannot use ref or out parameter 'outStr' inside an // anonymous method, lambda expression, or query expression" outStr = s; return s.GetHashCode(); }; // invoke delegate int i = dw("DoWorkMethodImpl1"); } public void TestRef(ref string refStr) { // declare instance DoWork dw = s => { Console.WriteLine(s); // Causes error CS1628: // "Cannot use ref or out parameter 'refStr' inside an // anonymous method, lambda expression, or query expression" refStr = s; return s.GetHashCode(); }; // invoke delegate int i = dw("DoWorkMethodImpl1"); }
Interestingly enough, lambda expressions can access outer variables with the params
modifier:
// declare delegate delegate int DoWork(string work); public void TestParams(params string[] items) { // declare instance DoWork dw = s => { Console.WriteLine(s); foreach (string item in items) Console.WriteLine(item); return s.GetHashCode(); }; // invoke delegate int i = dw("DoWorkMethodImpl1"); }
Because the params
modifier is there for the benefit of the calling site (so the compiler knows to make this a method call that supports variable-length argument lists) and because lambda expressions are never called directly (they’re always called via a delegate), it makes no sense for a lambda expression to be decorated with something there for the benefit of the calling site—as there is no calling site. This is why it doesn’t matter that you can’t use the params
keyword on a lambda expression. For lambda expressions, the calling site is always calling through the delegate, so what matters is whether that delegate has the params
keyword or not.
See Also
Recipe 1.17; the “CS1670,” “CS1525,” “CS1628,” “out,” “ref,” “params,” and “System.ParamArrayAttribute” topics in the MSDN documentation.
4.14 Speeding Up LINQ Operations with Parallelism
Solution
Use PLINQ (Parallel LINQ) to utilize the full capacities of your machine to process the query faster.
To demonstrate this, let’s consider the plight of Brooke and Katie. Brooke and Katie are working on a cookbook together and they need to evaluate all of the recipes for all of the chapters. Since there are so many recipes, they want to be able to hand off the rudimentary validation steps for the recipes and then Brooke or Katie gets a final pass at each recipe as the main editor for final fit and finish.
Each Chapter
has a number of Recipe
s in it, and the Recipe
validation steps are:
-
Read the text of the recipe for premise.
-
Check the recipe accuracy of ingredients and measurements.
-
Prepare the recipe and taste once for each rank of difficulty for the recipe.
-
Have Brooke or Katie perform the final editing pass.
If any stage of the recipe evaluation fails, that stage needs to be redone unless it is the tasting stage. If a Recipe
fails the tasting stage, it needs to start over.
To process the collection of RecipeChapter
s (chapters in the example) with regular LINQ, we could use the following statement:
chapters.Select(c => TimedEvaluateChapter(c, rnd)).ToList();
TimedEvaluateChapter
is a method that performs the evaluation of the RecipeChapter
and all of the Recipes
in the RecipeChapter
while timing the evaluation. EvaluateRecipe
is called once for each Recipe
in the RecipeChapter
to perform the Recipe
validation steps:
private static RecipeChapter TimedEvaluateChapter(RecipeChapter rc, Random rnd) { Stopwatch watch = new Stopwatch(); LogOutput($"Evaluating Chapter {rc}"); watch.Start(); foreach (var r in rc.Recipes) EvaluateRecipe(r, rnd); watch.Stop(); LogOutput($"Finished Evaluating Chapter {rc}"); return rc; }
In order to process the Recipe
s faster, we add a call to the AsParallel
extension method before we call Select
to invoke TimedEvaluateChapter
for each RecipeChapter
:
chapters.AsParallel().Select(c => TimedEvaluateChapter(c, rnd)).ToList();
Your results will vary based on your hardware, but the following times were recorded on a run using regular LINQ and then subsequently PLINQ:
Full Chapter Evaluation with LINQ took: 00:01:19.1395258 Full Chapter Evaluation with PLINQ took: 00:00:25.1708103
Discussion
When you’re using PLINQ, the main thing to keep in mind is that the unit of work being parallelized must be significant enough to justify the cost of the parallelization. There are additional setup and teardown costs to doing operations in parallel (like partitioning of the data set) and if the data set is too small or the operation on each member of the set is not expensive enough to be helped by using parallel techniques, you could actually perform worse. If PLINQ determines that it cannot effectively parallelize the query, it will process it sequentially. If this happens, there are a number of additional methods you can use to adjust depending upon your particular situation (WithExecutionMode
, WithDegreeOfParallelism
).
As in all engineering, measuring your results is the key to understanding if you are improving or not, so with that in mind, we created the TimedEvaluateChapter
method to call from our Select
statement:
chapters.AsParallel().Select(c => TimedEvaluateChapter(c, rnd)).ToList();
TimedEvaluateChapter
times the process of evaluating every Recipe
in the RecipeChapter
and wraps that value in calls to Stopwatch.Start
and Stopwatch.Stop
for timing. The timing results are then available in Stopwatch.Elapsed
. Note that if you restart the Stopwatch
without calling Stopwatch.Reset
, the timer will add to the value already in the Stopwatch
and you may get a bigger value than you expected:
private static RecipeChapter TimedEvaluateChapter(RecipeChapter rc, Random rnd) { Stopwatch watch = new Stopwatch(); LogOutput($"Evaluating Chapter {rc}"); watch.Start(); foreach (var r in rc.Recipes) EvaluateRecipe(r, rnd); watch.Stop(); LogOutput($"Finished Evaluating Chapter {rc}"); return rc; }
EvaluateRecipe
performs the validation steps on each recipe recursively until it passes the final edit from Brooke and Katie. Thread.Sleep
is called to simulate work for each step:
private static Recipe EvaluateRecipe(Recipe r, Random rnd) { //Recipe Editing steps if (!r.TextApproved) { //Read the recipe to make sure it makes sense Thread.Sleep(50); int evaluation = rnd.Next(1, 10); // 7 means it didn't make sense so don't approve it, // send it back for rework if (evaluation == 7) { LogOutput($"{r} failed the readthrough! Reworking..."); } else r.TextApproved = true; return EvaluateRecipe(r, rnd); } else if (!r.IngredientsApproved) { //Check the ingredients and measurements Thread.Sleep(100); int evaluation = rnd.Next(1, 10); // 3 means the ingredients or measurements are incorrect, // send it back for rework if (evaluation == 3) { LogOutput($"{r} had incorrect measurements! Reworking..."); } else r.IngredientsApproved = true; return EvaluateRecipe(r, rnd); } else if (r.RecipeEvaluated != r.Rank) { //Prepare recipe and taste Thread.Sleep(50 * r.Rank); int evaluation = rnd.Next(1, 10); // 4 means it didn't taste right, send it back for rework if (evaluation == 4) { r.TextApproved = false; r.IngredientsApproved = false; r.RecipeEvaluated = 0; LogOutput($"{r} tasted bad! Reworking..."); } else r.RecipeEvaluated++; return EvaluateRecipe(r, rnd); } else { //Final editing pass(Brooke or Katie) Thread.Sleep(50 * r.Rank); int evaluation = rnd.Next(1, 10); // 1 means it just wasn't quite ready, send it back for rework if (evaluation == 1) { r.TextApproved = false; r.IngredientsApproved = false; r.RecipeEvaluated = 0; LogOutput($"{r} failed final editing! Reworking..."); return EvaluateRecipe(r, rnd); } else { r.FinalEditingComplete = true; LogOutput($"{r} is ready for release!"); } } return r; }
Here are the definitions of the RecipeChapter
and Recipe
classes used to help Brooke and Katie evaluate all of the recipes:
public class RecipeChapter { public int Number { get; set; } public string Title { get; set; } public List<Recipe> Recipes { get; set; } public override string ToString() => $"{Number} - {Title}"; } public class Recipe { public RecipeChapter Chapter { get; set; } public string MainIngredient { get; set; } public int Number { get; set; } public bool TextApproved { get; set; } public bool IngredientsApproved { get; set; } /// <summary> /// Recipe should be evaluated as many times as the Rank of the recipe /// </summary> public int RecipeEvaluated { get; set; } public bool FinalEditingComplete { get; set; } public int Rank { get; set; } public override string ToString() => $"{Chapter.Number}.{Number} ({Chapter.Title}:{MainIngredient})"; }
Sample output from the LINQ run looks like this and processes the collection in sequential order:
Running Cookbook Evaluation Evaluating Chapter 1 - Soups 1.1 (Soups:Sprouts, Mung Bean) is ready for release! 1.2 (Soups:Potato Bread) is ready for release! 1.3 (Soups:Chicken Liver) tasted bad! Reworking... 1.3 (Soups:Chicken Liver) is ready for release! 1.4 (Soups:Cherimoya) tasted bad! Reworking... 1.4 (Soups:Cherimoya) had incorrect measurements! Reworking... 1.4 (Soups:Cherimoya) is ready for release! 1.5 (Soups:High-Protein Bread) is ready for release! 1.6 (Soups:Flat Bread) failed the readthrough! Reworking... 1.6 (Soups:Flat Bread) is ready for release! 1.7 (Soups:Pomegranate) is ready for release! 1.8 (Soups:Carissa, Natal Plum) had incorrect measurements! Reworking... 1.8 (Soups:Carissa, Natal Plum) is ready for release! 1.9 (Soups:Ideal Flat Bread) is ready for release! 1.10 (Soups:Banana Bread) tasted bad! Reworking... 1.10 (Soups:Banana Bread) is ready for release! Finished Evaluating Chapter 1 - Soups Evaluating Chapter 2 - Salads 2.1 (Salads:Caraway) tasted bad! Reworking... 2.1 (Salads:Caraway) tasted bad! Reworking... 2.1 (Salads:Caraway) had incorrect measurements! Reworking... 2.1 (Salads:Caraway) is ready for release! 2.2 (Salads:Potatoes, Red) had incorrect measurements! Reworking... 2.2 (Salads:Potatoes, Red) tasted bad! Reworking... 2.2 (Salads:Potatoes, Red) is ready for release! 2.3 (Salads:Lemon) is ready for release! 2.4 (Salads:Cream cheese) is ready for release! 2.5 (Salads:Artichokes, Domestic) is ready for release! 2.6 (Salads:Grapefruit) is ready for release! 2.7 (Salads:Lettuce, Iceberg) is ready for release! 2.8 (Salads:Fenugreek) is ready for release! 2.9 (Salads:Ostrich) is ready for release! 2.10 (Salads:Brazil Nuts) tasted bad! Reworking... 2.10 (Salads:Brazil Nuts) had incorrect measurements! Reworking... 2.10 (Salads:Brazil Nuts) tasted bad! Reworking... 2.10 (Salads:Brazil Nuts) is ready for release! Finished Evaluating Chapter 2 - Salads Evaluating Chapter 3 - Appetizers 3.1 (Appetizers:Loquat) tasted bad! Reworking... 3.1 (Appetizers:Loquat) had incorrect measurements! Reworking... 3.1 (Appetizers:Loquat) tasted bad! Reworking... 3.1 (Appetizers:Loquat) is ready for release! 3.2 (Appetizers:Bergenost) is ready for release! 3.3 (Appetizers:Tomato Red Roma) had incorrect measurements! Reworking... 3.3 (Appetizers:Tomato Red Roma) tasted bad! Reworking... 3.3 (Appetizers:Tomato Red Roma) tasted bad! Reworking... 3.3 (Appetizers:Tomato Red Roma) is ready for release! 3.4 (Appetizers:Guava) failed final editing! Reworking... 3.4 (Appetizers:Guava) is ready for release! 3.5 (Appetizers:Squash Flower) is ready for release! 3.6 (Appetizers:Radishes, Red) is ready for release! 3.7 (Appetizers:Goose Liver) tasted bad! Reworking... 3.7 (Appetizers:Goose Liver) had incorrect measurements! Reworking... 3.7 (Appetizers:Goose Liver) is ready for release! 3.8 (Appetizers:Okra) had incorrect measurements! Reworking... 3.8 (Appetizers:Okra) is ready for release! 3.9 (Appetizers:Borage) is ready for release! 3.10 (Appetizers:Peppers) is ready for release! Finished Evaluating Chapter 3 - Appetizers Evaluating Chapter 4 - Entrees 4.1 (Entrees:Plantain) is ready for release! 4.2 (Entrees:Pignola (Pine)) is ready for release! 4.3 (Entrees:Potatoes, Gold) is ready for release! 4.4 (Entrees:Ribeye) failed the readthrough! Reworking... 4.4 (Entrees:Ribeye) is ready for release! 4.5 (Entrees:Sprouts, Mung Bean) failed the readthrough! Reworking... 4.5 (Entrees:Sprouts, Mung Bean) had incorrect measurements! Reworking... 4.5 (Entrees:Sprouts, Mung Bean) failed final editing! Reworking... 4.5 (Entrees:Sprouts, Mung Bean) is ready for release! 4.6 (Entrees:Squash) had incorrect measurements! Reworking... 4.6 (Entrees:Squash) is ready for release! 4.7 (Entrees:Squash, Winter) tasted bad! Reworking... 4.7 (Entrees:Squash, Winter) is ready for release! 4.8 (Entrees:Corn, Blue) is ready for release! 4.9 (Entrees:Snake) had incorrect measurements! Reworking... 4.9 (Entrees:Snake) tasted bad! Reworking... 4.9 (Entrees:Snake) tasted bad! Reworking... 4.9 (Entrees:Snake) is ready for release! 4.10 (Entrees:Prosciutto) is ready for release! Finished Evaluating Chapter 4 - Entrees Evaluating Chapter 5 - Desserts 5.1 (Desserts:Mushroom, White, Silver Dollar) tasted bad! Reworking... 5.1 (Desserts:Mushroom, White, Silver Dollar) had incorrect measurements! Reworking... 5.1 (Desserts:Mushroom, White, Silver Dollar) tasted bad! Reworking... 5.1 (Desserts:Mushroom, White, Silver Dollar) tasted bad! Reworking... 5.1 (Desserts:Mushroom, White, Silver Dollar) had incorrect measurements! Reworking... 5.1 (Desserts:Mushroom, White, Silver Dollar) is ready for release! 5.2 (Desserts:Eggplant) is ready for release! 5.3 (Desserts:Asparagus Peas) tasted bad! Reworking... 5.3 (Desserts:Asparagus Peas) failed the readthrough! Reworking... 5.3 (Desserts:Asparagus Peas) failed the readthrough! Reworking... 5.3 (Desserts:Asparagus Peas) is ready for release! 5.4 (Desserts:Squash, Kabocha) failed the readthrough! Reworking... 5.4 (Desserts:Squash, Kabocha) tasted bad! Reworking... 5.4 (Desserts:Squash, Kabocha) is ready for release! 5.5 (Desserts:Sprouts, Radish) is ready for release! 5.6 (Desserts:Mushroom, Black Trumpet) is ready for release! 5.7 (Desserts:Tea Cakes) tasted bad! Reworking... 5.7 (Desserts:Tea Cakes) tasted bad! Reworking... 5.7 (Desserts:Tea Cakes) failed the readthrough! Reworking... 5.7 (Desserts:Tea Cakes) is ready for release! 5.8 (Desserts:Blueberries) had incorrect measurements! Reworking... 5.8 (Desserts:Blueberries) tasted bad! Reworking... 5.8 (Desserts:Blueberries) is ready for release! 5.9 (Desserts:Sago Palm) is ready for release! 5.10 (Desserts:Opossum) had incorrect measurements! Reworking... 5.10 (Desserts:Opossum) is ready for release! Finished Evaluating Chapter 5 - Desserts Evaluating Chapter 6 - Snacks 6.1 (Snacks:Cheddar) tasted bad! Reworking... 6.1 (Snacks:Cheddar) is ready for release! 6.2 (Snacks:Melon, Bitter) is ready for release! 6.3 (Snacks:Scallion) is ready for release! 6.4 (Snacks:Squash Chayote) failed final editing! Reworking... 6.4 (Snacks:Squash Chayote) is ready for release! 6.5 (Snacks:Roasted Turkey) is ready for release! 6.6 (Snacks:Lime) is ready for release! 6.7 (Snacks:Hazelnut) is ready for release! 6.8 (Snacks:Radishes, Daikon) tasted bad! Reworking... 6.8 (Snacks:Radishes, Daikon) tasted bad! Reworking... 6.8 (Snacks:Radishes, Daikon) failed the readthrough! Reworking... 6.8 (Snacks:Radishes, Daikon) tasted bad! Reworking... 6.8 (Snacks:Radishes, Daikon) is ready for release! 6.9 (Snacks:Salami) failed the readthrough! Reworking... 6.9 (Snacks:Salami) is ready for release! 6.10 (Snacks:Mushroom, Oyster) failed the readthrough! Reworking... 6.10 (Snacks:Mushroom, Oyster) is ready for release! Finished Evaluating Chapter 6 - Snacks Evaluating Chapter 7 - Breakfast 7.1 (Breakfast:Daikon Radish) had incorrect measurements! Reworking... 7.1 (Breakfast:Daikon Radish) is ready for release! 7.2 (Breakfast:Lettuce, Red Leaf) failed final editing! Reworking... 7.2 (Breakfast:Lettuce, Red Leaf) is ready for release! 7.3 (Breakfast:Alfalfa Sprouts) is ready for release! 7.4 (Breakfast:Tea Cakes) is ready for release! 7.5 (Breakfast:Chia seed) is ready for release! 7.6 (Breakfast:Tangerine) is ready for release! 7.7 (Breakfast:Spinach) is ready for release! 7.8 (Breakfast:Flank Steak) is ready for release! 7.9 (Breakfast:Loganberries) had incorrect measurements! Reworking... 7.9 (Breakfast:Loganberries) had incorrect measurements! Reworking... 7.9 (Breakfast:Loganberries) had incorrect measurements! Reworking... 7.9 (Breakfast:Loganberries) is ready for release! 7.10 (Breakfast:Opossum) is ready for release! Finished Evaluating Chapter 7 - Breakfast Evaluating Chapter 8 - Sandwiches 8.1 (Sandwiches:Rhubarb) tasted bad! Reworking... 8.1 (Sandwiches:Rhubarb) is ready for release! 8.2 (Sandwiches:Pickle, Brine) is ready for release! 8.3 (Sandwiches:Oranges) tasted bad! Reworking... 8.3 (Sandwiches:Oranges) had incorrect measurements! Reworking... 8.3 (Sandwiches:Oranges) is ready for release! 8.4 (Sandwiches:Chayote, Pipinella, Vegetable Pear) tasted bad! Reworking... 8.4 (Sandwiches:Chayote, Pipinella, Vegetable Pear) is ready for release! 8.5 (Sandwiches:Bear) is ready for release! 8.6 (Sandwiches:Panela) had incorrect measurements! Reworking... 8.6 (Sandwiches:Panela) is ready for release! 8.7 (Sandwiches:Peppers, Red) had incorrect measurements! Reworking... 8.7 (Sandwiches:Peppers, Red) tasted bad! Reworking... 8.7 (Sandwiches:Peppers, Red) failed the readthrough! Reworking... 8.7 (Sandwiches:Peppers, Red) failed the readthrough! Reworking... 8.7 (Sandwiches:Peppers, Red) had incorrect measurements! Reworking... 8.7 (Sandwiches:Peppers, Red) tasted bad! Reworking... 8.7 (Sandwiches:Peppers, Red) is ready for release! 8.8 (Sandwiches:Oat Bread) is ready for release! 8.9 (Sandwiches:Peppers, Green) is ready for release! 8.10 (Sandwiches:Garlic) is ready for release! Finished Evaluating Chapter 8 - Sandwiches *********************************************** Full Chapter Evaluation with LINQ took: 00:01:19.1395258 ***********************************************
Sample output from the PLINQ run looks like this, processes in parallel (note the evaluation of four RecipeChapter
s at the beginning), and processes items out of sequential order:
Evaluating Chapter 5 - Desserts Evaluating Chapter 3 - Appetizers Evaluating Chapter 1 - Soups Evaluating Chapter 7 - Breakfast 7.1 (Breakfast:Daikon Radish) failed the readthrough! Reworking... 1.1 (Soups:Sprouts, Mung Bean) failed the readthrough! Reworking... 3.1 (Appetizers:Loquat) had incorrect measurements! Reworking... 1.1 (Soups:Sprouts, Mung Bean) had incorrect measurements! Reworking... 7.1 (Breakfast:Daikon Radish) tasted bad! Reworking... 5.1 (Desserts:Mushroom, White, Silver Dollar) tasted bad! Reworking... 3.1 (Appetizers:Loquat) failed final editing! Reworking... 7.1 (Breakfast:Daikon Radish) is ready for release! 3.1 (Appetizers:Loquat) tasted bad! Reworking... 5.1 (Desserts:Mushroom, White, Silver Dollar) tasted bad! Reworking... 1.1 (Soups:Sprouts, Mung Bean) is ready for release! 3.1 (Appetizers:Loquat) is ready for release! 1.2 (Soups:Potato Bread) had incorrect measurements! Reworking... 1.2 (Soups:Potato Bread) is ready for release! 1.3 (Soups:Chicken Liver) failed the readthrough! Reworking... 3.2 (Appetizers:Bergenost) is ready for release! 1.3 (Soups:Chicken Liver) had incorrect measurements! Reworking... 7.2 (Breakfast:Lettuce, Red Leaf) failed final editing! Reworking... 5.1 (Desserts:Mushroom, White, Silver Dollar) is ready for release! 5.2 (Desserts:Eggplant) is ready for release! 7.2 (Breakfast:Lettuce, Red Leaf) tasted bad! Reworking... 3.3 (Appetizers:Tomato Red Roma) is ready for release! 1.3 (Soups:Chicken Liver) is ready for release! 3.4 (Appetizers:Guava) is ready for release! 5.3 (Desserts:Asparagus Peas) is ready for release! 1.4 (Soups:Cherimoya) is ready for release! 5.4 (Desserts:Squash, Kabocha) is ready for release! 1.5 (Soups:High-Protein Bread) had incorrect measurements! Reworking... 7.2 (Breakfast:Lettuce, Red Leaf) failed final editing! Reworking... 1.5 (Soups:High-Protein Bread) failed final editing! Reworking... 5.5 (Desserts:Sprouts, Radish) is ready for release! 3.5 (Appetizers:Squash Flower) is ready for release! 3.6 (Appetizers:Radishes, Red) failed the readthrough! Reworking... 1.5 (Soups:High-Protein Bread) is ready for release! 5.6 (Desserts:Mushroom, Black Trumpet) tasted bad! Reworking... 1.6 (Soups:Flat Bread) is ready for release! 1.7 (Soups:Pomegranate) is ready for release! 3.6 (Appetizers:Radishes, Red) is ready for release! 7.2 (Breakfast:Lettuce, Red Leaf) is ready for release! 5.6 (Desserts:Mushroom, Black Trumpet) failed final editing! Reworking... 1.8 (Soups:Carissa, Natal Plum) is ready for release! 7.3 (Breakfast:Alfalfa Sprouts) is ready for release! 7.4 (Breakfast:Tea Cakes) is ready for release! 5.6 (Desserts:Mushroom, Black Trumpet) is ready for release! 3.7 (Appetizers:Goose Liver) is ready for release! 1.9 (Soups:Ideal Flat Bread) is ready for release! 5.7 (Desserts:Tea Cakes) tasted bad! Reworking... 3.8 (Appetizers:Okra) is ready for release! 3.9 (Appetizers:Borage) tasted bad! Reworking... 3.9 (Appetizers:Borage) failed the readthrough! Reworking... 3.9 (Appetizers:Borage) failed the readthrough! Reworking... 7.5 (Breakfast:Chia seed) is ready for release! 3.9 (Appetizers:Borage) is ready for release! 1.10 (Soups:Banana Bread) is ready for release! Finished Evaluating Chapter 1 - Soups Evaluating Chapter 2 - Salads 3.10 (Appetizers:Peppers) is ready for release! Finished Evaluating Chapter 3 - Appetizers Evaluating Chapter 4 - Entrees 5.7 (Desserts:Tea Cakes) is ready for release! 7.6 (Breakfast:Tangerine) is ready for release! 4.1 (Entrees:Plantain) is ready for release! 4.2 (Entrees:Pignola (Pine)) failed the readthrough! Reworking... 2.1 (Salads:Caraway) is ready for release! 5.8 (Desserts:Blueberries) is ready for release! 5.9 (Desserts:Sago Palm) failed the readthrough! Reworking... 5.9 (Desserts:Sago Palm) tasted bad! Reworking... 5.9 (Desserts:Sago Palm) is ready for release! 4.2 (Entrees:Pignola (Pine)) is ready for release! 2.2 (Salads:Potatoes, Red) is ready for release! 2.3 (Salads:Lemon) had incorrect measurements! Reworking... 4.3 (Entrees:Potatoes, Gold) is ready for release! 7.7 (Breakfast:Spinach) failed final editing! Reworking... 2.3 (Salads:Lemon) had incorrect measurements! Reworking... 4.4 (Entrees:Ribeye) had incorrect measurements! Reworking... 7.7 (Breakfast:Spinach) tasted bad! Reworking... 4.4 (Entrees:Ribeye) is ready for release! 2.3 (Salads:Lemon) tasted bad! Reworking... 5.10 (Desserts:Opossum) is ready for release! Finished Evaluating Chapter 5 - Desserts Evaluating Chapter 6 - Snacks 6.1 (Snacks:Cheddar) is ready for release! 4.5 (Entrees:Sprouts, Mung Bean) is ready for release! 7.7 (Breakfast:Spinach) is ready for release! 6.2 (Snacks:Melon, Bitter) is ready for release! 6.3 (Snacks:Scallion) failed the readthrough! Reworking... 7.8 (Breakfast:Flank Steak) tasted bad! Reworking... 2.3 (Salads:Lemon) failed final editing! Reworking... 7.8 (Breakfast:Flank Steak) is ready for release! 4.6 (Entrees:Squash) is ready for release! 2.3 (Salads:Lemon) tasted bad! Reworking... 4.7 (Entrees:Squash, Winter) failed the readthrough! Reworking... 4.7 (Entrees:Squash, Winter) had incorrect measurements! Reworking... 6.3 (Snacks:Scallion) is ready for release! 6.4 (Snacks:Squash Chayote) is ready for release! 4.7 (Entrees:Squash, Winter) is ready for release! 7.9 (Breakfast:Loganberries) is ready for release! 2.3 (Salads:Lemon) is ready for release! 7.10 (Breakfast:Opossum) is ready for release! Finished Evaluating Chapter 7 - Breakfast Evaluating Chapter 8 - Sandwiches 8.1 (Sandwiches:Rhubarb) had incorrect measurements! Reworking... 4.8 (Entrees:Corn, Blue) is ready for release! 2.4 (Salads:Cream cheese) failed final editing! Reworking... 2.4 (Salads:Cream cheese) is ready for release! 6.5 (Snacks:Roasted Turkey) failed final editing! Reworking... 4.9 (Entrees:Snake) is ready for release! 4.10 (Entrees:Prosciutto) failed the readthrough! Reworking... 6.5 (Snacks:Roasted Turkey) had incorrect measurements! Reworking... 2.5 (Salads:Artichokes, Domestic) tasted bad! Reworking... 4.10 (Entrees:Prosciutto) tasted bad! Reworking... 8.1 (Sandwiches:Rhubarb) tasted bad! Reworking... 4.10 (Entrees:Prosciutto) had incorrect measurements! Reworking... 4.10 (Entrees:Prosciutto) is ready for release! Finished Evaluating Chapter 4 - Entrees 6.5 (Snacks:Roasted Turkey) is ready for release! 6.6 (Snacks:Lime) had incorrect measurements! Reworking... 2.5 (Salads:Artichokes, Domestic) failed final editing! Reworking... 8.1 (Sandwiches:Rhubarb) is ready for release! 6.6 (Snacks:Lime) tasted bad! Reworking... 6.6 (Snacks:Lime) is ready for release! 2.5 (Salads:Artichokes, Domestic) is ready for release! 6.7 (Snacks:Hazelnut) is ready for release! 8.2 (Sandwiches:Pickle, Brine) is ready for release! 2.6 (Salads:Grapefruit) is ready for release! 2.7 (Salads:Lettuce, Iceberg) failed final editing! Reworking... 2.7 (Salads:Lettuce, Iceberg) is ready for release! 6.8 (Snacks:Radishes, Daikon) is ready for release! 8.3 (Sandwiches:Oranges) is ready for release! 6.9 (Snacks:Salami) tasted bad! Reworking... 2.8 (Salads:Fenugreek) is ready for release! 8.4 (Sandwiches:Chayote, Pipinella, Vegetable Pear) tasted bad! Reworking... 2.9 (Salads:Ostrich) failed the readthrough! Reworking... 6.9 (Snacks:Salami) is ready for release! 6.10 (Snacks:Mushroom, Oyster) is ready for release! Finished Evaluating Chapter 6 - Snacks 2.9 (Salads:Ostrich) failed final editing! Reworking... 2.9 (Salads:Ostrich) failed the readthrough! Reworking... 2.9 (Salads:Ostrich) failed the readthrough! Reworking... 8.4 (Sandwiches:Chayote, Pipinella, Vegetable Pear) is ready for release! 8.5 (Sandwiches:Bear) is ready for release! 2.9 (Salads:Ostrich) failed final editing! Reworking... 8.6 (Sandwiches:Panela) tasted bad! Reworking... 8.6 (Sandwiches:Panela) failed the readthrough! Reworking... 2.9 (Salads:Ostrich) is ready for release! 8.6 (Sandwiches:Panela) had incorrect measurements! Reworking... 8.6 (Sandwiches:Panela) is ready for release! 2.10 (Salads:Brazil Nuts) is ready for release! Finished Evaluating Chapter 2 - Salads 8.7 (Sandwiches:Peppers, Red) tasted bad! Reworking... 8.7 (Sandwiches:Peppers, Red) tasted bad! Reworking... 8.7 (Sandwiches:Peppers, Red) is ready for release! 8.8 (Sandwiches:Oat Bread) is ready for release! 8.9 (Sandwiches:Peppers, Green) is ready for release! 8.10 (Sandwiches:Garlic) is ready for release! Finished Evaluating Chapter 8 - Sandwiches *********************************************** Full Chapter Evaluation with PLINQ took: 00:00:25.1708103 *********************************************** Cookbook Evaluation Complete
If you are running a PLINQ query and the operation invoked throws an exception, it will not stop the set evaluation at that point but will continue to the end, recording any exceptions into an AggregateException
, which can be caught after the query is evaluated (not declared).
See Also
The “Parallel LINQ” topic in the MSDN documentation.
Get C# 6.0 Cookbook, 4th Edition now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.