Chapter 11. LINQ to SQL: Advanced Concepts and Features

In the previous chapter, we learned some basic concepts and features of LINQ to SQL, such as querying and updating databases with tables and views, and changing loading behaviors by using load options.

In this chapter, we will learn some advanced features of LINQ to SQL such as stored procedure support, concurrency control, and transactional processing. After this chapter, we will rewrite the data access layer of our WCF service to utilize LINQ to SQL technology.

In this chapter, we will cover:

  • Calling a stored procedure
  • Compiled queries
  • Direct SQL
  • Dynamic querying
  • Inheritance support
  • Concurrency control
  • Transaction support
  • Entity class validation
  • Debugging LINQ to SQL programs

Calling a stored procedure

Calling a stored procedure is different from a table or a view, because a stored procedure can have input parameters, output parameters, and it can return multiple result-sets. It can also return different result-sets dynamically, which makes it even harder to interpret the results. The modeling of a stored procedure is also different from modeling a table or view. In the following sections, we will explain how to call a simple stored procedure, how to map the returned result of a stored procedure to an entity class, and how to handle output parameters, return codes, and multiple result-sets.

We will re-use the same application that we used in the previous chapter, and add more testing methods to the program.

Calling a simple stored procedure

First, we will try to call a simple stored procedure. In the sample database, there is a stored procedure called "Ten Most Expensive Products". We will call this stored procedure to get the top ten most expensive products.

Before we can call this stored procedure, we need to model it.

  1. Open the Northwind.dbml designer.
  2. In the Server Explorer, expand the node Stored Procedures.
  3. Drag the stored procedure Ten Most Expensive Products to the right-hand panel of the Northwind.dbml design window.

This will add the method Ten_Most_Expensive_Products to the NorthwindDataContext class, and add a new class, Ten_Most_Expensive_ProductsResult, as the result data type of the stored procedure.

Now, from the Program.cs, we can call this stored procedure as follows:

var tenProducts = from p in db.Ten_Most_Expensive_Products() select p;
foreach (var p in tenProducts)
{
Console.WriteLine("Product Name: {0}, Price; {1}", p.TenMostExpensiveProducts, p.UnitPrice);
}

Because we know exactly the return result of the stored procedure, we can also replace the var data type with the specific return type, as in the following code:

IEnumerable<Ten_Most_Expensive_ProductsResult> tenProducts = from p in db.Ten_Most_Expensive_Products() select p;
foreach (Ten_Most_Expensive_ProductsResult p in tenProducts)
{
Console.WriteLine("Product Name: {0}, Price; {1}", p.TenMostExpensiveProducts, p.UnitPrice);
}

The output will look like the following image:

Calling a simple stored procedure

Mapping a stored procedure to an entity class

In the above example, LINQ to SQL creates a new type for the return result of the stored procedure. It actually just added the word "Result" after the stored procedure name, to create the name of the return data type. If we know that the return result is a kind of entity, we can tell LINQ to SQL to use that specific entity as the return type instead of creating a new type.

For example, let us create a stored procedure like this:

Create PROCEDURE [dbo].[GetProduct]
(
@ProductID int
)
AS
SET NOCOUNT ON
Select * from Products where ProductID = @ProductID

You can create this stored procedure in Microsoft SQL Server Management Studio, or by right-clicking on the Stored Procedures node in the Server Explorer of Visual Studio 2008, and selecting Add New Stored Procedure from the context menu.

After the stored procedure has been created, drag and drop it into the Product class on the Northwind.dbml design pane. Now, LINQ to SQL will use the Product class as the return type of this stored procedure. The method for this stored procedure will be as follows:

[Function(Name="dbo.GetProduct")]
public ISingleResult<Product> GetProduct([Parameter(Name="ProductID", DbType="Int")] System.Nullable<int> productID)
{
IExecuteResult result = this.ExecuteMethodCall(this, ((MethodInfo)(MethodInfo.GetCurrentMethod())), productID);
return ((ISingleResult<Product>)(result.ReturnValue));
}

From the signature of the method, we know that the return type is of the Product class.

Interestingly, if you drag and drop the same stored procedure to the right-hand panel of the Northwind.dbml design window, instead of the Product class, LINQ to SQL will automatically create a new class for the return type. The new method might be as follows:

