Chapter 15. Putting LINQ to Work

LINQ may be the most anticipated, most exciting (and to some, most feared) feature in C# 3.0. The previous two chapters were, in large measure, a necessary introduction, an appetizer to whet your appetite and get you ready for the main meal: using LINQ to retrieve meaningful data in production applications.

Before we begin, let’s be clear: your DBA is terrified of LINQ, and not just as a matter of job security. Improperly used, LINQ has the ability to put queries into the hands of inexperienced, untrained goofballs (us) who know little or nothing about writing efficient queries, and who will bring carefully honed data-intensive enterprise systems to their knees (fun, eh?). OK, I said it out loud, so let’s all stop panicking.

As with all programming, the trick is to write the program, get it working, and then optimize. It may be that after you have your program up and working (and profiled), you’ll discover that there are some places that you’ve used LINQ that you’d be better off using stored procedures running within your database (that’s what databases do for a living), but we don’t know that a priori, and the advantages of LINQ are so tremendous (e.g., the ability to use an object-oriented unified syntax to access all your data regardless of source) that it cries out for a “code now, optimize later if needed" approach.

The two most common sources you’ll use LINQ with are, no doubt, SQL and XML, but they are certainly not the only sources of data. You may well find yourself retrieving data from:

  • Files

  • Flat databases

  • Mail messages

  • Web services

  • Legacy systems

  • In memory data structures

And most exciting are sources you haven’t anticipated yet. With the understanding of LINQ fundamentals you gained in Chapter 13, and the grounding in XML you gained in Chapter 14, you are now just about ready to dig in and put LINQ to work.

Getting Set Up

Examples in this section use the SQL Server 2005 Adventure Works LT sample database. To set up this database, download it from:

http://www.codeplex.com/msftdbprodsamples/release/projectreleases.aspx?releaseid=4004

Warning

Please note that although this database is a simplified version of the more comprehensive AdventureWorks, the two are quite different, and the examples in this chapter will not work with the full AdventureWorks database. Please select the AdventureWorksLT MSI package applicable for your platform—32-bit, x64, or IA64. If SQL Server is installed in the default directory, install the sample database to C:Program FilesMicrosoft SQL ServerMSSQL.1MSSQLData. Otherwise, install the database to the Data subdirectory under its installation directory.

