Posts Tagged WCF
Cloud-based Continuous Integration and Deployment for .NET Development
Posted by Gary A. Stafford in .NET Development, Build Automation, Client-Side Development, DevOps, Enterprise Software Development, PowerShell Scripting, Software Development on May 25, 2014
Introduction
Whether you are part of a large enterprise development environment, or a member of a small start-up, you are likely working with remote team members. You may be remote, yourself. Developers, testers, web designers, and other team members, commonly work remotely on software projects. Distributed teams, comprised of full-time staff, contractors, and third-party vendors, often work in different buildings, different cities, and even different countries.
If software is no longer strictly developed in-house, why should our software development and integration tools be located in-house? We live in a quickly evolving world of Saas, PaaS, and IaaS. Popular SaaS development tools include Visual Studio Online, GitHub, BitBucket, Travis-CI, AppVeyor, CloudBees, JIRA, AWS, Microsoft Azure, Nodejitsu, and Heroku, to name just a few. With all these ‘cord-cutting’ tools, there is no longer a need for distributed development teams to be tethered to on-premise tooling, via VPN tunnels and Remote Desktop Connections.
There are many combinations of hosted software development and integration tools available, depending on your technology stack, team size, and budget. In this post, we will explore one such toolchain for .NET development. Using Git, GitHub, AppVeyor, and Microsoft Azure, we will continuously build, test, and deploy a multi-tier .NET solution, without ever leaving Visual Studio. This particular toolchain has strong integration between tools, and will scale to fit most development teams.
Git and GitHub
Git and GitHub are widely used in development today. Visual Studio 2013 has fully-integrated Git support and Visual Studio 2012 has supported Git via a plug-in since early last year. Git is fully compatible with Windows. Additionally, there are several third party tools available to manage Git and GitHub repositories on Windows. These include Git Bash (my favorite), Git GUI, and GitHub for Windows.
GitHub acts as a replacement for your in-house Git server. Developers commit code to their individual local Git project repositories. They then push, pull, and merge code to and from a hosted GitHub repository. For security, GitHub requires a registered username and password to push code. Data transfer between the local Git repository and GitHub is done using HTTPS with SSL certificates or SSH with public-key encryption. GitHub also offers two-factor authentication (2FA). Additionally, for those companies concerned about privacy and added security, GitHub offers private repositories. These plans range in price from $25 to $200 per month, currently.
AppVeyor
AppVeyor’s tagline is ‘Continuous Integration for busy developers’. AppVeyor automates building, testing and deployment of .NET applications. AppVeyor is similar to Jenkins and Hudson in terms of basic functionality, except AppVeyor is only provided as a SaaS. There are several hosted solutions in the continuous integration and delivery space similar to AppVeyor. They include CloudBees (hosted-Jenkins) and Travis-CI. While CloudBees and Travis CI works with several technology stacks, AppVeyor focuses specifically on .NET. Its closest competitor may be Microsoft’s new Visual Studio Online.
Identical to GitHub, AppVeyor also offers private repositories (spaces for building and testing code). Prices for private repositories currently range from $39 to $319 per month. Private repositories offer both added security and support. AppVeyor integrates nicely with several cloud-based code repositories, including GitHub, BitBucket, Visual Studio Online, and Fog Creek’s Kiln.
Azure
This post demonstrates continuous deployment from AppVeyor to a Microsoft Server 2012-based Azure VM. The VM has IIS 8.5, Web Deploy 3.5, IIS Web Management Service (WMSVC), and other components and configuration necessary to host the post’s sample Solution. AppVeyor would work just as well with Azure’s other hosting options, as well as other cloud-based hosting providers, such as AWS or Rackspace, which also supports the .NET stack.
Sample Solution
The Visual Studio Solution used for this post was originally developed as part of an earlier post, Consuming Cross-Domain WCF REST Services with jQuery using JSONP. The original Solution, from 2011, demonstrated jQuery’s AJAX capabilities to communicate with a RESTful WCF service, cross-domains, using JSONP. I have since updated and modernized the Solution for this post. The revised Solution is on a new branch (‘rev2014’) on GitHub. Major changes to the Solution include an upgrade from VS2010 to VS2013, the use of Git DVCS, NuGet package management, Web Publish Profiles, Web Essentials for bundling JS and CSS, Twitter Bootstrap, unit testing, and a lot of code refactoring.
The updated VS Solution contains the following four Projects:
- Restaurant – C# Class Library
- RestaurantUnitTests – Unit Test Project
- RestaurantWcfService – C# WCF Service Application
- RestaurantDemoSite – Web Site (JS/HTML5)
The Visual Studio Solution Explorer tab, here, shows all projects contained in the Solution, and the primary files and directories they contain.
As explained in the earlier post, the ‘RestaurantDemoSite’ web site makes calls to the ‘RestaurantWcfService’ WCF service. The WCF service exposes two operations, one that returns the menu (‘GetCurrentMenu’), and the other that accepts an order (‘SendOrder’). For simplicity, orders are stored in the files system as JSON files. No database is required for the Solution. All business logic is contained in the ‘Restaurant’ class library, which is referenced by the WCF service. This architecture is illustrated in this Visual Studio Assembly Dependencies Diagram.
Installing and Configuring the Solution
The README.md file in the GitHub repository contains instructions for installing and configuring this Solution. In addition, a set of PowerShell scripts, part of the Solution’s repository, makes the installation and configuration process, quick and easy. The scripts handle creating the necessary file directories and environment variables, setting file access permissions, and configuring IIS websites. Make sure to change the values of the environment variables before running the script. For reference, below are the contents of several of the supplied scripts. You should use the supplied scripts.
# Create environment variables [Environment]::SetEnvironmentVariable("AZURE_VM_HOSTNAME", ` "{YOUR HOSTNAME HERE}", "User") [Environment]::SetEnvironmentVariable("AZURE_VM_USERNAME", ` "{YOUR USERNME HERE}", "User") [Environment]::SetEnvironmentVariable("AZURE_VM_PASSWORD", ` "{YOUR PASSWORD HERE}", "User") # Create new restaurant orders JSON file directory $newDirectory = "c:\RestaurantOrders" if (-not (Test-Path $newDirectory)){ New-Item -Type directory -Path $newDirectory } $acl = Get-Acl $newDirectory $ar = New-Object System.Security.AccessControl.FileSystemAccessRule(` "INTERACTIVE","Modify","ContainerInherit, ObjectInherit", "None", "Allow") $acl.SetAccessRule($ar) Set-Acl $newDirectory $acl # Create new website directory $newDirectory = "c:\RestaurantDemoSite" if (-not (Test-Path $newDirectory)){ New-Item -Type directory -Path $newDirectory } $acl = Get-Acl $newDirectory $ar = New-Object System.Security.AccessControl.FileSystemAccessRule(` "IUSR","ReadAndExecute","ContainerInherit, ObjectInherit", "None", "Allow") $acl.SetAccessRule($ar) Set-Acl $newDirectory $acl # Create new WCF service directory $newDirectory = "c:\MenuWcfRestService" if (-not (Test-Path $newDirectory)){ New-Item -Type directory -Path $newDirectory } $acl = Get-Acl $newDirectory $ar = New-Object System.Security.AccessControl.FileSystemAccessRule(` "IUSR","ReadAndExecute","ContainerInherit, ObjectInherit", "None", "Allow") $acl.SetAccessRule($ar) Set-Acl $newDirectory $acl $ar = New-Object System.Security.AccessControl.FileSystemAccessRule(` "IIS_IUSRS","ReadAndExecute","ContainerInherit, ObjectInherit", "None", "Allow") $acl.SetAccessRule($ar) Set-Acl $newDirectory $acl # Create main website in IIS $newSite = "MenuWcfRestService" if (-not (Test-Path IIS:\Sites\$newSite)){ New-Website -Name $newSite -Port 9250 -PhysicalPath ` c:\$newSite -ApplicationPool "DefaultAppPool" } # Create WCF service website in IIS $newSite = "RestaurantDemoSite" if (-not (Test-Path IIS:\Sites\$newSite)){ New-Website -Name $newSite -Port 9255 -PhysicalPath ` c:\$newSite -ApplicationPool "DefaultAppPool" }
Cloud-Based Continuous Integration and Delivery
Webhooks
The first point of integration in our hosted toolchain is between GitHub and AppVeyor. In order for AppVeyor to work with GitHub, we use a Webhook. Webhooks are widely used to communicate events between systems, over HTTP. According to GitHub, ‘every GitHub repository has the option to communicate with a web server whenever the repository is pushed to. These webhooks can be used to update an external issue tracker, trigger CI builds, update a backup mirror, or even deploy to your production server.‘ Basically, we give GitHub permission to tell AppVeyor every time code is pushed to the GitHub. GitHub sends a HTTP POST to a specific URL, provided by AppVeyor. AppVeyor responds to the POST by cloning the GitHub repository, and building, testing, and deploying the Projects. Below is an example of a webhook for AppVeyor, in GitHub.
Unit Tests
To help illustrate the use of AppVeyor for automated unit testing, the updated Solution contains a Unit Test Project. Every time code is committed to GitHub, AppVeyor will clone and build the Solution, followed by running the set of unit tests shown below. The project’s unit tests test the Restaurant class library (‘restaurant.dll’). The unit tests provide 100% code coverage, as shown in the Visual Studio Code Coverage Results tab, below:
AppVeyor runs the Solution’s automated unit tests using VSTest.Console.exe. VSTest.Console calls the unit test Project’s assembly (‘restaurantunittests.dll’). As shown below, the VSTest command (in light blue) runs all tests, and then displays individual test results, a results summary, and the total test execution time.
VSTest.Console has several command line options similar to MSBuild. They can be adjusted to output various levels of feedback on test results. For larger projects, you can selectively choose which pre-defined test sets to run. Test sets needs are set-up in Solution, in advance.
Configuring Azure VM
Before we publish the Solution from AppVeyor to the Azure, we need to configure the VM. Again, we can use PowerShell to script most of the configuration. Most scripts are the same ones we used to configure our local environment. The README.md file in the GitHub repository contains instructions. The scripts handle creating the necessary file directories, setting file access permissions, configuring the IIS websites, creating the Web Deploy User account, and assigning it in IIS. For reference, below are the contents of several of the supplied scripts. You should use the supplied scripts.
# Create new restaurant orders JSON file directory $newDirectory = "c:\RestaurantOrders" if (-not (Test-Path $newDirectory)){ New-Item -Type directory -Path $newDirectory } $acl = Get-Acl $newDirectory $ar = New-Object System.Security.AccessControl.FileSystemAccessRule(` "INTERACTIVE","Modify","ContainerInherit, ObjectInherit", "None", "Allow") $acl.SetAccessRule($ar) Set-Acl $newDirectory $acl # Create new website directory $newDirectory = "c:\RestaurantDemoSite" if (-not (Test-Path $newDirectory)){ New-Item -Type directory -Path $newDirectory } $acl = Get-Acl $newDirectory $ar = New-Object System.Security.AccessControl.FileSystemAccessRule(` "IUSR","ReadAndExecute","ContainerInherit, ObjectInherit", "None", "Allow") $acl.SetAccessRule($ar) Set-Acl $newDirectory $acl # Create new WCF service directory $newDirectory = "c:\MenuWcfRestService" if (-not (Test-Path $newDirectory)){ New-Item -Type directory -Path $newDirectory } $acl = Get-Acl $newDirectory $ar = New-Object System.Security.AccessControl.FileSystemAccessRule(` "IUSR","ReadAndExecute","ContainerInherit, ObjectInherit", "None", "Allow") $acl.SetAccessRule($ar) Set-Acl $newDirectory $acl $ar = New-Object System.Security.AccessControl.FileSystemAccessRule(` "IIS_IUSRS","ReadAndExecute","ContainerInherit, ObjectInherit", "None", "Allow") $acl.SetAccessRule($ar) Set-Acl $newDirectory $acl # Create main website in IIS $newSite = "MenuWcfRestService" if (-not (Test-Path IIS:\Sites\$newSite)){ New-Website -Name $newSite -Port 9250 -PhysicalPath ` c:\$newSite -ApplicationPool "DefaultAppPool" } # Create WCF service website in IIS $newSite = "RestaurantDemoSite" if (-not (Test-Path IIS:\Sites\$newSite)){ New-Website -Name $newSite -Port 9255 -PhysicalPath ` c:\$newSite -ApplicationPool "DefaultAppPool" } # Create new local non-admin User and Group for Web Deploy # Main variables (Change these!) [string]$userName = "USER_NAME_HERE" # mjones [string]$fullName = "FULL USER NAME HERE" # Mike Jones [string]$password = "USER_PASSWORD_HERE" # pa$$w0RD! [string]$groupName = "GROUP_NAME_HERE" # Development # Create new local user account [ADSI]$server = "WinNT://$Env:COMPUTERNAME" $newUser = $server.Create("User", $userName) $newUser.SetPassword($password) $newUser.Put("FullName", "$fullName") $newUser.Put("Description", "$fullName User Account") # Assign flags to user [int]$ADS_UF_PASSWD_CANT_CHANGE = 64 [int]$ADS_UF_DONT_EXPIRE_PASSWD = 65536 [int]$COMBINED_FLAG_VALUE = 65600 $flags = $newUser.UserFlags.value -bor $COMBINED_FLAG_VALUE $newUser.put("userFlags", $flags) $newUser.SetInfo() # Create new local group $newGroup=$server.Create("Group", $groupName) $newGroup.Put("Description","$groupName Group") $newGroup.SetInfo() # Assign user to group [string]$serverPath = $server.Path $group = [ADSI]"$serverPath/$groupName, group" $group.Add("$serverPath/$userName, user") # Assign local non-admin User in IIS for Web Deploy [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Web.Management") [Microsoft.Web.Management.Server.ManagementAuthorization]::Grant(` $userName, "$Env:COMPUTERNAME\MenuWcfRestService", $FALSE) [Microsoft.Web.Management.Server.ManagementAuthorization]::Grant(` $userName, "$Env:COMPUTERNAME\RestaurantDemoSite", $FALSE)
Publish Profiles
The second point of integration in our toolchain is between AppVeyor and the Azure VM. We will be using Microsoft’s Web Deploy to deploy our Solution from AppVeyor to Azure. Web Deploy integrates with the IIS Web Management Service (WMSVC) for remote deployment by non-administrators. I have already configured Web Deploy and created a non-administrative user on the Azure VM. This user’s credentials will be used for deployments. These are the credentials in the username and password environment variables we created.
To continuously deploy to Azure, we will use Web Publish Profiles with Microsoft’s Web Deploy technology. Both the website and WCF service projects contain individual profiles for local development (‘LocalMachine’), as well as deployment to Azure (‘AzureVM’). The ‘AzureVM’ profiles contain all the configuration information AppVeyor needs to connect to the Azure VM and deploy the website and WCF service.
The easiest way to create a profile is by right-clicking on the project and selecting the ‘Publish…’ and ‘Publish Web Site’ menu items. Using the Publish Web wizard, you can quickly build and validate a profile.
Each profile in the above Profile drop-down, represents a ‘.pubxml’ file. The Publish Web wizard is merely a visual interface to many of the basic configurable options found in the Publish Profile’s ‘.pubxml’ file. The .pubxml profile files can be found in the Project Explorer. For the website, profiles are in the ‘App_Data’ directory (i.e. ‘Restaurant\RestaurantDemoSite\App_Data\PublishProfiles\AzureVM.pubxml’). For the WCF service, profiles are in the ‘Properties’ directory (i.e. ‘Restaurant\RestaurantWcfService\Properties\PublishProfiles\AzureVM.pubxml’).
As an example, below are the contents of the ‘LocalMachine’ profile for the WCF service (‘LocalMachine.pubxml’). This is about as simple as a profile gets. Note since we are deploying locally, the profile is configured to open the main page of the website in a browser, after deployment; a helpful time-saver during development.
<?xml version="1.0" encoding="utf-8"?> <!-- This file is used by the publish/package process of your Web project. You can customize the behavior of this process by editing this MSBuild file. In order to learn more about this please visit http://go.microsoft.com/fwlink/?LinkID=208121. --> <Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <PropertyGroup> <WebPublishMethod>FileSystem</WebPublishMethod> <LastUsedBuildConfiguration>Debug</LastUsedBuildConfiguration> <LastUsedPlatform>Any CPU</LastUsedPlatform> <SiteUrlToLaunchAfterPublish>http://localhost:9250/RestaurantService.svc/help</SiteUrlToLaunchAfterPublish> <LaunchSiteAfterPublish>True</LaunchSiteAfterPublish> <ExcludeApp_Data>True</ExcludeApp_Data> <publishUrl>C:\MenuWcfRestService</publishUrl> <DeleteExistingFiles>True</DeleteExistingFiles> </PropertyGroup> </Project>
A key change we will make is to use environment variables in place of sensitive configuration values in the ‘AzureVM’ Publish Profiles. The Web Publish wizard does not allow this change. To do this, we must edit the ‘AzureVM.pubxml’ file for both the website and the WCF service. We will replace the hostname of the server where we will deploy the projects with a variable (i.e. AZURE_VM_HOSTNAME = ‘MyAzurePublicServer.net’). We will also replace the username and password used to access the deployment destination. This way, someone accessing the Solution’s source code, won’t be able to obtain any sensitive information, which would give them the ability to hack your site. Note the use of the ‘AZURE_VM_HOSTNAME’ and ‘AZURE_VM_USERNAME’ environment variables, show below.
<?xml version="1.0" encoding="utf-8"?> <!-- This file is used by the publish/package process of your Web project. You can customize the behavior of this process by editing this MSBuild file. In order to learn more about this please visit http://go.microsoft.com/fwlink/?LinkID=208121. --> <Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <PropertyGroup> <WebPublishMethod>MSDeploy</WebPublishMethod> <LastUsedBuildConfiguration>AppVeyor</LastUsedBuildConfiguration> <LastUsedPlatform>Any CPU</LastUsedPlatform> <SiteUrlToLaunchAfterPublish /> <LaunchSiteAfterPublish>False</LaunchSiteAfterPublish> <ExcludeApp_Data>True</ExcludeApp_Data> <MSDeployServiceURL>https://$(AZURE_VM_HOSTNAME):8172/msdeploy.axd</MSDeployServiceURL> <DeployIisAppPath>MenuWcfRestService</DeployIisAppPath> <RemoteSitePhysicalPath /> <SkipExtraFilesOnServer>False</SkipExtraFilesOnServer> <MSDeployPublishMethod>WMSVC</MSDeployPublishMethod> <EnableMSDeployBackup>True</EnableMSDeployBackup> <UserName>$(AZURE_VM_USERNAME)</UserName> <_SavePWD>False</_SavePWD> <_DestinationType>AzureVirtualMachine</_DestinationType> </PropertyGroup> </Project>
The downside of adding environment variables to the ‘AzureVM’ profiles, the Publish Profile wizard feature within Visual Studio will no longer allow us to deploy, using the ‘AzureVM’ profiles. As demonstrated below, after substituting variables for actual values, the ‘Server’ and ‘User name’ values will no longer display properly. We can confirm this by trying to validate the connection, which fails. This does not indicate your environment variable values are incorrect, only that Visual Studio can longer correctly parse the ‘AzureVM.pubxml’ file and display it properly in the IDE. No big deal…
We can use the command line or PowerShell to deploy with the ‘AzureVM’ profiles. AppVeyor accepts both command line input, as well as PowerShell for most tasks. All examples in this post and in the GitHub repository use PowerShell.
To build and deploy (publish) to Azure from the command line or PowerShell, we will use MSBuild. Below are the MSBuild commands used by AppVeyor to build our Solution, and then deploy our Solution to Azure. The first two MSBuild commands build the WCF service and the website. The second two deploy them to Azure. There are several ways you could construct these commands to successfully build and deploy this Solution. I found these commands to be the most succinct. I have split the build and the deploy functions so that the AppVeyor can run the automated unit tests, in between. If the tests don’t pass, we don’t want to deploy the code.
# Build WCF service # (AppVeyor config ignores website Project in Solution) msbuild Restaurant\Restaurant.sln ` /p:Configuration=AppVeyor /verbosity:minimal /nologo # Build website msbuild Restaurant\RestaurantDemoSite\website.publishproj ` /p:Configuration=Release /verbosity:minimal /nologo Write-Host "*** Solution builds complete."
# Deploy WCF service # (AppVeyor config ignores website Project in Solution) msbuild Restaurant\Restaurant.sln ` /p:DeployOnBuild=true /p:PublishProfile=AzureVM /p:Configuration=AppVeyor ` /p:AllowUntrustedCertificate=true /p:Password=$env:AZURE_VM_PASSWORD ` /verbosity:minimal /nologo # Deploy website msbuild Restaurant\RestaurantDemoSite\website.publishproj ` /p:DeployOnBuild=true /p:PublishProfile=AzureVM /p:Configuration=Release ` /p:AllowUntrustedCertificate=true /p:Password=$env:AZURE_VM_PASSWORD ` /verbosity:minimal /nologo Write-Host "*** Solution deployments complete."
Below is the output from AppVeyor showing the WCF Service and website’s deployment to Azure. Deployment is the last step in the continuous delivery process. At this point, the Solution was already built and the automated unit tests completed, successfully.
Below is the final view of the sample Solution’s WCF service and web site deployed to IIS 8.5 on the Azure VM.
Links
- Introduction to Web Deploy (see ‘How does it work?’ diagram on non-admin deployments)
- ASP.NET Web Deployment using Visual Studio: Command Line Deployment
- IIS: Enable IIS remote management
- Sayed Ibrahim Hashimi’s Blog
- Continuous Integration and Continuous Delivery
- How to: Edit Deployment Settings in Publish Profile (.pubxml) Files
Using the WCF Web HTTP Programming Model with Entity Framework 5
Posted by Gary A. Stafford in .NET Development, Client-Side Development, Software Development on December 12, 2012
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.
Consuming Cross-Domain WCF REST Services with jQuery using JSONP
Posted by Gary A. Stafford in .NET Development, Software Development, SQL Server Development on September 25, 2011
Introduction
In a previous article, Interactive Form Functionality on the Client-Side Using jQuery, I demonstrated the use of HTML, JavaScript, jQuery, and jQuery’s AJAX API to create a simple restaurant menu/order form. Although the previous article effectively demonstrated the use of these client-side technologies, the source of the restaurant’s menu items, a static XML file, was not intended to represent a true ‘production-class’ data source. Nowadays, to access data and business logic across the Enterprise or across the Internet, developers are more apt to build service-oriented applications that expose RESTful web services, and client applications that consume those services. RESTful services are services which conform to the REST (Representational State Transfer) architectural pattern. More information on REST can be obtained by reading Chapter 5 and 6 of REST’s author Roy Fielding’s Doctoral Dissertation. Most modern web technologies communicate with RESTful web services, including Microsoft’s Silverlight, Web Forms, and MVC, JavaFX, Adobe Flash, PHP, Python, and Ruby on Rails.
This article will expand on the restaurant menu/order form example from the previous article, replacing the static XML file with a WCF Service. The article will demonstrate the following:
- Use of jQuery’s AJAX API to bi-bidirectionally communicate with WCF Services
- Cross-domain communication with WCF Services using JSONP
- Serialization of complex, nested .NET objects into JSONP-format HTTP Response Messages
- Deserialization of JSONP-format HTTP Request Messages into complex, nested .NET objects
- Optimization of JavaScript and the use of caching to maximize the speed of content delivery to the Client
Source code is now available on GitHub. As of May 2014, there is a revised version of the project on the ‘rev2014′ branch, on GitHub. The below post describes the original code on the ‘Master’ branch. All details are posted on GitHub.
Background
WCF
For .NET developers, Windows Communication Foundation (WCF), Microsoft’s platform for Service Oriented Architecture (SOA), is the current preferred choice for building service-oriented applications. According to Microsoft, WCF is part of the .NET Framework that provides a unified programming model for rapidly building service-oriented applications that communicate across the web and the enterprise.
Prior to WCF, Microsoft offered ASP.NET XML Web Service, or ASP.NET Web Services for short. ASP.NET Web Services send and receive messages using Simple Object Access Protocol (SOAP) via HTTP. Data is serialized from instances of .NET objects into XML-format SOAP messages (or, ‘XML in a SOAP envelop’ as they are also known), and vice-versus. Metadata about the ASP.NET Web Services is contained in the Web Services Description Language (WSDL). Although still prevalent, ASP.NET Web Services is now considered a legacy technology with the advent of WCF, according to Microsoft. SOAP, a protocol for accessing a Web Service, does not conform to REST architecture guidelines.
Hosted on Microsoft’s IIS (Internet Information Services) Web Server, WCF is a complex, yet robust and flexible service-oriented framework. By properly configuring WCF Services, developers can precisely expose business logic and data sources to clients in a variety of ways. WCF Services can send and receive messages as XML in a SOAP envelop, as well as RESTful formats, including POX (plain old XML), ATOM (an XML language used for web feeds), and JSON (JavaScript Object Notation).
JSON/JSONP
The example in this article uses JSON, more specifically JSONP (JSON with Padding), a specialized type of JSON, to exchange information with WCF Services. JSON is an open and text-based data exchange format that provides a standardized data exchange format better suited for AJAX-style web applications. Compared to XML, JSON-formatted messages are smaller in size. For example, the restaurant menu used in this article, formatted as XML, is 927 bytes. The same message, formatted in JSONP is only 311 bytes, about one-third the size. The savings when transmitting JSON-format messages over slow connections, to mobile devices, or to potentially millions of simultaneous web-browsers, is significant.
Since the WCF Service will be hosted in a different domain (a different port in the example) than the web site with the restaurant menu and order form, we must use JSONP. JSONP, based on JSON, that allows pages to request data from a server in a different domain, normally disallowed, due to ‘same origin policy’. The same origin policy is an important security concept for browser-side programming languages, such as JavaScript. According to Wikipedia, same origin policy permits scripts running on pages originating from the same site to access each others methods and properties with no specific restrictions, but prevents access to most methods and properties across pages on different sites. JSONP takes advantage of the open policy for HTML <script>
elements.
Below is an example of the article’s restaurant menu formatted in JSONP, and returned by the WCF Service as part of the HTTP Response to the client’s HTTP Request’s GET
method.
RestaurantMenu([ {"Description":"Cheeseburger","Id":1,"Price":3.99}, {"Description":"Chicken Sandwich","Id":4,"Price":4.99}, {"Description":"Coffee","Id":7,"Price":0.99},{"Description":"French Fries", "Id":5,"Price":1.29},{"Description":"Hamburger","Id":2,"Price":2.99}, {"Description":"Hot Dog","Id":3,"Price":2.49}, {"Description":"Ice Cream Cone","Id":9,"Price":1.99}, {"Description":"Soft Drink","Id":6,"Price":1.19},{"Description":"Water", "Id":8,"Price":0}]);
AJAX (well, not really…)
AJAX (Asynchronous JavaScript and XML) asynchronously exchanges data between the browser and web server, avoiding page reloads, using object. Despite the name, XMLHttpRequest
, AJAX can work with JSON in addition to XML message formatting. Other formats include JSONP, JavaScript, HTML, and text. Using jQuery’s AJAX API, we will make HTTP Requests to the server using the GET
method. Other HTTP methods include POST
, PUT
, and DELETE
. To access cross-domain resources, in this case the WCF Service, the client makes a HTTP Request using the GET
method.
Writing this article, I discovered that using JSONP technically isn’t AJAX because it does not use the XMLHttpRequest
object, a primary requirement of AJAX. JSONP-format HTTP Requests are made by inserting the HTML <script>
tag into the DOM, dynamically. The Content-Type
of the HTTP Response from the WCF Service, as seen with Firebug, is application/x-javascript
, not application/json
, as with regular JSON. I’m just happy if it all works, AJAX or not.
Using the Code
The Visual Studio 2010 Solution used in this article contains (3) projects shown below. All code for this article is available for download at on The Code Project.
- Restaurant – C# Class Library
- RestaurantWcfService – C# WCF REST Service Application
- RestaurantDemoSite – Existing Web Site
Restaurant Class Library
The C# Class Library Project, Restaurant, contains the primary business objects and business logic. Classes that will be instantiated to hold the restaurant menu and restaurant orders include RestaurantMenu
, MenuItem
, RestaurantOrder
, and OrderItem
. Both RestaurantMenu
and RestaurantOrder
inherit from System.Collections.ObjectModel.Collection<T>
. RestaurantMenu
contains instances of MenuItem
, while RestaurantOrder
contains instances of OrderItem
.
The business logic for deserializing the JSON-format HTTP Request containing the restaurant order is handled by the ProcessOrder
class. I struggled with deserializing the JSONP-formatted HTTP Request into an instance of RestaurantOrder
with the standard .NET System.Web.Script.Serialization.JavaScriptSerializer
class. I solved the deserialization issue by using Json.NET
. This .NET Framework, described as a flexible JSON serializer to convert .NET objects to JSON and back again, was created by James Newton-King. It was a real lifesaver. Json.NET is available on Codeplex. Before passing the RAW JSONP-format HTTP Request to Json.NET, I still had to clean it up using the NormalizeJsonString
method I wrote.
Lastly, ProcessOrder
includes the method WriteOrderToFile
, which writes the restaurant order to a text file. This is intended to demonstrate how orders could be sent from the client to the server, stored, and then reloaded and deserialized later, as needed. In order to use this method successfully, you need to create the ‘c:\RestaurantOrders‘ folder path and add permissions for the IUSR
user account to read and write to the RestaurantOrders folder.
The ProcessOrder
class (note the reference to Json.NET: Newtonsoft.Json
):
using Newtonsoft.Json; using System; using System.Collections.Generic; using System.IO; using System.Linq; namespace Restaurant { public class ProcessOrder { public const string STR_JsonFilePath = @"c:\RestaurantOrders\"; public string ProcessOrderJSON(string restaurantOrder) { if (restaurantOrder.Length < 1) { return "Error: Empty message string..."; } try { var orderId = Guid.NewGuid(); NormalizeJsonString(ref restaurantOrder); //Json.NET: http://james.newtonking.com/projects/json-net.aspx var order = JsonConvert.DeserializeObject <restaurantorder>(restaurantOrder); WriteOrderToFile(restaurantOrder, orderId); return String.Format( "ORDER DETAILS{3}Time: {0}{3}Order Id: {1}{3}Items: {2}", DateTime.Now.ToLocalTime(), Guid.NewGuid(), order.Count(), Environment.NewLine); } catch (Exception ex) { return "Error: " + ex.Message; } } private void NormalizeJsonString(ref string restaurantOrder) { restaurantOrder = Uri.UnescapeDataString(restaurantOrder); int start = restaurantOrder.IndexOf("["); int end = restaurantOrder.IndexOf("]") + 1; int length = end - start; restaurantOrder = restaurantOrder.Substring(start, length); } private void WriteOrderToFile(string restaurantOrder, Guid OrderId) { //Make sure to add permissions for IUSR to folder path var fileName = String.Format("{0}{1}.txt", STR_JsonFilePath, OrderId); using (TextWriter writer = new StreamWriter(fileName)) { writer.Write(restaurantOrder); } } } }
Restaurant WCF Service
If you’ve built WCF Services before, you’ll be familiar with the file structure of this project. The RestaurantService.svc, the WCF Service file, contains no actual code, only a pointer to the code-behind RestaurantService.cs file. This file contains each method which will be exposed to the client through the WCF Service. The IRestaurantService.cs Interface file, defines the Service Contract between the RestaurantService
class and the WCF Service. The IRestaurantService
Interface also defines each Operational Contract with the class’s methods. The Operational Contract includes Operational Contract Attributes, which define how the Service Operation (a method with an Operational Contract) will operate as part of the WCF Service. Operational Contract Attributes in this example include the required invocation (HTTP method – GET
), format of the HTTP Request and Response (JSON), and caching (for the restaurant menu). The WFC Service references (has a dependency on) the Restaurant Class Library.
The WCF Web Service Project, RestaurantWcfService
, contains two methods that are exposed to the client. The first, GetCurrentMenu
, serializes an instance of RestaurantMenu
, containing nested instances of MenuItem
. It returns the JSONP-format HTTP Response to the client. There are no parameters passed to the method by the HTTP Request.
The second method, SendOrder
, accepts the JSONP-format order, through an input parameter of the string
data type, from the client’s HTTP Request. SendOrder
then passes the order to the ProcessOrderJSON
method, part of the Restaurant.ProcessOrder
class. ProcessOrderJSON
returns a string
to SendOrder
, containing some order information (Order Id, date/time, and number of order items). This information is serialized and returned in the JSONP-format HTTP Response to the client. The Response verifies that the order was received and understood.
Lastly, the web.config file contains the WCF bindings, behaviors, endpoints, and caching configuration. I always find configuring this file properly to be a challenge due to the almost-infinite number of WCF configuration options. There are many references available on configuring WCF, but be careful, many were written prior to .NET Framework 4. Configuring WCF for REST and JSONP became much easier with .NET Framework 4. Make sure you refer to the latest materials from MSDN on WCF for .NET Framework 4.
The IRestaurantService.cs Interface:
using Restaurant; using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.ServiceModel; using System.ServiceModel.Web; namespace RestaurantWcfService { [ServiceContract] public interface IRestaurantService { [OperationContract] [Description("Returns a copy of the restaurant menu.")] [WebGet(BodyStyle = WebMessageBodyStyle.Bare, RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)] [AspNetCacheProfile("CacheFor10Seconds")] RestaurantMenu GetCurrentMenu(); [OperationContract] [Description("Accepts a menu order and return an order confirmation.")] [WebGet(BodyStyle = WebMessageBodyStyle.Bare, RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json, UriTemplate = "SendOrder?restaurantOrder={restaurantOrder}")] string SendOrder(string restaurantOrder); } }
The RestaurantService.cs Class (inherits from IRestaurantService.cs):
using Restaurant; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.ServiceModel.Activation; namespace RestaurantWcfService { [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)] public class RestaurantService : IRestaurantService { public RestaurantMenu GetCurrentMenu() { //Instantiates new RestaurantMenu object and //sorts MeuItem objects by byDescription using LINQ var menuToReturn = new RestaurantMenu(); var menuToReturnOrdered = ( from items in menuToReturn orderby items.Description select items).ToList(); menuToReturn = new RestaurantMenu(menuToReturnOrdered); return menuToReturn; } public string SendOrder(string restaurantOrder) { //Instantiates new ProcessOrder object and //passes JSON-format order string to ProcessOrderJSON method var orderProcessor = new ProcessOrder(); var orderResponse = orderProcessor.ProcessOrderJSON(restaurantOrder); return orderResponse; } } }
The WCF Service’s web.config File:
<?xml version="1.0"?> <configuration> <system.web> <compilation debug="false" targetFramework="4.0" /> <caching> <outputCacheSettings> <outputCacheProfiles> <add name="CacheFor10Seconds" duration="10" varyByParam="none" /> </outputCacheProfiles> </outputCacheSettings> </caching> </system.web> <system.serviceModel> <bindings> <webHttpBinding> <binding name="webHttpBindingWithJsonP" crossDomainScriptAccessEnabled="true" /> </webHttpBinding> </bindings> <behaviors> <endpointBehaviors> <behavior name="webHttpBehavior"> <webHttp helpEnabled="true"/> </behavior> </endpointBehaviors> <serviceBehaviors> <behavior> <serviceMetadata httpGetEnabled="true" /> </behavior> </serviceBehaviors> </behaviors> <serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" /> <services> <service name="RestaurantWcfService.RestaurantService"> <endpoint address="" behaviorConfiguration="webHttpBehavior" binding="webHttpBinding" bindingConfiguration="webHttpBindingWithJsonP" contract="RestaurantWcfService.IRestaurantService" /> </service> </services> </system.serviceModel> <system.webServer> <modules runAllManagedModulesForAllRequests="true"/> </system.webServer> </configuration>
WCF Web HTTP Service Help
Once you have the article’s code installed and running, you can view more details about the WCF Service’s operations (methods) using the new .NET Framework 4 WCF Web HTTP Service Help Page feature. Depending on your IIS configuration, the local address should be similar to: http://localhost/MenuWcfRestService/RestaurantService.svc/Help.
Restaurant Demo Site
RestaurantDemoSite
is a non-ASP.NET website, just HTML and JavaScript. For this article, I chose to host the RestaurantDemoSite
web site on a different port (2929) than the WCF Service on default port 80. I did this to demonstrate the necessity of JSONP for cross-domain scripting. Hosting them on two different ports is considered hosting on two different domains. Port 2929 is a randomly-selected open port on my particular development machine. Both the WCF Service and the website were setup as Virtual Directories in IIS, and then added to the Visual Studio 2010 Solution, along with the Restaurant Class Library.
Following the format of the first article, the website contains two identical pages, each with the same restaurant menu/order form. The ‘Development’ version is optimized for debugging and demonstration. The other, ‘Production’, with the JavaScript and CSS files minified and packed, is optimized for use in production. The demo uses the latest available jQuery JavaScript Library (jquery-1.6.3.js) and the jQuery plug-in, Format Currency (jquery.formatCurrency-1.4.0.js).
The page contains the new HTML5 <!DOCTYPE>
declaration. I used HTML5’s new numeric input type for inputting the number of items to order. I defined a min and max value, also a new HTML5 feature. You can these HTML features working in the latest version of Google Chrome.
All of the client-side business logic is contained in the restaurant.js JavaScript file. This file makes calls to jQuery and Format Currency. I chose the sometimes controversial, static code analysis tool JSLint to help debug and refactor my JavaScript code. Even if you don’t agree with all of JSLint’s warnings, understanding the reason for them will really enhance your overall knowledge of JavaScript. A good alternative to JSLint, which I’ve also tried, is JSHint, a fork of the JSLint project. JSHint advertises itself as a more configurable version of JSLint.
The restaurant.js JavaScript file:
var addMenuItemToOrder, calculateSubtotal, clearForm, clickRemove, formatRowColor, formatRowCurrency, getRestaurantMenu, handleOrder, orderTotal, populateDropdown, tableToJson, sendOrder, wcfServiceUrl; // Populate drop-down box with JSON data (menu) populateDropdown = function () { var id, price, description; id = this.Id; price = this.Price; description = this.Description; $("#select_item") .append($("<option></option>") .val(id) .html(description) .attr("title", price)); }; // Use strict for all other functions // Based on post at: // http://ejohn.org/blog/ecmascript-5-strict-mode-json-and-more/ (function () { "use strict"; wcfServiceUrl = "http://localhost/MenuWcfRestService/RestaurantService.svc/"; // Execute when the DOM is fully loaded $(document).ready(function () { getRestaurantMenu(); }); // Add selected item to order $(function () { $("#add_btn").click(addMenuItemToOrder); }); // Place order if it contains items $(function () { $("#order_btn").click(handleOrder); }); // Retrieve JSON data (menu) and loop for each menu item getRestaurantMenu = function () { $.ajax({ cache: true, url: wcfServiceUrl + "GetCurrentMenu", data: "{}", type: "GET", jsonpCallback: "RestaurantMenu", contentType: "application/javascript", dataType: "jsonp", error: function () { alert("Menu failed!"); }, success: function (menu) { $.each(menu, populateDropdown); // must call function as var } }); }; // Add selected menu item to order table addMenuItemToOrder = function () { var order_item_selected_quantity, selected_item, order_item_selected_id, order_item_selected_description, order_item_selected_price, order_item_selected_subtotal; // Limit order quantity to between 1-99 order_item_selected_quantity = parseInt($("#select_quantity").val(), 10); if (order_item_selected_quantity < 1 || order_item_selected_quantity > 99 || isNaN(order_item_selected_quantity)) { return; } // Can't add 'Select an Item...' to order if ($("#select_item").get(0).selectedIndex === 0) { return; } // Get values selected_item = $("#select_item option:selected"); order_item_selected_id = parseInt(selected_item.val(), 10); order_item_selected_description = selected_item.text(); order_item_selected_price = parseFloat(selected_item.attr("title")); // Calculate subtotal order_item_selected_subtotal = calculateSubtotal(order_item_selected_price, order_item_selected_quantity); // Write out menu selection to table row $("<tr class='order_row'></tr>").html("<td>" + order_item_selected_quantity + "</td><td class='order_item_id'>" + order_item_selected_id + "</td><td class='order_item_name'>" + order_item_selected_description + "</td><td class='order_item_price'>" + order_item_selected_price + "</td><td class='order_item_subtotal'>" + order_item_selected_subtotal + "</td><td><input type='button' value='remove' /></td>") .appendTo("#order_cart").hide(); // Display grand total of order_item_selected_id $("#order_cart tr.order_row:last").fadeIn("medium", function () { // Callback once animation is complete orderTotal(); }); formatRowCurrency(); formatRowColor(); clickRemove(); clearForm(); }; // Calculate subtotal calculateSubtotal = function (price, quantity) { return price * quantity; }; // Create alternating colored rows in order table formatRowColor = function () { $("#order_cart tr.order_row:odd").css("background-color", "#FAF9F9"); $("#order_cart tr.order_row:even").css("background-color", "#FFF"); }; // Format new order item values to currency formatRowCurrency = function () { $("#order_cart td.order_item_price:last").formatCurrency(); $("#order_cart td.order_item_subtotal:last").formatCurrency(); }; // Bind a click event to the correct remove button clickRemove = function () { $("#order_cart tr.order_row:last input").click(function () { $(this).parent().parent().children().fadeOut("fast", function () { $(this).parent().slideUp("slow", function () { // the row (tr) $(this).remove(); // the row (tr) orderTotal(); }); }); }); }; // Clear order input form and re-focus cursor clearForm = function () { $("#select_quantity").val(""); $("#select_item option:first-child").attr("selected", "selected"); $("#select_quantity").focus(); }; // Calculate new order total orderTotal = function () { var order_total = 0; $("#order_cart td.order_item_subtotal").each(function () { var amount = ($(this).html()).replace("$", ""); order_total += parseFloat(amount); }); $("#order_total").text(order_total).formatCurrency(); }; // Call functions to prepare order and send to WCF Service handleOrder = function () { if ($("#order_cart tr.order_row:last").length === 0) { alert("No items selected..."); } else { var data = tableToJson(); sendOrder(data); } }; // Convert HTML table data into an array // Based on code from: // http://johndyer.name/post/table-tag-to-json-data.aspx tableToJson = function () { var data, headers, orderCartTable, myTableRow, rowData, i, j; headers = ["Quantity", "Id"]; data = []; orderCartTable = document.getElementById("order_cart"); // Go through cells for (i = 1; i < orderCartTable.rows.length - 1; i++) { myTableRow = orderCartTable.rows[i]; rowData = {}; for (j = 0; j < 2; j++) { rowData[headers[j]] = myTableRow.cells[j].innerHTML; } data.push(rowData); } return data; }; // Convert array to JSON and send to WCF Service sendOrder = function (data) { var jsonString = JSON.stringify({ restaurantOrder: data }); $.ajax({ url: wcfServiceUrl + "SendOrder?restaurantOrder=" + jsonString, type: "GET", contentType: "application/javascript", dataType: "jsonp", jsonpCallback: "OrderResponse", error: function () { alert("Order failed!"); }, success: function (confirmation) { alert(confirmation.toString()); } }); }; } ());
Using Firebug to Look Behind the Scenes
In real life, a restaurant’s menu changes pretty infrequently. Therefore, to speed page delivery, I chose to cache the restaurant’s menu on the client-side. Caching is configured as part of the Operational Contract in IRestaurantService
, as well as in the jQuery AJAX call to GetCurrentMenu
in restaurant.js. In this example, I set the cache to 10 seconds, which can be confirmed by looking at the Cache-Control
property in the HTTP Response Header of the call to GetCurrentMenu
, using Firebug.
Below is a screen grab of initial load of the restaurant menu/order form page in Firefox with Firebug running. Note the ‘Domain’ of the AJAX call is different than the page and associated files. Also, both the ‘Status’ and ‘Remote IP’ indicate the HTTP Response to GetCurrentMenu
(the restaurant’s menu) is cached, along with the page and associated files. Firebug is an invaluable tool in the development and debugging of JavaScript, especially when working with AJAX.
Points of Interest
Several things stood out to me as a result of writing this article:
- WCF – No matter how many times I work with WCF Services, getting them configured properly seems like 90% technical knowledge and 10% luck. Ok, maybe 20% luck! Seriously, there are a lot of great resources on the web regarding WCF configuration issues. If you have a specific problem with WCF, odds are someone else already had it and has published a solution. Make sure the information is current to the .NET Framework you are working with.
- Third-party Libraries, Plug-ins, and Frameworks – Don’t confine yourself to using the out-of-the-box .NET Framework, JavaScript, or jQuery to solve all your coding challenges. There are an endless variety of Frameworks, JavaScript Libraries, and jQuery Plug-ins, available. Being a good developer is about providing the best solution to a problem, not necessarily writing each and every line of code, yourself. A few minutes of research can be worth hours of coding!
- Refactoring – Refactoring your code is critical. Just making it work is not good enough. Added bonus? I’ve personally gained a considerable amount of knowledge about software development through refactoring. Forcing yourself to go back and optimize code can be a tremendous learning opportunity. Using third-party refactoring tools such JSLint/JSHint, FxCop, RefactorPro!, CodeRush, ReSharper, and others is a great way to improve both your refactoring and coding skills. I use all these tools as much as possible.
- Cross-Domain with JSONP – Using JSONP is one technique to get around the limitations imposed by the same origin policy. JSONP has its pros and cons. Spend some time to research other methods that might better benefit your project requirements.