Mixing Add Service Reference and WCF Web HTTP (a.k.a. REST) endpoint does not work

I’ve seen this question to many times in different forums, and answered in many different ways that I decided to write a post with more details about it. The title says it all, but I want to reiterate the answer that I had to give so many times:

Using svcutil.exe and Add Service Reference to create a proxy for REST (*) endpoints in WCF does not work, even if it seems like it does.

(*) I know, they’re not technically “REST” endpoints. They’re officially called "WCF Web HTTP” endpoints, not REST ones, since the term REST means a lot more than responding to plain-old HTTP requests using JSON or XML. But many people call (correctly or not) them WCF REST endpoints (or WCF REST services), so if they find an issue and search for it, I hope this is one of the links they get.

By a WCF Web HTTP endpoint, I mean an endpoint which uses the WebHttpBinding (or an equivalent custom binding), and the WebHttpBehavior (<webHttp/> if using config). The WebScriptEnablingBehavior (<enableWebScript/>) is a subclass of WebHttpBehavior, so that’s also covered. Also, endpoints defined using the WebServiceHostFactory class (<%ServiceHost Service=”your-service-class” Factory=”System.ServiceModel.Activation.WebServiceHostFactory”%> in a .svc file). Any of those are “web” endpoints, and I can’t repeat enough – using svcutil.exe and Add Service Reference to create a proxy for them does not work.

Why doesn’t it work?

In order for the tools to be able to generate a client which can consume the service, the service must expose data about itself, or metadata. That metadata includes things such as the endpoint ABCs (address, binding, contract) for all the endpoints exposed by the service. WCF supports exposing metadata using either the WSDL (Web Service Description Language), also known as “HTTP GET metadata”, or using the WS-MetadataExchange protocol (the two are fairly the same). Those are protocols are defined by the W3 consortium for SOAP Services, and are fairly widely adopted by most SOAP stacks in the industry, and WCF also implements them.

WCF Web HTTP services (endpoints) are not SOAP services (endpoints), so WSDL doesn’t work for them. There is a proposal for a description for resource-based services, called WADL (Web Application Description Language), but the W3C doesn’t have any plans to work on this specification, it has never really been widely adopted and WCF also hasn’t supported it in its service description. Even if we could create a metadata extension to the service to expose its information in that format, the proxy-generation tools (svcutil.exe and Add Service Reference) do not understand them. So this is why it doesn’t work, regardless of what people want to believe.

But I used it and it generated a proxy class and configuration! What gives?

Ok, if you still don’t believe, keep reading. Let’s look at the actual service definition of a WCF service with Web HTTP endpoints. The code below defines 3 services: one only with a Web endpoint, one with a SOAP endpoint, and one with both. In each case, we’ll save the WSDL produced by the service will be saved to a local file.

  1. [DataContract]
  2. public class Person
  3. {
  4.     [DataMember]
  5.     public string Name { get; set; }
  6.     [DataMember]
  7.     public int Age { get; set; }
  8. }
  9. [ServiceContract]
  10. public interface ITest
  11. {
  12.     [OperationContract, WebGet]
  13.     Person CreatePerson(string name, int age);
  14.     [OperationContract, WebInvoke(BodyStyle = WebMessageBodyStyle.WrappedRequest)]
  15.     int Add(int x, int y);
  16. }
  17. public class Service : ITest
  18. {
  19.     public Person CreatePerson(string name, int age)
  20.     {
  21.         return new Person { Name = name, Age = age };
  22.     }
  23.  
  24.     public int Add(int x, int y)
  25.     {
  26.         return x + y;
  27.     }
  28. }
  29. enum Endpoints
  30. {
  31.     HttpOnly,
  32.     SoapOnly,
  33.     HttpAndSoap,
  34. }
  35. class Program
  36. {
  37.     static void Main(string[] args)
  38.     {
  39.         foreach (Endpoints whichEndpoint in Enum.GetValues(typeof(Endpoints)))
  40.         {
  41.             Console.WriteLine(whichEndpoint);
  42.             string baseAddress = "https://" + Environment.MachineName + ":8000/Service";
  43.             ServiceHost host = new ServiceHost(typeof(Service), new Uri(baseAddress));
  44.             host.Description.Behaviors.Add(new ServiceMetadataBehavior { HttpGetEnabled = true });
  45.  
  46.             if (whichEndpoint == Endpoints.HttpAndSoap || whichEndpoint == Endpoints.HttpOnly)
  47.             {
  48.                 ServiceEndpoint endpoint = host.AddServiceEndpoint(typeof(ITest), new WebHttpBinding(), "web");
  49.                 endpoint.Behaviors.Add(new WebHttpBehavior());
  50.             }
  51.          
  52.             if (whichEndpoint == Endpoints.HttpAndSoap || whichEndpoint == Endpoints.SoapOnly)
  53.             {
  54.                 host.AddServiceEndpoint(typeof(ITest), new BasicHttpBinding(), "");
  55.             }
  56.  
  57.             host.Open();
  58.             Console.WriteLine("Host opened");
  59.  
  60.             XElement xe = XElement.Load("https://localhost:8000/service?wsdl");
  61.             File.WriteAllText(@"c:\temp\" + whichEndpoint + ".xml", xe.ToString());
  62.  
  63.             Console.WriteLine("Press ENTER to close");
  64.             Console.ReadLine();
  65.             host.Close();
  66.         }
  67.     }
  68. }

Ok, let’s now run svcutil.exe to generate a proxy for the Web endpoint case. It generates the following proxy class (some classes removed). Notice that it has all the data contracts, and even the service contract with all the operations as we defined. Isn’t it working?

  1. [System.Diagnostics.DebuggerStepThroughAttribute()]
  2. [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Runtime.Serialization", "4.0.0.0")]
  3. [System.Runtime.Serialization.DataContractAttribute(Name="Person", Namespace="https://schemas.datacontract.org/2004/07/ConsoleApplication5")]
  4. public partial class Person : object, System.Runtime.Serialization.IExtensibleDataObject
  5. {
  6.         
  7.     private System.Runtime.Serialization.ExtensionDataObject extensionDataField;
  8.         
  9.     private int AgeField;
  10.         
  11.     private string NameField;
  12.         
  13.     public System.Runtime.Serialization.ExtensionDataObject ExtensionData
  14.     {
  15.         get
  16.         {
  17.             return this.extensionDataField;
  18.         }
  19.         set
  20.         {
  21.             this.extensionDataField = value;
  22.         }
  23.     }
  24.         
  25.     [System.Runtime.Serialization.DataMemberAttribute()]
  26.     public int Age
  27.     {
  28.         get
  29.         {
  30.             return this.AgeField;
  31.         }
  32.         set
  33.         {
  34.             this.AgeField = value;
  35.         }
  36.     }
  37.         
  38.     [System.Runtime.Serialization.DataMemberAttribute()]
  39.     public string Name
  40.     {
  41.         get
  42.         {
  43.             return this.NameField;
  44.         }
  45.         set
  46.         {
  47.             this.NameField = value;
  48.         }
  49.     }
  50. }
  51.  
  52. [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")]
  53. [System.ServiceModel.ServiceContractAttribute(ConfigurationName="ITest")]
  54. public interface ITest
  55. {
  56.     
  57.     [System.ServiceModel.OperationContractAttribute(Action="https://tempuri.org/ITest/CreatePerson", ReplyAction="https://tempuri.org/ITest/CreatePersonResponse")]
  58.     ConsoleApplication5.Person CreatePerson(string name, int age);
  59.     
  60.     [System.ServiceModel.OperationContractAttribute(Action="https://tempuri.org/ITest/Add", ReplyAction="https://tempuri.org/ITest/AddResponse")]
  61.     int Add(int x, int y);
  62. }

No, it’s not working. Notice that although the service contract and all the operations are there, none of the WebInvokeAttribute / WebGetAttribute attributes are, so if we try to use that proxy to call the service, it will fail. Also, it didn’t generate any configuration file (where the address and binding information are stored by the tool).

Looking at the WSDLs which were saved when running the tool sheds some information on that. Using a diff tool (such as WinDiff, which is on the path on the Visual Studio Command Prompt), compare the files called HttpOnly.xml and SoapOnly.xml. The files are exactly the same up to a certain point. Then, the HttpOnly file has an empty <wsdl:service> element, while the SoapOnly.xml file has a lot more information about the service – namely, the binding information and the endpoint address (see below). With that information, the tool is able to create the configuration file which contains that information and lets the client call the service correctly. Also notice that the contract definition (<wsdl:message> and <wsdl:portType> elements) are exactly the same, without any annotation for things such as the WebInvokeAttribute / WebGetAttribute  properties – so the tool has no information about what they should be. Which is, as I think I already mentioned, it doesn’t work.

  1. <wsdl:binding name="BasicHttpBinding_ITest" type="tns:ITest">
  2.   <soap:binding transport="https://schemas.xmlsoap.org/soap/http" />
  3.   <wsdl:operation name="CreatePerson">
  4.     <soap:operation soapAction="https://tempuri.org/ITest/CreatePerson" style="document" />
  5.     <wsdl:input>
  6.       <soap:body use="literal" />
  7.     </wsdl:input>
  8.     <wsdl:output>
  9.       <soap:body use="literal" />
  10.     </wsdl:output>
  11.   </wsdl:operation>
  12.   <wsdl:operation name="Add">
  13.     <soap:operation soapAction="https://tempuri.org/ITest/Add" style="document" />
  14.     <wsdl:input>
  15.       <soap:body use="literal" />
  16.     </wsdl:input>
  17.     <wsdl:output>
  18.       <soap:body use="literal" />
  19.     </wsdl:output>
  20.   </wsdl:operation>
  21. </wsdl:binding>
  22. <wsdl:service name="Service">
  23.   <wsdl:port name="BasicHttpBinding_ITest" binding="tns:BasicHttpBinding_ITest">
  24.     <soap:address location="https://MACHINE_NAME:8000/Service" />
  25.   </wsdl:port>
  26. </wsdl:service>

Another interesting thing to notice: the files SoapOnly.xml and HttpAndSoap.xml are exactly the same. The contract is the same for both endpoints, and if you had more endpoints, they would show up as different bindings (<wsdl:binding>) or different addresses (wsdl:port) – and that information doesn’t exist for web endpoints.

Ok, I’m convinced. It doesn’t work. But why does the WCF provide an incomplete metadata, if it doesn’t work?

The answer to this question is simple, it does because we told WCF to do so, by adding the service metadata behavior. We asked WCF to provide metadata for the service, and it happily did its best effort for that. I think the main question is, should it throw when the service host is opening if there is a Web endpoint in the service, since that metadata is useless?

Well, it’s not really useless. If there are multiple endpoints in the service, one Web endpoint and a SOAP one, having metadata in that case is perfectly valid. With that, SOAP clients can consume it to create a proxy to talk to the service, and the contract for the Web endpoint is somehow shared out-of-band between the client and the service.

Ok, what if there is only one endpoint in the service description, shouldn’t the service then throw in this case? Well, no again. Remember that WCF is (very) extensible, so it’s possible that someone wanted to actually implement a WADL metadata extension and expose that information for their service. We don’t want to block that scenario. The WCF-specific tools (svcutil.exe and Add Service Reference) don’t understand them, but WCF was made to be consumed by any clients, not only WCF (or even .NET) ones, so it’s possible that someone liked the WADL specification enough that they created a tool for their favorite language to create a proxy for it.

And that’s it. Do not try to point svcutil.exe and Add Service Reference to a WCF REST Service and expect it to work. It won’t.