REST based services are to a higher degree than SOAP or other services based on the basic HTTP protocol mechanisms. This will also often mean that you are using http headers to relay information, as well as possibly cookies and also utilizing the different available request methods as per RFC for Hypertext Transfer Protocol -- HTTP/1.1 (Method Definitions).
Let us assume that we have the following REST based service.
using System.Net; using System.Collections.Generic; using System.ServiceModel; using System.ServiceModel.Activation; using System.ServiceModel.Web; using Common; namespace WcfRestService1 { // Start the service and browse to http://<machine_name>:<port>/Service1/help to view the service's generated help page // NOTE: By default, a new instance of the service is created for each call; change the InstanceContextMode to Single if you want // a single instance of the service to process all calls. [ServiceContract] [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)] [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)] public class Service1 { [WebGet(UriTemplate = "{id}")] public SampleItem[] GetProductById(string id) { if (WebOperationContext.Current.IncomingRequest.Headers["MyCustomHttpHeader"] == null) throw new WebFaultException<string>("The custom httpheader 'MyCustomHttpHeader' has not been set.", HttpStatusCode.BadRequest); return new[] { new SampleItem() { Id = 1, StringValue = "Product1" } }; } } }
This service requires a http header named "MyCustomHttpHeader" is part of the request. The service does currently no care what values are sent with this header, but it needs to be part of the request.
We have defined the DataContract for SampleItem in a Common project.
using System.Runtime.Serialization; namespace Common { [DataContract] public class SampleItem { [DataMember] public int Id { get; set; } [DataMember] public string StringValue { get; set; } } }
On the client side we have defined an interface that describes our service, so its methods can be accessed by client code.
using System.ServiceModel; using System.ServiceModel.Web; using Common; namespace CRTest { [ServiceContract] public interface IService1Wrapper { [OperationContract] [WebGet( BodyStyle = WebMessageBodyStyle.Bare, ResponseFormat = WebMessageFormat.Xml, UriTemplate = "/Service1/GetByProductId?id={id}")] SampleItem[] GetProductById(string id); } }
Finally we have some client code that is trying to use our service.
using System; using System.ServiceModel; using System.Windows.Forms; namespace CRTest { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { var factory = new ChannelFactory<IService1Wrapper>("Service1WrapperREST"); var proxy = factory.CreateChannel(); var response = proxy.GetProductById("1"); ((IDisposable)proxy).Dispose(); } } }
There is nothing in this code that indicates that custom headers are added, and indeed there are 3 more classes that needs to be presented and we also need to see the app.config file to get the full picture.
First let us have a look at the app.config file.
<?xml version="1.0"?> <configuration> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/> </startup> <system.serviceModel> <client> <endpoint address="http://localhost:62510" binding="webHttpBinding" contract="CRTest.IService1Wrapper" name="Service1WrapperREST" behaviorConfiguration="CustomHeaderBehavior" /> </client> <behaviors> <endpointBehaviors> <behavior name="CustomHeaderBehavior"> <customHttpHeaders> <headers> <add key="MyCustomHttpHeader" value="some_value"></add> <add key="MyCustomHttpHeader2" value="yet_another_value"></add> </headers> </customHttpHeaders> <webHttp/> </behavior> </endpointBehaviors> </behaviors> <extensions> <behaviorExtensions> <add name="customHttpHeaders" type="CRTest.CustomHeaderBehaviorExtensionElement, CRTest, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" /> </behaviorExtensions> </extensions> </system.serviceModel> </configuration>
Looking at the system.servicemodel/client/endpoint we see nothing unusual in the first 4 lines, but then I have added a behaviorConfiguration named CustomHeaderBehavior.
When we continue down through the config file and at system.servicemodel/behaviors/endpointBehaviors/behavior name="CustomHeaderBehavior" we have an element named customHttpHeaders which basically define all the custom headers that we want to include in our REST request.
Finally in the config file we see that a behaviorExtensions has been added to add support for these new customHttpHeaders xml elements in our configuration file.
Let us start with the CRTest.CustomHeaderBehaviorExtensionElement class.
using System; using System.Collections.Generic; using System.Configuration; using System.Linq; using System.ServiceModel.Configuration; namespace CRTest { public class CustomHeaderBehaviorExtensionElement : BehaviorExtensionElement { protected override object CreateBehavior() { Dictionary<string, string> customHeaders = null; if (CustomHeaders != null) { customHeaders = CustomHeaders.AllKeys.ToDictionary(key => key, key => CustomHeaders[key].Value); } return new CustomHeaderBehavior(customHeaders); } public override Type BehaviorType { get { return typeof (CustomHeaderBehavior); } } [ConfigurationProperty("headers", IsRequired = true)] [ConfigurationCollection(typeof(KeyValueConfigurationCollection))] public KeyValueConfigurationCollection CustomHeaders { get { return (KeyValueConfigurationCollection) base["headers"]; } } } }
Starting from the bottom of the class and moving up we see the CustomHeaders property which is tied to the headers XML element in the config file. Since we have used the XML attribues key/value we are returning a KeyValueConfigurationCollection. This collection is rather specific to configuration and config files and I do not want that being passed around in the application.
That is why I in the CreateBehavior() converts it into a Dictionary<string, string> customHeaders.
Then I return a new instance of the CustomHeaderBehavior which as an argument in the constructor takes the custom headers as a Dictionary<string, string>.
using System.Collections.Generic; using System.ServiceModel.Channels; using System.ServiceModel.Description; using System.ServiceModel.Dispatcher; namespace CRTest { public class CustomHeaderBehavior : IEndpointBehavior { private readonly IDictionary<string, string> customHttpHeaders; public CustomHeaderBehavior(IDictionary<string, string> customHttpHeaders) { this.customHttpHeaders = customHttpHeaders; } public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime) { var customHeaderMessageInspector = new CustomHeaderMessageInspector(customHttpHeaders); clientRuntime.MessageInspectors.Add(customHeaderMessageInspector); } public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) { } public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) { } public void Validate(ServiceEndpoint endpoint) { } } }
In ApplyClientBehavior() we add a MessageInspector of type CustomHeaderMessageInspector and this is the class that is actually adding those custom headers to the request.
using System; using System.Collections.Generic; using System.ServiceModel; using System.ServiceModel.Channels; using System.ServiceModel.Dispatcher; namespace CRTest { public class CustomHeaderMessageInspector : IClientMessageInspector { private readonly IDictionary<string, string> customHttpHeaders; public CustomHeaderMessageInspector(IDictionary<string,string> customHttpHeaders) { if (customHttpHeaders == null) throw new ArgumentNullException("customHttpHeaders"); this.customHttpHeaders = customHttpHeaders; } public object BeforeSendRequest(ref Message request, IClientChannel channel) { // Making sure we have a HttpRequestMessageProperty HttpRequestMessageProperty httpRequestMessageProperty; if (request.Properties.ContainsKey(HttpRequestMessageProperty.Name)) { httpRequestMessageProperty = request.Properties[HttpRequestMessageProperty.Name] as HttpRequestMessageProperty; if (httpRequestMessageProperty == null) { httpRequestMessageProperty = new HttpRequestMessageProperty(); request.Properties.Add(HttpRequestMessageProperty.Name, httpRequestMessageProperty); } } else { httpRequestMessageProperty = new HttpRequestMessageProperty(); request.Properties.Add(HttpRequestMessageProperty.Name, httpRequestMessageProperty); } // Add custom headers to the WCF request foreach (var header in customHttpHeaders) { httpRequestMessageProperty.Headers.Add(header.Key, header.Value); } return null; } public void AfterReceiveReply(ref Message reply, object correlationState) { } } }
I case you are interested in the solution file it can be downloaded here WCFCustomHeaderClient.zip.
3 comments:
Very impressive demo! Thank you.
nice, that wasn't easy to find, we're using it to version our REST API by relying on the ACCEPT header
Hi,
The is a very complete example. I really enjoy reading it.
But I couldn't understand all.
When is the BeforeRequest() method called?
I only can see the CreateChannel() and then the properly call to the webservice, but I can't see the custom headers addition.
Post a Comment