If you are using SQL Server Express included in Visual Studio 2008, you will need to enable the Named Pipes protocol:

  1. Open SQL Server Configuration Manager under Start → All Programs → Microsoft SQL Server 2005 → Configuration Tools → SQL Server Configuration Manager.

  2. In the left pane, select SQL Server Configuration Manager (Local) → SQL Server 2005 Network Configuration → Protocols for SQLEXPRESS.

  3. In the right pane, right-click the Named Pipes protocol and select Enable, as shown in Figure 15-1.

    Enabling the Named Pipes protocol in SQL Server 2005 Express
    Figure 15-1. Enabling the Named Pipes protocol in SQL Server 2005 Express
  4. In the left pane, select SQL Server 2005 Services, then right-click SQL Server (SQLEXPRESS), and select Restart to restart SQL Server, as shown in Figure 15-2.

    Restarting SQL Server 2005 Express
    Figure 15-2. Restarting SQL Server 2005 Express
  5. Attach the sample database to SQL Server Express using one of the following methods:

    1. If you already have SQL Server Client tools installed, open SQL Server Management Studio under Start → All Programs → Microsoft SQL Server 2005 → SQL Server Management Studio and connect to the local SQL Server Express database.

    2. Download SQL Server Express Management Studio from the Microsoft SQL Server Express page (http://msdn2.microsoft.com/en-us/express/bb410792.aspx), and install it on your machine. Then, open it and connect to the local SQL Server Express database.

  6. In the left pane, right-click Databases and select Attach (see Figure 15-3).

    Attaching the database to SQL Server 2005 Express
    Figure 15-3. Attaching the database to SQL Server 2005 Express
  7. On the Attach Databases dialog click Add.

  8. Click OK to close this dialog, and OK again to close the Attach Database dialog.

LINQ to SQL Fundamentals

To begin, open Visual Studio, and create a new application named “Simple Linq to SQL” as a console application. Once the IDE is open, click on View, and open the Server Explorer and make a connection to the AdventureWorksLT database, and test that connection.

With that in place, you are ready to create a program that uses LINQ to connect your SQL database. You’ll need to include the System.Data.Linq namespace in the references for your project as shown in Figure 15-4 so that the last two using statements will compile.

Adding a reference to System.Data.Linq
Figure 15-4. Adding a reference to System.Data.Linq

This will also create the mapping between each class property and the corresponding database column:

public class Customer
 {[Column] public string FirstName      { get; set; }
    [Column] public string LastName       { get; set; }
    [Column] public string EmailAddress   { get; set; }

Complete analysis follows Example 15-1.

Example 15-1. Simple LINQ to SQL
using System;
using System.Data.Linq;
using System.Data.Linq.Mapping;
using System.Linq;

namespace Simple_Linq_to_SQL
{
    // customer class
    [Table(Name="SalesLT.Customer")]
    public class Customer
    {
        [Column] public string FirstName    { get; set; }
        [Column] public string LastName     { get; set; }
        [Column] public string EmailAddress { get; set; }

        // Overrides the Object.ToString(  ) to provide a
        // string representation of the object properties.
        public override string ToString(  )
        {
            return string.Format("{0} {1}
Email:   {2}",
                        FirstName, LastName, EmailAddress);
        }
    }


    public class Tester
    {
        static void Main(  )
        {
            DataContext db = new DataContext(
                @"Data Source=.SqlExpress;
                    Initial Catalog=AdventureWorksLT;
                    Integrated Security=True");

            Table<Customer> customers = db.GetTable<Customer>(  );
            var query =
                from customer in customers
                where customer.FirstName == "Donna"
                select customer;

            foreach(var c in query)
                Console.WriteLine(c.ToString(  ));

            Console.ReadKey(  );
        }
    }
}

Output:
Donna Carreras
Email:   [email protected]

The key to this program is in the first line of Main( ), where you define db to be of type DataContext. A DataContext object is the entry point for the LINQ to SQL framework, providing a bridge between the application code and database-specific commands. Its job is to translate high-level C# LINQ to SQL code to corresponding database commands and execute them behind the scenes. It maintains a connection to the underlying database, fetches data from the database when requested, tracks changes made to every entity retrieved from the database, and updates the database as needed. It maintains an “identity cache” to guarantee that if you retrieve an entity more than once, all duplicate retrievals will be represented by the same object instance (thereby preventing database corruption; for more information, see the "Database Corruption" sidebar).

Once the DataContext is instantiated, you can access the objects contained in the underlying database. This example uses the Customer table in the AdventureWorksLT database using the DataContext’s GetTable( ) function:

            Table<Customer> customers = db.GetTable<Customer>(  );

This function is a generic function so that you can specify that the table should be mapped to a collection of Customer objects.

DataContext has a great many methods and properties, one of which is a Log. This property allows you to specify the destination where it logs the SQL queries and commands executed. By redirecting it to where you can access it, you can see how LINQ does its magic. For instance, you can redirect the Log to Console.Out so that you can see the output on the system console:

Output:
SELECT [t0].[FirstName], [t0].[LastName], [t0].[EmailAddress]
FROM [SalesLT].[Customer] AS [t0]
WHERE [t0].[FirstName] = @p0
-- @p0: Input String (Size = 5; Prec = 0; Scale = 0) [Donna]
-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1

Using Visual Studio LINQ to SQL Designer

Rather than working out the data relationships in the underlying database and mapping them in the DataContext manually, you can use the designer built into Visual Studio. This is a very powerful mechanism that makes working with LINQ painfully simple. To see how this works, first open the AdventureWorksLT database in SQL Server Management Studio Express and examine the Customer, CustomerAddress, and Address tables so that you feel comfortable you understand their relationship, as illustrated by the Entity-Relationship diagram shown in Figure 15-5.

AdventureWorksLT database diagram
Figure 15-5. AdventureWorksLT database diagram

Create a new Visual Studio console application called AdventureWorksDBML. Make sure the Server Explorer is visible and you have a connection to AdventureWorksLT, as shown in Figure 15-6. If the connection is not available, follow the instructions mentioned earlier to create it.

Server Explorer window
Figure 15-6. Server Explorer window

To create your LINQ to SQL classes, right-click on the project, and choose Add → New Item, as shown in Figure 15-7.

Selecting Add→New Item
Figure 15-7. Selecting Add→New Item

When the New Item dialog opens, choose LINQ to SQL Classes. You can use the default name (probably DataClasses1), or replace it with a more meaningful name. In this case, replace it with AdventureWorksAddress, and click Add. The name you select will become the name of your DataContext object with the word DataContext appended. Therefore, the data context name in this case will be AdventureWorksAddressDataContext.

The center window shows changes to the Object Relational Designer. You can now drag tables from Server Explorer or Toolbox to the designer. Drag the Address, Customer, and CustomerAddress tables from the Server Explorer onto this space, as shown in Figure 15-8.

Dragging tables onto the work surface
Figure 15-8. Dragging tables onto the work surface

In the image, two tables have been dragged on, and the third is about to be dropped. Once your tables are dropped, Visual Studio 2008 automatically retrieves and displays the relationship between the tables. You can arrange them to ensure that the relationships between the tables are displayed clearly.

Once you’ve done that, two new files have been created: AdventureWorksAddress.dbml.layout and AdventureWorksAddress.designer.cs. The former has the XML representation of the tables you’ve put on the surface, a short segment of which is shown here:

<?xml version="1.0" encoding="utf-8"?>
<ordesignerObjectsDiagram dslVersion="1.0.0.0" absoluteBounds="0, 0, 11, 8.5"
name="AdventureWorksAddress">
  <DataContextMoniker Name="/AdventureWorksAddressDataContext" />
  <nestedChildShapes>
    <classShape Id="4a893188-c5cd-44db-a114-0444cced4057" absoluteBounds="1.125,
1.375, 2, 2.5401025390625">
      <DataClassMoniker Name="/AdventureWorksAddressDataContext/Address" />
      <nestedChildShapes>
        <elementListCompartment Id="d59f1bc4-752e-41db-a940-4a9938014ca7"
absoluteBounds="1.1400000000000001, 1.835, 1.9700000000000002, 1.9801025390625"
name="DataPropertiesCompartment" titleTextColor="Black" itemTextColor="Black" />
      <
estedChildShapes>
    </classShape>
    <classShape Id="c432968b-f644-4ca3-b26b-61dfe4292884" absoluteBounds="5.875, 1,
2, 3.6939111328124996">
      <DataClassMoniker Name="/AdventureWorksAddressDataContext/Customer" />
      <nestedChildShapes>
        <elementListCompartment Id="c240ad98-f162-4921-927a-c87781db6ac4"
absoluteBounds="5.8900000000000006, 1.46, 1.9700000000000002, 3.1339111328125"
name="DataPropertiesCompartment" titleTextColor="Black" itemTextColor="Black" />
      <
estedChildShapes>
    </classShape>

The .cs file has the code to handle all the LINQ to SQL calls that you otherwise would have to write by hand. Like all machine-generated code, it is terribly verbose; here is a very brief excerpt:

public Address(  )
{
    OnCreated(  );
    this._CustomerAddresses = new EntitySet<CustomerAddress>(new
      Action<CustomerAddress>(this.attach_CustomerAddresses),
      new Action<CustomerAddress>(this.detach_CustomerAddresses));
}

[Column(Storage="_AddressID", AutoSync=AutoSync.OnInsert,
DbType="Int NOT NULL IDENTITY", IsPrimaryKey=true, IsDbGenerated=true)]
public int AddressID
{
    get
    {
        return this._AddressID;
    }
    set
    {
        if ((this._AddressID != value))
        {
            this.OnAddressIDChanging(value);
            this.SendPropertyChanging(  );
            this._AddressID = value;
            this.SendPropertyChanged("AddressID");
            this.OnAddressIDChanged(  );
        }
    }
}

The classes that are generated are strongly typed, and a class is generated for each table you place in the designer.

Tip

For a review of strongly typed versus loosely typed classes, see Chapter 9, particularly the section on Generics.

The DataContext class exposes each table as a property, and the relationships between the tables are represented by properties of the classes representing data records. For example, the CustomerAddress table is mapped to the CustomerAddresses property, which is a strongly typed collection (LINQ table) of CustomerAddress objects. You can access the parent Customer and Address objects of a CustomerAddress object through its Customer and Address properties, respectively. This makes it quite easy to write the code to retrieve data.

Retrieving Data

Replace the contents of Program.cs with the code shown in Example 15-2 to use the generated LINQ to SQL code to retrieve data from the three tables you’ve mapped using the designer.

Example 15-2. Using LINQ to SQL designer-generated classes
using System;
using System.Linq;
using System.Text;

namespace AdventureWorksDBML
{
    // Main program
    public class Tester
    {
        static void Main(  )
        {
            AdventureWorksAddressDataContext dc = new
                AdventureWorksAddressDataContext(  );
            // Uncomment the statement below to show the
            // SQL statement generated by LINQ to SQL.
            // dc.Log = Console.Out;

            // Find one customer record.
            Customer donna = dc.Customers.Single(c => c.FirstName == "Donna");");
            Console.WriteLine(donna);

            // Find a list of customer records.
            var customerDs =
                from customer in dc.Customers
                where customer.FirstName.StartsWith("D")
                orderby customer.FirstName, customer.LastName
                select customer;

            foreach (Customer customer in customerDs)
            {
                Console.WriteLine(customer);
            }
        }
    }

    // Add a method to the generated Customer class to
    // show formatted customer properties.
    public partial class Customer
    {
        public override string ToString(  )
        {
            StringBuilder sb = new StringBuilder(  );
            sb.AppendFormat("{0} {1} {2}",
                    FirstName, LastName, EmailAddress);
            foreach (CustomerAddress ca in CustomerAddresses)
            {
                sb.AppendFormat("
	{0}, {1}",
                    ca.Address.AddressLine1,
                    ca.Address.City);
            }
            sb.AppendLine(  );
            return sb.ToString(  );
        }
    }
}


Output:
Donna Carreras [email protected]
        12345 Sterling Avenue, Irving

(only showing the first 5 customers):
Daniel Blanco [email protected]
        Suite 800 2530 Slater Street, Ottawa
Daniel Thompson [email protected]
        755 Nw Grandstand, Issaquah
Danielle Johnson [email protected]
        955 Green Valley Crescent, Ottawa
Darrell Banks [email protected]
        Norwalk Square, Norwalk
Darren Gehring [email protected]
        509 Nafta Boulevard, Laredo

Creating Properties for Each Table

As you can see, you begin by creating an instance of the DataContext object you asked the tool to generate:

AdventureWorksAddressDataContext dc = new AdventureWorksAddressDataContext(  );

When you use the designer, one of the things it does, besides creating the DataContext class, is define a property for each table you’ve placed in the designer (in this case, Customer, Address, and CustomerAddress). It names those properties by making them plural. Therefore, the properties of AdventureWorksAddressDataContext include Customers, Addresses, and CustomerAddresses.

Tip

One side effect of this convention is that it would be a good idea to name your database tables in singular form to avoid potential confusion in your code. By default, the LINQ to SQL designer names the generated data classes the same as the table names. If you use plural table names, the class names will be the same as the DataContext property names. Therefore, you will need to manually modify the generated class names to avoid such naming conflicts.

You can access these properties through the DataContext instance:

dc.Customers

These properties are themselves table objects that implement the IQueryable interface, which itself has a number of very useful methods that allow you to filter, traverse, and project operations over the data in a LINQ table.

Most of these methods are extension methods of the LINQ types, which means they can be called just as though they were instance methods of the object that implements IQueryable<T> (in this case, the tables in the DataContext). Therefore, because Single is a method of IQueryable that returns the only element in a collection that meets a given set of criteria, we’ll use that to find the one customer whose first name is Donna. If there is more than one customer with that specific first name, only the first customer record is returned:

Customer donna = dc.Customers.Single(c => c.FirstName == "Donna");

Let’s unpack this line of code.

You begin by getting the Customers property of the DataContext instance, dc:

dc.Customers

What you get back is a Customer table object, which implements IQueryable. You can therefore call the method Single on this object:

dc.Customers.Single(condition);

The result will be to return a Customer object, which you can assign to a local variable of type Customer:

Customer donna = dc.Customers.Single(condition);

Tip

Notice that everything we are doing here is strongly typed. This is goodness.

Inside the parentheses, you must place the expression that will filter for the one record we need, and this is a great opportunity to use lambda expressions:

c => c.FirstName == "Donna"

Tip

You read this as "c goes to c.FirstName where c.FirstName equals Donna.”

In this notation, c is an implicitly typed variable (of type Customer). LINQ to SQL translates this expression into a SQL statement similar to the following:

Select * from Customer where FirstName = ‘Donna’;

Please note that this is just an arbitrary sample SQL. You can see the exact SQL as generated by LINQ to SQL by redirecting the DataContext log and examining the output, as described earlier in this chapter.

This SQL statement is executed when the Single method is executed:

Customer donna = dc.Customers.Single(c => c.FirstName == "Donna");

This Customer object (donna) is then printed to the console:

Console.WriteLine(donna);

The output is:

Donna Carreras [email protected]
        12345 Sterling Avenue, Irving,

Note that although you searched only by first name, what you retrieved was a complete record, including the address information. Also note that the output is created just by passing in the object, using the overridden method we created for the tool-generated class (see the earlier sidebar, "Appending a Method to a Generated Class“).

A LINQ Query

The next block uses the new-to-C# 3.0 keyword var to declare a variable customerDS which is implicitly typed by the compiler, based on the information returned by the LINQ query:

 var customerDs =
     from customer in dc.Customers
     where customer.FirstName.StartsWith("D")
     orderby customer.FirstName, customer.LastName
     select customer;

This query is similar to a SQL query (as noted in the previous chapter), and as you can see, you are selecting from the DataContext Customers property (e.g., the Customer table) each customer whose FirstName property (e.g., the FirstName column) begins with D. You are ordering by FirstName and then LastName, and returning all of the results into customerDs, whose implicit type is a TypedCollection of Customers.

With that in hand, you can iterate through the collection and print the data about these customers to the console, treating them as Customer objects rather than as data records:

foreach (Customer customer in customerDs)
{
    Console.WriteLine(customer);
}

This is reflected in this excerpt of the output:

Delia Toone [email protected]
        755 Columbia Ctr Blvd, Kennewick

Della Demott Jr [email protected]
        25575 The Queensway, Etobicoke

Denean Ison [email protected]
        586 Fulham Road,, London

Denise Maccietto [email protected]
        Port Huron, Port Huron

Derek Graham [email protected]
        655-4th Ave S.W., Calgary

Derik Stenerson [email protected]
        Factory Merchants, Branson

Diane Glimp [email protected]
        4400 March Road, Kanata

Updating Data Using LINQ to SQL

To add or modify data to the database using LINQ, you interact with objects in C#, make your changes, and then tell the DataContext to SubmitChanges, allowing LINQ to take care of the details. This is an extremely object-oriented way to approach data storage. Your code remains strongly typed and yet decoupled from the underlying persistence mechanism.

If you want to add new data to the database, you instantiate a new object and then save it. If you want to modify data already persisted (stored) in the database, you retrieve the object, modify it, and then store it. The key to Example 15-3 is that from a C# perspective, you are interacting with objects and letting LINQ worry about the details of interacting with SQL Server.

Example 15-3. Modifying data using LINQ to SQL
using System;
using System.Collections.Generic;
using System.Data.Linq;
using System.Data.Linq.Mapping;
using System.Linq;
using System.Text;

namespace Modifying_Data_Using_Linq_To_SQL
{
    // Main program
    public class Tester
    {
        static void Main(  )
        {
            AddCustomer(  );
            UpdateCustomer(  );
            Console.ReadKey(  );
        }

        private static void AddCustomer(  )
        {
            Console.WriteLine("Adding a new customer...");
            AdventureWorksDataContext dc = new AdventureWorksDataContext(  );
            // Uncomment the statement below to show the
            // SQL statement generated by LINQ to SQL.
            // dc.Log = Console.Out;

            // Add a new customer with address
            Customer douglas = new Customer(  );
            douglas.FirstName = "Douglas";
            douglas.LastName = "Adams";
            douglas.EmailAddress = "[email protected]";
            douglas.PasswordHash = "fake";
            douglas.PasswordSalt = "fake";
            douglas.ModifiedDate = DateTime.Today;
            douglas.rowguid = Guid.NewGuid(  );

            Address addr = new Address(  );
            addr.AddressLine1 = "1c Sharp Way";
            addr.City = "Seattle";
            addr.PostalCode = "98011";
            addr.StateProvince = "Washington";
            addr.CountryRegion = "United States";
            addr.ModifiedDate = DateTime.Today;
            addr.rowguid = Guid.NewGuid(  );

            CustomerAddress ca = new CustomerAddress(  );
            ca.AddressType = "Main Office";
            ca.Address = addr;
            ca.Customer = douglas;
            ca.ModifiedDate = DateTime.Today;
            ca.rowguid = Guid.NewGuid(  );

            ?dc.Customers.InsertOnSubmit(douglas);
            dc.SubmitChanges(  );

            ShowCustomersByFirstName("Douglas");
        }

        // Update a customer record
        private static void UpdateCustomer(  )
        {
            Console.WriteLine("Updating a customer...");
            AdventureWorksDataContext dc = new AdventureWorksDataContext(  );
            // Uncomment the statement below to show the
            // SQL statement generated by LINQ to SQL.
            //dc.Log = Console.Out;

            Customer dAdams = dc.Customers.Single(
                c => (c.FirstName == "Douglas" && c.LastName == "Adams"));
            Console.WriteLine("Before:
{0}", dAdams);

            dAdams.Title = "Mr.";

            // Add a new shipping address
            Address addr = new Address(  );
            addr.AddressLine1 = "1 Warehouse Place";
            addr.City = "Los Angeles";
            addr.PostalCode = "30210";
            addr.StateProvince = "California";
            addr.CountryRegion = "United States";
            addr.ModifiedDate = DateTime.Today;
            addr.rowguid = Guid.NewGuid(  );

            CustomerAddress ca = new CustomerAddress(  );
            ca.AddressType = "Shipping";
            ca.Address = addr;
            ca.Customer = dAdams;
            ca.ModifiedDate = DateTime.Today;
            ca.rowguid = Guid.NewGuid(  );

            dc.SubmitChanges(  );

            Customer dAdams1 = dc.Customers.Single(
                c => (c.FirstName == "Douglas" && c.LastName == "Adams"));
            Console.WriteLine("After:
{0}", dAdams);
        }

        // Find a list of customer records with a specific first name.
        private static void ShowCustomersByFirstName(string firstName)
        {
            AdventureWorksDataContext dc = new AdventureWorksDataContext(  );
            var customers =
                from customer in dc.Customers
                where customer.FirstName == "Douglas"
                orderby customer.FirstName, customer.LastName
                select customer;

            Console.WriteLine("Customers whose first name is {0}:", firstName);
            foreach (Customer customer in customers)
                Console.WriteLine(customer);
        }
    }

    // Add a method to the generated Customer class to
    // show formatted customer properties.
    public partial class Customer
    {
        public override string ToString(  )
        {
            StringBuilder sb = new StringBuilder(  );
            sb.AppendFormat("{0} {1} {2} {3}",
                    Title, FirstName, LastName, EmailAddress);
            foreach (CustomerAddress ca in CustomerAddresses)
            {
                sb.AppendFormat("
	{0}: {1}, {2}",
                    ca.AddressType,
                    ca.Address.AddressLine1,
                    ca.Address.City);
            }
            sb.AppendLine(  );
            return sb.ToString(  );
        }
    }
}

The test program takes two actions: AddCustomer and then UpdateCustomer, each of which is encapsulated in a method call.

Adding a Customer Record

AddCustomer begins by creating an instance of the Customer class and populating its properties:

Customer douglas = new Customer(  );
douglas.FirstName = "Douglas";
douglas.LastName = "Adams";
douglas.EmailAddress = "[email protected]";
douglas.PasswordHash = "fake";
douglas.PasswordSalt = "fake";
douglas.ModifiedDate = DateTime.Today;
douglas.rowguid = Guid.NewGuid(  );

It does the same for the Address class:

Address addr = new Address(  );
addr.AddressLine1 = "1c Sharp Way";
addr.City = "Seattle";
addr.PostalCode = "98011";
addr.StateProvince = "Washington";
addr.CountryRegion = "United States";
addr.ModifiedDate = DateTime.Today;
addr.rowguid = Guid.NewGuid(  );

Finally, the class that joins an address to a customer is created:

CustomerAddress ca = new CustomerAddress(  );
ca.AddressType = "Main Office";ca.Address = addr;
ca.Customer = douglas;
ca.ModifiedDate = DateTime.Today;
ca.rowguid = Guid.NewGuid(  );

Notice that the relationship among these three objects is created through the properties of the CustomerAddress object (highlighted).

Tip

The advantage to this approach is that the customer may have more than one address (e.g., work, home, vacation home, etc.).

With all three new objects created, you can add them to the database just by adding the new Customer object to the Customers table using the InsertOnSubmit() method, and then telling the DataContext to submit the changes:

dc.Customers.InsertOnSubmit(douglas);
dc.SubmitChanges(  );

Because the Customer “has” an address, the Address and the joining table that represents the “has-a” relationship come along for the ride.

When ShowCustomersByFirstName("Douglas") is called, you find every customer whose first name is Douglas and display the object:

private static void ShowCustomersByFirstName(string firstName)
{
    AdventureWorksDataContext dc = new AdventureWorksDataContext(  );
    var customers =
        from customer in dc.Customers
        where customer.FirstName == "Douglas"
        orderby customer.FirstName, customer.LastName
        select customer;

    Console.WriteLine("Customers whose first name is {0}:", firstName);
    foreach (Customer customer in customers)
        Console.WriteLine(customer);
}

The newly added Customer (complete with its address) is displayed appropriately:

Douglas Adams [email protected]
  Main Office: 1c Sharp Way, Seattle

Modifying a Customer Record

Modifying the customer involves finding the record you want to modify, retrieving it, modifying it as an object, and then storing it back in the database.

You retrieve Douglas Adams’ record much as you saw earlier:

Customer dAdams = dc.Customers.Single(
    c => (c.FirstName == "Douglas" && c.LastName == "Adams"));

With the record in hand, you add a new shipping address, which requires creating an address record and a CustomerAddress record (to tie the new Address record to the existing Customer record):

Address addr = new Address(  );
addr.AddressLine1 = "1 Warehouse Place";
addr.City = "Los Angeles";
addr.PostalCode = "30210";
addr.StateProvince = "California";
addr.CountryRegion = "United States";
addr.ModifiedDate = DateTime.Today;
addr.rowguid = Guid.NewGuid(  );

CustomerAddress ca = new CustomerAddress(  );
ca.AddressType = "Shipping";
ca.Address = addr;
ca.Customer = dAdams;
ca.ModifiedDate = DateTime.Today;
ca.rowguid = Guid.NewGuid(  );

For a bit of sport, you also change his title, with all due respect, from blank to “Mr.”:

dAdams.Title = "Mr.";

By using Console.WriteLine statements before and after the modification, you can see the changes:

Updating a customer...
Before:
 Douglas Adams [email protected]
        Main Office: 1c Sharp Way, Seattle

After:
Mr. Douglas Adams [email protected]
        Main Office: 1c Sharp Way, Seattle
        Shipping: 1 Warehouse Place, Los Angeles

You should also now see one record in the Customer table, and two each in the CustomerAddress and Address tables, as shown in Figure 15-9.

Modified data in the database
Figure 15-9. Modified data in the database

Deleting Relational Data

Deleting a customer is a bit trickier than adding or modifying one, because the relational database is going to enforce referential integrity. That is, to avoid data inconsistency, the relational database (e.g., SQL Server) is going to ensure that the rows in CustomerAddress be deleted before the rows in Address or Customer are deleted. Ideally, in fact, you’d like the entire deletion of all the related rows, in all the related tables, to be within a transaction so that if any of the deletions fail, the entire set is “rolled back” to its initial state. That way, you don’t end up with orphaned address records, or customers who have somehow lost data. (See the sidebar "Data Consistency" later in this chapter.)

The easiest way to do this is to ask the database to help you. After all, this is what databases do for a living. One solution they offer is stored procedures (they offer others, such as cascading deletes, etc.). To create a stored procedure, begin by right-clicking on the Stored Procedures file folder of your data connection and choose Add New Stored Procedure, as shown in Figure 15-10.

Adding a new stored procedure
Figure 15-10. Adding a new stored procedure

Replace the code in the prototype stored procedure with that shown in Example 15-4. You don’t need to fully understand the SQL at this point; you can trust that it will properly delete customers with the given first and last names and all related address records. With that said, I’ve added some comments (after a double dash) to help you along with what it is we’re doing in the SPROC (geek speak for Stored PROCedure).

Example 15-4. Stored procedure
Create PROCEDURE [SalesLT].[DeleteCustomer]
    @firstName Name, -- parameters with their type
    @lastName  Name
AS
BEGIN
    SET NOCOUNT ON;     --- administrative stuff
    SET ANSI_NULLS ON
    SET QUOTED_IDENTIFIER ON
    declare @customerId   int;  -- local variable
    declare @addressId    int;
    declare addressCursor cursor for  -- keep track of where we're up to
            select  CustomerId, AddressId  -- find this record
              from  CustomerAddress  -- fromt this table
             where  CustomerId in  -- where this column is found in these results:
                    (
                        select  CustomerId - find this column
                          from  Customer  -- in this table
                         where  FirstName = @firstName  -- where this is true
                           and  LastName  = @lastName -- and this is also true
                    );

    begin transaction; -- start a transaction
    open addressCursor;  -- go get that cursor
      -- get the next record and put results into our variables
    fetch next from addressCursor into @customerId, @addressId;
       -- start a while loop
    while @@fetch_status = 0 begin
        -- delete the matching records
        delete CustomerAddress where customerId = @customerId
          and addressId =  @addressId
               --delete these matching records too
        delete Address where addressId = @addressId;
    loop
        fetch next from addressCursor into @customerId, @addressId;
    end;  -- end the while
    close addressCursor;  -- close the cursor
    deallocate addressCursor;  -- clean up the resource

    delete  Customer  -- delete where you have a match
     where  FirstName = @firstName
       and  LastName  = @lastName;

    commit;  -- once everything worked, commit it all
           -- (implicit - if anything fails, roll back to your starting point)
END

Open the Stored Procedures folder and locate your new stored procedure (DeleteCustomer). Next, double-click on Adventureworks.dbml in the Solution Explorer, which will reopen the designer.

Drag the new stored procedure onto the designer. It is now registered with the designer and will appear in the righthand window of the designer, as shown in Figure 15-11.

Stored procedure in the designer
Figure 15-11. Stored procedure in the designer

You now have access to that stored procedure from within your DataContext, as shown in Example 15-5.

Example 15-5. Calling stored procedures using LINQ to SQL: C# code
       private static void DeleteCustomer(  )
        {
            Console.WriteLine("Deleting a customer...");
            Console.Write("Before: ");
            ShowCustomersByFirstName("Douglas");

            AdventureWorksDataContext dc = new AdventureWorksDataContext(  );
            // Uncomment the statement below to show the
            // SQL statement generated by LINQ to SQL.
            // dc.Log = Console.Out;

            dc.DeleteCustomer("Douglas", "Adams");
            Console.Write("After: ");
            ShowCustomersByFirstName("Douglas");
        }

Output:

Deleting a customer...
Before: Customers whose first name is Douglas:
Mr. Douglas Adams [email protected]
        Main Office: 1c Sharp Way, Seattle
        Shipping: 1 Warehouse Place, Los Angeles

Mr. Douglas Baldwin [email protected]
        Main Office: Horizon Outlet Center, Holland

Mr. Douglas Groncki [email protected]
        Main Office: 70259 West Sunnyview Ave, Visalia

After: Customers whose first name is Douglas:
Mr. Douglas Baldwin [email protected]
        Main Office: Horizon Outlet Center, Holland

Mr. Douglas Groncki [email protected]
        Main Office: 70259 West Sunnyview Ave, Visalia

The code is unchanged from the previous example, except for a new method (which you must call from Main( )), named DeleteCustomer( ). This method gets the AdventureWorksDataContext, but then just calls DeleteCustomer, passing in the two parameters of the first and last names. That’s it!

Because DeleteCustomer is a stored procedure registered with the DataContext, the DataContext knows just what to do: it calls the stored procedure, and the Sproc does all the work. Wicked cool.

LINQ to XML

If you would like the output of your work to go to an XML document rather than to a SQL database, you have only to create a new XML element for each object in the Customers table, and a new XML attribute for each property representing a column in the table. To do this, you use the LINQ to XML API, as illustrated in Example 15-6.

Note carefully that this code takes advantage of the new LINQ to XML classes, such as XElement, XAttribute, and XDocument. Working with XAttributes, for example, is very similar to working with standard XML elements. However, note carefully that, for example, XAttributes are not nodes in an XML tree, but instead are name/value pairs associated with an actual XML element. This is also quite different from what you are used to in working with the DOM.

The XElement object represents an actual XML element and can be used to create elements. It interoperates cleanly with System.XML, and makes for a terrific transition class between LINQ to XML and XML itself.

Finally, the XDocument class derives from XContainer and has exactly one child node (you guessed it: an XElement). It can also have an XDeclaration, zero or more XProcessingInstructions, XComments, and one XDocumentType (for the DTD), but that is more detail than we need.

In the next example, we’re going to create some XElements and assign some XAttributes. This should be very familiar to anyone comfortable with XML and a relatively easy first glimpse for those who are totally new to raw XML (see Chapter 14).

Example 15-6. Constructing an XML document using LINQ to XML
using System;
using System.Data.Linq;
using System.Linq;
using System.Xml.Linq;

namespace LinqToXML
{
    // Main program
    public class Tester
    {
        static void Main(  )
        {
            XElement customerXml = CreateCustomerListXml(  );
            Console.WriteLine(customerXml);
        }

        /// <summary>
        /// Create an XML document containing a list of customers.
        /// </summary>
        /// <returns>XML document containing a list of customers.</returns>
        private static XElement CreateCustomerListXml(  )
        {
            AdventureWorksDataContext dc = new AdventureWorksDataContext(  );
            // Uncomment the statement below to show the
            // SQL statement generated by LINQ to SQL.
            // dc.Log = Console.Out;

            // Find a list of customer records.
            var customerDs =
                from customer in dc.Customers
                where customer.FirstName.StartsWith("D")
                orderby customer.FirstName, customer.LastName
                select customer;

            XElement customerXml = new XElement("Customers");
            foreach (Customer customer in customerDs)
            {
                customerXml.Add(new XElement("Customer",
                    new XAttribute("FirstName", customer.FirstName),
                    new XAttribute("LastName", customer.LastName),
                    new XElement("EmailAddress", customer.EmailAddress)));
            }
            return customerXml;
        }
    }
}

In this example, rather than simply writing out the values of the CustomerDS that we’ve retrieved from the database, we convert the object to an XML file by using the LINQ to XML API. It is so straightforward as to be almost startling.

Let’s take this example apart. We begin by calling CreateCustomerListXml and assigning the results to an XElement named customerXml. CreateCustomerListXml begins by creating a LINQ statement (those of us who grew up with SQL will take a few years to get used to having the select statement come at the end!):

var customerDs =
     from customer in dc.Customers
     where customer.FirstName.StartsWith("D")
     orderby customer.FirstName, customer.LastName
     select customer;

Let me remind you that even though we use the keyword var here, which in JavaScript is not type-safe, in C#, this is entirely type-safe; the compiler imputes the type based on the query.

Next, we create an XElement named customerXml:

XElement customerXml = new XElement("Customers");

Here’s another potentially confusing aspect. We’ve given the C# XElement an identifier, customerXml, so that we can refer to it in C# code, but when we instantiated the XElement, we passed a name to the constructor (Customers). It is that name (Customers) that will appear in the XML file.

Next, we iterate through the CustomerDS collection that we retrieved earlier, pulling out each Customer object in turn, and create a new XElement based on the Customer object, adding an XAttribute for the FirstName, LastName, and EmailAddress “columns”:

foreach (Customer customer in customerDs)
{
    XElement cust = new XElement("Customer",
         new XAttribute("FirstName", customer.FirstName),
         new XAttribute("LastName", customer.LastName),
         new XElement("EmailAddress", customer.EmailAddress));

As we iterate through each customer, we also iterate through each customer’s associated CustomerAddress collection (customer.Addresses). These return an object of type Customer.Address, and we add to the XElement cust the Attributes for the Address, beginning with a new XElement named Address. This gives our Customer element a subelement of Addresses, with attributes for AddressLine1, AddressLine2, City, and so on.

Thus, a single Address object in the XML will look like this:

  <Customer FirstName="Dora" LastName="Verdad">
    <EmailAddress>[email protected]</EmailAddress>
    <Address AddressLine1="Suite 2502 410 Albert Street" AddressLine2=""
      City="Waterloo" StateProvince="Ontario" PostalCode="N2V" />
  </Customer>

Finally, we want each of these Customer elements (with their child Address elements) to be child elements of the Customers (plural) element that we created earlier. We accomplish this by opening the C# object and adding the new customer to the element after each iteration of the loop:

customerXml.Add(cust);

Notice that because we’re doing this in the C#, we access the Element through its C# identifier, not through its XML identifier. In the resulting XML document, the name of the outer element will be Customers and within Customers will be a series of Customer elements, each of which will contain Address elements:

<Customers>
  <Customer ...
    <Address ....    </Address>
    <EmailAddress ...  /EmailAddress/>
  </Customer>
  <Customer ...
    <Address ....    </Address>
    <EmailAddress ...  /EmailAddress/>
  </Customer>
</Customers>

Once we’ve iterated through the lot, we return the customerXml (the Customers element) which contains all the Customer elements, which in turn contain all the address elements; that is, the entire tree:

return customerXml;

Piece of pie; easy as cake.

Here is an excerpt from the complete output (slightly reformatted to fit the page):

<Customers>
  <Customer FirstName="Daniel" LastName="Blanco">
    <EmailAddress>[email protected]</EmailAddress>
    <Address AddressLine1="Suite 800 2530 Slater Street"
       AddressLine2="" City="Ottawa"
    StateProvince="Ontario" PostalCode="K4B 1T7" />
  </Customer>
  <Customer FirstName="Daniel" LastName="Thompson">
    <EmailAddress>[email protected]</EmailAddress>
    <Address AddressLine1="755 Nw Grandstand" AddressLine2="" City="Issaquah"
      StateProvince="Washington" PostalCode="98027" />
  </Customer>
  <Customer FirstName="Danielle" LastName="Johnson">
    <EmailAddress>[email protected]</EmailAddress>
    <Address AddressLine1="955 Green Valley Crescent" AddressLine2=""
      City="Ottawa" StateProvince="Ontario" PostalCode="K4B 1S1" />
  </Customer>
  <Customer FirstName="Darrell" LastName="Banks">
    <EmailAddress>[email protected]</EmailAddress>
    <Address AddressLine1="Norwalk Square" AddressLine2=""
      City="Norwalk" StateProvince="California" PostalCode="90650" />
  </Customer>
  <Customer FirstName="Darren" LastName="Gehring">
    <EmailAddress>[email protected]</EmailAddress>
    <Address AddressLine1="509 Nafta Boulevard" AddressLine2=""
        City="Laredo" StateProvince="Texas" PostalCode="78040" />
  </Customer>
  <Customer FirstName="David" LastName="Givens">
    <EmailAddress>[email protected]</EmailAddress>
    <Address AddressLine1="#500-75 O'Connor Street" AddressLine2=""
         City="Ottawa" StateProvince="Ontario" PostalCode="K4B 1S2" />
  </Customer>
</Customers>
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.222.231.205