While investigating how to make C# and PHP interact with each other I run into an interesting extension of WCF Service Model for working over XML-RPC protocol. I played a bit with this extension and after fixing some bug in its source code I made it work with my little custom XML-RPC server implemented as Joomla XML-RPC plugin.
The bug was in the XML parsing code. For example, if my XML-RPC server returns the following XML:
<methodResponse> <params> <param> <value> <struct> <member> <name>total_charcount</name> <value> <i4>11005</i4> </value> </member> <member> <name>total_spacecount</name> <value> <i4>1433</i4> </value> </member> </struct> </value> </param> </params> </methodResponse>
I got XmlException with message “Start element ‘member’ expected. Found end element ‘struct’ from namespace ”. Line 28, position 10.’ To fix this problem I made the following changes in XmlRpcDataContractSerializer.cs:
private static object DeserializeStruct(XmlDictionaryReader reader, Type targetType) { if (targetType.IsDefined(typeof(DataContractAttribute), false)) { Dictionary<string, MemberInfo> dataMembers = GetDataMembers(targetType); object targetObject = Activator.CreateInstance(targetType); reader.ReadStartElement(XmlRpcProtocol.Struct); //Fixed by Dmitriano //reader.NodeType returns Whitespace instead of some element //so I replaced it with reader.MoveToContent() //while (reader.NodeType != XmlNodeType.EndElement) while (reader.MoveToContent() != XmlNodeType.EndElement) { string memberName; reader.ReadStartElement(XmlRpcProtocol.Member); reader.ReadStartElement(XmlRpcProtocol.Name); memberName = reader.ReadContentAsString(); reader.ReadEndElement(); reader.ReadStartElement(XmlRpcProtocol.Value); reader.MoveToContent(); if (dataMembers.ContainsKey(memberName)) { MemberInfo member = dataMembers[memberName]; if (member is PropertyInfo) { ((PropertyInfo)member).SetValue( targetObject, Deserialize(reader, ((PropertyInfo)member).PropertyType), BindingFlags.Instance | BindingFlags.SetProperty | BindingFlags.Public | BindingFlags.NonPublic, null, null, CultureInfo.CurrentCulture); } else if (member is FieldInfo) { ((FieldInfo)member).SetValue( targetObject, Deserialize(reader, ((FieldInfo)member).FieldType), BindingFlags.Instance|BindingFlags.SetField|BindingFlags.Public|BindingFlags.NonPublic, null, CultureInfo.CurrentCulture); } } reader.ReadEndElement(); // value reader.ReadEndElement(); // member } reader.ReadEndElement(); // struct reader.MoveToContent(); return targetObject; } else { throw new InvalidOperationException(); } }
Now it can even parse more complex data structures like this:
<methodResponse> <params> <param> <value> <struct> <member> <name>total_charcount</name> <value> <i4>11005</i4> </value> </member> <member> <name>total_spacecount</name> <value> <i4>1433</i4> </value> </member> <member> <name>summaries</name> <value> <array> <data> <value> <struct> <member> <name>article_id</name> <value> <i4>272</i4> </value> </member> <member> <name>title</name> <value> <string>Аллантоин</string> </value> </member> <member> <name>charcount</name> <value> <i4>1476</i4> </value> </member> <member> <name>spacecount</name> <value> <i4>189</i4> </value> </member> </struct> </value> <value> <struct> <member> <name>article_id</name> <value> <i4>274</i4> </value> </member> <member> <name>title</name> <value> <string>Кислота стеариновая 98%</string> </value> </member> <member> <name>charcount</name> <value> <i4>1505</i4> </value> </member> <member> <name>spacecount</name> <value> <i4>196</i4> </value> </member> </struct> </value> </data> </array> </value> </member> </struct> </value> </param> </params> </methodResponse>
in C# this XML turns into
[DataContract(Namespace="http://site.com/data")] public struct ContentSummary { [DataMember] public int article_id; [DataMember] public string title; [DataMember] public int charcount; [DataMember] public int spacecount; }; [DataContract(Namespace="http://site.com/data")] public struct TotalContentSummary { [DataMember] public int total_charcount; [DataMember] public int total_spacecount; [DataMember] public ContentSummary[] summaries; };
After doing some further experimentation I realized that this handmade extension can not handle ObservableCollection or Generic.List. If I generate WCF client proxy in a way described in article How To Create A WCF Client Proxy Without Having a Deployed WCF Service and specify ObservableCollection as the collection type with the following commands:
svcutil /d:.\gen MyApp.exe svcutil *.wsdl *.xsd /l:C# /out:Reference.cs /noconfig /s /ct:System.Collections.ObjectModel.ObservableCollection`1 /ser:Auto /tcv:Version35 /edb /r:"c:\Progra~1\Reference Assemblies\Microsoft\Framework\v3.0\WindowsBase.dll"
it throws InvalidOperationException in XmlRpcDataContractSerializer.cs at this point:
public static object Deserialize(XmlDictionaryReader reader, Type targetType) { object returnValue = null; if (reader.IsStartElement()) { switch (reader.LocalName) { case XmlRpcProtocol.Nil: returnValue = null; break; case XmlRpcProtocol.Bool: returnValue = Convert.ChangeType((reader.ReadElementContentAsInt()==1),targetType); break; case XmlRpcProtocol.ByteArray: if (targetType == typeof(Stream)) { returnValue = new MemoryStream(reader.ReadElementContentAsBase64()); } else { returnValue = Convert.ChangeType(reader.ReadElementContentAsBase64(), targetType); } break; case XmlRpcProtocol.DateTime: returnValue = Convert.ChangeType(reader.ReadElementContentAsDateTime(),targetType); break; case XmlRpcProtocol.Double: returnValue = Convert.ChangeType(reader.ReadElementContentAsDouble(),targetType); break; case XmlRpcProtocol.Int32: case XmlRpcProtocol.Integer: returnValue = Convert.ChangeType(reader.ReadElementContentAsString(),targetType); break; case XmlRpcProtocol.String: if (targetType == typeof(Uri)) { returnValue = new Uri(reader.ReadElementContentAsString()); } else { returnValue = Convert.ChangeType(reader.ReadElementContentAsString(), targetType); } break; case XmlRpcProtocol.Struct: returnValue = DeserializeStruct(reader, targetType); break; case XmlRpcProtocol.Array: if (targetType.IsArray || targetType is IEnumerable || targetType is IList || targetType is ICollection) { reader.ReadStartElement(XmlRpcProtocol.Array); ArrayList arrayData = new ArrayList(); reader.ReadStartElement(XmlRpcProtocol.Data); reader.MoveToContent(); while (reader.IsStartElement(XmlRpcProtocol.Value)) { reader.ReadStartElement(); arrayData.Add(Deserialize(reader, targetType.GetElementType())); reader.ReadEndElement(); reader.MoveToContent(); } if (reader.NodeType == XmlNodeType.EndElement && reader.LocalName == XmlRpcProtocol.Data) { reader.ReadEndElement(); } reader.ReadEndElement(); if (targetType is IEnumerable || targetType is IList || targetType is ICollection) { returnValue = arrayData; } else { returnValue = arrayData.ToArray(targetType.GetElementType()); } } else { throw new InvalidOperationException(); } break; } } return returnValue; }
so I need to leave default collection type that is Array and generate client proxy with the following command:
svcutil *.wsdl *.xsd /l:C# /out:Reference.cs /noconfig /s /ser:Auto /tcv:Version35 /edb
Works wonderfully! 🙂