Build a IIS-hosted WCF Service using the WCF Web HTTP Programming Model. Use basic HTTP Methods with the WCF Service to perform CRUD operations on a SQL Server database using a Data Access Layer, built with Entity Framework 5 and the Database First Development Model.
You can download a complete copy of this Post’s source code from DropBox.
Introduction
In the two previous Posts, we used the new Entity Framework 5 to create a Data Access Layer, using both the Code First and Database First Development Models. In this Post, we will create a Windows Communication Foundation (WCF) Service. The service will sit between the client application and our previous Post’s Data Access Layer (DAL), built with an ADO.NET Entity Data Model (EDM). Using the WCF Web HTTP Programming Model, we will expose the WCF Service’s operations to a non-SOAP endpoint, and call them using HTTP Methods.
Why use the WCF Web HTTP Programming Model? WCF is a well-established, reliable, secure, enterprise technology. Many large, as well as small, organizations use WCF to build service-oriented applications. However, as communications become increasingly Internet-enabled and mobile, the WCF Web HTTP Programming Model allows us to add the use of simple HTTP methods, such as POST, GET, DELETE, and PUT, to existing WCF services. Adding a web endpoint to an existing WCF service extends its reach to modern end-user platforms with minimal effort. Lastly, using the WCF Web HTTP Programming Model allows us to move toward the increasingly popular RESTful Web Service Model, so many organizations are finally starting to embrace in the enterprise.
Creating the WCF Service
The major steps involved in this example are as follows:
- Create a new WCF Service Application Project;
- Add the Entity Framework package via NuGet;
- Add a Reference the previous Post’s DAL project;
- Add a Connection String to the project’s configuration;
- Create the WCF Service Contract;
- Create the operations the service will expose via a web endpoint;
- Configure the service’s behaviors, binding, and web endpoint;
- Publish the WCF Service to IIS using VS2012’s Web Project Publishing Tool;
- Test the service’s operations with Fiddler.
The WCF Service Application Project
Start by creating a new Visual Studio 2012 WCF Service Application Project, named ‘HealthTracker.WcfService’. Add it to a new Solution, named ‘HealthTracker’. The WCF Service Application Project type is specifically designed to be hosted by Microsoft’s Internet Information Services (IIS).
Once the Project and Solution are created, install Entity Framework (‘System.Data.Entity’) into the Solution by right-clicking on the Solution and selecting ‘Manage NuGet Packages for Solution…’ Install the ‘EntityFramework’ package. If you haven’t discovered the power of NuGet for Visual Studio, check out their site.
Next, add a Reference in the new Project, to the previous ‘HealthTracker.DataAccess.DbFirst’ Project. When the WCF Service Application Project is built, a copy of the ‘HealthTracker.DataAccess.DbFirst.dll’ assembly will be placed into the ‘bin’ folder of the ‘HealthTracker.WcfService’ Project.
Next, copy the connection string from the previous project’s ‘App.Config file’ and paste into the new WCF Service Application Project’s ‘Web.config’ file. The connection is required by the ‘HealthTracker.DataAccess.DbFirst.dll’ assembly. The connection string should look similar to the below code.
<connectionStrings> <add name="HealthTrackerEntities" connectionString="metadata=res://*/HealthTracker.csdl|res://*/HealthTracker.ssdl|res://*/HealthTracker.msl;provider=System.Data.SqlClient;provider connection string="data source=[Your_Server]\[Your_SQL_Instance];initial catalog=HealthTracker;persist security info=True;user id=DemoLogin;password=[Your_Password];MultipleActiveResultSets=True;App=EntityFramework"" providerName="System.Data.EntityClient" /> </connectionStrings>
The WCF Service
Delete the default ‘Service.svc’ and ‘IService.cs’ created by the Project Template. You can also delete the default ‘App_Data’ folder. Add a new WCF Service, named ‘HealthTrackerWcfService.svc’. Adding a new service creates both the WCF Service file (.svc), as well as a WCF Service Contract file (.cs), an Interface, named ‘IHealthTrackerWcfService.cs’. The ‘HealthTrackerWcfService’ class implements the ‘IHealthTrackerWcfService’ Interface class (‘public class HealthTrackerWcfService : IHealthTrackerWcfService’).
The WCF Service file contains public methods, called service operations, which the service will expose through a web endpoint. The second file, an Interface class, is referred to as the Service Contract. The Service Contract contains the method signatures of all the operations the service’s web endpoint expose. The Service Contract contains attributes, part of the ‘System.ServiceModel’ and ‘System.ServiceModel.Web’ Namespaces, describing how the service and its operation will be exposed. To create the Service Contract, replace the default code in the file, ‘IHealthTrackerWcfService.cs’, with the following code.
using System.Collections.Generic; using System.ServiceModel; using System.ServiceModel.Web; using HealthTracker.DataAccess.DbFirst; namespace HealthTracker.WcfService { [ServiceContract] public interface IHealthTrackerWcfService { [OperationContract] [WebInvoke(UriTemplate = "GetPersonId?name={personName}", Method = "GET")] int GetPersonId(string personName); [OperationContract] [WebInvoke(UriTemplate = "GetPeople", Method = "GET")] List<Person> GetPeople(); [OperationContract] [WebInvoke(UriTemplate = "GetPersonSummaryStoredProc?id={personId}", Method = "GET")] List<GetPersonSummary_Result> GetPersonSummaryStoredProc(int personId); [OperationContract] [WebInvoke(UriTemplate = "InsertPerson", Method = "POST")] bool InsertPerson(Person person); [OperationContract] [WebInvoke(UriTemplate = "UpdatePerson", Method = "PUT")] bool UpdatePerson(Person person); [OperationContract] [WebInvoke(UriTemplate = "DeletePerson?id={personId}", Method = "DELETE")] bool DeletePerson(int personId); [OperationContract] [WebInvoke(UriTemplate = "UpdateOrInsertHydration?id={personId}", Method = "POST")] bool UpdateOrInsertHydration(int personId); [OperationContract] [WebInvoke(UriTemplate = "InsertActivity", Method = "POST")] bool InsertActivity(Activity activity); [OperationContract] [WebInvoke(UriTemplate = "DeleteActivity?id={activityId}", Method = "DELETE")] bool DeleteActivity(int activityId); [OperationContract] [WebInvoke(UriTemplate = "GetActivities?id={personId}", Method = "GET")] List<ActivityDetail> GetActivities(int personId); [OperationContract] [WebInvoke(UriTemplate = "InsertMeal", Method = "POST")] bool InsertMeal(Meal meal); [OperationContract] [WebInvoke(UriTemplate = "DeleteMeal?id={mealId}", Method = "DELETE")] bool DeleteMeal(int mealId); [OperationContract] [WebInvoke(UriTemplate = "GetMeals?id={personId}", Method = "GET")] List<MealDetail> GetMeals(int personId); [OperationContract] [WebInvoke(UriTemplate = "GetPersonSummaryView?id={personId}", Method = "GET")] List<PersonSummaryView> GetPersonSummaryView(int personId); } }
The service’s operations use a variety of HTTP Methods, including GET, POST, PUT, and DELETE. The operations take a mix of primitive data types, as well as complex objects as arguments. The operations also return the same variety of simple data types, as well as complex objects. Note the operation ‘InsertActivity’ for example. It takes a complex object, an ‘Activity’, as an argument, and returns a Boolean. All the CRUD operations dealing with inserting, updating, or deleting data return a Boolean, indicating success or failure of the operation’s execution. This makes unit testing and error handling on the client-side easier.
Next, we will create the WCF Service. Replace the existing contents of the ‘HealthTrackerWcfService.svc’ file with the following code.
using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.ServiceModel; using HealthTracker.DataAccess.DbFirst; namespace HealthTracker.WcfService { [ServiceBehavior(AddressFilterMode = AddressFilterMode.Any)] public class HealthTrackerWcfService : IHealthTrackerWcfService { private readonly DateTime _today = DateTime.Now.Date; #region Service Operations /// <summary> /// Example of Adding a new Person. /// </summary> /// <param name="person">New Person Object</param> /// <returns>True if successful</returns> public bool InsertPerson(Person person) { try { using (var dbContext = new HealthTrackerEntities()) { dbContext.People.Add(new DataAccess.DbFirst.Person { Name = person.Name }); dbContext.SaveChanges(); return true; } } catch (Exception exception) { Debug.WriteLine(exception); return false; } } /// <summary> /// Example of Updating a Person. /// </summary> /// <param name="person">New Person Object</param> /// <returns>True if successful</returns> public bool UpdatePerson(Person person) { try { using (var dbContext = new HealthTrackerEntities()) { var personToUpdate = dbContext.People.First(p => p.PersonId == person.PersonId); if (personToUpdate == null) return false; personToUpdate.Name = person.Name; dbContext.SaveChanges(); return true; } } catch (Exception exception) { Debug.WriteLine(exception); return false; } } /// <summary> /// Example of deleting a Person. /// </summary> /// <param name="personId">PersonId</param> /// <returns>True if successful</returns> public bool DeletePerson(int personId) { try { using (var dbContext = new HealthTrackerEntities()) { var personToDelete = dbContext.People.First(p => p.PersonId == personId); if (personToDelete == null) return false; dbContext.People.Remove(personToDelete); dbContext.SaveChanges(); return true; } } catch (Exception exception) { Debug.WriteLine(exception); return false; } } /// <summary> /// Example of finding a Person's Id. /// </summary> /// <param name="personName">Name of the Person to find</param> /// <returns>Person's unique Id (PersonId)</returns> public int GetPersonId(string personName) { try { using (var dbContext = new HealthTrackerEntities()) { var personId = dbContext.People .Where(person => person.Name == personName) .Select(person => person.PersonId) .First(); return personId; } } catch (Exception exception) { Debug.WriteLine(exception); return -1; } } /// <summary> /// Returns a list of all People. /// </summary> /// <returns>List of People</returns> public List<Person> GetPeople() { try { using (var dbContext = new HealthTrackerEntities()) { var people = (dbContext.People.Select(p => p)); var peopleList = people.Select(p => new Person { PersonId = p.PersonId, Name = p.Name }).ToList(); return peopleList; } } catch (Exception exception) { Debug.WriteLine(exception); return null; } } /// <summary> /// Example of adding a Meal. /// </summary> /// <param name="meal">New Meal Object</param> /// <returns>True if successful</returns> public bool InsertMeal(Meal meal) { try { using (var dbContext = new HealthTrackerEntities()) { dbContext.Meals.Add(new DataAccess.DbFirst.Meal { PersonId = meal.PersonId, Date = _today, MealTypeId = meal.MealTypeId, Description = meal.Description }); dbContext.SaveChanges(); return true; } } catch (Exception exception) { Debug.WriteLine(exception); return false; } } /// <summary> /// Example of deleting a Meal. /// </summary> /// <param name="mealId">MealId</param> /// <returns>True if successful</returns> public bool DeleteMeal(int mealId) { try { using (var dbContext = new HealthTrackerEntities()) { var mealToDelete = dbContext.Meals.First(m => m.MealTypeId == mealId); if (mealToDelete == null) return false; dbContext.Meals.Remove(mealToDelete); dbContext.SaveChanges(); return true; } } catch (Exception exception) { Debug.WriteLine(exception); return false; } } /// <summary> /// Return all Meals for a Person. /// </summary> /// <param name="personId">PersonId</param> /// <returns></returns> public List<MealDetail> GetMeals(int personId) { try { using (var dbContext = new HealthTrackerEntities()) { var meals = dbContext.Meals.Where(m => m.PersonId == personId) .Select(m => new MealDetail { MealId = m.MealId, Date = m.Date, Type = m.MealType.Description, Description = m.Description }).ToList(); return meals; } } catch (Exception exception) { Debug.WriteLine(exception); return null; } } /// <summary> /// Example of adding an Activity. /// </summary> /// <param name="activity">New Activity Object</param> /// <returns>True if successful</returns> public bool InsertActivity(Activity activity) { try { using (var dbContext = new HealthTrackerEntities()) { dbContext.Activities.Add(new DataAccess.DbFirst.Activity { PersonId = activity.PersonId, Date = _today, ActivityTypeId = activity.ActivityTypeId, Notes = activity.Notes }); dbContext.SaveChanges(); return true; } } catch (Exception exception) { Debug.WriteLine(exception); return false; } } /// <summary> /// Example of deleting a Activity. /// </summary> /// <param name="activityId">ActivityId</param> /// <returns>True if successful</returns> public bool DeleteActivity(int activityId) { try { using (var dbContext = new HealthTrackerEntities()) { var activityToDelete = dbContext.Activities.First(a => a.ActivityId == activityId); if (activityToDelete == null) return false; dbContext.Activities.Remove(activityToDelete); dbContext.SaveChanges(); return true; } } catch (Exception exception) { Debug.WriteLine(exception); return false; } } /// <summary> /// Return all Activities for a Person. /// </summary> /// <param name="personId">PersonId</param> /// <returns>List of Activities</returns> public List<ActivityDetail> GetActivities(int personId) { try { using (var dbContext = new HealthTrackerEntities()) { var activities = dbContext.Activities.Where(a => a.PersonId == personId) .Select(a => new ActivityDetail { ActivityId = a.ActivityId, Date = a.Date, Type = a.ActivityType.Description, Notes = a.Notes }).ToList(); return activities; } } catch (Exception exception) { Debug.WriteLine(exception); return null; } } /// <summary> /// Example of updating existing Hydration count. /// Else adding new Hydration if it doesn't exist. /// </summary> /// <param name="personId">PersonId</param> /// <returns>True if successful</returns> public bool UpdateOrInsertHydration(int personId) { try { using (var dbContext = new HealthTrackerEntities()) { var existingHydration = dbContext.Hydrations.First( hydration => hydration.PersonId == personId && hydration.Date == _today); if (existingHydration != null && existingHydration.HydrationId > 0) { existingHydration.Count++; dbContext.SaveChanges(); return true; } dbContext.Hydrations.Add(new Hydration { PersonId = personId, Date = _today, Count = 1 }); dbContext.SaveChanges(); return true; } } catch (Exception exception) { Debug.WriteLine(exception); return false; } } /// <summary> /// Return a count of all Meals, Hydrations, and Activities for a Person. /// Based on a Database View (virtual table). /// </summary> /// <param name="personId">PersonId</param> /// <returns>Summary for a Person</returns> public List<PersonSummaryView> GetPersonSummaryView(int personId) { try { using (var dbContext = new HealthTrackerEntities()) { var personView = (dbContext.PersonSummaryViews .Where(p => p.PersonId == personId)) .ToList(); return personView; } } catch (Exception exception) { Debug.WriteLine(exception); return null; } } /// <summary> /// Return a count of all Meals, Hydrations, and Activities for a Person. /// Based on a Stored Procedure. /// </summary> /// <param name="personId">PersonId</param> /// <returns>Summary for a Person</returns> public List<GetPersonSummary_Result> GetPersonSummaryStoredProc(int personId) { try { using (var dbContext = new HealthTrackerEntities()) { var personView = (dbContext.GetPersonSummary(personId) .Where(p => p.PersonId == personId)) .ToList(); return personView; } } catch (Exception exception) { Debug.WriteLine(exception); return null; } } #endregion } #region POCO Classes public class Person { public int PersonId { get; set; } public string Name { get; set; } } public class Meal { public int PersonId { get; set; } public int MealTypeId { get; set; } public string Description { get; set; } } public class MealDetail { public int MealId { get; set; } public DateTime Date { get; set; } public string Type { get; set; } public string Description { get; set; } } public class Activity { public int PersonId { get; set; } public int ActivityTypeId { get; set; } public string Notes { get; set; } } public class ActivityDetail { public int ActivityId { get; set; } public DateTime Date { get; set; } public string Type { get; set; } public string Notes { get; set; } } #endregion }
Each method instantiates an instance of ‘HeatlthTrackerEntities’, Referenced by the project and accessible to the class via the ‘using HealthTracker.DataAccess.DbFirst;’ statement, ‘HeatlthTrackerEntities’ implements ‘System.Data.Entity.DBContext’. Each method uses LINQ to Entities to interact with the Entity Data Model, through the ‘HeatlthTrackerEntities’ object.
In addition to the methods (service operations) contained in the HealthTrackerWcfService class, there are several POCO classes. Some of these POCO classes, such as ‘NewMeal’ and ‘NewActivity’, are instantiated to hold data passed in the operation’s arguments by the client Request message. Other POCO classes, such as ‘MealDetail’ and ‘ActivityDetail’, are instantiated to hold data passed back to the client by the operations, in the Response message. These POCO instances are serialized to and deserialized from JSON or XML.
The WCF Service’s Configuration
The most complex and potentially the most confusing part of creating a WCF Service, at least for me, is always the service’s configuration. Due in part to the flexibility of WCF Services to accommodate many types of client, server, network, and security situations, the configuration of the services takes an in-depth understanding of bindings, behaviors, endpoints, security, and associated settings. The best books I’ve found on configuring WCF Services is Pro WCF 4: Practical Microsoft SOA Implementation, by Nishith Pathak. The book goes into great detail on all aspects of configuring WCF Services to meet your particular project’s needs.
Since we are only using the WCF Web HTTP Programming Model to build and expose our service, the ‘webHttpBinding’ binding is the only binding we need to configure. I have made an effort to strip out all the unnecessary boilerplate settings from our service’s configuration.
<?xml version="1.0" encoding="utf-8"?> <configuration> <configSections> <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" /> </configSections> <appSettings> <add key="aspnet:UseTaskFriendlySynchronizationContext" value="true" /> </appSettings> <system.web> <compilation debug="true" targetFramework="4.5" /> <httpRuntime targetFramework="4.5" /> </system.web> <system.serviceModel> <serviceHostingEnvironment aspNetCompatibilityEnabled="true" /> <behaviors> <endpointBehaviors> <behavior name="webHttpBehavior"> <webHttp helpEnabled="true" defaultOutgoingResponseFormat="Json" defaultBodyStyle="Bare" automaticFormatSelectionEnabled="true"/> </behavior> </endpointBehaviors> </behaviors> <services> <service name="HealthTracker.WcfService.HealthTrackerWcfService"> <endpoint address="web" binding="webHttpBinding" behaviorConfiguration="webHttpBehavior" contract="HealthTracker.WcfService.IHealthTrackerWcfService" /> </service> </services> </system.serviceModel> <system.webServer> <modules runAllManagedModulesForAllRequests="true" /> <directoryBrowse enabled="false" /> </system.webServer> <entityFramework> <defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework" /> </entityFramework> <connectionStrings> <add name="HealthTrackerEntities" connectionString="metadata=res://*/HealthTracker.csdl|res://*/HealthTracker.ssdl|res://*/HealthTracker.msl;provider=System.Data.SqlClient;provider connection string="data source=gstafford-windows-laptop\DEVELOPMENT;initial catalog=HealthTracker;persist security info=True;user id=DemoLogin;password=DemoLogin123;MultipleActiveResultSets=True;App=EntityFramework"" providerName="System.Data.EntityClient" /> </connectionStrings> </configuration>
Some items to note in the configuration:
- Line 4: Entity Framework – The Entity Framework 5 reference you added earlier via NuGet.
- Line 18: Help – This enables an automatically generated Help page, displaying all the service’s operations for the endpoint, with details on how to call each operation.
- Lines 18-19: Request and Response Message Formats – Default settings for message format and body style of Request and Response messages. In this case, JSON and Bare. Setting defaults saves lots of time, not having to add attributes to each individual operation.
- Line 25-26: Endpoint – The service’s single endpoint, with a single binding and behavior. For this Post, we are only using the ‘webHttpBinding’ binding type.
- Line 38: Connection String – The SQL Server Connection String you copied from the previous Post’s Project. Required by the DAL Project Reference you added, earlier.
Deploying the Service to IIS
Now that the service is complete, we will deploy and host it in IIS. There are many options when it comes to creating and configuring a new website – setting up domain names, choosing ports, configuring firewalls, defining bindings, setting permissions, and so forth. This Post will not go into that level of detail. I will demonstrate how I chose to set up my website and publish my WCF Service.
We need a physical location to deploy the WCF Service’s contents. I recommend a location outside of the IIS root directory, such as ‘C:\HealthTrackerWfcService’. Create this folder on the server where you will be running IIS, either locally or remotely. This folder is where we will publish the service’s contents to from Visual Studio, next.
Create a new website in IIS to host the service. Name the site ‘HealthTracker’. You can configure and use a domain name or a specific port, from which to call the service. I chose to configure a domain name on my IIS Server, ”WcfService.HealthTracker.com’. If you are unsure how to setup a new domain name on your network, then a local, open port is probably easier for you. Pick any random port, like 15678.
Publish the WCF Service to the deployment location, explained above, using Visual Studio 2012’s Web Project Publishing Tool. Exactly how and where you set-up your website, and any security considerations, will affect the configuration of the Publishing Tool’s Profile. My options will not necessarily work for your specific environment.
- Publish Web – Profile
- Publish Web – Connection
- Publish Web – Settings
- Publish Web – Preview
Testing the WCF Service
Congratulations, your service is deployed. Now, let’s see if it works. Before we test the individual operations, we will ensure the service is being hosted correctly. Open the service’s Help page. This page automatically shows details on all operations of a particular endpoint. The address should follow the convention of http://%5Byour_domain%5D:%5Byour_port%5D/%5Byour_service%5D/%5Byour_endpoint_address%5D/help. In my case ‘http://wcfservice.healthtracker.com/HealthTrackerWcfService.svc/web/help’. If this page displays, then the service is deployed correctly and it’s web endpoint is responding as expected.
While on the Help page, click on any of the HTTP Methods to see a further explanation of that particular operation. This page is especially useful for copying the URL of the operation for use in Fiddler. It is even more useful for grabbing the sample JSON or XML Request messages. Just substitute your test values for the default values, in Fiddler. It saves a lot of typing and many potential errors.
Fiddler
The easiest way to test each of the service’s operations is Fiddler. Download and install Fiddler, if you don’t already have it. Using Fiddler, construct a Request message and call the operations by executing the operation’s associated HTTP Method. Below is an example of calling the ‘InsertActivity’ operation. This CRUD operation accepts a new Activity object as an argument, inserts into the database via the Entity Data Model, and returns a Boolean value indicating success.
To call the ‘InsertActivity’ operation, 1) select the ‘POST’ HTTP method, 2) input the URL for the ‘InsertActivity’ operation, 3) select a version of HTTP (1.2), 4) input the Content-Type (JSON or XML) in the Request Headers section, 5) input the body of the Request, a new ‘Activity’ as JSON, in the Request Body section, and 6) select ‘Execute’. The 7) Response should appear in the Web Sessions window.
Executing the 1) Request (constructed above), should result in a 2) Response in the Web Sessions window. Double clicking on the Web Session should result in the display of the 3) Response message in the lower righthand window. The operation returns a Boolean indicating if the operation succeeded or failed. In this case, we received a value of ‘true’.
To view the Activity we just inserted, we need to call the ‘GetActivities’ operation, passing it the same ‘PersonId’ argument. In Fiddler, 1) select the ‘GET’ HTTP method, 2) input the URL for the ‘GetActivities’ operation including a value for the ‘PersonId’ argument, 3) select the desired version of HTTP (1.2), 4) input a Content-Type (JSON or XML) in the Request Headers section, and 5) select ‘Execute’. Same as before, the 6) Response should appear in the Web Sessions window. This time there is no Request body content.
As before, executing the 1) Request should result in a 2) Response in the Web Sessions window. Doubling clicking on the Web Session should result in the display of the 3) Response in the lower left window. This method returns a JSON payload with each Activity, associated with the PersonId argument.
You can use this same process to test all the other operations at the WCF Service’s endpoint. You can also save the Request message or complete Web Sessions in Fiddler should you need to re-test.
Conclusion
We now have a WCF Service deployed and running in IIS, and tested. The service’s operations can be called from any application capable of making an HTTP call. Thank you for taking the time to read this Post. I hope you found it beneficial.
#1 by anon on February 6, 2014 - 3:30 pm
Good info, thanks.