Returning Custom Formats from WCF WebHttp Services
This is part eight of a twelve part series that introduces the features of WCF WebHttp Services in .NET 4. In this post we will cover:
- Using the new WCF WebHttp Services API for content negotiation
- Returning content with custom formats in addition to XML and JSON
- Configuring WCF WebHttp Services with the WebHttpEndpoint
Over the course of this blog post series, we are building a web service called TeamTask. TeamTask allows a team to track tasks assigned to members of the team. Because the code in a given blog post builds upon the code from the previous posts, the posts are intended to be read in-order.
Downloading the TeamTask Code
At the end of this blog post, you’ll find a link that will allow you to download the code for the current TeamTask Service as a compressed file. After extracting, you’ll find that it contains “Before” and “After” versions of the TeamTask solution. If you would like to follow along with the steps outlined in this post, download the code and open the "Before" solution in Visual Studio 2010. If you aren’t sure about a step, refer to the “After” version of the TeamTask solution.
Note: If you try running the sample code and see a Visual Studio Project Sample Loading Error that begins with “Assembly could not be loaded and will be ignored…”, see here for troubleshooting.
Getting Visual Studio 2010
To follow along with this blog post series, you will need to have Microsoft Visual Studio 2010 and the full .NET 4 Framework installed on your machine. (The client profile of the .NET 4 Framework is not sufficient.) At the time of this posting, the Microsoft Visual Studio 2010 Ultimate Beta 2 is available for free download and there are numerous resources available regarding how to download and install, including this Channel 9 video.
Step 1: Creating the DirectoryService class
If you have downloaded the TeamTask service code for any of the first seven parts of this blog post series, you may have noticed that all of the valid URIs for the service begin with either “https://<baseAddress>/TeamTask/Tasks” or “https://<baseAddress>/TeamTask/Users”. This makes sense because the service is really made up of a collection of task resources and a collection of user resources; we even refactored the service to better align with this resource model in part six.
However, if you try to navigate to just “https://<baseAddress>/TeamTask” (without specifying either “Users” or “Tasks”) you’ll find that you won’t get a response from the service. Obviously, this is awkward; it’s akin to going to a book seller’s website where there is a page for each book, but not a main page from which to find the individual book pages.
In this part of the blog post series, we’ll rectify this issue by adding a new service class that will provide a directory of all of the resource collections exposed by the service. At this point the directory will only list the task and user resource collections, but its easy to imagine adding new functionality to the TeamTask service in the future that might result in new resource collections appearing in the directory.
To demonstrate the advanced formatting APIs introduced in WCF WebHttp Services for .NET 4, we’ll implement this new DirectoryService class so that it can return responses in XML, JSON, or Atom. In the next part of this blog post series, we’ll extend the DirectoryService to also provide responses in a help-page-like HTML format.
We’ll start by creating the DirectoryService class from a copy of the TaskService.cs source file.
If you haven't already done so, download and open the “Before” solution of the code attached to this blog post.
In the Solution Explorer window select the newly renamed TaskService.cs file and press Ctrl+C, Ctrl+V to create a copy of the TaskService.cs file in the TeamTask.Service project. The new file should be named “Copy of TaskService.cs”.
Rename the “Copy of TaskService.cs” file to “DirectoryService.cs” by right-clicking on it in the Solution Explorer window and selecting “Rename” from the context menu.
Open the DirectoryService.cs file in the code editor. Because this file is actually a copy of the TaskService.cs file, it will contain a second definition of the TaskService class. Rename the class to “DirectoryService”, making sure NOT to update references throughout the project.
Delete all of the members of the newly renamed DirectoryService class.
Add a single operation, GetDirectory(), which will have an empty string UriTemplate value and a return type of Message as shown below. You’ll also have to add “using System.ServiceModel.Channels;”.
namespace TeamTask.Service
{
[ServiceContract]
[AspNetCompatibilityRequirements(RequirementsMode =
AspNetCompatibilityRequirementsMode.Allowed)]
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
public class DirectoryService
{
[WebGet(UriTemplate = "")]
public Message GetDirectory()
{}
}
}
Helpful Tip: Using a return type of Message with the GetDirectory() operation will allow us to return any arbitrary content in the body of our response. As we’ll see below, WCF WebHttp Services for .NET 4 introduces a useful set of Create*Response() APIs to make creating the Message instance painless. For binary data there is the CreateStreamResponse() method. For text-based custom formats there is CreateTextResponse(). There are also similar methods for XML, JSON and Atom, making it possible to return any arbitrary content or regular XML/JSON from the same operation. |
Step 2: Adding the DirectoryService Route
Just as we did in part six of this blog post series when we created the TaskService and UserService classes, we need to add a route in the Global.asax file for the new DirectoryService class.
When we added the TaskService and UserService routes in part six we used string literals for the route paths. For example, the TaskService route in the Global.asax file is currently given as:
RouteTable.Routes.Add(new ServiceRoute("Tasks", factory, typeof(TaskService)));
where “Tasks” is the route path. There is nothing inherently wrong with using string literals except that we can’t access the route paths from our code, which we’re going to need to do when we implement the DirectoryService class. Therefore as we add the route for the DirectoryService class, we’ll also replace the string literal route paths with public constants.
Open the TaskService.cs file in the code editor and add the following constant to the TaskService class:
public const string Route = "Tasks";
Open the UserService.cs file in the code editor and add the following constant to the UserService class:
public const string Route = "Users";
Open the DirectoryService.cs file in the code editor and add the following constant to the UserService class:
public const string Route = "";
Open the Global.asax file in the code editor and replace the route paths for the TaskService route and the UserService route with the Route constants from their respective classes like so:
RouteTable.Routes.Add(new ServiceRoute(TaskService.Route, factory,
typeof(TaskService)));
RouteTable.Routes.Add(new ServiceRoute(UserService.Route, factory,
typeof(UserService)));Add an additional route for the DirectoryService like so:
RouteTable.Routes.Add(new ServiceRoute(DirectoryService.Route, factory,
typeof(DirectoryService)));
Step 3: Creating the ResourceCollection Type
We’re almost ready to implement the DirectoryService class. However, we still need to create a type that will hold all of the information about a given resource collection just like the Task and User types hold all of the information about a given task or user. We’ll create a ResourceCollection class to fulfill this role.
In the Solution Explorer (Ctrl+W, S) right click on the TeamTask.Service project and select “Add”—>”Class" from the context menu that appears. In the Add New Item window enter the name “ResourceCollection.cs” and hit enter.
Open the newly created ResourceCollection.cs file in the code editor and add the code as shown below:
public class ResourceCollection
{
internal string Route { get; set; }
public string Name { get; set; }
public string Description { get; set; }public Uri Uri
{
get
{
return new Uri(this.Route, UriKind.Relative);
}
set { }
}public Uri HelpUri
{
get
{
return new Uri(this.Route + "/help", UriKind.Relative);
}
set { }
}
}Notice that the Route property is specified as internal. The access modifiers on the properties matter because instances of the ResourceCollection class will be serialized in the responses from the GetDirectory() operation. The route path is needed to create the URI’s but routes themselves are an internal implementation detail that we don’t want to expose.
Step 4: Implementing the DirectoryService Class
We are now ready to implement the DirectoryService class. We’ll be able to see how the new content negotiation and advanced formatting APIs in WCF WebHttp Services for .NET 4 will allow us to return responses in XML, JSON or Atom depending on the client’s preference.
The TeamTask service only exposes two collections of resources (tasks and users) and the information about these resource collections doesn’t change after the service starts. So we’ll first add to the DirectoryService class a static read-only list that contains a ResourceCollection instance for the TaskService and another for the UserService. Second, we’ll implement the GetDirectory() operation to use this static list of ResourceCollection instances when generating responses.
Since the GetDirectory() operation will support XML, JSON and Atom, we’ll need to be able to determine the client’s preference in order to generate the response in the correct format. We could use a “format” query string parameter as we did in part four of this blog post series with the GetTasks() operation. However, we’ll use the HTTP Accept header of the incoming request to determine the response format in order to demonstrate the content negotiation features in WCF WebHttp Services for .NET 4.
Parsing an HTTP Accept header to determine a client’s content-type preference can be a little tricky since the HTTP specification allows for media ranges with wildcard characters and relative quality factors. Fortunately, with WCF WebHttp Services in .NET 4 a new GetAcceptHeaderElements() method is available via the WebOperationContext and it handles all of this Accept header parsing, returning a list of strongly-typed ContentType instances in the order of client preference.
Knowing the content-type that the client prefers, we’ll use either the CreateXmlResponse(), CreateJsonResponse() or CreateAtom10Response() methods that hang off the WebOperationContext to serialize the list of ResourceCollection instances into the body of a Message instance in the appropriate format.
Open the DirectoryService.cs file in the code editor.
Add the following list of ResourceCollection instances as a static member of the DirectoryService class:
private static readonly List<ResourceCollection> resourceCollections =
new List<ResourceCollection>() {
new ResourceCollection()
{
Route = TaskService.Route,
Name = "Tasks Collection",
Description = "A collection of the current tasks for the team.",
},
new ResourceCollection()
{
Route = UserService.Route,
Name = "Users Collection",
Description = "A collection of the current users on the team.",
}
};Implement the GetDirectory() operation as shown below. You’ll also need to add “using System.Net.Mime;” and “using System.ServiceModel.Syndication;” to the source file.
[WebGet(UriTemplate = "")]
public Message GetDirectory()
{
WebOperationContext context = WebOperationContext.Current;
IncomingWebRequestContext request = context.IncomingRequest;foreach (ContentType acceptElement in
request.GetAcceptHeaderElements())
{
switch (acceptElement.MediaType.ToUpperInvariant())
{
case "APPLICATION/XML":
case "TEXT/XML":
return context.CreateXmlResponse(resourceCollections);
case "APPLICATION/JSON":
return context.CreateJsonResponse(resourceCollections);
case "APPLICATION/ATOM+XML":
SyndicationFeed feed = GetFeed();
return context.CreateAtom10Response(feed);
}
}return context.CreateXmlResponse(resourceCollections);
}Notice how the XML and JSON responses are being created. The static read-only list of ResourceCollections is being passed into the CreateXmlResponse() and CreateJsonResponse() methods. Both of these methods will create a new Message instance and serialize the list of ResourceCollections into the body of the Message in their respective format.
Creating the Atom response requires a little more code, but only because we have to map the ResourceCollection type into the Atom format. While the CreateXmlResponse() and CreateJsonResponse() methods will take any type that can be successfully serialized, the CreateAtom10Response() method expects either a SyndicationFeed instance, a SyndicationItem instance, or a ServiceDocument instance. Here we are supplying a SyndicationFeed that we create from the list of ResourceCollections in the GetFeed() method. We’ll implement the GetFeed() method next.
Copy the following implementation of the GetFeed() method into the DirectoryService class:
private static SyndicationFeed GetFeed()
{
DateTime itemDate = DateTime.Now;
string feedTitle = "Team Task Resources";
string feedDescription = "The Resource Collections of the Team Task Service.";
Uri feedUri = new Uri(DirectoryService.Route, UriKind.Relative);var items = resourceCollections.Select(resource =>
new SyndicationItem(resource.Name, resource.Description,
resource.Uri, "tag:???", itemDate));return new SyndicationFeed(feedTitle, feedDescription, feedUri, items);
}The GetFeed() method creates a SyndicationFeed by projecting the task and user ResourceCollection instances into SyndicationItems using the LINQ Select() method. Along with the syndication items, the feed’s title, description and URI are also provided to the SyndicationFeed constructor.
Helpful Tip: The SyndicationFeed, SyndicationItem and ServiceDocument classes are all a part of the System.ServiceModel.Syndication namespace, which provides the functionality for working with Atom, RSS and other syndication formats in WCF. |
Step 5: Getting the Resource Collections on the Client
With our DirectoryService implemented, we’re ready to retrieve the list of resource collections with our console client application. In the client app we’ll send three requests to https://localhost:8080/TeamTask/, each with different accept header values, and we’ll write out the responses to the console to verify that we are getting the correct content-type.
Open the Program.cs file from the TeamTask.Client project in the code editor.
Add the following static method to the Program class:
static void GetResourceCollections(HttpClient client, string acceptType)
{
Console.WriteLine("Getting the resource collections as '{0}':", acceptType);
using(HttpRequestMessage request =
new HttpRequestMessage("GET", string.Empty))
{
request.Headers.Accept.AddString(acceptType);
using(HttpResponseMessage response = client.Send(request))
{
response.EnsureStatusIsSuccessful();
if(response.Content.ContentType.Contains("xml"))
{
Console.WriteLine(response.Content
.ReadAsXElement().ToString());
}
else
{
Console.WriteLine(response.Content.ReadAsString());
}
}
}
Console.WriteLine();
}This client code is very similar to the client code we’ve been writing since part two of this blog post series. The only noteworthy point is that we’re checking if the response content-type is some form of XML and if so we’re using the ReadAsXElement() extension method for the HttpContent class to get whitespace formatting when writing out to the console.
Now we’ll implement the Main() method to retrieve the resource collections three separate times with different HTTP Accept header values. Replace any code within the Main() method with the following code:
using (HttpClient client = new HttpClient("https://localhost:8080/TeamTask/"))
{
GetResourceCollections(client, "application/xml");
GetResourceCollections(client, "application/atom+xml");
GetResourceCollections(client, "application/json");
Console.ReadKey();
}Start without debugging (Ctrl+F5) to get the TeamTask service running and then start the client by right clicking on the TeamTask.Client project in the “Solution Explorer” window and selecting “Debug”—>”Start New Instance”. The console should contain the following output:
Step 6: Disabling the Help page for the DirectoryService
As we demonstrated in the previous step, our DirectoryService behaves as expected and returns either XML, JSON or Atom depending on the client’s preference. The TeamTask service now feels like a single unified service and not just two disparate collections of Task and User resources.
However, there is one awkward aspect of the DirectoryService that we need to rectify—the presence of the automatic help page. If you were to start the TeamTask service and navigate to https://localhost:8080/TeamTask/help you’d find a not-too-useful help page. A useful help page would provide the list of resource collections for the TeamTask service and links to the respective User and Task collection help pages. We’ll create such a help page in the next part of this blog post series. However, the automatic help page generated for the DirectoryService doesn’t provide any of this information; it only lists the GetDirectory() operation. Therefore, we’ll disable the automatic help page for the DirectoryService.
Open the Web.config file from the TeamTask.Service project in the XML editor.
The configuration for WCF is under the <system.serviceModel> element and should currently look like so:
<system.serviceModel>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true"/>
<standardEndpoints>
<webHttpEndpoint>
<standardEndpoint name="" helpEnabled="true"
automaticFormatSelectionEnabled="true"/>
</webHttpEndpoint>
</standardEndpoints>
</system.serviceModel>Notice that there is a single <standardEndpoint> element that has an empty-string name and the help page and automatic format selection features enabled.
Now you might think that disabling the automatic help page for the DirectoryService is a simple matter of setting the helpEnabled value to “false”. However, this won’t work because the standard endpoint declared in the config snippet above is the default WebHttpEndpoint (the empty-string name makes it the default) and it applies to all of the services that make up the TeamTask service (TaskService, UserService, and now DirectoryService). If we set the helpEnabled value to “false”, we’ll also be disabling the help page for the TaskService and UserService, which we don’t want to do. Instead we need to add a second named webHttpEndpoint and disable the help page on this named webHttpEndpoint.
Add a second <standardEndpoint> element under the <webHttpEndpoint> element that has a name of “DirectoryEndpoint” like so:
<webHttpEndpoint>
<standardEndpoint name="" helpEnabled="true"
automaticFormatSelectionEnabled="true"/>
<standardEndpoint name="DirectoryEndpoint"/>
</webHttpEndpoint>Because the help page is disabled by default this DirectoryEndpoint will not expose an automatic help page.
Now that we have a named webHttpEndpoint, we need to indicate to the WCF infrastructure that the DirectoryService should use the DirectoryEndpoint instead of the default nameless webHttpEndpoint. To do this we need to explicitly declare the DirectoryService service endpoint in the Web.config by adding the following XML snippet under the <system.ServiceModel> element:
<services>
<service name="TeamTask.Service.DirectoryService">
<endpoint contract="TeamTask.Service.DirectoryService"
kind="webHttpEndpoint"
endpointConfiguration="DirectoryEndpoint" />
</service>
</services>The <service> element has a name attribute in which we’ve supplied the namespace- qualified class name of the DirectoryService and an <endpoint> child element. On the <endpoint> element we specify that the endpoint kind is “webHttpEndpoint” and that the endpointConfiguration is “DirectoryEndpoint”, the named webHttpEndpoint that we just added under the <standardEndpoints> element. We also have to specify the contract, which happens to be the namespace-qualified class name of the DirectoryService although it would be a different type if we had separated the contract from the actual implementation for the DirectoryService.
Note: We don’t need service endpoints for the TaskService or the UserService, since they use the default nameless webHttpEndpoint configuration.
To verify that these configuration changes disable the help page for just the DirectoryService and not the TaskService or UserService, start without debugging (Ctrl+F5) and navigate to https://localhost:8080/TeamTask/help. In Internet Explorer, the absence of the help page will result in an html page like the one below. This is the standard response when a request with an unknown URI is received.
Now navigate to https://localhost:8080/TeamTask/Tasks/help or to https://localhost:8080/TeamTask/Users/help and you should still see the help pages of the tasks or users resource collections respectively.
Helpful Tip: Standard endpoints are one of the service configuration improvements introduced in WCF in .NET 4. Standard endpoints are re-usable predefined endpoints that make service configuration more straightforward and WCF WebHttp Services in .NET 4 offers two of them: WebHttpEndpoint and WebScriptEndpoint. For the TeamTask service we’ve been using the webHttpEndpoint; it is the standard endpoint you’ll generally want to use for web services that return XML, JSON and other web formats. The webScriptEndpoint is for services that are designed specifically to support ASP.NET AJAX browser clients. |
Helpful Tip: If you’ve used WCF 3.5 or the WCF REST Starter Kit to build non-SOAP HTTP services in the past, you may be aware that WCF WebHttp Services are simply WCF services that use the WebHttpBinding and the WebHttpBehavior. The WebHttpEndpoint is really just a predefined standard endpoint that uses both of these. Therefore in WCF WebHttp Services for .NET 4 you no longer need to configure the WebHttpBinding or the WebHttpBehavior because the new WebHttpEndpoint surfaces all of the useful settings of both of them in one place in the Web.config file. |
Next Steps: Creating Views with T4
We’ve accomplished a lot in this part of the blog post series. We’ve implemented the new DirectoryService to provide clients with a list of the resource collections exposed by the TeamTask service. We demonstrated how to use the new content negotiation API to easily determine the client’s preferred response format and how to use the new Create*Response() APIs to generate the responses in that given format. Lastly, we saw how to configure the DirectoryService differently from the other services that make up the TeamTask service by creating a named webHttpEndpoint and referencing it from a new service endpoint for the DirectoryService.
In part nine of this blog post series we’ll continue to explore the new features introduced with WCF WebHttp Services in .NET 4 that make it possible to return arbitrary content in the body of an HTTP response. In particular, we’ll demonstrate how to generate responses in a WCF WebHttp Service using T4 to create templates that behave exactly like views in a model-view-controller architecture.
Randall Tombaugh
Developer, WCF WebHttp Services