Sometimes people change their mind and feel better when a parameter named green gets its name changed according its meaning in code, e.g. isProceedingAllowed. We all know that this is a kind of simplest refactoring steps improving code readablity. Why not to do it? It shouldnt break anything, it can be done even manually because parameter name scope is local to the method and if we keep rule of short and overviewable methods then it is a simple task.
But we can make a big mistake.
Renaming a parameter of an old-fashioned ASMX webservice method will break the SOAP contract resulting in magic problems!
Here is a demo code:
[WebMethod("A fancy method doing extraordinary things with its parameter")] public string CheckSMS(string strSMS) { // an undiscloseable thing happen here... }
If I would rename the parameter from Hungarian notation to smsNumber that parameter value would be always null until the callers refresh their access code or proxies to reflect change in WSDL we triggered via this rename. And these callers may spread around the whole world or simply sit at one hardly moving client, so the refresh wont be possible in acceptable time (people live only 70-80 years).
That null can be really anoying. Your webmethod may have other parameters which all get their right values from the caller, but the renamed one gets nothing. Without any signs of error. In not-so-often-used situation You may already forget about the rename and only get the issue ticket about some missbehaving service which throws NullReferenceExceptions with deep stacktraces in Your business logic (eh, You didnt read my article about Method implementation pattern? That exception must be thrown at all public entrypoints!). After checking the caller that it is really transmits that parameter value, and after checking that Your code really forwards it well You may think about communication related things.
The asmx trace can be switched on quickly by adding the following in web.config:
<configuration> <system.diagnostics> <trace autoflush="true" /> <sources> <source name="System.Web.Services.Asmx"> <listeners> <add name="AsmxTraceFile" type="System.Diagnostics.TextWriterTraceListener" initializeData="asmxtrace.log" traceOutputOptions="LogicalOperationStack, DateTime, Timestamp, ProcessId, ThreadId" /> </listeners> </source> </sources> <switches> <add name="System.Web.Services.Asmx" value="Verbose" /> </switches> </system.diagnostics> </configuration>
In log file You will notice the warning below:
System.Web.Services.Asmx Warning: 0 : Az adott kontextusban egy nem várt <strSMS xmlns='http://tempuri.org/'>..</strSMS> elem szerepel. A várt elemek: http://tempuri.org/:smsNumber.
Yes, it is in hungarian because the given locale there, but try to figure out, okay? 🙂
So how can we handle this?
We can always instruct XmlSerializer to use an alias name:
public string CheckSMS([XmlElement("strSMS")]string smsNumber)
But the result dont helps to much: our webmethod will be okay with the new name, but all the callers may use only the obsolete one. No fallbacks, cannot use multiple names for a parameter.
There is a XmlChoiceIdentifierAttribute which – according to its AttributeTargets may be used on parameters too, but I couldnt figure out how. All examples are about classes and their members not method parameters.
It would be nice to have an attribute which we could use like this even multiple times (because we cannot fix our always changing mind; naming is a BIG problem…):
[ParameterNameChangedSoapExtension("smsNumber", "strSMS")] [ParameterNameChangedSoapExtension("smsNumber", "SMS")] public string CheckSMS(string smsNumber)
Here came the SoapExtensions in the picture.
You can chain Your code into SOAP request-response (yes, must into both of them) processing.
Searching the net for SoapExtension You will find some examples and quickly realise that they are almost the same: how to log the contents of incoming and outgoing SOAP messages. But we need to modify those messages which isnt well documented. Even MSDN article named “Altering the SOAP Message Using SOAP Extensions” has only that logging sample which is the root of all other samples I mentioned before I think.
This is the reason of this post. It took hours to figure out how it works and how can I achive my goal here.
The results must be shared. Here comes the code:
public class ParameterNameChangedSoapExtension : SoapExtension { private Stream streamChainedAfterUs = null; private Stream streamChainedBeforeUs = null; private const int STREAMBUFFERSIZE = 65535; private ParameterNameChangedSoapExtensionAttribute parameterNameChangedSoapExtensionAttribute = null; public override Stream ChainStream(Stream stream) { if (stream == null) { throw new ArgumentNullException("stream"); } Stream ret = null; this.streamChainedBeforeUs = stream; this.streamChainedAfterUs = new MemoryStream(); ret = this.streamChainedAfterUs; return ret; } public override object GetInitializer(Type serviceType) { throw new NotSupportedException(); } public override object GetInitializer(LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute) { if (attribute == null) { throw new ArgumentNullException("attribute"); } object ret = attribute; return ret; } public override void Initialize(object initializer) { if (initializer == null) { throw new ArgumentNullException("initializer"); } parameterNameChangedSoapExtensionAttribute = initializer as ParameterNameChangedSoapExtensionAttribute; // sanity if (parameterNameChangedSoapExtensionAttribute == null) { throw new InvalidOperationException(String.Format("initializer must be of type {0}, but its a {1}!", typeof(ParameterNameChangedSoapExtensionAttribute), initializer.GetType())); } } public override void ProcessMessage(SoapMessage message) { if (message == null) { throw new ArgumentNullException("message"); } switch(message.Stage) { case SoapMessageStage.BeforeSerialize: break; case SoapMessageStage.AfterSerialize: // no business here; we are just part of chain so must participate well streamChainedAfterUs.Position = 0; Copy(streamChainedAfterUs, streamChainedBeforeUs); break; case SoapMessageStage.BeforeDeserialize: // here are we doing the magic! UpdateMessage(message); streamChainedAfterUs.Position = 0; break; case SoapMessageStage.AfterDeserialize: break; default: throw new NotImplementedException(message.Stage.ToString()); } } private void UpdateMessage(SoapMessage message) { // get the original raw msg var soapMsgAsString = ReadOriginalSoapMessage(); var soapMsgRootNode = XElement.Parse(soapMsgAsString); // extract namespace info var callDescriptorNode = FindCallDescriptorNode(soapMsgRootNode, message.MethodInfo.Name); var ns = callDescriptorNode.Name.Namespace; // look for parameter named obsolete var originalNameWeLookFor = ns + parameterNameChangedSoapExtensionAttribute.OriginalParameterName; var nodeWithOriginalName = callDescriptorNode.Elements().FirstOrDefault(i => i.Name == originalNameWeLookFor); if (nodeWithOriginalName != null) { // found, lets replace! var nodeWithCurrentName = new XElement(ns + parameterNameChangedSoapExtensionAttribute.CurrentParameterName, nodeWithOriginalName.Value); nodeWithOriginalName.AddAfterSelf(nodeWithCurrentName); nodeWithOriginalName.Remove(); } // write what we had or what we made from it WriteResultSoapMessage(soapMsgRootNode.ToString()); } private XElement FindCallDescriptorNode(XElement soapMsgRootNode, string methodName) { XElement ret = null; var soapBodyName = soapMsgRootNode.Name.Namespace + "Body"; var soapBodyNode = soapMsgRootNode.Elements().First(i => i.Name == soapBodyName); ret = soapBodyNode.Elements().First(i => i.Name.LocalName == methodName); return ret; } private void WriteResultSoapMessage(string msg) { streamChainedAfterUs.Position = 0; using (var sw = new StreamWriter(streamChainedAfterUs, Encoding.UTF8, STREAMBUFFERSIZE, true)) { sw.Write(msg); } } private string ReadOriginalSoapMessage() { string ret = null; using (var sr = new StreamReader(streamChainedBeforeUs, Encoding.UTF8, false, STREAMBUFFERSIZE, true)) { ret = sr.ReadToEnd(); } return ret; } private void Copy(Stream from, Stream to) { using (var sr = new StreamReader(from, Encoding.UTF8, false, STREAMBUFFERSIZE, true)) { using (var sw = new StreamWriter(to, Encoding.UTF8, STREAMBUFFERSIZE, true)) { var content = sr.ReadToEnd(); sw.Write(content); } } } }
First You must understand the thing about ChainStream method and the streams around it. All samples mention them as oldStream and newStream but that isnt correct. Our extension fits inside other extensions in some order. Our input is the output of an other extension or the framework itself and our output will be input for yet another one. And there is a twist: in response processing these steps occure in reverse: the stream which was output before becomes our input now and vica versa! So the old/new or input/output distinction is very misleading thats why I call them streamChainedAfterUs and streamChainedBeforeUs.
According to this usage we should always care about our stream position so the next processing entity can find it at right place.
Between public methods the ProcessMessage is the important one. All others are described in documentation and has kind of infrastructure rules.
As I mentioned before when we write a SoapExtension we must participate in both of request and response processing. Thats the reason of the simple Copy step in ProcessMessage implemetation.
The solution to our problem can be found in UpdateMessage method. We parse the SOAP message, we look for obsolete parameter name and replace it with the current one if found. Thats all.
As a result our webmethod has a “good” parameter name (at the time of this writing, hehehe), generates WSDL with that name, BUT accepts calls with all the obsolete names too!
At the end here is the attribute code to have a full solution:
[AttributeUsage(AttributeTargets.Method, AllowMultiple=true)] public class ParameterNameChangedSoapExtensionAttribute : SoapExtensionAttribute { public override Type ExtensionType { get { return typeof(ParameterNameChangedSoapExtension); } } public override int Priority { get; set; } public string CurrentParameterName { get; private set; } public string OriginalParameterName { get; private set; } public ParameterNameChangedSoapExtensionAttribute(string currentParameterName, string originalParameterName) { if (String.IsNullOrEmpty(currentParameterName)) { throw new ArgumentNullException("currentParameterName"); } if (String.IsNullOrEmpty(originalParameterName)) { throw new ArgumentNullException("originalParameterName"); } this.CurrentParameterName = currentParameterName; this.OriginalParameterName = originalParameterName; } }
Jesus, still reading? This is the 280th line! 🙂