Interop between ASMX and WCF Services
My esteemed colleague, Kevin, is working on a project where the customer has a bunch of ASP.NET (ASMX) web services, and they are looking at migrating to WCF. The customer's goal is simplicity: in stage 1, they'd like to keep their ASMX files, keep the IIS hosting, but add a .svc file to also host the service also in WCF. In stage 2, they'd like to turn off the ASMX endpoint and use only the WCF (.svc) endpoint. All this while keeping the existing client code unchanged: no new code, no re-compile, nothing. A .NET app with a web service stub generated from wsdl.exe should be able to talk to the new service hosted in WCF.
This ought to be pretty simple, right? Kevin found an MSDN article on this topic, How to: Migrate a ASP.NET Web Service Code to the Windows Communication Foundation. This article seems to work, though it doesn't address the specific requirements for Kevin's customer, which included simplicity. Following this article, which by the way is the "right" way to migrate from ASP.NET to WCF, you essentially re-implement everything in WCF. Isn't there a simpler way?
What Kevin did was pretty straightforward and sensible, and he thought it should work. He had a simple .ASMX file, which looked like this:
<%@ WebService Language="C#" CodeBehind="~/App_Code/CommunicationService.cs" Class="CommunicationService" %>
The code-behind file contained the implementation of the service. It looked like this:
[WebService(Namespace = "urn:namespace1")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class CommunicationService : System.Web.Services.WebService
{
public CommunicationService() { }
[WebMethod]
public ComplexType SendCommunication(CommunicationEntity Request) {...}
...
Kevin created a new .svc file for the WCF stuff:
<%@ ServiceHost Language="C#" CodeBehind="~/App_Code/CommunicationService.cs" Service="CommunicationService" %>
And then he decorated the existing web service implementation class with the appropriate WCF attributes, something like this:
[WebService(Namespace = "urn:namespace1")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[ServiceContract(Namespace= "urn:namespace1")]
[XmlSerializerFormat]
public class CommunicationService : System.Web.Services.WebService
{
public CommunicationService() { }
[WebMethod]
[OperationContract(Action= "SoapAction1Here")]
public ComplexType SendCommunication(CommunicationEntity Request) {...}
...
The OperationContract attribute decorates the service method, like WebMethod does for ASP.NET. The XmlSerializerFormat attribute on the service class tells WCF to not use the default DataContract serialization behavior, but instead to use the XmlSerializer. This, ostensibly, to maintain consistency and compatibility with data types that have been decorated with the XmlSerializer attributes, like XmlElement, XmlIgnore, XmlType, and so on.
This all sounds pretty easy right? The problem is that the actual XML messages expected by WCF for this service, were different than the XML messages expected and accepted by ASP.NET. Now it's XML, so, we don't need an exact string match. We need equivalence of the Xml Infoset. That means, element names must be the same, XML namespaces must be the same, but prefixes attached to the namespaces can be different. But we did not have infoset equivalence! The first puzzle was, WHY?
As in many scenarios involving interop between different web services libraries, it's the XML namespaces that cause lots of insidious problems. In this case, the ASP.NET-hosted web service was serializing and de-serializing the CommunicationEntity, using an XML namespace derived from the XmlTypeAttribute attached to that type definition. On the other hand, WCF was serializing the same object using the XML namespace associated to the ServiceContract. If those two xml namespaces are the same, no problem. But in general, they are different, and in this specific case, they were different. The result was that ASMX clients (and by this I mean .NET apps compiled with webservice stubs generated from wsdl.exe in .NET 2.0) would send a serialized XML stream to the WCF service, which was ever so close to the format WCF wanted. But close doesn't cut it, and so WCF would not de-serialize the request, and the app received a null instance for CommunicationEntity.
This stinks.
You would think it would be automatic, but it sure isn't.
The next puzzle was, how to fix this? If you are familiar with the ASP.NET and XmlSerializer model, there are lots of attributes you can apply to types to specify the namespaces to use, the element names to use, and so on. It's very flexible, so flexible that some people find it unfathomable. The WCF serializer is much simpler, which is nice, but on the downside, there is no special attribute that WCF uses to specify the namespace here.
After a little spelunking, I found what I think is a useful hack to work around this problem. It is a custom ServiceHost, which, in the OnOpening() event, changes the MessageParts where the part namespace disagrees with the namespace specified in the XmlTypeAttribute associated to the CLR type for that message part. Huh?
What this means is, now the WCF endpoint and the ASP.NET endpoint serialize and de-serialize the same way, for complex message types. Even better, when you query the WCF service for metadata, you get the "correct" WSDL – with the updated xml namespace.
The ServiceHost uses reflection to inspect the message parts and decide on whether to change the XML namespace or not. Reflection is expensive at runtime, but this happens only once, when the service is opened. So it should have essentially no impact, after startup of the service.
Here's the code:
using System;
using System.ServiceModel;
using System.ServiceModel.Description;
// if your original .svc file was:
// <%@ ServiceHost Language="C#" Debug="true" Service="CommunicationService" CodeBehind="~/App_Code/CommunicationService.cs" %>
//
// then use this, to get the custom service host:
// <%@ ServiceHost Language="C#" Debug="true" Factory="dinoch.wcf.fixup.EnforceXmlTypeServiceHostFactory"
// Service="CommunicationService" CodeBehind="~/App_Code/CommunicationService.cs" %>
//
namespace dinoch.wcf.fixup
{
/// <summary>
/// This custom ServiceHost modifies the ServiceDescription to use the "preferred"
/// namespaces on message parts. The ServiceDescription is used by the WCF service
/// in generating metadata (eg WSDL) and also in driving the serialization behavior.
///
/// We do this because the default behavior in WCF when using the XmlSerializer format
/// (via the XmlSerializerFormat attribute) is to NOT use the namespace specified in the
/// XmlTypeAttribute, if such an attribute is attached to the data type. This means that
/// WCF services will not accept the same input messages as ASMX Services, and will not
/// generate the same response messages as ASMX services - they will differ by XML namespace.
///
/// The way this works - on opening the service, which happens before WSDL is generated and
/// before any messages and dispatched to methods, this ServiceHost modifies the ServiceDescription
/// of the service. It examines message parts, looks for XmlTypeAttributes attached to the
/// types associated to those message parts, and then compares the xml namespace used in the message
/// part to the xml namespace specified in the XmlTypeAttribute. If they namespaces differ, then
/// this ServiceHost overrides the default namespace, and uses the namespace from the XmlTypeAttribute
/// in its stead.
///
/// Because this ServiceHost modifies the ServiceDescription before starting up the service, at runtime,
/// this technique has the same effect as applying an (imaginary) attribute
/// to the service implementation code; the metadata (WSDL) generated from this service
/// will indicate the desired namespace for the message parts.
///
/// This ServiceHost has no effect on service operations that do not use the XmlSerializer.
///
/// There is one catch: running svcutil.exe over the assembly
/// (as opposed to the service itself, hosted and running within IIS) will not
/// detect the proper namespaces.
///
/// To use this service host, use something like this in your .svc file:
/// <%@ ServiceHost
/// Language="C#"
/// Debug="true"
/// Factory="dinoch.wcf.fixup.EnforceXmlTypeServiceHostFactory"
/// Service="CommunicationService"
/// CodeBehind="~/App_Code/CommunicationService.cs"
/// %>
///
/// For some additional background, see https://msdn2.microsoft.com/en-us/library/Aa395224.aspx
///
/// </summary>
public class EnforceXmlTypeServiceHost : ServiceHost
{
public EnforceXmlTypeServiceHost(Type t, params Uri[] baseAddresses) : base(t, baseAddresses) { }
public EnforceXmlTypeServiceHost(object singletonInstance, params Uri[] baseAddresses)
: base(singletonInstance, baseAddresses) { }
public EnforceXmlTypeServiceHost() : base() { }
protected override void OnOpening()
{
ServiceEndpointCollection sec = this.Description.Endpoints;
foreach (ServiceEndpoint se in sec)
{
if (se.Address.ToString().EndsWith(".svc")) // eliminate mex endpoints
{
foreach (OperationDescription opDesc in se.Contract.Operations)
{
// Are we using XmlSerializer?
if (opDesc.Behaviors.Find<XmlSerializerOperationBehavior>() != null)
{
foreach (MessageDescription messDesc in opDesc.Messages)
{
for (int i = 0; i < messDesc.Body.Parts.Count; i++)
{
MessagePartDescription part = messDesc.Body.Parts[i];
System.Reflection.MemberInfo info = (System.Reflection.MemberInfo)part.Type;
System.Xml.Serialization.XmlTypeAttribute[] attributes =
(System.Xml.Serialization.XmlTypeAttribute[])
info.GetCustomAttributes(typeof(System.Xml.Serialization.XmlTypeAttribute), true);
if ((attributes!= null) && (attributes.Length > 0))
// check to see if the xml namespace specified to the XmlSerializer via
// the XmlTypeAttribute attribute, disagrees with the xml namespace used
// in the WCF message description.
if (attributes[0].Namespace != part.Namespace)
{
// the namespaces do not agree. we need to swap them.
// this won't work, the property is readonly
// messDesc.Body.Parts[i].Namespace = attributes[0].Namespace;
// Duplicate the old part description, except for the XML namespace
MessagePartDescription newPart = new MessagePartDescription(part.Name, attributes[0].Namespace);
newPart.Index = part.Index;
newPart.MemberInfo = part.MemberInfo;
newPart.Multiple = part.Multiple;
newPart.ProtectionLevel = part.ProtectionLevel;
newPart.Type = part.Type;
// Replace the old part with the new one
messDesc.Body.Parts[i] = newPart;
}
}
}
}
}
}
}
base.OnOpening();
}
}
public class EnforceXmlTypeServiceHostFactory : System.ServiceModel.Activation.ServiceHostFactory
{
protected override ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses)
{
return new EnforceXmlTypeServiceHost(serviceType, baseAddresses);
}
}
}
And here's how you specify the custom ServiceHost in the .svc file:
<%@ ServiceHost
Language="C#"
Factory="dinoch.wcf.fixup.EnforceXmlTypeServiceHostFactory"
CodeBehind="~/App_Code/CommunicationService.cs"
Service="CommunicationService" %>
I hope this is useful! The same principle would apply, of course, when migrating from ASMX to WCF on the server side, if you are using a Java-based client, or a web service client on any other platform. So this definitely falls in the arena of interop. If any of you have other requests on interop topics relating to WCF, let me hear 'em!
Cheers,
-Dino
Comments
Anonymous
July 19, 2007
Thanks for sharing this. One of the problems I am facing with the migration of ASMX to WCF is the security aspect using basicHttpBinding under IIS. In asmx I could restrict access to the ASMX webservice to particular users (uses Windows Authentication) by using the <security/> element in the web.config file. It worked very well and is very easy to accomplish. However I am struggling to implement the same in WCF. Any ideas you can share on this? Thanks.Anonymous
July 24, 2007
I'm currently working on an interop with WCF in which I need to receive server driven async callbacks from WCF service in non .NET platform clients (Mono). I'm exploring several ways of doing this. One is to create a custom channel in which I serialize all of the callbacks via a socket we maintain (requiring custom code on the client). The other is to create legacy webservices on the client and 'callback' into those. At that point, I would need to hook the legacy webservice back into the client app as well. Any thoughts? At this point I'm leaning towards a custom channel. I've got a preliminary prototype working.Anonymous
July 27, 2007
Have you recently tried to migrate an existing ASMX Web service to Windows Communication Foundation,Anonymous
July 27, 2007
Dave, sounds like you could just use "legacy webservices". When you say Mono, I am assuming the implication is "no WCF". I think that is right. WCF intercommunicates with other webservices stacks, though, including, one would suppose, ASMX on Mono. It's a mono "client" app, but you can host the ASP.NET runtime in a "client" app, in which case the client app becomes both a client and a server. The cassini web server is a great example of this, there's source code you can learn from. This should work nicely.Anonymous
July 29, 2007
Thanks for the reply. I'm currently working on a prototype in Mono that will allow me to host an asmx service within a client. I've got it working in .NET (before I try porting it to Mono) and I'm trying to work out how I can communicate back into the main application (say, update a textbox on a form) from the asmx implementation. In fact, if I can do it this way, it will fit nicely into the pub/sub architecture that we are looking at implementing. Let me know if I'm off base. Once I get something that is somewhat working, I'll let you all know. I'm using the example from http://msdn.microsoft.com/msdnmag/issues/04/12/ServiceStation/default.aspx that Aaron Skonnard put together. I found another but it looked like it was based on some MS specific namespaces (http://msdn2.microsoft.com/EN-US/library/aa529311.aspx) Thanks again. I'll post some samples on my blog if I can.Anonymous
August 20, 2007
I wrote about Christian's foray into this area a while back . Now comes news that other people are confrontingAnonymous
August 20, 2007
I wrote about Christian's foray into this area a while back . Now comes news that other people areAnonymous
October 03, 2007
Thanks Dino. You saved my day. This fixed the issues with the request schema. I had to extend this to the response schema also...I.e. modify the messDesc.Body.ReturnValue. :)Anonymous
September 22, 2008
I am constantly developing new WCF services to try out various techniques, ideas, scenarios. Many times