Applying LINQ to Entities to a WCF Service - WCF Multi-layer Services Development with Entity Framework Fourth Edition (2014)

WCF Multi-layer Services Development with Entity Framework Fourth Edition (2014)

Chapter 9. Applying LINQ to Entities to a WCF Service

Now that we have learned all of the features related to LINQ and LINQ to Entities, we will use them in the data access layer of a WCF service. We will create a new WCF service very similar to the one we created in the previous chapters, but in this service, we will use LINQ to Entities to connect to the Northwind database to retrieve and update a product.

In this chapter, we will cover the following topics:

· Creating a test solution

· Modeling the Northwind database in the LINQ to Entities designer

· Adding the business domain object project

· Implementing the data access layer using LINQ to Entities

· Adding the business logic layer

· Adding the service interface layer

· Implementing the test client

· Testing the get and update operations of the WCF service

· Testing concurrent updates with LINQ to Entities

Creating the LINQNorthwind solution

The first thing we need to do is create a test solution. In this chapter, we will start from the data access layer. Perform the following steps:

1. Start Visual Studio.

2. Create a new class library project, NorthwindDAL, with solution name LINQNorthwind (make sure that the Create directory for solution option is checked to specify the solution name).

3. Change the Class1.cs file to ProductDAO.cs. This will also change the class name and all related places in the project.

Now, you should have a new solution with the empty data access layer class. Next, we will add a model to this layer and create the business logic layer and the service interface layer.

Installing Entity Framework

Just as we did in the last chapter, we first need to install Entity Framework into the project, as in this project, we will use LINQ to Entities to connect to the database. Follow these steps to do this:

1. From the Solution Explorer, right-click on the project item, then select Manage NuGet Packages....

2. On the Manage NuGet Packages window, select Online | nuget.org | Entity Framework.

3. Click on the Install button to install Entity Framework in the project.

Modeling the Northwind database

In the previous section, we created the LINQNorthwind solution. Next, we will apply LINQ to Entities to this new solution.

For the data access layer, we will use LINQ to Entities instead of the raw ADO.NET data adapters. As you will see in the next section, we will use one LINQ statement to retrieve product information from the database, and the update LINQ statements will handle the concurrency control for us easily and reliably.

As you might recall, to use LINQ to Entities in the data access layer of our WCF service, we first need to add an entity data model to the project. The following steps are very similar to those described in Chapter 8, LINQ to Entities – Advanced Concepts and Features. You can refer to that chapter for more information and screenshots if necessary.

1. In the Solution Explorer, right-click on the project item, NorthwindDAL, select menu options Add | New Item…, and then choose Visual C# Items | ADO.NET Entity Data Model as Template and enter Northwind.edmx as the name.

2. Select EF Designer from database, choose the existing Northwind connection and add the Products table to the model.

3. Click on the Finish button to add the model to the project.

4. The new column, RowVersion, should be in the Product entity as we added it in the previous chapter. If it is not there, add it to the database table with a type of timestamp and refresh the entity data model from the database (you can look at the Modeling the Products table with a version column section in Chapter 8, LINQ to Entities – Advanced Concepts and Features, for more details on how to refresh an entity model from a database).

5. In the EMD designer, select the RowVersion property of the Product entity and change its Concurrency Mode from None to Fixed. Note that its StoreGeneratedPattern should remain as Computed. You can refer to the Turning on concurrency verification section in Chapter 8, LINQ to Entities – Advanced Concepts and Features, for a screenshot.

Just as in the previous chapter, this will generate a file called Northwind.Context.cs, which contains the Db context for the Northwind database. Another file called Product.cs is also generated, which contains the Product entity class. You need to save the data model in order to see these two files in the Solution Explorer.

Note

In Visual Studio's Solution Explorer, the Northwind.Context.cs file is under the template file, Northwind.Context.tt, and Product.cs is under Northwind.tt. However, in Windows Explorer, both the C# files and template files are within the project folder and they are at the same level.

Creating the business domain object project

In Chapter 5, Implementing a Three-layer WCF Service, we created a business domain object (BDO) project to hold the intermediate data between the data access objects and the service interface objects. In this section, we will also add such a project to the solution for the same purpose.

1. In the Solution Explorer, right-click on the LINQNorthwind solution.

2. Select Add | New Project... to add a new class library project named NorthwindBDO.

3. Rename the Class1.cs file to ProductBDO.cs. This will also change the class name and all related files in the project.

4. Add the following properties to this class:

o ProductID

o ProductName

o QuantityPerUnit

o UnitPrice

o Discontinued

o UnitsInStock

o UnitsOnOrder

o ReorderLevel

o RowVersion

The following is the code list of the ProductBDO class:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Threading.Tasks;

namespace NorthwindBDO

{

public class ProductBDO

{

public int ProductID { get; set; }

public string ProductName { get; set; }

public string QuantityPerUnit { get; set; }

public decimal UnitPrice { get; set; }

public int UnitsInStock { get; set; }

public int ReorderLevel { get; set; }

public int UnitsOnOrder { get; set; }

public bool Discontinued { get; set; }

public byte[] RowVersion { get; set; }

}

}

As noted earlier, in this chapter, we will use BDO to hold the intermediate data between the data access objects and the data contract objects. Besides this approach, there are some other ways to pass data back and forth between the data access layer and the service interface layer, and two of them are listed as follows:

· The first one is to expose the Entity Framework context objects from the data access layer up to the service interface layer. In this way, both the service interface layer and the business logic layer can interact directly with Entity Framework. This approach is not recommended as it goes against the best practice of service layering, as we have discussed in Chapter 5, Implementing a Three-layer WCF Service.

· Another approach is to use self-tracking entities. Self-tracking entities are entities that know how to track their own changes regardless of which tier those changes are made on. You can expose self-tracking entities from the data access layer to the business logic layer and then to the service interface layer and even share the entities with the clients. As self-tracking entities are independent of the entity context, you don't need to expose the entity context objects. The problem with this approach is that you have to share the binary files with all the clients, and thus, it is the least interoperable approach for a WCF service. Now, this approach is not recommended by Microsoft, so we will not discuss it in this book.

Using LINQ to Entities in the data access layer

Next, we will modify the data access layer to use LINQ to Entities to retrieve and update products. We will first create GetProduct to retrieve a product from the database and then create UpdateProduct to update a product in the database.

Adding a reference to the BDO project

Now that we have the BDO project in the solution, we need to modify the data access layer project to reference it.

1. In the Solution Explorer, right-click on the NorthwindDAL project.

2. Select Add | Reference....

3. Select the NorthwindBDO project from the Projects tab under Solution.

4. Click on the OK button to add the reference to the project.

Creating GetProduct in the data access layer

We can now create the GetProduct method in the data access layer's class, ProductDAO, to use LINQ to Entities to retrieve a product from the database. Just as we did in the previous chapter, we will first create an entity DbContext object and then use LINQ to Entities to get the product from the DbContext object. The product we get from DbContext will be a conceptual entity model object. However, we don't want to pass this product object back to the upper-level layer because we don't want to tightly couple the business logic layer with the data access layer. Therefore, we will convert this entity model product object to a ProductBDO object and then pass this ProductBDO object back to the upper-level layers.

To create the GetProduct method, first add the following using statement to the ProductDAO class:

using NorthwindBDO;

Then, add the following GetProduct method to the ProductDAO class:

public ProductBDO GetProduct(int id)

{

ProductBDO productBDO = null;

using (var NWEntities = new NorthwindEntities())

{

var product = (from p in NWEntities.Products

where p.ProductID == id

select p).FirstOrDefault();

if (product != null)

productBDO = new ProductBDO()

{

ProductID = product.ProductID,

ProductName = product.ProductName,

QuantityPerUnit = product.QuantityPerUnit,

UnitPrice = (decimal)product.UnitPrice,

UnitsInStock = (int)product.UnitsInStock,

ReorderLevel = (int)product.ReorderLevel,

UnitsOnOrder = (int)product.UnitsOnOrder,

Discontinued = product.Discontinued,

RowVersion = product.RowVersion

};

}

return productBDO;

}

You will recall from Chapter 5, Implementing a Three-layer WCF Service, that within the GetProduct method, we had to create an ADO.NET connection, create an ADO.NET command object with that connection, specify the command text, connect to the Northwind database, and send the SQL statement to the database for execution. After the result was returned from the database, we had to loop through the DataReader and cast the columns to our entity object one by one.

With LINQ to Entities, we only construct one LINQ to Entities statement and everything else is handled by LINQ to Entities. Not only do we need to write less code, but now the statement is also strongly typed. We won't have a runtime error such as invalid query syntax or invalid column name. Also, a SQL Injection attack is no longer an issue, as LINQ to Entities will also take care of this when translating LINQ expressions to the underlying SQL statements.

Note

There might be a performance impact in some cases, since LINQ to Entities uses autogenerated queries, which are not always optimized. If this happens to you, you can always plug in your own SQL queries.

Creating UpdateProduct in the data access layer

In the previous section, we created the GetProduct method in the data access layer using LINQ to Entities instead of ADO.NET. Now, in this section, we will create the UpdateProduct method using LINQ to Entities instead of ADO.NET.

The code for the UpdateProduct method in the data access layer class, ProductDAO, will be as follows:

public bool UpdateProduct(

ref ProductBDO productBDO,

ref string message)

{

message = "product updated successfully";

var ret = true;

using (var NWEntities = new NorthwindEntities())

{

var productID = productBDO.ProductID;

Product productInDB =

(from p

in NWEntities.Products

where p.ProductID == productID

select p).FirstOrDefault();

// check product

if (productInDB == null)

{

throw new Exception("No product with ID " +

productBDO.ProductID);

}

// update product

productInDB.ProductName = productBDO.ProductName;

productInDB.QuantityPerUnit = productBDO.QuantityPerUnit;

productInDB.UnitPrice = productBDO.UnitPrice;

productInDB.Discontinued = productBDO.Discontinued;

productInDB.RowVersion = productBDO.RowVersion;

NWEntities.Products.Attach(productInDB);

NWEntities.Entry(productInDB).State =

System.Data.Entity.EntityState.Modified;

var num = NWEntities.SaveChanges();

productBDO.RowVersion = productInDB.RowVersion;

if (num != 1)

{

ret = false;

message = "no product is updated";

}

}

return ret;

}