[Function(Name="dbo.GetProduct")]
public ISingleResult<GetProductResult> GetProduct1([Parameter(Name="ProductID", DbType="Int")] System.Nullable<int> productID)
{
IExecuteResult result = this.ExecuteMethodCall(this, ((MethodInfo)(MethodInfo.GetCurrentMethod())), productID);
return ((ISingleResult<GetProductResult>)(result.ReturnValue));
}

The generated return type class GetProductResult is almost identical to the Product class, except that there are no event handling methods.

Another difference between the GetProduct and GetProduct1 methods is that the product you retrieved using GetProduct is within the DataContext. So, any changes you made to it will be committed back to the database if you call db.SubmitChanges() later. However, the product you retrieved using GetProduct1 is not within the DataContext, and thus won't be committed back to the database if you call db.SubmitChanges() later.

Also, when a stored procedure is dropped to an entity class, LINQ to SQL will first check the return result of the stored procedure to make sure it is compatible with the target class. If not, you will get a warning message, and the stored procedure won't be mapped on the model. For example, if you drag and drop the stored procedure Ten Most Expensive Products to the Product class, you will see a dialog box like this:

Mapping a stored procedure to an entity class

Handling output parameters, return codes, multiple shapes of a single result-set, and multiple result-sets

Now that we have a basic understanding of LINQ to SQL stored procedure processing, we will create a fairly complex stored procedure with an input parameter, an output parameter, a return code, multiple shapes of a single result-set, and multiple result-sets.

Creating a complex stored procedure

Before we explain the LINQ to SQL comprehensive stored procedure support, we need to create a complex stored procedure. We will create a stored procedure called GetCategoryDetails. The stored procedure will have one input parameter, CategoryID, which will specify which category it is for, and one output parameter AveProductPrice, which will return the average price of all the products in that category.

The first result-set of this stored procedure will give some information about the category, depending on the value of another input parameter, FullOrPartial. If FullOrPartial is true (1), this result-set will contain all of the columns of the Categories table for the requested category. Otherwise, it will contain only the CategoryID and CategoryName columns of the category.

The second result-set will contain all of the products for the category.

If the input parameter is not a valid category ID, the procedure returns an error code of 10001, and stops. Otherwise, it returns 0 at the end of the stored procedure, to indicate a success.

The SQL to create this stored procedure is:

CREATE PROCEDURE [dbo].[GetCategoryDetails]
@CategoryID int,
@FullOrPartial bit,
@AveProductPrice money OUTPUT
AS
SET NOCOUNT ON
if not exists (select 1
from categories
where categoryID = @categoryID)
return 10001
if @FullOrPartial = 1
select * from Categories
where categoryID = @categoryID
else
select categoryID, categoryName from Categories
where categoryID = @categoryID
select * from products
where categoryID = @categoryID
select @AveProductPrice = avg(UnitPrice)
from products
where categoryID = @CategoryID
return 0

Modeling the stored procedure

In order to call this complex stored procedure, we first need to add it into the Northwind.dbml model. Just drag and drop it from the Server Explorer to the right-hand panel of the Northwind.dbml design window. If you have created it in the SQL Management Studio and can't see it in the Server Explorer, try to refresh your Server Explorer.

LINQ to SQL designer will create the following method in the class NorthwindDataContext within the file, Northwind.designer.cs:

[Function(Name="dbo.GetCategoryDetails")]
public ISingleResult<GetCategoryDetailsResult> GetCategoryDetails([Parameter(Name="CategoryID", DbType="Int")] System.Nullable<int> categoryID, [Parameter(Name="FullOrPartial", DbType="Bit")] System.Nullable<bool> fullOrPartial, [Parameter(Name="AveProductPrice", DbType="Money")] ref System.Nullable<decimal> aveProductPrice)
{
IExecuteResult result = this.ExecuteMethodCall(this, ((MethodInfo)(MethodInfo.GetCurrentMethod())), categoryID, fullOrPartial, aveProductPrice);
aveProductPrice = ((System.Nullable<decimal>)(result.GetParameterValue(2)));
return ((ISingleResult<GetCategoryDetailsResult>)(result.ReturnValue));
}

