Introduction
Overview
In that article, we also manually created a host application, generated the proxy and configuration files, and created a test client to test the WCF Service. You can review that article to learn what a WCF Service is really like under the hood.Now in this article, we will implement a simple WCF Service using built-in Visual Studio WCF templates. This simple WCF Service will have only one operation,
GetProduct
. This operation will accept
an integer input as the product ID, and connect to a backend database to
retrieve the product details for the specified product ID using LINQ to
Entities. The Microsoft sample database Northwind will be used for the
backend database, and Visual Studio 2010 will be used as the IDE. In a future article (Concurrency Control of a WCF Service with Entity Framework), we will implement another operation,
UpdateProduct
,
to update a product in the database through the Entity Framework. We
will cover concurrency control in that article, and we will create a WPF
test client in that article to test the UpdateProduct
operation with concurrency support.In this article, we will build the solution in the following order:
- Create the WCF Service
- Create a new solution and project using WCF templates
- Create the service interface
- Create the operation contract
- Create the data contract
- Implement the service interface
- Modify the app.config file
- Test the service using the WCF Test Client
- Apply LINQ to Entities to the WCF Service
- Prepare the database
- Model the Northwind database
- Rename the EF Product class
- Retrieve product details from the database using EF
- Translate the ProductEntity object to a Product object
- Test the WCF Service with EF
- Test exceptions
Creating the WCF Service
In the first half of this article, we will create the simple WCF Service. This WCF Service will contain theGetProduct
operation, which will return a product object to the client. The
product detail is hard coded in this WCF Service, except the ID will be
the input from the client. Later in this article, we will enhance this
WCF Service to use LINQ to Entities to retrieve the real product detail
from a real backend database. Creating a new solution and project using WCF templates
We need to create a new solution for this article, and add a new WCF project to this solution. There are a few built-in WCF service templates within Visual Studio 2010. In this article, we will use the service library template to create the WCF Service.Follow these steps to create the WCF and EF solution and the project using the service library template:
- Start Visual Studio 2010, select the menu option File | New | Project..., and you will see the New Project dialog box.
- In the New Project window, specify Visual C# | WCF | WCF Service Library as the project template, WCFandEFService as the (project) name, and WCFandEFSolution as the solution name. Make sure that the Create directory for solution checkbox is selected.
- Click the OK button, and the solution is created with a WCF project inside it. The project already has a IService1.cs file to define a service interface, and Service1.cs to implement the service. It also has an app.config file, which we will cover shortly.
To try it, just press Ctrl+F5, and you will see the WCF Test Client is up and running! Double click the
GetData
operation in the left panel, enter a number in the Value textbox in the
right panel, then click the button Invoke, and you will see the WCF
Service has been invoked: the response of the WCF Service is now
displayed in the bottom part of the right panel.Note: in order to run the WCF Test Client, you have to log in to your machine as a local administrator. You may also need to run Visual Studio as an administrator. We will discuss more about the WCF Test Client later.
Creating the service interface
In the previous section, we created a WCF project using the WCF Service Library template. In this section, we will create the service interface contracts.Because two sample files have already been created for us, we will try to re-use them as much as possible. Then, we will start customizing these two files to create the service contracts.
Creating the operation contract
To create the service operation contract, we need to open the IService1.cs file and do the following:- Change the interface name from
IService1
toIProductService
. Don't be worried if you see the warning message before the interface definition line, as we will change the web.config file in one of the following steps. - Change the first operation contract definition from this line:
- Change the file's name from IService1.cs to IProductService.cs.
string GetData(int value);
to this line:Product GetProduct(int id);
The content of the service interface for
IProductService
should look like this now:using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
namespace WCFandEFService
{
[ServiceContract]
public interface IProductService
{
[OperationContract]
Product GetProduct(int id);
}
// original code for CompositeType
}
Note: this is not the whole content of the IProductService.cs file. The bottom part of this file now should still have the class CompositeType
, which we will change to our Product
type in the next section.Creating the data contract
Another important aspect of SOA design is that you shouldn't assume that the consuming application supports a complex object model. A part of the service boundary definition is the data contract definition for the complex types that will be passed as operation parameters or return values.For maximum interoperability and alignment with SOA principles, you should not pass any .NET specific types such as
DataSet
or Exception
s
across the service boundary. You should stick to fairly simple data
structure objects such as classes with properties, and backing member
fields. You can pass objects that have nested complex types such as
'Customer with an Order collection'. However, you shouldn't make any
assumption about the consumer being able to support object-oriented
constructs such as inheritance, or base-classes for interoperable Web
Services. In our example, we will create a complex data type to represent a product object. This data contract will have five properties:
ProductID
, ProductName
, QuantityPerUnit
, UnitPrice
, and Discontinued
.
These will be used to communicate with client applications. For
example, a supplier may call the Web Service to update the price of a
particular product, or to mark a product for discontinuation. It is preferable to put data contracts in separate files within a separate assembly, but to simplify our example, we will put the
DataContract
within the same file as the service contract. So, we will modify the file IProductService.cs as follows:- Change the
DataContract
name fromCompositeType
toProduct
. - Remove the following two lines of code:
- Delete the old
BoolValue
, andStringValue
DataMember
properties. Then, for each product property, add aDataMember
property. For example, forProductID
, we will have thisDataMember
property:
bool boolValue = true;
string stringValue = "Hello ";
[DataMember]
public int ProductID { get; set; }
[DataContract]
public class Product
{
[DataMember]
public int ProductID { get; set; }
[DataMember]
public string ProductName { get; set; }
[DataMember]
public string QuantityPerUnit { get; set; }
[DataMember]
public decimal UnitPrice { get; set; }
[DataMember]
public bool Discontinued { get; set; }
}
The final content of the file IProductService.cs should be like this now:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
namespace WCFandEFService
{
[ServiceContract]
public interface IProductService
{
[OperationContract]
Product GetProduct(int id);
}
[DataContract]
public class Product
{
[DataMember]
public int ProductID { get; set; }
[DataMember]
public string ProductName { get; set; }
[DataMember]
public string QuantityPerUnit { get; set; }
[DataMember]
public decimal UnitPrice { get; set; }
[DataMember]
public bool Discontinued { get; set; }
}
}
Implementing the service interface
To implement the service interface that we defined in the previous section, open the Service1.cs file and do the following:- Change the class name from
Service1
toProductService
. Make it inherit from theIProductService
interface, instead ofIService1
. The class definition line should be like this: - Delete the
GetData
andGetDataUsingDataContract
methods. - Add the following method, to get a product:
- Change the file's name from Service1.cs to ProductService.cs. The contents of the ProductService.cs file should be like this:
public class ProductService : IProductService
public Product GetProduct(int id)
{
// TODO: retrieve the real product info from DB using EF
Product product = new Product();
product.ProductID = id;
product.ProductName = "fake product name";
product.UnitPrice = (decimal)10.0;
return product;
}
In this method, we created a fake product and returned it to the
client. Later, we will remove the hard-coded product from this method
and retrieve the real product info from the database using EF.using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
namespace WCFandEFService
{
public class ProductService : IProductService
{
public Product GetProduct(int id)
{
// TODO: retrieve the real product info from DB using EF
Product product = new Product();
product.ProductID = id;
product.ProductName = "fake product name";
product.UnitPrice = (decimal)10.0;
return product;
}
}
}
Modifying the app.config file
Because we have changed the service name, we have to make the appropriate changes to the configuration file. Note: when you rename the service, if you have used the refactor feature of Visual Studio, some of the following tasks may have been done by Visual Studio.Follow these steps to change the configuration file:
- Open the app.config file from Solution Explorer.
- Change all instances of
Service1
toProductService
.
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.web>
<compilation debug="true" />
</system.web>
<system.serviceModel>
<services>
<service name="WCFandEFService.ProductService">
<host>
<baseAddresses>
<add baseAddress = "http://localhost:8732/
Design_Time_Addresses/WCFandEFService/ProductService/" />
</baseAddresses>
</host>
<endpoint address ="" binding="wsHttpBinding"
contract = "WCFandEFService.IProductService">
<identity>
<dns value="localhost"/>
</identity>
</endpoint>
<endpoint address="mex" binding="mexHttpBinding"
contract = "IMetadataExchange"/>
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior>
<serviceMetadata httpGetEnabled="True"/>
<serviceDebug includeExceptionDetailInFaults="False" />
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>
</configuration>
Testing the service using the WCF Test Client
Because we are using the WCF Service Library template in this example, we are now ready to test this Web Service. As we pointed out when creating this project, this service will be hosted in the Visual Studio 2010 WCF Service Host environment.To start the service, press F5 or Ctrl+F5. WcfSvcHost will be started and the WCF Test Client is also started. This is a Visual Studio 2010 built-in test client for WCF Service Library projects.
Note: in order to run the WCF Test Client, you have to log in to your machine as a local administrator. You also have to start Visual Studio as an administrator if you have changed the port from the default value 8732 to some other port, like 8080. Otherwise, you may get an "Access is denied" error.
Now, from this WCF Test Client, we can test our simple
GetProduct
operation. - In the left panel of the client, double-click on the
GetProduct
operation; theGetProduct
Request will be shown on the right-side panel. - In this Request panel, specify an integer for the product ID, and click the Invoke button to let the client call the service. You may get a dialog box to warn you about the security of sending information over the network. Click the OK button to acknowledge this warning (you can check the 'In the future, do not show this message' option, so that it won't be displayed again).
If you have started the test client in debugging mode (by pressing F5), you can set a breakpoint at a line inside the
GetProduct
method in the ProductService.cs
file, and when the Invoke button is clicked, the breakpoint will be hit
so that you can debug the service as you usually do with any other .NET
application.Note that the response is always the same, no matter what product ID you use to retrieve the product (except the product ID field). Specifically, the product name is hard-coded, as shown in the diagram. Moreover, from the client response panel, we can see that several properties of the
Product
object have been assigned default values.Also, because the product ID is an integer value from the WCF Test Client, you can only enter an integer for it. If a non-integer value is entered, when you click the Invoke button, you will get an error message box to warn you that you have entered a value with the wrong type.
The Request/Response packages are displayed in grids by default, but you have the option of displaying them in XML format. Just select the XML tab from the bottom of the right-hand side panel, and you will see the XML formatted Request/Response packages. From these XML strings, you will discover that they are SOAP messages.
Besides testing operations, you can also look at the configuration settings of the Web Service. Just double-click on Config File from the left-side panel and the configuration file will be displayed in the right-side panel. This will show you the bindings for the service, the addresses of the service, and the contract for the service.
Note: what you see here for the configuration file is not an exact image of the actual configuration file. It hides some information, such as debugging mode and service behavior, and includes some additional information on reliable sessions and compression mode.
If you are satisfied with the test results, just close the WCF Test Client, and you will go back to the Visual Studio IDE. Note that as soon as you close the client, the WCF Service Host is stopped. This is different from hosting a service inside the ASP.NET Development Server, where after you close the client, the ASP.NET Development Server still stays active.
Applying LINQ to Entities to the WCF Service
In previous sections, we have created a simple WCF Service to get production details for an input product ID. The production details are hard coded in the service implementation.In the following sections, we will apply LINQ to Entities to the WCF Service. We will connect to a database through Entity Framework, and retrieve the real product information from the database.
Preparing the database
In this article, we will use the Microsoft sample database, Northwind, as the backend database. This database is not installed by default in SQL Server 2005 or SQL Server 2008, so first, we need to install it to our database server.- Download the database package. Just search for "Northwind Sample Databases download" on the Internet, or go to this page: http://www.microsoft.com/downloads/details.aspx?FamilyId=06616212-0356-46A0-8DA2-EEBC53A68034&displaylang=en and download the file SQL2000SampleDb.msi. Note: this sample database was designed for SQL Server 2000, but it can also be used in SQL Server 2005 and SQL Server 2008.
- Install (extract) it to: C:\SQL Server 2000 Sample Databases.
- Change the security of both Northwnd.mdf and Northwnd.ldf to be read/write-able to your SQL Server service account user (or just give Everyone full access).
- Open SQL Server 2005/2008 Management Studio.
- Connect to your database engine.
- Right click on the Databases node, and select Attach... from the context menu, as shown in the SQL Server Management Studio diagram below:
- In the pop-up Attach Databases dialog box, click Add, browse to the file C:\SQL Server 2000 Sample Databases\NORTHWND.MDF, click OK, and you now have the Northwind database attached to your SQL Server 2005 or 2008 engine.
Modeling the Northwind database
Now we have the database ready, we can model this database using Entity Framework. After we have the database modeled, we will use LINQ to Entities to retrieve the real product information from the database.You can follow these steps to add an Entity data model to the project.
- In Solution Explorer, right-click on the project item WCFandEFService, select menu option Add | New Item..., and then choose Visual C# Items | ADO.NET Entity Data Model as the Template, and enter Northwind.edmx as the name.
- Clicking the Add button will bring you to the Entity Data Model Wizard. In this wizard, select "Generate from database", click the Next button, and the connection dialog window will pop up. Enter your database server name, like localhost, specify the logon details, and choose Northwind as the database, then click the OK button to close this window.
- From the Entity Data Model Wizard, click button Next to go to the "Choose Your Database Objects" screen. Select the Products table, and click the Finish button to close the wizard.
This will generate a file called Northwind.designer.cs which contains the object context for the Northwind database. This file also contains the
Product
entity class.Renaming the EF Product class
If you compile the project now, you will see an error "Missing partial modifier on declaration of type 'WCFandEFService.Product'; another partial declaration of this type exists". This is because earlier in this article, we have created aProduct
class for the service data contract, but now another Product
class was created by the Entity Framework. Both are within the same
namespace, and one is a partial class, while another is not.There are two ways to solve this problem. First, we can remove our own
Product
class definition, and expose the EF Product
class as the WCF service data contract class. In this way, we can work on only one Product
entity class through the whole project, and avoid some duplication of
entity definitions. However, it is not a best practice to expose an EF
entity class outside of a WCF Service. You can just search for "expose
EF entities as data contracts" on Google and get an idea of what people
are talking about this.Another way is to separate our own
Product
class and the EF Product
class. Our own Product
class will serve as the WCF data contract class, and the EF Product
class will serve as the ORM data entity class. Now we have a clear
separation of concerns, and the outside world will never know what kind
of ORM we are using underneath. I like this idea, so in this article, we
will go this way.However, keeping our own
Product
class and the EF Product
class inside the same project causes a problem, as we have noted in the
beginning of this session. To solve this problem, we have a few
options. First, we can add a data access layer to the WCF Service
solution and put the EF classes in the data access layer. Actually, this
is the preferred way to develop an enterprise WCF Service, just as I
discussed in my WCF and LINQ to Entities book (you can find more
information about this book at the end of this article). But in this
article, we will have only one layer for the WCF Service to keep it easy
and simple, so we won't take this approach. A second way is to put the
EF entity classes in a separate namespace, so two Product
classes can co-exist without a problem. But having two Product
classes in the same project is confusing, even if they are in two
different namespaces. So we will go the third way, that is, rename the
EF Product
entity class to ProductEntity
.To rename the EF
Product
class, open the file Northwind.edmx, click on the class name, and rename it. The renamed model should be like this:After you rename the EF
Product
class, if you re-build the service, you should see no errors.Retrieving product details from the database using EF
Now, inside theGetProduct
method, we can use the following statements to get the product details from the database using LINQ to Entities:NorthwindEntities context = new NorthwindEntities();
var productEntity = (from p
in context.ProductEntities
where p.ProductID == id
select p).FirstOrDefault();
Here, we first created a ObjectContext
object for the EF
model, then we used the LINQ to Entities statement to get the product
entities from the database for the given ID. Because the return result
of the LINQ to Entities statement is IQueryable
, we called the FirstOrDefault
method to get only the first record. This method also gives a null
result if the query doesn't yield any result from the database.Translating the ProductEntity object to a Product object
However, we cannot return this product object back to the caller, because this product is of typeProductEntity
, which is not the type that the caller is expecting. The caller is expecting a return value of type Product
, which is a data contract defined within the service interface. We need to translate this ProductEntity
object to a Product
object. To do this, we add the following new method to the ProductService
class:
private Product TranslateProductEntityToProduct(
ProductEntity productEntity)
{
Product product = new Product();
product.ProductID = productEntity.ProductID;
product.ProductName = productEntity.ProductName;
product.QuantityPerUnit = productEntity.QuantityPerUnit;
product.UnitPrice = (decimal)productEntity.UnitPrice;
product.Discontinued = productEntity.Discontinued;
return product;
}
Inside this translation method, we copy all of the properties from the
ProductEntity
object to the service contract data object, but not the last three properties - UnitsInStock
, UnitsOnOrder
, and ReorderLevel
. We assume these three properties are used only inside the service implementations. Outside callers cannot see them at all.The
GetProduct
method should now look like this:
public Product GetProduct(int id)
{
NorthwindEntities context = new NorthwindEntities();
var productEntity = (from p
in context.ProductEntities
where p.ProductID == id
select p).FirstOrDefault();
if (productEntity != null)
return TranslateProductEntityToProduct(productEntity);
else
throw new Exception("Invalid product id");
}
Note: inside the
GetProduct
method, after we retrieve the product details from the database using EF, we first test the object to see if it is null
.
If so, we know the input ID is not a valid product ID in our database.
We then throw an exception to tell the client. However, in a real WCF
Service, you shouldn't throw any exceptions to the client; instead, you
should define and throw a Fault to the client. Again, more information
about Faults can be found in my WCF and LINQ to Entities book. The final contents of the ProductService.cs file should be like this:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
namespace WCFandEFService
{
public class ProductService : IProductService
{
public Product GetProduct(int id)
{
NorthwindEntities context = new NorthwindEntities();
var productEntity = (from p
in context.ProductEntities
where p.ProductID == id
select p).FirstOrDefault();
if (productEntity != null)
return TranslateProductEntityToProduct(productEntity);
else
throw new Exception("Invalid product id");
}
private Product TranslateProductEntityToProduct(
ProductEntity productEntity)
{
Product product = new Product();
product.ProductID = productEntity.ProductID;
product.ProductName = productEntity.ProductName;
product.QuantityPerUnit = productEntity.QuantityPerUnit;
product.UnitPrice = (decimal)productEntity.UnitPrice;
product.Discontinued = productEntity.Discontinued;
return product;
}
}
}
Testing the WCF Service with EF
We can now compile and test the new service with EF support. We will still use the WCF Test Client to simplify the process.- Start the WCF Service Host application and WCF Service Test Client, by pressing F5 or Ctrl+F5.
- In the WCF Service Test Client, double-click on the
GetProduct
operation, to bring up the GetProduct test screen. - Enter a value of 56 for the ID field, and then click the Invoke button.
UnitsOnOrder
property is not displayed as it is not part of the service contract data type.Testing exceptions
Now, enter an invalid product ID, like 0, and you will get this error message:This is because inside the WCF Service, we couldn't find the product in the Northwind database with ID 0, so we threw an exception for it. Because we haven't turned on the
includeExceptionDetailInFaults
flag in the app.config, the client application couldn't get the exception details. To display the exception details, you can open the app.config file, change the value of
includeExceptionDetailInFaults
from False to True, and try again. This time, you will get this error message:This time, the exception detail is returned back to the client application, as you can see; however, this is not recommended. The reason is, not all clients can understand a .NET exception. It will be treated as an unknown fault by certain types of clients. To make it understandable by all kinds of clients, we need to define and throw a fault when we can't get the product from the database for the given ID.
Another drawback of throwing a .NET exception instead of a fault is, once an exception is thrown, the communication channel is no longer valid (the channel is in faulted state). It can't be used for subsequent service calls.
To test this, first enter an invalid product ID, like 0, to trigger the exception, then enter another valid product ID, like 56, and you will get an error message like this:
From the error message, we know the communication channel is now faulted. You have to restart the WCF Test Client to create a new communication channel. Alternatively, you can force the test client to use a new proxy for the service call by checking "Start a new proxy", but this is not a recommended way in production for performance reasons.
No comments:
Post a Comment