Within this method, we first get the product from the database, making sure that the product ID is a valid value in the database. Then, we apply the changes from the entered object to the object we have just retrieved from the database and submit the changes back to the database. Let'sgo through a few notes about this method:

1. You have to save productID in a new variable and then use it in the LINQ query. Otherwise, you will get an error that says Cannot use ref or out parameter 'productBDO' inside an anonymous method, lambda expression, or query expression.

2. If Attach is not called, RowVersion (from the database, not from the client) will be used when submitting to the database, even though you have updated its value before submitting to the database. An update will always succeed, but without concurrency control.

3. If the object state is not set to Modified, Entity Framework will not honor your changes to the entity object and you will not be able to save any of the changes to the database.

Note

If you don't want to use the ref parameters, you can define a class for the return result and encapsulate all the ref parameters inside this class.

Creating the business logic layer

Now, let's create the business logic layer. The steps here are very similar to the steps in Chapter 5, Implementing a Three-layer WCF Service, so you can refer to that chapter for more details:

1. Right-click on the solution item and select Add | New Project.... Add a class library project with the name NorthwindLogic.

2. Add a project reference to NorthwindDAL and NorthwindBDO to this new project.

3. Rename the Class1.cs file to ProductLogic.cs. This will also change the class name and all related places in the project.

4. Add the following two using statements to the ProductLogic.cs class file:

5. using NorthwindDAL;

using NorthwindBDO;

5. Add the following class member variable to the ProductLogic class:

ProductDAO productDAO = new ProductDAO();

6. Add the following new method, GetProduct, to the ProductLogic class:

7. public ProductBDO GetProduct(int id)

8. {

9. return productDAO.GetProduct(id);

}

7. Add the following new method, UpdateProduct, to the ProductLogic class:

8. public bool UpdateProduct(

9. ref ProductBDO productBDO,

10. ref string message)

11.{

12. var productInDB =

13. GetProduct(productBDO.ProductID);

14. // invalid product to update

15. if (productInDB == null)

16. {

17. message = "cannot get product for this ID";

18. return false;

19. }

20. // a product cannot be discontinued

21. // if there are non-fulfilled orders

22. if (productBDO.Discontinued == true

23. && productInDB.UnitsOnOrder > 0)

24. {

25. message = "cannot discontinue this product";

26. return false;

27. }

28. else

29. {

30. return productDAO.UpdateProduct(ref productBDO,

31. ref message);

32. }

}

Build the solution. We now have only one more step to go, that is, adding the service interface layer.

Creating the service interface layer

The last step is to create the service interface layer. Again, the steps here are very similar to the steps in Chapter 5, Implementing a Three-layer WCF Service, so you can refer to this chapter for more details.

1. Right-click on the solution item and select Add | New Project.... Add a WCF Service Library project with the name NorthwindService.

2. Add a project reference to NorthwindLogic and NorthwindBDO to this new service interface project.

3. Right-click on the project item NorthwindService, select Manage NuGet Packages… and then install Entity Framework.

4. Change the service interface file, IService1.cs, as follows:

1. Change its filename from IService1.cs to IProductService.cs. This will also change the interface name and all related places in the project.

2. Remove the original two service operations and add the following two new operations:

3. [OperationContract]

4. [FaultContract(typeof(ProductFault))]

5. Product GetProduct(int id);

6.

7. [OperationContract]

8. [FaultContract(typeof(ProductFault))]

bool UpdateProduct(ref Product product, ref string message);

3. Remove the original CompositeType and add the following data contract classes:

4. [DataContract]

5. public class Product

6. {

7. [DataMember]

8. public int ProductID { get; set; }

9. [DataMember]

10. public string ProductName { get; set; }

11. [DataMember]

12. public string QuantityPerUnit { get; set; }

13. [DataMember]

14. public decimal UnitPrice { get; set; }

15. [DataMember]

16. public bool Discontinued { get; set; }

17. [DataMember]

18. public byte[] RowVersion { get; set; }

19.}

20.

21.[DataContract]

22.public class ProductFault

23.{

24. public ProductFault(string msg)

25. {

26. FaultMessage = msg;

27. }

28.

29. [DataMember]

30. public string FaultMessage;

}

5. The following is the content of the IProductService.cs file now:

6. using System;

7. using System.Collections.Generic;

8. using System.Linq;

9. using System.Runtime.Serialization;

10.using System.ServiceModel;

11.using System.Text;

12.

13.namespace NorthwindService

14.{

15. [ServiceContract]

16. public interface IProductService

17. {

18. [OperationContract]

19. [FaultContract(typeof(ProductFault))]

20. Product GetProduct(int id);

21.

22. [OperationContract]

23. [FaultContract(typeof(ProductFault))]

24. bool UpdateProduct(ref Product product,

25. ref string message);

26. }

27.

28. [DataContract]

29. public class Product

30. {

31. [DataMember]

32. public int ProductID { get; set; }

33. [DataMember]

34. public string ProductName { get; set; }

35. [DataMember]

36. public string QuantityPerUnit { get; set; }

37. [DataMember]

38. public decimal UnitPrice { get; set; }

39. [DataMember]

40. public bool Discontinued { get; set; }

41. [DataMember]

42. public byte[] RowVersion { get; set; }

43. }

44. [DataContract]

45. public class ProductFault

46. {

47. public ProductFault(string msg)

48. {

49. FaultMessage = msg;

50. }

51.

52. [DataMember]

53. public string FaultMessage;

54. }

55.}

