Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 50 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
50
Dung lượng
187,21 KB
Nội dung
282 | Chapter 13: Introducing LINQ Example 13-1 defines a simple Customer class with three properties: FirstName, LastName, and EmailAddress. It overrides the Object.ToString( ) method to provide a string representation of its instances. Creating the Query The program starts by creating a customer list with some sample data, taking advan- tage of object initialization as discussed in Chapter 4. Once the list of customers is created, Example 13-1 defines a LINQ query: IEnumerable<Customer> result = from customer in customers where customer.FirstName == "Donna" select customer; The result variable is initialized with a query expression. In this example, the query will retrieve all Customer objects whose first name is “Donna” from the customer list. The result of such a query is a collection that implements IEnumerable<T>, where T is the type of the result object. In this example, because the query result is a set of Customer objects, the type of the result variable is IEnumerable<Customer>. Let’s dissect the query and look at each part in more detail. The from clause The first part of a LINQ query is the from clause: from customer in customers The generator of a LINQ query specifies the data source and a range variable. A LINQ data source can be any collection that implements the System.Collections. Generic.IEnumerable<T> interface. In this example, the data source is customers,an instance of List<Customer> that implements IEnumerable<T>. You’ll see how to do the same query against a SQL database in Chapter 15. A LINQ range variable is like an iteration variable in a foreach loop, iterating over the data source. Because the data source implements IEnumerable<T>, the C# com- piler can infer the type of the range variable from the data source. In this example, because the type of the data source is List<Customer>, the range variable customer is of type Customer. Filtering The second part of this LINQ query is the where clause, which is also called a filter. This portion of the clause is optional: where customer.FirstName == "Donna" Defining and Executing a Query | 283 The filter is a Boolean expression. It is common to use the range variable in a where clause to filter the objects in the data source. Because customer in this example is of type Customer, you use one of its properties, in this case FirstName, to apply the filter for your query. Of course, you may use any Boolean expression as your filter. For instance, you can invoke the String.StartsWith( ) method to filter customers by the first letter of their last name: where customer.LastName.StartsWith("G") You can also use composite expressions to construct more complex queries. In addi- tion, you can use nested queries where the result of one query (the inner query) is used to filter another query (the outer query). Projection (or select) The last part of a LINQ query is the select clause (known to database geeks as the “projection”), which defines (or projects) the results: select customer; In this example, the query returns the customer objects that satisfy the query condi- tion. You may constrain which fields you project, much as you would with SQL. For instance, you can return only the qualified customers’ email addresses only: select customer.EmailAddress; Deferred Query Evaluation LINQ implements deferred query evaluation, meaning that the declaration and ini- tialization of a query expression do not actually execute the query. Instead, a LINQ query is executed, or evaluated, when you iterate through the query result: foreach (Customer customer in result) Console.WriteLine(customer.ToString( )); Because the query returns a collection of Customer objects, the iteration variable is an instance of the Customer class. You can use it as you would any Customer object. This example simply calls each Customer object’s ToString( ) method to output its property values to the console. Each time you iterate through this foreach loop, the query will be reevaluated. If the data source has changed between executions, the result will be different. This is dem- onstrated in the next code section: customers[3].FirstName = "Donna"; Here, you modify the first name of the customer “Janet Gates” to “Donna” and then iterate through the result again: Console.WriteLine("FirstName == \"Donna\" (take two)"); 284 | Chapter 13: Introducing LINQ foreach (Customer customer in result) Console.WriteLine(customer.ToString( )); As shown in the sample output, you can see that the result now includes Donna Gates as well. In most situations, deferred query evaluation is desired because you want to obtain the most recent data in the data source each time you run the query. However, if you want to cache the result so that it can be processed later without having to reexecute the query, you can call either the ToList( ) or the ToArray( ) method to save a copy of the result. Example 13-2 demonstrates this technique as well. Example 13-2. A simple LINQ query with cached results using System; using System.Collections.Generic; using System.Linq; namespace Programming_CSharp { // Simple customer class public class Customer { // Same as in Example 13-1 } // Main program public class Tester { static void Main( ) { List<Customer> customers = CreateCustomerList( ); // Find customer by first name IEnumerable<Customer> result = from customer in customers where customer.FirstName == "Donna" select customer; List<Customer> cachedResult = result.ToList<Customer>( ); Console.WriteLine("FirstName == \"Donna\""); foreach (Customer customer in cachedResult) Console.WriteLine(customer.ToString( )); customers[3].FirstName = "Donna"; Console.WriteLine("FirstName == \"Donna\" (take two)"); foreach (Customer customer in cachedResult) Console.WriteLine(customer.ToString( )); } // Create a customer list with sample data private static List<Customer> CreateCustomerList( ) { // Same as in Example 13-1 LINQ and C# | 285 In this example, you call the ToList<T> method of the result collection to cache the result. Note that calling this method causes the query to be evaluated immediately. If the data source is changed after this, the change will not be reflected in the cached result. You can see from the output that there is no Donna Gates in the result. One interesting point here is that the ToList<T> and ToArray<T> methods are not actually methods of IEnumerable; that is, if you look in the documentation for IEnumerable, you will not see them in the methods list. They are actually extension methods provided by LINQ. We will look at extension methods in more detail later in this chapter. If you are familiar with SQL, you will notice a striking similarity between LINQ and SQL, at least in their syntax. The only odd-one-out at this stage is that the select statement in LINQ appears at the end of LINQ query expressions, instead of at the beginning, as in SQL. Because the generator, or the from clause, defines the range variable, it must be stated first. Therefore, the projection part is pushed back. LINQ and C# LINQ provides many of the common SQL operations, such as join queries, grouping, aggregation, and sorting of results. In addition, it allows you to use the object- oriented features of C# in query expressions and processing, such as hierarchical query results. Joining You will often want to search for objects from more than one data source. LINQ pro- vides the join clause that offers the ability to join many data sources, not all of which need be databases. Suppose you have a list of customers containing customer names and email addresses, and a list of customer home addresses. You can use LINQ to combine both lists to produce a list of customers, with access to both their email and home addresses: from customer in customers join address in addresses on } } } Output: FirstName == "Donna" Donna Carreras Email: donna0@adventure-works.com FirstName == "Donna" (take two) Donna Carreras Email: donna0@adventure-works.com Example 13-2. A simple LINQ query with cached results (continued) 286 | Chapter 13: Introducing LINQ customer.Name equals address.Name The join condition is specified in the on subclause, similar to SQL, except that the objects joined need not be tables or views in a database. The join class syntax is: [data source 1] join [data source 2] on [join condition] Here, we are joining two data sources, customers and addresses, based on the cus- tomer name properties in each object. In fact, you can join more than two data sources using a combination of join clauses: from customer in customers join address in addresses on customer.Name equals address.Name join invoice in invoices on customer.Id equals invoice.CustomerId join invoiceItem in invoiceItems on invoice.Id equals invoiceItem.invoiceId A LINQ join clause returns a result only when objects satisfying the join condition exist in all data sources. For instance, if a customer has no invoice, the query will not return anything for that customer, not even her name and email address. This is the equiva- lent of a SQL inner join clause. LINQ cannot perform an outer join (which returns a result if either of the data sources contains objects that meet the join condition). Ordering and the var Keyword You can also specify the sort order in LINQ queries with the orderby clause: from customer in Customers orderby customer.LastName select customer; This sorts the result by customer last name in ascending order. Example 13-3 shows how you can sort the results of a join query. Example 13-3. A sorted join query using System; using System.Collections.Generic; using System.Linq; namespace Programming_CSharp { // Simple customer class public class Customer { // Same as in Example 13-1 } LINQ and C# | 287 // Customer address class public class Address { public string Name { get; set; } public string Street { get; set; } public string City { get; set; } // Overrides the Object.ToString( ) to provide a // string representation of the object properties. public override string ToString( ) { return string.Format("{0}, {1}", Street, City); } } // Main program public class Tester { static void Main( ) { List<Customer> customers = CreateCustomerList( ); List<Address> addresses = CreateAddressList( ); // Find all addresses of a customer var result = from customer in customers join address in addresses on string.Format("{0} {1}", customer.FirstName, customer.LastName) equals address.Name orderby customer.LastName, address.Street descending select new { Customer = customer, Address = address }; foreach (var ca in result) { Console.WriteLine(string.Format("{0}\nAddress: {1}", ca.Customer, ca.Address)); } } // Create a customer list with sample data private static List<Customer> CreateCustomerList( ) { // Same as in Example 13-1 } // Create a customer list with sample data private static List<Address> CreateAddressList( ) { List<Address> addresses = new List<Address> { Example 13-3. A sorted join query (continued) 288 | Chapter 13: Introducing LINQ The Customer class is identical to the one used in Example 13-1. The address is also very simple, with a customer name field containing customer names in the <first name> <last name> form, and the street and city of customer addresses. The CreateCustomerList( ) and CreateAddressList( ) methods are just helper func- tions to create sample data for this example. This example also uses the new C# object and collection initializers, as explained in Chapter 4. The query definition, however, looks quite different from the last example: var result = from customer in customers join address in addresses on string.Format("{0} {1}", customer.FirstName, customer.LastName) equals address.Name new Address { Name = "Janet Gates", Street = "165 North Main", City = "Austin" }, new Address { Name = "Keith Harris", Street = "3207 S Grady Way", City = "Renton" }, new Address { Name = "Janet Gates", Street = "800 Interchange Blvd.", City = "Austin" }, new Address { Name = "Keith Harris", Street = "7943 Walnut Ave", City = "Renton" }, new Address { Name = "Orlando Gee", Street = "2251 Elliot Avenue", City = "Seattle" } }; return addresses; } } } Output: Janet Gates Email: janet1@adventure-works.com Address: 800 Interchange Blvd., Austin Janet Gates Email: janet1@adventure-works.com Address: 165 North Main, Austin Orlando Gee Email: orlando0@adventure-works.com Address: 2251 Elliot Avenue, Seattle Keith Harris Email: keith0@adventure-works.com Address: 7943 Walnut Ave, Renton Keith Harris Email: keith0@adventure-works.com Address: 3207 S Grady Way, Renton Example 13-3. A sorted join query (continued) LINQ and C# | 289 orderby customer.LastName, address.Street descending select new { Customer = customer, Address = address.Street }; The first difference is the declaration of the result. Instead of declaring the result as an explicitly typed IEnumerable<Customer> instance, this example declares the result as an implicitly typed variable using the new var keyword. We will leave this for just a moment, and jump to the query definition itself. The generator now contains a join clause to signify that the query is to be operated on two data sources: customers and addresses. Because the customer name property in the Address class is a concatenation of customer first and last names, you con- struct the names in Customer objects to the same format: string.Format("{0} {1}", customer.FirstName, customer.LastName) The dynamically constructed customer full name is then compared with the customer name property in the Address objects using the equals operator: string.Format("{0} {1}", customer.FirstName, customer.LastName) equals address.Name The orderby clause indicates the order in which the result should be sorted: orderby customer.LastName, address.Street descending In the example, the result will be sorted first by customer last name in ascending order, then by street address in descending order. The combined customer name, email address, and home address are returned. Here you have a problem—LINQ can return a collection of objects of any type, but it can’t return multiple objects of different types in the same query, unless they are encapsu- lated in one type. For instance, you can select either an instance of the Customer class or an instance of the Address class, but you cannot select both, like this: select customer, address The solution is to define a new type containing both objects. An obvious way is to define a CustomerAddress class: public class CustomerAddress { public Customer Customer { get; set; } public Address Address { get; set; } } You can then return customers and their addresses from the query in a collection of CustomerAddress objects: var result = from customer in customers join address in addresses on string.Format("{0} {1}", customer.FirstName, customer.LastName) equals address.Name orderby customer.LastName, address.Street descending Select new CustomerAddress { Customer = customer, Address = address }; 290 | Chapter 13: Introducing LINQ Grouping and the group Keyword Another powerful feature of LINQ, commonly used by SQL programmers but now integrated into the language itself, is grouping, as shown in Example 13-4. Example 13-4. A group query using System; using System.Collections.Generic; using System.Linq; namespace Programming_CSharp { // Customer address class public class Address { // Same as in Example 13-3 } // Main program public class Tester { static void Main( ) { List<Address> addresses = CreateAddressList( ); // Find addresses grouped by customer name var result = from address in addresses group address by address.Name; foreach (var group in result) { Console.WriteLine("{0}", group.Key); foreach (var a in group) Console.WriteLine("\t{0}", a); } } // Create a customer list with sample data private static List<Address> CreateAddressList( ) { // Same as in Example 13-3 } } } Output: Janet Gates 165 North Main, Austin 800 Interchange Blvd., Austin Keith Harris 3207 S Grady Way, Renton 7943 Walnut Ave, Renton Orlando Gee 2251 Elliot Avenue, Seattle Implicitly Typed Local Variables | 291 Example 13-4 makes use of the group keyword, a query operator that splits a sequence into a group given a key value—in this case, customer name ( address.Name). The result is a collection of groups, and you’ll need to enumerate each group to get the objects belonging to it. Anonymous Types Often, you do not want to create a new class just for storing the result of a query. C# 3.0 provides anonymous types that allow us to declare both an anonymous class and an instance of that class using object initializers. For instance, we can initialize an anonymous customer address object: new { Customer = customer, Address = address } This declares an anonymous class with two properties, Customer and Address, and initializes it with an instance of the Customer class and an instance of the Address class. The C# compiler can infer the property types with the types of assigned values, so here, the Customer property type is the Customer class, and the Address property type is the Address class. As a normal, named class, anonymous classes can have properties of any type. Behind the scenes, the C# compiler generates a unique name for the new type. This name cannot be referenced in application code; therefore, it is considered nameless. Implicitly Typed Local Variables Now, let’s go back to the declaration of query results where you declare the result as type var: var result = Because the select clause returns an instance of an anonymous type, you cannot define an explicit type IEnumerable<T>. Fortunately, C# 3.0 provides another fea- ture—implicitly typed local variables—that solves this problem. You can declare an implicitly typed local variable by specifying its type as var: var id = 1; var name = "Keith"; var customers = new List<Customer>( ); var person = new {FirstName = "Donna", LastName = "Gates", Phone="123-456-7890" }; The C# compiler infers the type of an implicitly typed local variable from its initial- ized value. Therefore, you must initialize such a variable when you declare it. In the preceding code snippet, the type of id will be set as an integer, the type of name as a string, and the type of customers as a strongly typed List<T> of Customer objects. The type of the last variable, person, is an anonymous type containing three properties: FirstName, LastName, and Phone. Although this type has no name in our code, the C#