Linq To SQL Hands On Lab
Linq To SQL Hands On Lab
Hands-on Lab
November 2007 For the latest information, please see www.microsoft.com/vstudio
Information in this document is subject to change without notice. The example companies, organizations, products, people, and events depicted herein are fictitious. No association with any real company, organization, product, person or event is intended or should be inferred. Complying with all applicable copyright laws is the responsibility of the user. Without limiting the rights under copyright, no part of this document may be reproduced, stored in or introduced into a retrieval system, or transmitted in any form or by any means (electronic, mechanical, photocopying, recording, or otherwise), or for any purpose, without the express written permission of Microsoft Corporation.
Microsoft may have patents, patent applications, trademarked, copyrights, or other intellectual property rights covering subject matter in this document. Except as expressly provided in any written license agreement from Microsoft, the furnishing of this document does not give you any license to these patents, trademarks, copyrights, or other intellectual property.
Microsoft, MS-DOS, MS, Windows, Windows NT, MSDN, Active Directory, BizTalk, SQL Server, SharePoint, Outlook, PowerPoint, FrontPage, Visual Basic, Visual C++, Visual J++, Visual InterDev, Visual SourceSafe, Visual C#, Visual J#, and Visual Studio are either registered trademarks or trademarks of Microsoft Corporation in the U.S.A. and/or other countries.
Other product and company names herein may be the trademarks of their respective owners.
Page i
Contents
LAB 1: LINQ TO SQL: DATABASE LANGUAGE INTEGRATED QUERIES............................................................1 Lab Objective........................................................................................................................................................1 Exercise 1 Creating your first LINQ to SQL Application......................................................................................2 Task 1 Creating a LINQ Project.....................................................................................................................2 Task 2 - Adding a reference to the System.Data.Linq assembly........................................................................3 Task 3 Mapping Northwind Customers..........................................................................................................3 Task 4 Querying Database Data....................................................................................................................4 Task 5 Exploring the IDE...............................................................................................................................6 Exercise 2 Creating an Object Model.................................................................................................................7 Task 1 Creating the order entity.....................................................................................................................7 Task 2 Mapping Relationships ......................................................................................................................8 Task 3 Strongly Typing the DataContext Object...........................................................................................10 Exercise 3 Using Code Generation to Create the Object Model...........................................................................10 Task 1 - Adding a LINQ to SQL Classes file....................................................................................................10 Task 2 Create your object model.................................................................................................................11 Task 3 Querying your object model..............................................................................................................12 Task 4 Mapping a stored procedure.............................................................................................................13 Task 5 Retrieving new results......................................................................................................................14 Exercise 4 Modifying Database Data................................................................................................................15 Task 1 Modifying your object model.............................................................................................................15 Task 2 Creating a new Entity.......................................................................................................................16 Task 3 Updating an Entity............................................................................................................................16 Task 4 Deleting an Entity.............................................................................................................................17 Task 5 Submitting changes..........................................................................................................................18 Task 6 Using Transactions...........................................................................................................................19
Page ii
Lab Objective
Estimated time to complete this lab: 60 minutes The objective of this lab is to learn how LINQ to SQL can be used for accessing relational data. This covers: 1. Creation of an object model from the database and customization of mapping between objects and tables; and 2. Data access tasks often called CRUD operations an acronym for Create, Retrieve, Update, and Delete operations. These tasks can be performed with a simple API without creating explicit SQL insert/update/delete commands. Exercise 1 Creating your first LINQ to SQL application Exercise 2 Creating an object model from a database Exercise 3 Using code generation to create the object model Exercise 4 Modifying database data
In this exercise, you will learn how to map a class to a database table, and how to retrieve objects from the underlying table using LINQ.
Page 1
These exercises require the Northwind database. Please follow the instructions in the LINQ to SQL section of the Essence of LINQ paper to get set up with Northwind before proceeding.
Click the Start | Programs | Microsoft Visual Studio 2008 | Microsoft Visual Studio 2008 menu command. In Microsoft Visual Studio, click the File | New | Project menu command In the New Project dialog, in Visual C# | Templates, click Console Application Provide a name for the new solution by entering LINQ to SQL HOL in the Name field Click OK
2. 3. 4. 5.
Page 2
In Microsoft Visual Studio, click the Project | Add Reference menu command In the Add Reference dialog make sure the .NET tab is selected click System.Data.Linq assembly Click OK
In Program.cs import the relevant LINQ to SQL namespaces by adding the following lines just before the namespace declaration:
using System.Data.Linq; using System.Data.Linq.Mapping;
Create an entity class to map to the Customer table by entering the following code in Program.cs (put the Customer class declaration immediately above the Program class declaration):
[Table(Name = "Customers")] public class Customer { [Column(IsPrimaryKey = true)] public string CustomerID; }
The Table attribute maps a class to a database table. The Column attribute then maps each field to a table column. In the Customers table, CustomerID is the primary key and it will be used to establish the identity of the mapped object. This is accomplished by setting the IsPrimaryKey parameter to true. An object mapped to the database through a unique key is referred to as an entity. In this example, instances of Customer class are entities.
2.
Page 3
Fields can be mapped to columns as shown in the previous step, but in most cases properties would be used instead. When declaring public properties, you must specify the corresponding storage field using the Storage parameter of the Column attribute.
3.
Enter the following code within the Main method to create a typed view of the Northwind database and establish a connection between the underlying database and the code-based data structures:
static void Main(string[] args) { // Use a standard connection string DataContext db = new DataContext(@"Data Source=.\sqlexpress;Initial Catalog=Northwind"); // Get a typed table to run queries Table<Customer> Customers = db.GetTable<Customer>(); }
You need to replace the connection string here with the correct string for your specific connection to Northwind. You will see later that after generating strongly typed classes with the designer, it is not necessary to embed the connection string directly in your code like this. The Customers table acts as the logical, typed table for queries. It does not physically contain all the rows from the underlying table but acts as a typed proxy for the table . The next step retrieves data from the database using the DataContext object, the main conduit through which objects are retrieved from the database and changes are submitted. Task 4 Querying Database Data
1.
Although the database connection has been established, no data is actually retrieved until a query is executed. This is known as lazy or deferred evaluation. Add the following query for London-based customers:
static void Main(string[] args) { // Use a standard connection string DataContext db = new DataContext(@"Data Source=.\sqlexpress;Initial Catalog=Northwind"); // Get a typed table to run queries Table<Customer> Customers = db.GetTable<Customer>(); // Attach the log showing generated SQL to console // This is only for debugging / understanding the working of LINQ to SQL db.Log = Console.Out; // Query for customers in London var custs = from c in Customers where c.City == "London" select c; }
Page 4
This query, which returns all of the customers from London defined in the Customers table, is expressed in query expression syntax, which the compiler will translate into explicit method-based syntax. Notice that the type for custs is not declared. This is a convenient feature of C# 3.0 that allows you to rely on the compiler to infer the correct data type while ensuring strong typing. This is especially useful since queries can return complex multi-property types that the compiler will infer for you, with no need for explicit declaration.
2.
Add the following code to execute the query and print the results:
static void Main(string[] args) { // Use a standard connection string DataContext db = new DataContext(@"Data Source=.\sqlexpress;Initial Catalog=Northwind"); // Get a typed table to run queries Table<Customer> Customers = db.GetTable<Customer>(); // Attach the log showing generated SQL to console // This is only for debugging / understanding the working of LINQ to SQL db.Log = Console.Out; // Query for customers in London var custs = from c in Customers where c.City == "London" select c; foreach (var cust in custs) { Console.WriteLine("ID={0}, City={1}", cust.CustomerID, cust.City); } Console.ReadLine(); }
The example in step 1 of task 3 shows a query. The query is only executed when the code above consumes the results. At that point, a corresponding SQL command is executed and objects are materialized. This concept, called lazy evaluation, allows queries to be composed without incurring the cost of an immediate round-trip to the database for query execution and object materialization. Query expressions are not evaluated until the results are needed. The code above results in the execution of the query defined in step 1 of task 3.
3. 4.
The call to the Console.ReadLine method prevents the console window from disappearing immediately. In subsequent tasks, this step will not be stated explicitly.
Page 5
The first part of the screen shows the log of the SQL command generated by LINQ and sent to the database. You can then see the results of our query. Notice that that the rows retrieved from the db are transformed into real CLR objects. This can be confirmed using the debugger. Task 5 Exploring the IDE
1. 2. 3. 4.
In the C# editor select the Console.WriteLine line inside the foreach loop In Microsoft Visual Studio, click the Debug | Toggle breakpoint menu command (or click F9) Press F5 to debug the application When the debugger stops the execution look at the locals window (or press Ctrl+D,L if the window doesnt appear) Inspect the variable cust to see its properties
5.
Page 6
You can also move the mouse over the variables and see how the IDE is fully aware of the type of the objects we have created.
After the Customer class definition, create the Order entity class definition with the following code:
[Table(Name = "Orders")] public class Order { private int _OrderID; private string _CustomerID; [Column(Storage = "_OrderID", DbType = "Int NOT NULL IDENTITY", IsPrimaryKey = true, IsDbGenerated = true)] public int OrderID { get { return this._OrderID; } // No need to specify a setter because IsDbGenerated is true }
Page 7
[Column(Storage = "_CustomerID", DbType = "NChar(5)")] public string CustomerID { get { return this._CustomerID; } set { this._CustomerID = value; } } }
Add a relationship between Orders and Customers with the following code, indicating that Orders.Customer relates as a foreign key to Customers.CustomerID:
[Table(Name = "Orders")] public class Order { private int _OrderID; private string _CustomerID; [Column(Storage = "_OrderID", DbType = "Int NOT NULL IDENTITY", IsPrimaryKey = true, IsDbGenerated = true)] public int OrderID { get { return this._OrderID; } // No need to specify a setter because AutoGen is true } [Column(Storage = "_CustomerID", DbType = "NChar(5)")] public string CustomerID { get { return this._CustomerID; } set { this._CustomerID = value; } } private EntityRef<Customer> _Customer; public Order() { this._Customer = new EntityRef<Customer>(); } [Association(Storage = "_Customer", ThisKey = "CustomerID")] public Customer Customer { get { return this._Customer.Entity; } set { this._Customer.Entity = value; } } } LINQ to SQL allows to you express one-to-one and one-to-many relationships using the EntityRef and EntitySet types. The Association attribute is used for mapping a relationship. By creating the association above, you will be able to use the Order.Customer property to relate directly to the appropriate Customer object. By setting this declaratively, you avoid working with foreign key values to associate the corresponding objects manually. The EntityRef type is used in class Order because there is only one customer corresponding to a given Order.
2.
Annotate the Customer class to indicate its relationship to the Order class. This is not strictly necessary, as defining it in either direction is sufficient to create the link; however, it allows you to
Page 8
easily navigate objects in either direction. Add the following code to the Customer class to navigate the association from the other direction:
public class Customer { private EntitySet<Order> _Orders; public Customer() { this._Orders = new EntitySet<Order>(); } [Association(Storage = "_Orders", OtherKey = "CustomerID")] public EntitySet<Order> Orders { get { return this._Orders; } set { this._Orders.Assign(value); } }
}
Notice that you do not set the value of the _Orders object, but rather call its Assign method to create the proper assignment. The EntitySet type is used because from Customers to Orders, rows are related one-tomany: one Customers row to many Orders rows.
3.
You can now access Order objects directly from the Customer objects, or vice versa. Modify the Main method with the following code to demonstrate an implicit join:
static void Main(string[] args) { // Use a standard connection string DataContext db = new DataContext(@"Data Source=.\sqlexpress;Initial Catalog=Northwind"); // Get a typed table to run queries Table<Customer> Customers = db.GetTable<Customer>(); // Attach the log showing generated SQL to console // This is only for debugging / understanding the working of LINQ to SQL db.Log = Console.Out; // Query for customers who have placed orders var custs = from c in Customers where c.Orders.Any() select c; foreach (var cust in custs) { Console.WriteLine("ID={0}, Qty={1}", cust.CustomerID, cust.Orders.Count); }
4.
Page 9
2.
Make the following changes to the Main method to use the strongly-typed DataContext.
static void Main(string[] args) { // Use a standard connection string NorthwindDataContext db = new NorthwindDataContext(@"Data Source=.\sqlexpress;Initial Catalog=Northwind"); // Attach the log showing generated SQL to console // This is only for debugging / understanding the working of LINQ to SQL db.Log = Console.Out; // Query for customers from London var custs = from c in db.Customers where c.City == "London" select c; foreach (var cust in custs) { Console.WriteLine("ID={0}, Qty={1}", cust.CustomerID, cust.Orders.Count); } }
3.
This optional feature is convenient since calls to GetTable<T> are not needed. Strongly typed tables can be used in all queries once such a DataContext-derived class is used.
First, to clean up, delete all the classes in Program.cs weve defined, keeping only the Program class. In Microsoft Visual Studio, click the Project | Add New Item menu command In the Templates click Linq to SQL Classes
2. 3.
Page 10
4. 5.
Provide a name for the new item by entering Northwind in the Name field Click OK
In Server Explorer, expand Data Connections. Open the Northwind server (NORTHWND.MDF if you are using SQL Server Express). Open the Northwind.dbml file double clicking it from the solution explorer From the Tables folder drag the Customers table onto the designer surface From the Tables folder drag the Orders table onto the designer surface
Page 11
6.
Make the following changes to the Main method to use the model created by the designer.
static void Main(string[] args) { // If we query the db just designed we dont need a connection string NorthwindDataContext db = new NorthwindDataContext(); // Attach the log showing generated SQL to console // This is only for debugging / understanding the working of LINQ to SQL db.Log = Console.Out; // Query for customers from London var custs = from c in db.Customers where c.City == "London" select c; foreach (var cust in custs) { Console.WriteLine("ID={0}, Qty={1}", cust.CustomerID, cust.Orders.Count); } } Console.ReadLine();
Page 12
As you can see the designer has written all the plumbing code for you. You can find it in the Northwind.designer.cs file.
Task 4 Mapping a stored procedure Weve seen how to map tables to objects and how to represent relationships between tables. Now we are going to see how we can map a stored procedure to our object model.
1. 2. 3. 4.
In Server Explorer, expand Data Connections. Open the Northwind server (NORTHWND.MDF if you are using SQL Server Express). Open the Northwind.dbml file double clicking it from the solution explorer From the Stored Procedures folder drag the Ten Most Expensive Products into the method pane on the right Make the following changes to the Main method to use the method created by the designer.
5.
static void Main(string[] args) { // If we query the db just designed we dont need a connection string NorthwindDataContext db = new NorthwindDataContext(); // Attach the log showing generated SQL to console // This is only for debugging / understanding the working of LINQ to SQL db.Log = Console.Out; // Query for customers from London var custs = from c in db.Customers where c.City == "London" select c; var p10 = db.Ten_Most_Expensive_Products(); foreach (var p in p10) { Console.WriteLine("Product Name={0}, UnitPrice={1}", p.TenMostExpensiveProducts, p.UnitPrice); } Console.ReadLine(); }
6.
As you type the code in, notice how, in the IDE, IntelliSense is able to show the mapped stored procedure Ten_Most_Expensive_Products as a method of the strongly typed DataContext the designer has generated. Notice also that the designer has created a Ten_Most_Expensive_Product type containing two typed properties that map to the fields returned by the stored procedure.
Generating the database table relationships can be tedious and prone to error. Until Visual Studio was extended to support LINQ, there were a code generation tool, SQLMetal, you can use to create your object model manually. The final result is the same, but you need to explicit execute external code. The easiest and better option is to use the new designer completely integrated with Visual Studio. Code generation is strictly an option you can always write your own classes or use a different code generator if you prefer. Page 13
Task 5 Retrieving new results So far we have run queries that retrieve entire objects. But you can also select the properties of interest. It is also possible to create composite results, as in traditional SQL, where an arbitrary collection of columns can be returned as a result set. In LINQ to SQL, this is accomplished through the use of anonymous types.
1.
Modify the code in the Main method as shown to create a query that only retrieves the ContactName property:
static void Main(string[] args) { // If we query the db just designed we dont need a connection string NorthwindDataContext db = new NorthwindDataContext(); // Attach the log showing generated SQL to console // This is only for debugging / understanding the working of LINQ to SQL db.Log = Console.Out; var q = from c in db.Customers where c.Region == null select c.ContactName; foreach (var c in q) Console.WriteLine(c); Console.ReadLine(); }
2.
Modify the code as shown to create a new object type to return the desired information:
static void Main(string[] args) { // If we query the db just designed we dont need a connection string NorthwindDataContext db = new NorthwindDataContext(); // Attach the log showing generated SQL to console // This is only for debugging / understanding the working of LINQ to SQL db.Log = Console.Out; var q = from c in db.Customers where c.Region == null select new { Company = c.CompanyName, Contact = c.ContactName }; foreach (var c in q) Console.WriteLine("{0}/{1}", c.Contact, c.Company); Console.ReadLine(); }
3.
Notice that the new operator is invoked with no corresponding type name. This causes the compiler to create a new anonymous type based on the names and types of the selected columns. Also notice that its members are named Contact and Company. Specifying explicit names is optional, the default behavior is to
Page 14
map members based on the source field name. Finally, notice how in the foreach statement, an instance of the new type is referenced and its properties are accessed.
In Server Explorer, expand Data Connections. Open the Northwind server (NORTHWND.MDF if you are using SQL Server Express). Open the Northwind.dbml file double clicking it from the solution explorer From the Tables folder drag the Employees table onto the designer surface Change the code as follows to do a join:
static void Main(string[] args) { // If we query the db just designed we dont need a connection string NorthwindDataContext db = new NorthwindDataContext(); // Attach the log showing generated SQL to console // This is only for debugging / understanding the working of LINQ to SQL db.Log = Console.Out; var ids = ( from c in db.Customers join e in db.Employees on c.City equals e.City select e.EmployeeID) .Distinct(); foreach (var id in ids) { Console.WriteLine(id); } } Console.ReadLine();
6.
The above example illustrates how a SQL style join can be used when there is no explicit relationship to navigate. It also shows how a specific property can be selected (projection) instead of the entire object. It also shows how the query expression syntax can be blended with the Standard Query Operators Distinct() in this case.
Open the Northwind server (NORTHWND.MDF if you are using SQL Server Express).
Page 15
3. 4. 5.
Open the Northwind.dbml file double clicking it from the solution explorer From the Tables folder drag the Order Details table onto the designer surface From the Tables folder drag the Products table onto the designer surface
Creating a new entity is straightforward. Objects such as Customer and Order can be created with the new operator as with regular C# Objects. Of course you will need to make sure that foreign key validations succeed. Change the Main method entering the following code to create a new customer: Modify the Main method so that it appears as the following:
static void Main(string[] args) { NorthwindDataContext db = new NorthwindDataContext(); // Create the new Customer object Customer newCust = new Customer(); newCust.CompanyName = "AdventureWorks Cafe"; newCust.CustomerID = "ADVCA"; // Add the customer to the Customers table db.Customers.InsertOnSubmit(newCust); Console.WriteLine("\nCustomers matching CA before update"); var customers = db.Customers .Where(cust => cust.CustomerID.Contains("CA")); foreach (var c in customers) Console.WriteLine("{0}, {1}, {2}, {3}", c.CustomerID, c.CompanyName, c.ContactName, c.Orders.Count); Console.ReadLine(); }
2.
3.
Notice that the new row does not show up in the results. The data is not actually added to the database by this code yet.
Once you have a reference to an entity object, you can modify its properties like you would with any other object. Add the following code to modify the contact name for the first customer retrieved:
static void Main(string[] args) { NorthwindDataContext db = new NorthwindDataContext(); // Create the new Customer object Customer newCust = new Customer(); newCust.CompanyName = "AdventureWorks Cafe"; newCust.CustomerID = "ADVCA";
Page 16
// Add the customer to the Customers table db.Customers.InsertOnSubmit(newCust); Console.WriteLine("\nCustomers matching CA before update"); var customers = db.Customers .Where(cust => cust.CustomerID.Contains("CA")); foreach (var c in customers) Console.WriteLine("{0}, {1}, {2}, {3}", c.CustomerID, c.CompanyName, c.ContactName, c.Orders.Count); Customer existingCust = customers.First(); // Change the contact name of the customer existingCust.ContactName = "New Contact"; } Console.ReadLine();
As in the last task, no changes have actually been sent to the database yet.
Using the same customer object, you can delete the first order detail. The following code demonstrates how to sever relationships between rows, and how to remove a row from the database.
static void Main(string[] args) { NorthwindDataContext db = new NorthwindDataContext(); // Create the new Customer object Customer newCust = new Customer(); newCust.CompanyName = "AdventureWorks Cafe"; newCust.CustomerID = "ADVCA"; // Add the customer to the Customers table db.Customers.InsertOnSubmit(newCust); Console.WriteLine("\nCustomers matching CA before update"); var customers = db.Customers .Where(cust => cust.CustomerID.Contains("CA")); foreach (var c in customers) Console.WriteLine("{0}, {1}, {2}, {3}", c.CustomerID, c.CompanyName, c.ContactName, c.Orders.Count); Customer existingCust = customers.First(); // Change the contact name of the customer existingCust.ContactName = "New Contact"; if (existingCust.Orders.Count > 0) { // Access the first element in the Orders collection Order ord0 = existingCust.Orders[0]; // Mark the Order row for deletion from the database db.Orders.DeleteOnSubmit(ord0); }
Page 17
Console.ReadLine();
The final step required for creating, updating, and deleting objects is to actually submit the changes to the database. Without this step, the changes will only be local, will not be persisted and will not show up in query results. Insert the following code to finalize the changes:
db.SubmitChanges(); Console.ReadLine();
Modify the main method so you can see how the code behaves before and after submitting changes to the database:
static void Main(string[] args) { NorthwindDataContext db = new NorthwindDataContext(); // Create the new Customer object Customer newCust = new Customer(); newCust.CompanyName = "AdventureWorks Cafe"; newCust.CustomerID = "ADVCA"; // Add the customer to the Customers table db.Customers.InsertOnSubmit(newCust); Console.WriteLine("\nCustomers matching CA before update"); var customers = db.Customers .Where(cust => cust.CustomerID.Contains("CA")); foreach (var c in customers) Console.WriteLine("{0}, {1}, {2}, {3}", c.CustomerID, c.CompanyName, c.ContactName, c.Orders.Count); Customer existingCust = customers.First(); // Change the contact name of the customer existingCust.ContactName = "New Contact"; if (existingCust.Orders.Count > 0) { // Access the first element in the Orders collection Order ord0 = existingCust.Orders[0]; // Mark the Order row for deletion from the database db.Orders.DeleteOnSubmit(ord0); } db.SubmitChanges(); Console.WriteLine("\nCustomers matching CA after update"); foreach (var c in db.Customers .Where(cust => cust.CustomerID.Contains("CA"))) Page 18
Naturally, once the new customer has been inserted, it cannot be inserted again due to the primary key uniqueness constraint. Therefore this program can only be run once.
In the Solution Explorer, right-click References, then click Add Reference In the .NET tab, click System.Transactions, then click OK By default LINQ to SQL uses implicit transactions for insert/update/delete operations. When SubmitChanges() is called, it generates SQL commands for insert/update/delete and wraps them in a transaction. But it is also possible to define explicit transaction boundaries using the TransactionScope the .NET Framework 2.0 provides. In this way, multiple queries and calls to SubmitChanges() can share a single transaction. The TransactionScope type is found in the System.Transactions namespace, and operates as it does with standard ADO.NET.
3.
4.
In Main, replace the existing code with the following code to have the query and the update performed in a single transaction:
static void Main(string[] args) { NorthwindDataContext db = new NorthwindDataContext(); using (TransactionScope ts = new TransactionScope()) { var q = from p in db.Products where p.ProductID == 15 select p; Product prod = q.First(); // Show UnitsInStock before update Console.WriteLine("In stock before update: {0}", prod.UnitsInStock); if (prod.UnitsInStock > 0) prod.UnitsInStock--; db.SubmitChanges(); ts.Complete(); } Console.WriteLine("Transaction successful");
Console.ReadLine();
Page 19
}
5.
Page 20
Lab Summary
Instructor's Note: LINQ to SQL is still an early technology that is evolving, but sufficient progress has been made to demonstrate powerful data capabilities. In this lab you performed the following exercises: Exercise 1 Creating your first LINQ to SQL application Exercise 2 Creating an object model from a database Exercise 3 Using code generation to create the object model Exercise 4 Modifying database data
In this lab, you learned about how the LINQ Project is advancing query capabilities in the .NET Framework. You mapped database tables to .NET classes and populated objects with live data. You also saw how seamlessly data can be retrieved and updated. Finally, you saw how richer capabilities such as transactions, custom queries, and object identity can make it easier for developers to simply concentrate on the application logic. LINQ to SQL provides a powerful bridge from objects to relational data and makes data-driven applications easier to build than ever. Thank you for trying LINQ to SQL. We hope you enjoy using LINQ in Visual Studio 2008.
Page 21