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 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.
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.
Northwind.dbml
designer.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:
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:
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.
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
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.
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.
NorthwindDataContext
by adding a new class file called NorthwindDataContext.cs
. NorthwindDataContext.cs
, add the following using
statements:using System.Data.Linq; using System.Data.Linq.Mapping; using System.Reflection;
NorthwindDataContext.cs
, for one of the return results:public class PartialCategory { public int CategoryID; public string CategoryName; }
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.
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: