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.
- [DataContract]
- public class Person
- {
- [DataMember]
- public string Name { get; set; }
- [DataMember]
- public int Age { get; set; }
- }
- [ServiceContract]
- public interface ITest
- {
- [OperationContract, WebGet]
- Person CreatePerson(string name, int age);
- [OperationContract, WebInvoke(BodyStyle = WebMessageBodyStyle.WrappedRequest)]
- int Add(int x, int y);
- }
- public class Service : ITest
- {
- public Person CreatePerson(string name, int age)
- {
- return new Person { Name = name, Age = age };
- }
- public int Add(int x, int y)
- {
- return x + y;
- }
- }
- enum Endpoints
- {
- HttpOnly,
- SoapOnly,
- HttpAndSoap,
- }
- class Program
- {
- static void Main(string[] args)
- {
- foreach (Endpoints whichEndpoint in Enum.GetValues(typeof(Endpoints)))
- {
- Console.WriteLine(whichEndpoint);
- string baseAddress = "https://" + Environment.MachineName + ":8000/Service";
- ServiceHost host = new ServiceHost(typeof(Service), new Uri(baseAddress));
- host.Description.Behaviors.Add(new ServiceMetadataBehavior { HttpGetEnabled = true });
- if (whichEndpoint == Endpoints.HttpAndSoap || whichEndpoint == Endpoints.HttpOnly)
- {
- ServiceEndpoint endpoint = host.AddServiceEndpoint(typeof(ITest), new WebHttpBinding(), "web");
- endpoint.Behaviors.Add(new WebHttpBehavior());
- }
- if (whichEndpoint == Endpoints.HttpAndSoap || whichEndpoint == Endpoints.SoapOnly)
- {
- host.AddServiceEndpoint(typeof(ITest), new BasicHttpBinding(), "");
- }
- host.Open();
- Console.WriteLine("Host opened");
- XElement xe = XElement.Load("https://localhost:8000/service?wsdl");
- File.WriteAllText(@"c:\temp\" + whichEndpoint + ".xml", xe.ToString());
- Console.WriteLine("Press ENTER to close");
- Console.ReadLine();
- host.Close();
- }
- }
- }
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?
- [System.Diagnostics.DebuggerStepThroughAttribute()]
- [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Runtime.Serialization", "4.0.0.0")]
- [System.Runtime.Serialization.DataContractAttribute(Name="Person", Namespace="https://schemas.datacontract.org/2004/07/ConsoleApplication5")]
- public partial class Person : object, System.Runtime.Serialization.IExtensibleDataObject
- {
- private System.Runtime.Serialization.ExtensionDataObject extensionDataField;
- private int AgeField;
- private string NameField;
- public System.Runtime.Serialization.ExtensionDataObject ExtensionData
- {
- get
- {
- return this.extensionDataField;
- }
- set
- {
- this.extensionDataField = value;
- }
- }
- [System.Runtime.Serialization.DataMemberAttribute()]
- public int Age
- {
- get
- {
- return this.AgeField;
- }
- set
- {
- this.AgeField = value;
- }
- }
- [System.Runtime.Serialization.DataMemberAttribute()]
- public string Name
- {
- get
- {
- return this.NameField;
- }
- set
- {
- this.NameField = value;
- }
- }
- }
- [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")]
- [System.ServiceModel.ServiceContractAttribute(ConfigurationName="ITest")]
- public interface ITest
- {
- [System.ServiceModel.OperationContractAttribute(Action="https://tempuri.org/ITest/CreatePerson", ReplyAction="https://tempuri.org/ITest/CreatePersonResponse")]
- ConsoleApplication5.Person CreatePerson(string name, int age);
- [System.ServiceModel.OperationContractAttribute(Action="https://tempuri.org/ITest/Add", ReplyAction="https://tempuri.org/ITest/AddResponse")]
- int Add(int x, int y);
- }
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.
- <wsdl:binding name="BasicHttpBinding_ITest" type="tns:ITest">
- <soap:binding transport="https://schemas.xmlsoap.org/soap/http" />
- <wsdl:operation name="CreatePerson">
- <soap:operation soapAction="https://tempuri.org/ITest/CreatePerson" style="document" />
- <wsdl:input>
- <soap:body use="literal" />
- </wsdl:input>
- <wsdl:output>
- <soap:body use="literal" />
- </wsdl:output>
- </wsdl:operation>
- <wsdl:operation name="Add">
- <soap:operation soapAction="https://tempuri.org/ITest/Add" style="document" />
- <wsdl:input>
- <soap:body use="literal" />
- </wsdl:input>
- <wsdl:output>
- <soap:body use="literal" />
- </wsdl:output>
- </wsdl:operation>
- </wsdl:binding>
- <wsdl:service name="Service">
- <wsdl:port name="BasicHttpBinding_ITest" binding="tns:BasicHttpBinding_ITest">
- <soap:address location="https://MACHINE_NAME:8000/Service" />
- </wsdl:port>
- </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.
Comments
- Anonymous
April 15, 2012
Really interesting post.Thank you! - Anonymous
July 11, 2012
Great post, I've been struggling with this. I'm onboard with it not working. However, can you direct me to what I should be doing? I've seen snippets of it in many of your posts, but nothing that is complete. Is there a good guide on what to do instead?Thanks! - Anonymous
July 11, 2012
Brian, if you want to use WCF as a client for a REST service, the simplest way is to literally copy the contracts (the service contract, and any data contracts it may use) to the client application, then use the WebChannelFactory<T> to create a client to talk to the service:WebChannelFactory<IService> factory = new WebChannelFactory<IService>(new Uri(endpointAddress));IService proxy = factory.CreateChannel();int result = proxy.Operation(1, 2, 3); - Anonymous
October 19, 2012
Useful post. Thank you! - Anonymous
April 23, 2013
Hi,I have a .svc URL and can access the WSDL portion.I want to use the service in my Android and iOS application. Generally for WSDL I use SOAP UI, but in SOAP UI it is not able to generate the operations/methods. For one of the login operation, I am getting a JSON response. But for other methods I am not sure how to access the operations.Is there a tool/way to know what will be the sample request and corresponding response for a particular .svc service.Regards,Nirav - Anonymous
April 27, 2013
If you get a JSON response, then the WSDL you get is probably not complete. As I mentioned in this post, if you have a WCF service which only exposes JSON endpoints, you cannot trust the WSDL to consume it, it doesn't have enough information. - Anonymous
April 09, 2014
Thanks for helping.I too was struggling with this. - Anonymous
April 11, 2014
Then how do you consume WCF services from an ASP.net application?In other words, what is necessary in the Web.Config file to make the connection? - Anonymous
April 14, 2014
Mike, to consume REST endpoints you'd normally use regular HTTP clients such as HttpClient, HttpWebRequest or some other HTTP library. You can use the WCF client as well - copy the data and service contracts to your client project (in your case, the ASP.NET app), create a WebChannelFactory<YourServiceContractInterfaceType> passing the endpoint URL for the REST endpoint, and call CreateChannel() on your interface to give you a typed client. - Anonymous
April 29, 2014
This is an older entry, but still helped me figure out why I wasn't able to consume my REST WCF service via Visual Studio. What I found that worked pretty well was to:1) Do the Add Service Reference like usual, to get the service Request/Response model classes.2) Use JSON.NET to serialize and deserialize those objects to JSON, passing them to3) RestSharp which makes the actual REST calls.However, knowing now that Visual Studio's support when doing WCF REST is pretty limited, we'll probably start creating REST endpoints in addition to the normal WCF bindings, rather than replacing them.Thanks! - Anonymous
July 24, 2014
Thanks for your post.I too was struggling with this issue.Can you tell me instead of webHttpBinding which one I should use...I want to call the wcf service in my asp.net application ... - Anonymous
August 08, 2014
@Sugendar, you can still call a webHttpBinding endpoint via ASP.NET - in fact, if you use the HttpClient classes it's fairly simple to do. What you won't have is a typed proxy that you can use in the client, but for REST endpoints this is usually not a big drawback. - Anonymous
October 19, 2014
Sir Very Interesting Explanation....Even i also facing this issue since very long time..and i m not able to fine solution...thanks for such a very good and exact explanation that why its not possible.... - Anonymous
March 10, 2015
Nice post, but due to my limited knowledge of WCF I'm experiencing problem in working properly with it.I created A VB.NET project where a web service (server side) is implemented and correctly working. Then I want to consume this web service into another project, so what I did is to' Enable metadata publishing. Dim smb As New ServiceMetadataBehavior() smb.HttpGetEnabled = True smb.MetadataExporter.PolicyVersion = PolicyVersion.Policy15 host.Description.Behaviors.Add(smb) 'Add MEX endpoint host.AddServiceEndpoint( _ ServiceMetadataBehavior.MexContractName, _ MetadataExchangeBindings.CreateMexHttpBinding(), _ "mex")so into my client project I added the service by Visual Studio with rigth click on Service References and indeed I found my interfaces (take care that the Interface Service Description, and the data definition classes I put them into a class library into the server project and I imported it into the client project)Into the client project I wrote as suggested into this post the following lines in order to consume the webservice:Dim myBinding As New ServiceModel.WebHttpBindingDim factory As System.ServiceModel.Web.WebChannelFactory(Of IFCDMapMatchedResultsWebService) = New System.ServiceModel.Web.WebChannelFactory(Of IFCDMapMatchedResultsWebService)(myBinding, New Uri("http://localhost:8082/fcdmapmatchedresults/mex"))'Create a channel. Dim wcfClient As IFCDMapMatchedResultsWebService = factory.CreateChannel() Dim parameters As New FCDMapMatchedResults.inputParam parameters.id = 1 parameters.treq = "info" parameters.VehicleTrajID = "1"Dim trj As FCDMapMatchedResults_response = wcfClient.PostXML(parameters) '("match", 11, "3-846758BUT i get the following error:"no endpoint listening on "http://localhost:8082/fcdmapmatchedresults/PostXML"SOI added the following URI "http://localhost:8082/fcdmapmatchedresults/PostXML/mex" ad it seems working except that I get the following error now:content type application/xml; charset utf-8 not supported by the service "http://localhost:8082/fcdmapmatchedresults/mex/PostXML"WHAT IS IT? - Anonymous
June 29, 2015
What if we add the WebGet/WebInvoke attribute above the method in the client proxy (generated by Add Service Reference) for the REST service? I saw this solution in some post.