A WCF Data service with OData (Open Data protocol) protocol provides rich data access capabilities such as flexible querying etc. By consuming Data Services and using Data Service Context at WCF client side, we can query the data base in different ways.
However, in real time SOA scenarios we may also need to develop a regular WCF service which provides functionalities other than DB operations such as:
- File I/O: Reading/Writing data from xml or other type of files and/or combining the data with data from Database.
- Complex business logic which is not ideal to execute on light weight client.
So, we need to have two different services:
- Data Service
- Business Logic Service
Having two different services will have more maintenance. So, in this article I would explain how to host a single service with two different contracts with different endpoints. I will also cover how to consume the service at client side and resolving issues if we face any.
Here are the steps. You may skip any of these steps if you already know it and jump to appropriate.
STEPS:
- Creating Entity Framework .edmx using pre-existing Database
- Implementing WCF Data service (manually and not using template)
- Implementing WCF service (Business logic Service)
- Hosting WCF Dataservice and WCF service side by side.
- Consuming service at client side
1. Creating .edmx file from an existing database:
Create a new solution by adding a WPF project. I have named my project as ‘AccountDataService’. Add a new project (class library template) for Data base/.edmx. Follow the steps as mentioned here:
http://msdn.microsoft.com/en-us/library/cc716703(v=vs.103).aspx
I have named my entities class as ‘AccountDataEntities’.
Tip: When we add entity data model, it automatically adds connection string inside App.Config file of that project. In step 4, we add a separate project which takes care of hosting WCF service. Make sure you copy the connection string to Service host project App.config file.
2. Implementing WCF Data Service (manually and not using template):
Since I have created a WPF desktop application project, I will create WCF service class manually.
Tip: You can also create using template. Note that it is not a project template so if you try to add a new project you don’t see WCF Data Service template. So if you try to add a new Item inside a project, you should see the ‘WCF Data Service’ template under Web category. Important point to note is, it is only available for ASP.net project and you don’t see this template in other project types. This is one of the reasons why I am creating service manually.
Add new class to ‘AccountDataService’ project and name it as AccountDataService.cs. This is the class for WCF Service. It implements DataService class for type ‘AccountDataEntities’. My class typically looks like below:
public partial class AccountDataService : DataService, AccountDataEntities { public static void InitializeService(DataServiceConfiguration config) { config.DataServiceBehavior.MaxProtocolVersion = System.Data.Services.Common.DataServiceProtocolVersion.V2; config.SetEntitySetAccessRule("*", EntitySetRights.AllRead | EntitySetRights.AllWrite); } }
It implements only one method InitializeService(). SetEntitySetAccessRule is to set the access type for your database entities. I have enabled all read write permissions.
You need to add following project references to your project:
- Microsoft.Data.OData
- Microsoft.Data.Services
- Microsoft.Data.Services.Client
Note that I have defined the class as partial. I will add another partial class file which implements WCF service contract for non-data related.
By overriding following methods you can see what’s going on at server side. This is useful for debugging issues at WCF data service level. I am currently writing it to console however it can be written to appropriate source based on the requirement.
protected override void HandleException(HandleExceptionArgs args) { Console.WriteLine(args.Exception); base.HandleException(args); } protected override void OnStartProcessingRequest(ProcessRequestArgs args) { Console.WriteLine(args.RequestUri); base.OnStartProcessingRequest(args); }
3. Implementing WCF service (Business logic Service):
Add an interface to the project and name it as ‘IBLLContract’. I have added a simple method to the contract which just returns the count. My contract looks like below:
[ServiceContract(Namespace = "http://accountservices/data/v1.0", ConfigurationName = "IBLLContract")] interface IBLLContract { [OperationContract] int GetXmlDocumentCount(); }
ConfigurationName is important as this is used for defining end point in config file in Service host project (step 4).
Add a new class file and name it as AccountDataService.cs. Define the class as partial and implement IBLLContract. My service class looks like below:
public partial class AccountDataService : IBLLContract { public int GetXmlDocumentCount() { //returning some dummy number return 10; } }
4. Hosting WCF Dataservice and WCF service side by side:
Add a new project (console application type) ‘ServiceHost’. Add following code in Program.cs.
static void Main(string[] args) { DataServiceHost serviceHost = new DataServiceHost(typeof(AccountDataService), new Uri[] { }); serviceHost.Open(); Console.WriteLine("Service has been started! press any key to stop."); Console.ReadLine(); }
Important things note here are, we are using DataServiceHost class to host both the contracts. DataServiceHost is derived from ServiceHost, so it can be used for hosting WCF Data Service and regular WCF service. We are passing empty Uri collection so that endpoints are taken from App.config file.
Add following settings to App.config file of ServiceHost project.
<system.serviceModel> <diagnostics> <messageLogging logMessagesAtServiceLevel="true" /> </diagnostics> <behaviors> <serviceBehaviors> <behavior name="DefaultBehavior"> <serviceMetadata httpGetEnabled="true" policyVersion="Policy15"/> <serviceDebug includeExceptionDetailInFaults="true"/> </behavior> </serviceBehaviors> </behaviors> <services> <service name="NewAccountDataService.AccountDataService" behaviorConfiguration="DefaultBehavior"> <host> <baseAddresses> <add baseAddress="http://localhost:18000/accountservice"/> </baseAddresses> </host> <endpoint address="data" binding="webHttpBinding" contract="System.Data.Services.IRequestHandler"/> <endpoint address="bll" binding="basicHttpBinding" contract="IBLLContract"/> <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/> </service> </services> </system.serviceModel>
There are total three end points defined here. The one with ‘IBLLContract’ contract is a regular WCF service end point which serves as bll (business logic service).
The end point with ‘System.Data.Services.IRequestHandler’ contract is a WCF Data service.
The last one with ‘IMetadataExchange’ contract is for enabling meta data for the service. This is required for adding service reference at WCF client side.
5. Consuming the service:
Add a WPF application project which will be acting as a client.
Adding WCF Data Service reference:
Right click on project and select Add service reference. Enter ‘http://localhost:18000/accountservice’ in Address field and click on Go. The service will be discovered. Select the service from Service list and click on OK to add the reference. If you expand service reference from solution explorer you can see a file Reference.cs and a class AccountDataEntities class which implements DataServiceContext.
Go to MainWindow.xaml.cs file and write the code to create instance of service reference. My MainWindow class looks like below:
public MainWindow() { InitializeComponent(); LoadAccounts(); } private void LoadAccounts() { using (AccountDataEntities context = new AccountDataEntities (new Uri(ConfigurationManager.AppSettings["dataserviceuri"]))) { var data = (from a in context.Accounts where (a.Id == new Guid("FED48A9D-8D89-498c-9CB7-623C8FB0B2D6")) select a).ToList(); } }
AccountDataEntities constructor takes one parameter which is a URI to the service. I am reading the service Uri address from configuration file instead of hardcoding.
I have added following entry in the app.config file.
<appSettings> <add key="dataserviceuri" value="http://localhost:18000/accountservice/data"/> </appSettings>
If you run the application, you can see the Accounts details for the given ID.
Adding WCF (BLL) service reference:
If you try to add by right clicking, selecting service reference and specifying address as http://localhost:18000/accountservice/mex, you would get following error.
There was an error downloading 'http://localhost:18000/accountservice/bll/_vti_bin/ListData.svc/$metadata'. The request failed with HTTP status 400: Bad Request. Metadata contains a reference that cannot be resolved: 'http://localhost:18000/accountservice/bll'. Content Type application/soap+xml; charset=utf-8 was not supported by service http://localhost:18000/accountservice/bll. The client and service bindings may be mismatched. The remote server returned an error: (415) Cannot process the message because the content type 'application/soap+xml; charset=utf-8' was not the expected type 'text/xml; charset=utf-8'.. If the service is defined in the current solution, try building the solution and adding the service reference again.
If you try with ‘WCF Test Client’ tool you would see the following error:
Error: Cannot obtain Metadata from http://localhost:18000/accountservice/mex If this is a Windows (R) Communication Foundation service to which you have access, please check that you have enabled metadata publishing at the specified address. For help enabling metadata publishing, please refer to the MSDN documentation at http://go.microsoft.com/fwlink/?LinkId=65455.WS-Metadata Exchange Error URI: http://localhost:18000/accountservice/mex Metadata contains a reference that cannot be resolved: 'http://localhost:18000/accountservice/mex'. <?xml version="1.0" encoding="utf-16"?><Fault xmlns="http://www.w3.org/2003/05/soap-envelope"><Code><Value>Receiver</Value><Subcode><Value xmlns:a="http://schemas.microsoft.com/net/2005/12/windowscommunicationfoundation/dispatcher">a:InternalServiceFault</Value></Subcode></Code><Reason><Text xml:lang="en-US">An exception was thrown in a call to a WSDL export extension: System.ServiceModel.Description.DataContractSerializerOperationBehavior contract: http://tempuri.org/:IRequestHandler</Text></Reason><Detail xmlns:s="http://www.w3.org/2003/05/soap-envelope"><ExceptionDetail xmlns="http://schemas.datacontract.org/2004/07/System.ServiceModel" xmlns:i="http://www.w3.org/2001/XMLSchema-instance"><HelpLink i:nil="true"></HelpLink><InnerException><HelpLink i:nil="true"></HelpLink><InnerException i:nil="true"></InnerException><Message> The operation 'ProcessRequestForMessage' could not be loaded because it has a parameter or return type of type System.ServiceModel.Channels.Message or a type that has MessageContractAttribute and other parameters of different types. When using System.ServiceModel.Channels.Message or types with MessageContractAttribute, the method must not use any other types of parameters.</Message><StackTrace> at System.ServiceModel.Dispatcher.OperationFormatter.Validate(OperationDescription operation, Boolean isRpc, Boolean isEncoded) at System.ServiceModel.Description.MessageContractExporter.ExportMessageContract() at System.ServiceModel.Description.DataContractSerializerOperationBehavior.System.ServiceModel.Description.IWsdlExportExtension.ExportContract(WsdlExporter exporter, WsdlContractConversionContext contractContext) at System.ServiceModel.Description.WsdlExporter.CallExtension(WsdlContractConversionContext contractContext, IWsdlExportExtension extension)</StackTrace><Type>System.InvalidOperationException</Type></InnerException><Message>An exception was thrown in a call to a WSDL export extension: System.ServiceModel.Description.DataContractSerializerOperationBehavior contract: http://tempuri.org/:IRequestHandler</Message><StackTrace> at System.ServiceModel.Description.ServiceMetadataBehavior.MetadataExtensionInitializer.GenerateMetadata() at System.ServiceModel.Description.ServiceMetadataExtension.EnsureInitialized() at System.ServiceModel.Description.ServiceMetadataExtension.WSMexImpl.GatherMetadata(String dialect, String identifier) at System.ServiceModel.Description.ServiceMetadataExtension.WSMexImpl.Get(Message request) at SyncInvokeGet(Object , Object[] , Object[] ) at System.ServiceModel.Dispatcher.SyncMethodInvoker.Invoke(Object instance, Object[] inputs, Object[]& outputs) at System.ServiceModel.Dispatcher.DispatchOperationRuntime.InvokeBegin(MessageRpc& rpc) at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage5(MessageRpc& rpc) at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage41(MessageRpc& rpc) at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage4(MessageRpc& rpc) at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage31(MessageRpc& rpc) at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage3(MessageRpc& rpc) at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage2(MessageRpc& rpc) at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage11(MessageRpc& rpc) at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage1(MessageRpc& rpc) at System.ServiceModel.Dispatcher.MessageRpc.Process(Boolean isOperationContextSet)</StackTrace><Type>System.InvalidOperationException</Type></ExceptionDetail></Detail></Fault>HTTP GET Error URI: http://localhost:18000/accountservice/mex There was an error downloading 'http://localhost:18000/accountservice/mex'. The request failed with HTTP status 400: Bad Request.
The error basically says that the meta data is not enabled, however we know we have already enabled MEX end point in server side. To identify the issue I had to enable server side logging and after analyzing server logs WCF Data service end point was preventing the enabling of meta data.
So what we can do is, disable/comment out the Data service end point from service configuration temporarily. Go to App.config file of ServiceHost project and comment out following line:
<!--<endpoint address="data" binding="webHttpBinding" contract="System.Data.Services.IRequestHandler"/>-->
Build the project and rerun the ServiceHost to start the services.
Now try to add service reference in client project. Note that we are using mex end point address while adding the service reference.
http://localhost:18000/accountservice/mex
Service reference will be added successfully. Now go back to service and re-enable the WCF Data service. You may also want to disable mex end point as we have already added service reference.
You can instantiate the service reference as shown below and you can access the contract methods.
using (BLLContractClient client = new BLLContractClient()) { int count = client.GetXmlDocumentCount(); }
So we have successfully hosted WCF Data Service and regular WCF service end points side by side successfully.
Please provide your valuable feedback/comments/suggestions that will help me improve my writing.
Hello there! Would you mind if I share your blog with my twitter group?
There’s a lot of people that I think would really enjoy your content.
Please let me know. Cheers
Sure, I really appreciate it. I would like to hear more from different groups so that it would be a great learning for me for my future posts. Thanks.
Excellent blog here! Also your website loads up fast! What
host are you using? Can I get your affiliate link to your host?
I wish my web site loaded up as fast as yours lol