Note that the variable aveProductPrice is passed to the method call ExecuteMethodCall, but its actual value doesn't come back after the call. The output value has to be retrieved using result.GetParameterValue.

This class is also added to the return result (this is really the first result-set in the stored procedure):

public partial class GetCategoryDetailsResult

However, this is not what we want. The GetCategoryDetails method only returns one result-set, instead of two. We have to customize it for our needs.

Customizing DataContext class for the stored procedure

In the previous sections, we modeled the stored procedure with LINQ to SQL designer, but the retuning result is not correct. In this section, we will customize it.

  1. Extend the class NorthwindDataContext by adding a new class file called NorthwindDataContext.cs.
  2. Inside the new class file NorthwindDataContext.cs, add the following using statements:
    using System.Data.Linq;
    using System.Data.Linq.Mapping;
    using System.Reflection;
    
  3. Add the following class inside the file NorthwindDataContext.cs, for one of the return results:
    public class PartialCategory
    {
    public int CategoryID;
    public string CategoryName;
    }
    
    • This class is parallel to the NorthwindDataContext class. Next, we will use this class to define a new method.
  4. Change the class definition to the following code (note that it should be changed to a partial class):
public partial class NorthwindDataContext
{
// modified GetCategoryDetails, to overwrite the generated one
[Function(Name = "dbo.GetCategoryDetails")]
[ResultType(typeof(PartialCategory))]
[ResultType(typeof(Category))]
[ResultType(typeof(Product))]
public IMultipleResults GetWholeOrPartialCategoryDetails(
[Parameter(Name="CategoryID", DbType="Int")]
System.Nullable<int> categoryID,
[Parameter(Name="FullOrPartial", DbType="Bit")]
System.Nullable<bool> fullOrPartial,
[Parameter(Name="AveProductPrice", DbType="Money")]
ref System.Nullable<decimal> aveProductPrice)
{
IExecuteResult result = this.ExecuteMethodCall(this, ((MethodInfo)(MethodInfo.GetCurrentMethod())), categoryID, fullOrPartial, aveProductPrice);
aveProductPrice = ((System.Nullable<decimal>)(result. GetParameterValue(2)));
return ((IMultipleResults)(result.ReturnValue));
}
}

As you can see, we defined a method GetWholeOrPartialCategoryDetails to map the results of the stored procedure to different types.

We can also modify the generated method inside of the Northwind.designer.cs file to meet our needs. However, it is recommended that we don't do so, because if you modify this file, and it is regenerated later on, you will lose all your changes.

Testing the stored procedure

Now, inside the file program.cs, we can add this test method:

static void TestComplexStoredProcedure(int categoryID, bool wholeOrPartial)
{
decimal? avePrice = 0;
IMultipleResults result = db.GetWholeOrPartialCategoryDetails(cate goryID, wholeOrPartial, ref avePrice);
int returnCode = (int)result.ReturnValue;
if (returnCode == 0)
{
if (wholeOrPartial == true)
{
Category wholeCategory = result.GetResult<Category>(). FirstOrDefault();
Console.WriteLine("Category name: {0}", wholeCategory. CategoryName);
Console.WriteLine("Category description: {0}", wholeCategory.Description);
}
else
{
PartialCategory partialCategory = result.GetResult<PartialCategory>().FirstOrDefault();
Console.WriteLine("Category name: {0}", partialCategory.CategoryName);
}
Console.WriteLine("Average product price: {0}", avePrice);
IEnumerable<Product> products = result.GetResult<Product>();
Console.WriteLine("Total products in category: {0}", products.Count());
}
else
{
Console.WriteLine("No category is retrieved, return code : {0}", returnCode);
}
}

Inside the Main method, we call the above method three times as follows:

// get full category details
TestComplexStoredProcedure (2, true);
// get partail category details
TestComplexStoredProcedure (6, false);
// invalid category ID
TestComplexStoredProcedure (999, true);

The first call will return the full category information for category two, including category ID, name, description, and picture. The second call will return only partial information for category six, including category ID, and name. In both of the cases, it will return the products in the category, and the average product price in that category. The third call will print an error message because there is no category with ID 999.

The output is as shown in the following image:

Testing the stored procedure
..................Content has been hidden....................

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