5. Change the service implementation file, Service1.cs, as follows:

1. Change its filename from Service1.cs to ProductService.cs. This will also change the class name and all related places in the project.

2. Add the following two using statements to the ProductService.cs file:

3. using NorthwindLogic;

using NorthwindBDO;

3. Add the following class member variable:

ProductLogic productLogic = new ProductLogic();

4. Remove the original two GetProduct and UpdateProduct methods and add the following two methods:

5. public Product GetProduct(int id)

6. {

7. ProductBDO productBDO = null;

8. try

9. {

10. productBDO = productLogic.GetProduct(id);

11. }

12. catch (Exception e)

13. {

14. var msg = e.Message;

15. var reason = "GetProduct Exception";

16. throw new FaultException<ProductFault>

17. (new ProductFault(msg), reason);

18. }

19.

20. if (productBDO == null)

21. {

22. var msg =

23. string.Format("No product found for id {0}",

24. id);

25. var reason = "GetProduct Empty Product";

26. throw new FaultException<ProductFault>

27. (new ProductFault(msg), reason);

28. }

29. var product = new Product();

30. TranslateProductBDOToProductDTO(productBDO, product);

31. return product;

32.}

33.

34.public bool UpdateProduct(ref Product product,

35. ref string message)

36.{

37. var result = true;

38.

39. // first check to see if it is a valid price

40. if (product.UnitPrice <= 0)

41. {

42. message = "Price cannot be <= 0";

43. result = false;

44. }

45. // ProductName can't be empty

46. else if (string.IsNullOrEmpty(product.ProductName))

47. {

48. message = "Product name cannot be empty";

49. result = false;

50. }

51. // QuantityPerUnit can't be empty

52. else if (string.IsNullOrEmpty(product.QuantityPerUnit))

53. {

54. message = "Quantity cannot be empty";

55. result = false;

56. }

57. else

58. {

59. try

60. {

61. var productBDO = new ProductBDO();

62. TranslateProductDTOToProductBDO(product, productBDO);

63. result = productLogic.UpdateProduct(

64. ref productBDO, ref message);

65. product.RowVersion =

66. productBDO.RowVersion;

67. }

68. catch (Exception e)

69. {

70. var msg = e.Message;

71. throw new FaultException<ProductFault>

72. (new ProductFault(msg), msg);

73. }

74. }

75. return result;

}

6. As we have to translate the data contract objects to the business domain objects, we need to add the following two methods:

7. private void TranslateProductBDOToProductDTO(

8. ProductBDO productBDO,

9. Product product)

10.{

11. product.ProductID = productBDO.ProductID;

12. product.ProductName = productBDO.ProductName;

13. product.QuantityPerUnit = productBDO.QuantityPerUnit;

14. product.UnitPrice = productBDO.UnitPrice;

15. product.Discontinued = productBDO.Discontinued;

16. product.RowVersion = productBDO.RowVersion;

17.}

18.

19.private void TranslateProductDTOToProductBDO(

20. Product product,

21. ProductBDO productBDO)

22.{

23. productBDO.ProductID = product.ProductID;

24. productBDO.ProductName = product.ProductName;

25. productBDO.QuantityPerUnit = product.QuantityPerUnit;

26. productBDO.UnitPrice = product.UnitPrice;

27. productBDO.Discontinued = product.Discontinued;

28. productBDO.RowVersion = product.RowVersion;

}

7. Change the config file, App.config, as follows:

1. Change Service1 to ProductService.

2. Remove the word, Design_Time_Addresses/.

3. Change the port to 8080.

4. Now, BaseAddress should be as follows:

http://localhost:8080/NorthwindService/ProductService/

5. Copy the connection string from the App.config file in the NorthwindDAL project to the App.config file in the NorthwindService project, after the configSections node:

6. <connectionStrings>

7. <add name="NorthwindEntities"

8. connectionString="metadata=res://*/Northwind.csdl|res://*/Northwind.ssdl|res://*/Northwind.msl;provider=System.Data.SqlClient;provider connection string="data source=localhost;initial catalog=Northwind;integrated security=True;MultipleActiveResultSets=True;App=EntityFramework"" providerName="System.Data.EntityClient" />

</connectionStrings>

Note

You should leave the original connection string untouched in the App.config file in the data access layer project. This connection string is used by the entity model designer at design time. It is not used at all during runtime, but if you remove it, whenever you open the entity model designer in Visual Studio, you will be prompted to specify a connection to your database.

Now, build the solution and there should be no errors.

Testing the service with the WCF Test Client

Now, we can run the program to test the GetProduct and UpdateProduct operations with the WCF Test Client.

Note

You might need to run Visual Studio as an administrator to start the WCF Test Client. You can refer to Chapter 2, Hosting the HelloWorld WCF Service, for more details on how to set up your WCF development environment.

First, set NorthwindService as the startup project and then press Ctrl + F5 to start the WCF Test Client. Double-click on the GetProduct operation, enter a valid product ID, and click on the Invoke button. The detailed product information should be retrieved and displayed on the screen, as shown in the following screenshot:

Testing the service with the WCF Test Client

Note

You might get an error that says No connection string named 'NorthwindEntities' could be found in the application config file. This means you forgot to copy the connection string from the DAL project to the service project.

If you get an error that says Schema specified is not valid. Errors: Northwind.ssdl(2,2) : error 0152: No Entity Framework provider found for the ADO.NET provider, it means that you forgot to install Entity Framework to the service project.

Now, double-click on the UpdateProduct operation, enter a valid product ID, and specify a name, price, quantity per unit, and then click on Invoke.

This time, you will get an exception as shown, in the following screenshot:

Testing the service with the WCF Test Client

From this screenshot, we can see that the update failed. The error details actually tell us that it is a concurrency error. This is because, using the WCF Test Client, we can't enter a row version as it is not a simple data type parameter. Thus, we didn't succeed in the original RowVersionfor the object to be updated, and when updating the object in the database, Entity Framework thinks that this product has been updated by some other users.

Testing concurrency with our own client

From an earlier section, we know that with WCF Test Client we cannot test the UpdateProduct method, nor can we test the concurrency support of the WCF service, because with WCF Test Client, we cannot update products. To test the concurrency support of Entity Framework, we have to use our own test client.

Hosting the WCF service in IIS

The WCF service is now hosted within the WCF Service Host. To test it with our own test client, we will have to start WCF Service Host before we run our own test client. To make things easier, in this section, we will first decouple our WCF service from Visual Studio. We will host it in IIS.

You can follow these steps to host this WCF service in IIS (refer to the Hosting the service in IIS using the HTTP protocol section in Chapter 2, Hosting the HelloWorld WCF Service).

1. In the Solution Explorer, under the NorthwindService project, copy the App.config file to Web.config.

2. Within the Web.config file, add the following node as a child node of the service model node, system.serviceModel:

3. <serviceHostingEnvironment >

4. <serviceActivations>

5. <add factory="System.ServiceModel.Activation.ServiceHostFactory"

6. relativeAddress="./ProductService.svc"

7. service="NorthwindService.ProductService"/>

8. </serviceActivations>

</serviceHostingEnvironment>

3. Remove the following lines from the Web.config file:

4. <host>

5. <baseAddresses>

6. <add baseAddress="http://localhost:8080/NorthwindService/ProductService/" />

7. </baseAddresses>

</host>

4. Now, open IIS Manager, add a new application pool, NorthwindAppPool. Make sure that it is a .NET 4.0 application pool, as shown in the following screenshot:

Hosting the WCF service in IIS

5. After the new application pool is created, select this application pool, go to Advanced Settings | Process Model | Identity, click on the button, select Custom Account, and then click on the Set button. The Set Credentials dialog window should pop up on your screen.

6. Change the identity of this new application pool to a user that has appropriate access to the Northwind database. The service that is going to be created in the next step will use this identity to access the Northwind database (the default application pool identity for the new application pool, ApplicationPoolIdentity, does not have access to the Northwind database.).

Hosting the WCF service in IIS

7. Now, in IIS Manager, add a new application, LINQNorthwindService, and set its physical path to C:\SOAwithWCFandEF\Projects\LINQNorthwind\NorthwindService. Choose the newly created application pool as this service's application pool.

Hosting the WCF service in IIS

8. Within Visual Studio, in the Solution Explorer, right-click on the NorthwindService project item, select Properties, then click on the Build Events tab, and enter the following code to the Post-build event command line box:

copy .\*.* ..\

With this post-build event command line, whenever NorthwindService is rebuilt, the service binary files will be copied to the C:\SOAwithWCFandEF\Projects\LINQNorthwind\NorthwindService\bin directory so that the service hosted in IIS will always be up to date.

9. Within Visual Studio, in the Solution Explorer, right-click on the NorthwindService project item and select Rebuild.

Note

The steps here are very similar to the steps in Chapter 2, Hosting the HelloWorld WCF Service. You can refer to that chapter for more details, such as how to install IIS and enable WCF support within IIS.

Now, you have finished setting up the service to be hosted in IIS. Open Internet Explorer, go to the following address, and you should see the ProductService description in the browser, http://localhost/LINQNorthwindService/ProductService.svc.

Note

If you get an error that says Cannot read configuration file due to insufficient permissions, you should give identity of the new app pool read access to the NorthwindService folder.

Creating the test client

In this section, we will create a WinForm client to get the product details and update the price of a product.

Follow these steps to create the test client:

1. In the Solution Explorer, right-click on the solution item, and select Add | New Project….

2. Select Visual C# | Windows Forms Application as the template and change the name to LINQNorthwindClient. Click on the OK button to add the new project.

3. On the form designer, add the following five controls:

o A label named lblProductID with the text, Product ID

o A textbox named txtProductID

o A button named btnGetProduct with the text, &Get Product Details

o A label named lblProductDetails with the text, Product Details

o A textbox named txtProductDetails with the Multiline property set to True

The layout of the form is as shown in the following screenshot:

Creating the test client

4. In the Solution Explorer, right-click on the LINQNorthwindClient project and select Add | Service Reference….

5. On the Add Service Reference window, enter this address: http://localhost/LINQNorthwindService/ProductService.svc.

6. Change Namespace from ServiceReference1 to ProductServiceRef and click on the OK button.

The Add Service Reference window should appear, as shown in the following screenshot:

Creating the test client

Implementing the GetProduct functionality

Now that we have the test client created, we will customize the client application to test the new WCF service.

First, we need to customize the test client to call the WCF service to get a product from the database so that we can test the GetProduct operation with LINQ to Entities.

We will call a WCF service through the proxy, so let's add the following using statements to the form class in the Form1.cs file:

using LINQNorthwindClient.ProductServiceRef;

using System.ServiceModel;

Then, in the Forms Designer, double-click on the btnGetProduct button and add an event handler for this button, as follows:

private void btnGetProduct_Click(object sender, EventArgs e)

{

var client = new ProductServiceClient();

string result = "";

try

{

var productID = Int32.Parse(txtProductID.Text);

var product = client.GetProduct(productID);

var sb = new StringBuilder();

sb.Append("ProductID:" +

product.ProductID.ToString() + "\r\n");

sb.Append("ProductName:" +

product.ProductName + "\r\n");

sb.Append("QuantityPerUnit:" +

product.QuantityPerUnit + "\r\n");

sb.Append("UnitPrice:" +

product.UnitPrice.ToString() + "\r\n");

sb.Append("Discontinued:" +

product.Discontinued.ToString() + "\r\n");

sb.Append("RowVersion:");

foreach (var x in product.RowVersion.AsEnumerable())

{

sb.Append(x.ToString());

sb.Append(" ");

}

result = sb.ToString();

}

catch (TimeoutException ex)

{

result = "The service operation timed out. " +

ex.Message;

}

catch (FaultException<ProductFault> ex)

{

result = "ProductFault returned: " +

ex.Detail.FaultMessage;

}

catch (FaultException ex)

{

result = "Unknown Fault: " +

ex.ToString();

}

catch (CommunicationException ex)

{

result = "There was a communication problem. " +

ex.Message + ex.StackTrace;

}

catch (Exception ex)

{

result = "Other exception: " +

ex.Message + ex.StackTrace;

}

txtProductDetails.Text = result;

}

Implementing the UpdateProduct functionality

Next, we need to modify the client program to call the UpdateProduct operation of the web service. This method is particularly important to us because we will use it to test the concurrent update control of LINQ to Entities.

First, we need to add some more controls to the form. We will modify the form UI as follows:

1. Open the Form1.cs file from the LINQNorthwindClient project.

2. Add a label named lblNewPrice with the text, New Price.

3. Add a textbox named txtNewPrice.

4. Add a button named btnUpdatePrice with the text, &Update Price.

5. Add a label named lblUpdateResult with the text, Update Result.

6. Add a textbox control named txtUpdateResult with the Multiline property set to True and Scrollbars set to Both.

The form should now appear as shown in the following screenshot:

Implementing the UpdateProduct functionality

Now, double-click on the Update Price button and add the following event handler method for this button:

private void btnUpdatePrice_Click(object sender, EventArgs e)

{

var result = "";

if (product != null)

{

try

{

// update its price

product.UnitPrice =

Decimal.Parse(txtNewPrice.Text);

var client = new ProductServiceClient();

var sb = new StringBuilder();

var message = "";

sb.Append("Price updated to ");

sb.Append(txtNewPrice.Text);

sb.Append("\r\n");

sb.Append("Update result:");

sb.Append(client.UpdateProduct(ref product,

ref message).ToString());

sb.Append("\r\n");

sb.Append("Update message: ");

sb.Append(message);

sb.Append("\r\n");

sb.Append("New RowVersion:");

foreach (var x in product.RowVersion.AsEnumerable())

{

sb.Append(x.ToString());

sb.Append(" ");

}

result = sb.ToString();

}

catch (TimeoutException ex)

{

result = "The service operation timed out. " +

ex.Message;

}

catch (FaultException<ProductFault> ex)

{

result = "ProductFault returned: " +

ex.Detail.FaultMessage;

}

catch (FaultException ex)

{

result = "Unknown Fault: " +

ex.ToString();

}

catch (CommunicationException ex)

{

result = "There was a communication problem. " +

ex.Message + ex.StackTrace;

}

catch (Exception ex)

{

result = "Other exception: " +

ex.Message + ex.StackTrace;

}

}

else

{

result = "Get product details first";

}

txtUpdateResult.Text = result;

}

Note that inside the Update Price button's event handler listed previously, we don't get the product from the database first. Instead, we reuse the same product object from the btnGetProduct_Click method, which means that we will update whatever product we get when we click on the Get Product Details button. In order to do this, we need to move the product variable outside the private method, btnGetProduct_Click, to be a class variable as follows:

Product product;

Inside the btnGetProduct_Click method, we need not define another variable, product, but we can now use the class member product. Now, the first few lines of code for the Form1 class should be as follows:

public partial class Form1 : Form

{

Product product;

public Form1()

{

InitializeComponent();

}

private void btnGetProductDetail_Click

(object sender, EventArgs e)

{

var client = new ProductServiceClient();

var result = "";

try

{

var productID =

Int32.Parse(txtProductID.Text);

product = client.GetProduct(productID);

// More code to follow

As you can see, we didn't do anything specific with the concurrent update control of the update, but later in the Testing concurrent updates manually section within this chapter, we will learn how LINQ to Entities inside the WCF service handles this for us.
As done in the previous chapters, we will also capture all kinds of exceptions and display appropriate messages for them.

Testing the GetProduct and UpdateProduct operations

We can build and run the program to test the GetProduct and UpdateProduct operations now. Make the LINQNorthwindClient project the startup project and start it.

1. On the client form (UI), enter 10 as the product ID in the Product ID textbox, and click on the Get Product Details button to get the product details. Note that UnitPrice is now 31.0000, as shown in the following screenshot (the price and row version might be different in your database):

Testing the GetProduct and UpdateProduct operations

2. Now, enter 32 as the product price in the New Price textbox and click on the Update Price button to update its price. Update result should be True. Note that RowVersion has been changed and displayed as New RowVersion:

Testing the GetProduct and UpdateProduct operations

3. To verify the new price, click on the Get Product Details button again to get the product details for this product and you will see that UnitPrice has been updated to 32.0000.

Testing concurrent updates manually

We can also test concurrent updates by using the client application, LINQNorthwindClient.

In this section, we will start two clients (let's call them Client A and Client B) and update the same product from these two clients at the same time. We will create a conflict between the updates from these two clients so that we can test whether this conflict is properly handled by LINQ to Entities.

The test sequence will be as follows:

1. Client A starts.

2. Client B starts.

3. Client A reads the product information.

4. Client B reads the same product information.

5. Client B updates the product successfully.

6. Client A tries to update the product but fails.

The last step is where the conflict occurs as the product has been updated in between the read and update operations by Client A.

The steps to deal with this situation are as follows:

1. Stop the client application if it is still running in the debugging mode.

2. Start the client application in the non-debugging mode by pressing Ctrl + F5. We will refer to this client as Client A.

3. In the Client A application, enter 11 in the Product ID textbox and click on the Get Product Details button to get the product's details. Note that UnitPrice is 21.0000.

4. Start another client application in the non-debugging mode by pressing Ctrl + F5. We will refer to this client as Client B.

5. In the Client B application, enter 11 in the Product ID textbox and click on the Get Product Details button to get the product's details. Note that UnitPrice is still 21.0000. Client B's form window should be identical to Client A's form window.

6. In Client B's form, enter 22 as the product price in the New Price textbox and click on the Update Price button to update its price.

7. Client B's update is committed to the database and the Update result value is True. The price of this product has now been updated to 22 and RowVersion has been updated to a new value.

8. In the Client B application, click on the Get Product Details button to get the product details to verify the update. Note that UnitPrice is now 22.0000.

9. On Client A's form, enter 23 as the product price in the New Price textbox and click on the Update Price button to update its price.

10.Client A's update fails with an error message, Entities may have been modified or deleted since entities were loaded.

11.In the Client B application, click on the Get Product Details button again to get the product's details. You will see that UnitPrice is still 22.0000 and RowVersion is not changed, which means that Client A's update didn't get committed to the database.

The following screenshot is for Client B. You can see Update result is True and the price after the update is 22:

Testing concurrent updates manually

The following screenshot is for Client A. You can see that the price before the update is 21.0000 and the update fails with an error message. This error message is caught as an unknown fault from the client side because we didn't handle the concurrency exception in our service:

Testing concurrent updates manually

From the preceding test, we know that the concurrent update is controlled by LINQ to Entities. An optimistic locking mechanism is enforced and one client's update won't overwrite another client's update. The client that has a conflict will be notified by a fault message.

Note

Concurrent update locking is applied at the record level in the database. If two clients try to update different records in the database, they will not interfere with each other. For example, if you repeat the previous steps to update product 10 in one client and product 11 in another client, there will be no problem at all.

Testing concurrent updates automatically

In the previous section, we tested the concurrent update control of LINQ to Entities, but as you can see, the timing of the update is fully controlled by our input. We know exactly when the conflict will happen. In a real production environment, a conflict may happen at any time, with no indication as to when and how it will happen. In this section, we will simulate a situation such that a conflict happens randomly. We will add a new functionality to update one product 100 times and let two clients compete with each other until one of the updates fails.

For this test, we will put the actual updates in a background worker thread so that the main UI thread won't be blocked:

1. Open the Form1.cs file from the LINQNorthwindClient project.

2. Add a new class member to the form class for the worker thread, as follows:

BackgroundWorker bw;

3. Go to the Form1.cs design mode.

4. Add another button called btnAutoUpdate with the text, &Auto Update.

5. Add the following click event handler for this new button:

6. private void btnAutoUpdate_Click

7. (object sender, EventArgs e)

8. {

9. if (product != null)

10. {

11. btnAutoUpdate.Text = "Updating Price ...";

12. btnAutoUpdate.Enabled = false;

13.

14. bw = new BackgroundWorker();

15. bw.WorkerReportsProgress = true;

16. bw.DoWork += AutoUpdatePrice;

17. bw.ProgressChanged += PriceChanged;

18. bw.RunWorkerCompleted += AutoUpdateEnd;

19. bw.RunWorkerAsync();

20. }

21. else

22. {

23. txtUpdateResult.Text = "Get product details first";

24. }

}

6. Add the following methods to track the status of the updates, as the updates might take a while:

7. private void AutoUpdateEnd

8. (object sender, RunWorkerCompletedEventArgs e)

9. {

10. btnAutoUpdate.Text = "&Auto Update";

11. btnAutoUpdate.Enabled = true;

12.}

13.

14.private void PriceChanged

15. (object sender, ProgressChangedEventArgs e)

16.{

17. txtUpdateResult.Text = e.UserState.ToString();

18. // Scroll to end of textbox

19. txtUpdateResult.SelectionStart = txtUpdateResult.TextLength-4;

20. txtUpdateResult.ScrollToCaret();

}

7. Finally, add the following method to do the actual update:

8. private void AutoUpdatePrice

9. (object sender, DoWorkEventArgs e)

10.{

11. var client = new ProductServiceClient();

12. var result = "";

13. try

14. {

15. // update its price

16. for (int i = 0; i < 100; i++)

17. {

18. // refresh the product first

19. product = client.GetProduct(product.ProductID);

20. // update its price

21. product.UnitPrice += 1.0m;

22.

23. var sb = new StringBuilder();

24. var message = "";

25. sb.Append("Price updated to ");

26. sb.Append(product.UnitPrice.ToString());

27. sb.Append("\r\n");

28. sb.Append("Update result:");

29. bool updateResult = client.UpdateProduct(

30. ref product, ref message);

31. sb.Append(updateResult.ToString());

32. sb.Append("\r\n");

33. sb.Append("Update message: ");

34. sb.Append(message);

35. sb.Append("\r\n");

36. sb.Append("New RowVersion:");

37. foreach (var x in product.RowVersion.AsEnumerable())

38. {

39. sb.Append(x.ToString());

40. sb.Append(" ");

41. }

42. sb.Append("\r\n");

43.

44. sb.Append("Price updated ");

45. sb.Append((i + 1).ToString());

46. sb.Append(" times\r\n\r\n");

47.

48. result += sb.ToString();

49.

50. // report progress

51. bw.ReportProgress(i+1, result);

52.

53. // sleep a while

54. var random = new Random();

55. var randomNumber = random.Next(0, 1000);

56. System.Threading.Thread.Sleep(randomNumber);

57. }

58. }

59. catch (TimeoutException ex)

60. {

61. result += "The service operation timed out. " +

62. ex.Message;

63. }

64. catch (FaultException<ProductFault> ex)

65. {

66. result += "ProductFault returned: " +

67. ex.Detail.FaultMessage;

68. }

69. catch (FaultException ex)

70. {

71. result += "Unknown Fault: " +

72. ex.ToString();

73. }

74. catch (CommunicationException ex)

75. {

76. result += "There was a communication problem. " +

77. ex.Message + ex.StackTrace;

78. }

79. catch (Exception ex)

80. {

81. result += "Other exception: " +

82. ex.Message + ex.StackTrace;

83. }

84.

85. // report progress

86. bw.ReportProgress(100, result);

}

The concept here is that once the btnAutoUpdate button is clicked, it will keep updating the price of the selected product 100 times, with a price increase of 1.00 in each iteration. If two clients—again let's call them Client A and Client B—are running, and this button is clicked for both the clients, one of the updates will fail as the other client will also be updating the same record.

The sequence of the updates will be as follows:

1. Client A reads the product's details, updates the product, and commits the changes back to the database.

2. Client A sleeps for a while, then repeats the preceding step.

3. Client B reads the product's details, updates the same product, and commits the changes back to the database.

4. Client B sleeps for a while, then repeats the preceding step.

5. At some point, these two sets of processes will cross; so the following events will happen:

1. Client A reads the product's details.

2. Client A processes the product in memory.

3. Client B reads the product's details.

4. Client A finishes processing and commits the changes back to the database.

5. Client B finishes processing and tries to commit the changes back to the database.

6. Client B's update fails because it finds that the product has been updated while it was still processing the product.

7. Client B stops.

8. Client A keeps updating the product until it has done so 100 times.

Now, follow these steps to finish this test:

1. Start the client program twice in the non-debugging mode by pressing Ctrl + F5. Two clients should be up and running.

2. For each client, enter 3 in the Product ID textbox and click on Get Product Details to get the product details. Both clients should display the price as 10.0000.

3. Click on the Auto Update button on each client.

You will see that one of the clients fails while the other one keeps updating the database for 100 times.

The following screenshot shows the results in the successful client. As you can see, the initial price of the product was 10.0000, and after the updates, this price has been changed to a new one. From the source code, we know that this client only updates the price 100 times with anincrease of 1.00 each time. The new price now is not 110.00, because another client has updated this product a few more times:

Testing concurrent updates automatically

The following screenshot shows the results in the failed client. As you can see, the initial price of the product was 10.000. After updating the price a few times, when this client tries to update the price again, it fails with the error message, Entities may have been modified or deleted since entities were loaded:

Testing concurrent updates automatically

Note

However, if you enter two different product IDs in each client, both client updates will be successful until all 100 updates have been made. This again proves that locking is applied on a record level of the database.

Summary

In this chapter, we used LINQ to Entities to communicate with the database in the data access layer, rather than using the raw ADO.NET APIs. We have used only one LINQ statement to retrieve product information from the database, and as you have seen, the updates with LINQ to Entities prove to be much easier than with the raw ADO.NET data adapters. Now, WCF and LINQ are combined together for our services, so we can take advantage of both technologies.

In the next chapter, we will explore transaction support of WCF with LINQ to Entities so that we can write transactional WCF services.