Thursday, August 21, 2008

Multiple WCF Endpoints & .NET 2.0 Clients

I recently ran into a problem where .NET 2.0 clients were having problems connecting to the enterprise .NET 3.0 WCF web services platform that we built for internal use. We are hosting all of the web services in IIS6 (no upgrade to IIS7 anytime in the near future) and everything was working great for anyone running .NET 3.0 or later on their client apps.
The root of the problem was that we had only implemented one Endpoint, using the WSHttpBinding. This was great for any clients running .NET 3.0 or later, but this binding doesn't provide any backwards compatibility for clients running a framework earlier than .NET 3.0.

Endpoint Comparison Chart (plus Client .NET Framework column)

(This comparison chart was created by Aaron Skonnard and the original can be found here: http://www.pluralsight.com/community/blogs/aaron/archive/2007/03/22/46560.aspx)
image
So the answer seems simple enough right? If I need .NET 2.0 clients to connect to my WCF web services then all I need to do is to add an Endpoint that uses the BasicHttpBinding and voila, I should be good to go right? Wrong - that is where my troubles started.

WSHttpBinding vs. BasicHttpBinding

There are some MAJOR differences between the two bindings and for all intents & purposes you should stay away from using the BasicHttpBinding whenever possible. However in some cases (like the one I found myself in) you need that backwards compatibility with .NET 2.0 clients and the BasicHttpBinding is the only way to get it. I would only recommend using the BasicHttpBinding in an intranet scenario however & in conjunction with Windows Authentication.
A great article on the differences between the bindings can be found here: http://www.topxml.com/rbnews/WSCF/WCF/re-89303_WCF---BasicHttpBinding-compared-to-WSHttpBinding-at-SOAP-packet-level-.aspx
A great post on how to use Windows Authentication with the BasicHttpBinding can be found here: http://www.rickgaribay.net/archive/2007/04/04/recipe-wcf-basichttpbinding-with-windows-authentication.aspx
NOTE: I am writing a follow up post on getting the BasicHttpBinding with Windows Authentication and a .NET 2.0 client.

Creating Multiple WCF Endpoints

So according to this article: http://www.devx.com/codemag/Article/33655/1763/page/4 adding an Endpoint that uses BasicHttpBinding should be fairly straight forward & easy. I would end up with something like this:
In cases where you might want to expose multiple endpoints for the same .svc endpoint, you can use relative addressing. This example illustrates a case where clients can access the same Service.svc endpoint using BasicHttpBinding or WSHttpBinding to support different Web service protocols. The <endpoint> configuration for the service uses relative addressing, appending "/Soap11" and "/Soap12" to the endpoint address:
   <service name="HelloIndigo.HelloIndigoService" >
<endpoint address="Soap11"
contract="HelloIndigo.IHelloIndigoService"
binding="basicHttpBinding"/>
<endpoint address="Soap12"
contract="HelloIndigo.IHelloIndigoService"
binding="wsHttpBinding"/>
</service>

Clients address each service endpoint in this way:

   http://localhost/IISHost/Service.svc/Soap11
http://localhost/IISHost/Service.svc/Soap12
Ok, great, no problemo. I just need to add an Endpoint that uses the BasicHttpBinding and assign it's address attribute a value. To simplify things, I created a quick test WCF Service host & a client (both are downloadable at the bottom of this post.)
1: <services>
   2:     <service name="Service" behaviorConfiguration="ServiceBehavior">
   3:         <!-- Service Endpoints -->
   4:         <endpoint 
   5:             address="ws" 
   6:             binding="wsHttpBinding" 
   7:             contract="IService" />
   8:         <endpoint 
   9:             address="basic" 
  10:             binding="basicHttpBinding" 
  11:             contract="IService" />
  12:         <endpoint 
  13:             address="mex" 
  14:             binding="basicHttpBinding" 
  15:             contract="IMetadataExchange"  />
  16:     </service>
  17: </services>


And based on the articles I read my client svc URIs should look like this:

http://localhost:12345/WCFMultipleEndpointTest/Service.svc/ws

http://localhost:12345/WCFMultipleEndpointTest/Service.svc/basic



(the 12345 will be replaced by whatever dynamic port number the built in Visual Studio host assigns when you run the host locally.)

Will The Real URI Please Stand Up

So since we added endpoint address attributes to all of our endpoints and we have different URIs now for each endpoint we should use the new URIs when trying to connect to our services right? Wrong.
Even though you now have different URIs for each endpoint, you still use the base ".svc" URI when connecting clients to the service. I.E. to connect to the BasicHttpBinding Enpoint you would use

"http://localhost:12345/WCFMultipleEndpointTest/Service.svc"

and to connect to the WsHttpBinding Endpoint you would also use

"http://localhost:12345/WCFMultipleEndpointTest/Service.svc"


When you add a reference to these endpoints using Visual Studio a client connection will actually be specified for both endpoints if you are adding a Service Reference. If you are adding a Web Reference a connection will be configured for the BasicHttpBinding endpoint only as it is the only endpoint the Web Reference is compatible with.
Example client web.config Endpoint configurations:
   1: <client>
   2:     <endpoint address="http://localhost:1564/WCFMultipleEndpointTest/Service.svc/ws" binding="wsHttpBinding" bindingConfiguration="WSHttpBinding_IService" contract="TestServiceReference.IService" name="WSHttpBinding_IService">
   3:         <identity>
   4:             <userPrincipalName value="youridentity@domainname.com"/>
   5:         </identity>
   6:     </endpoint>
   7:     <endpoint address="http://localhost:1564/WCFMultipleEndpointTest/Service.svc/basic" binding="basicHttpBinding" bindingConfiguration="BasicHttpBinding_IService" contract="TestServiceReference.IService" name="BasicHttpBinding_IService"/>
   8: </client>


Notice that even though you used the base .svc URI to address the web service, svcutil.exe created proxy classes to connect to both endpoints using the full address of each endpoint

Client Code

Here is the sample code that connects to the WsHttpBinding & BasicHttpBinding Endpoints using the proxy created by svcutil.exe and the code that connects to the BasicHttpBinding using the proxy created by wsdl.exe.
' Create a web service client using .NET 3.0+ & the WSHttpBinding
Dim wsServiceClient As New TestServiceReference.ServiceClient("WSHttpBinding_IService")

Try
' Open a connection to the web service

' Call the web service method and get the response


Dim wsResponse As String = wsServiceClient.GetData(255)




' Write out the response


Response.Write("<strong>Service Reference WsHttpBinding GetData Call:</strong> " + wsResponse)




Catch fx As System.ServiceModel.FaultException




' Show the fault we encountered


Response.Write("WsHttpBinding call failed. Fault Exception")


Response.Write("<br /><br />")


Response.Write(fx.Message)


Response.Write("<br /><br />")


Response.Write(fx.StackTrace)




Catch ex As Exception




' Show the error we encountered


Response.Write("WsHttpBinding call failed. General Exception")


Response.Write("<br /><br />")


Response.Write(ex.Message)


Response.Write("<br /><br />")


Response.Write(ex.StackTrace)




Finally




' If the connection was opened, close it


If (wsServiceClient.State = System.ServiceModel.CommunicationState.Opened) Then


wsServiceClient.Close()


End If




' If the connection was faulted, abort it


If (wsServiceClient.State = System.ServiceModel.CommunicationState.Faulted) Then


wsServiceClient.Abort()


End If




End Try




Response.Write("<br /><br />")




' Create a web service client using .NET 3.0+ & the BasicHttpBinding


Dim basicServiceClient As New TestServiceReference.ServiceClient("BasicHttpBinding_IService")




Try




' Open a connection to the web service


basicServiceClient.Open()




' Call the web service method and get the response


Dim basicResponse As String = basicServiceClient.GetData(100)




' Write out the response


Response.Write("<strong>Service Reference BasicHttpBinding GetData Call:</strong> " + basicResponse)




Catch fx As System.ServiceModel.FaultException




' Show the fault we encountered


Response.Write("BasicHttpBinding call failed. Fault Exception")


Response.Write("<br /><br />")


Response.Write(fx.Message)


Response.Write("<br /><br />")


Response.Write(fx.StackTrace)




Catch ex As Exception




' Show the error we encountered


Response.Write("BasicHttpBinding call failed. General Exception")


Response.Write("<br /><br />")


Response.Write(ex.Message)


Response.Write("<br /><br />")


Response.Write(ex.StackTrace)




Finally




' If the connection was opened, close it


If (basicServiceClient.State = System.ServiceModel.CommunicationState.Opened) Then


basicServiceClient.Close()


End If




' If the connection was faulted, abort it


If (basicServiceClient.State = System.ServiceModel.CommunicationState.Faulted) Then


basicServiceClient.Abort()


End If




End Try




Response.Write("<br /><br />")




' Create a web service client using .NET 2.0 & the BasicHttpBinding - This is the only binding that will work for .NET 2.0


' clients and is provided for backwards compatibility


Try




' Create the web service client


Dim basicWebClient As New TestServiceReferenceBackwardsCompatibility.Service()




' Call the web service


Response.Write("<strong>Web Refrence BasicHttpBinding GetData Call:</strong> " + basicWebClient.GetData(3, True))




Catch ex As Exception




' Show the error we encountered


Response.Write("Web Reference BasicHttpBinding call failed. General Exception")


Response.Write("<br /><br />")


Response.Write(ex.Message)


Response.Write("<br /><br />")


Response.Write(ex.StackTrace)




End Try



Code Download

The sample host & client projects I put together for this post are available for download below.

- Aaron

http://www.churchofficeonline.com

kick it on DotNetKicks.com

4 comments:

Anonymous said...

Solved my problem, Thanks a lot

Anonymous said...

loudest engagements coal ontological recognize renamed leavis senda fins hong destined
masimundus semikonecolori

Anonymous said...

Should it really be a space in the url after Service.svc ?

Aaron Schnieder said...

No, the space was a typo. Thanks for catching that, I removed it. :)