Integrating C# and PHP using XML-RPC and WCF

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

1 Response to Integrating C# and PHP using XML-RPC and WCF

  1. Zack Loveless on December 7, 2012 at 5:05 AM:

    Works wonderfully! 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *