0

My C# project is a class library that provides an easy to use interface to Magento 2. Consumers of the library call methods such as DownloadSalesOrder(string orderId) etc. Internally, the library has a Service Reference to a standard Magento 2 SOAP API WSDL. Visual Studio generates all the code necessary to interact with the service and deserialize the XML in the file Reference.cs. All classes in Reference.cs are declared partial.

All was working well until I discovered that one of the elements (called <extensionAttributes>) within the sales order type was customisable in Magento. For example, one API will return this:

            <extensionAttributes>
              <paymentAdditionalInfo>
                <item>
                  <key>processorResponseText</key>
                  <value>Approved</value>
                </item>
                <item>
                  <key>cc_type</key>
                  <value>Visa</value>
                </item>
              </paymentAdditionalInfo>
              <giftCards>
                <item>
                  <id>14</id>
                  <code>0BVH6GOQ291C</code>
                  <amount>20</amount>
                  <baseAmount>20</baseAmount>
                </item>
              </giftCards>
            </extensionAttributes>

... while another API could return a completely different structure.

The library is intended to be usable with any API, and consuming applications need this data, but obviously the library has no knowledge of the possible data types. The consuming application however would know the data type.

Could I somehow map extensionAttributes to:

  1. a string property, and return the inner XML as string, or
  2. a generically typed property, that the consuming application passes in, e.g. DownloadSalesOrder<MyExtensionAttributes>(string orderId)

All I have access to is hand editing Reference.cs and/or extending via partial classes.

I tried idea #1 by adding a string property like below, but this causes an exception: Error in deserializing body of reply message for operation 'salesOrderRepositoryV1GetList' (which I am not surprised by as <extensionAttributes> contains un-escaped XML).


    public partial class SalesDataOrderInterface
    {
        private string extensionAttributesField;

        [System.Xml.Serialization.XmlElementAttribute(Form=System.Xml.Schema.XmlSchemaForm.Unqualified, Order=136)]
        public string extensionAttributes
        {
            get {
                return this.extensionAttributesField;
            }
            set {
                this.extensionAttributesField = value;
                this.RaisePropertyChanged("extensionAttributes");
            }
        }
    }

Re. idea #2, I cannot make the above partial class generic, i.e. public partial class SalesDataOrderInterface<T> where T : class, because then it is no longer an extension of the original class.

2 Answers 2

0

Try following :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Serialization;

namespace ConsoleApplication167
{
    class Program
    {
        const string FILENAME = @"c:\temp\test.xml";
        static void Main(string[] args)
        {
            XmlReader reader = XmlReader.Create(FILENAME);
            XmlSerializer serializer = new XmlSerializer(typeof(extensionAttributes));
            extensionAttributes attributes = (extensionAttributes)serializer.Deserialize(reader);

        }
    }
    public class extensionAttributes
    {
        [XmlArray("paymentAdditionalInfo")]
        [XmlArrayItem("item")]
        public List<paymentAdditionalInfo> paymentAdditionalInfo { get; set; }
        [XmlArray("giftCards")]
        [XmlArrayItem("item")]
        public List<giftCardsItem> giftCarItem { get; set; }
    }
    public class paymentAdditionalInfo
    {
        public string key { get; set; }
        public string value { get; set; }
    }
    public class giftCardsItem
    {
        public int id { get;set;}
        public string code { get; set; }
        public int amount { get; set; }
        public int baseAmount { get; set; }
    }

 
}

Here is 2nd solution

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
using System.Xml.Schema;
using System.Xml.Linq;

namespace ConsoleApplication1
{
    class Program
    {
        const string INPUT_FILENAME = @"c:\temp\test.xml";
        const string OUTPUT_FILENAME = @"c:\temp\test1.xml";
        static void Main(string[] args)
        {
            XmlReader reader = XmlReader.Create(INPUT_FILENAME);
            XmlSerializer serializer = new XmlSerializer(typeof(extensionAttributes));
            extensionAttributes attributes = (extensionAttributes)serializer.Deserialize(reader);

            XmlWriterSettings settings = new XmlWriterSettings();
            settings.Indent = true;
            XmlWriter writer = XmlWriter.Create(OUTPUT_FILENAME, settings);
            serializer.Serialize(writer, attributes);
        }
    }
    public class extensionAttributes : IXmlSerializable
    {
        public Dictionary<string, List<Dictionary<string,string>>> dict { get;set;}


        // Xml Serialization Infrastructure

        public void WriteXml (XmlWriter writer)
        {
            foreach (KeyValuePair<string, List<Dictionary<string, string>>> key in dict)
            {
                XElement attribute = new XElement(key.Key);
                foreach (Dictionary<string, string> itemDict in key.Value)
                {
                    XElement items = new XElement("item");
                    attribute.Add(items);
                    foreach (KeyValuePair<string, string> item in itemDict)
                    {
                        items.Add(new XElement(item.Key, item.Value));
                    }
                }
                attribute.WriteTo(writer);
            }
        }

        public void ReadXml (XmlReader reader)
        {
            XElement elements = (XElement)XElement.ReadFrom(reader);
            foreach (XElement element in elements.Elements())
            {
                string name = element.Name.LocalName;
                List<Dictionary<string, string>> childDict = element.Elements("item")
                    .Select(x => x.Elements()
                        .GroupBy(y => y.Name.LocalName, z => (string)z)
                        .ToDictionary(y => y.Key, z => z.FirstOrDefault())
                        ).ToList();
                if (dict == null)
                {
                    dict = new Dictionary<string,List<Dictionary<string,string>>>();
                }
                dict.Add(name, childDict);
            }
        }

        public XmlSchema GetSchema()
        {
            return(null);
        }


    }
}
Sign up to request clarification or add additional context in comments.

3 Comments

Thanks, but I think you miss the point of the question. Your example deserialises to a type that is completely known by the code. I have to deserialise to a type that is partially known by the code ... it contains one property whose type is unknown.
Do you have a schema? The schema should have all the possible types (unless it is a user defined type).
I added another solution using IXmlSerializable
0

I could not find a simple solution, but the following worked:

  1. For every type needing an extensionAttributes property, create a partial class that adds the property with string type (i.e. that extends the existing partial class inside Reference.cs). For example:
    public partial class SalesDataOrderInterface
    {
        public string extensionAttributes { get; set; }
    }
  1. Create a WCF message inspector that intercepts the incoming XML and rewrites it, finding any elements and escaping the inner XML by wrapping it in a CDATA block. All elements therefore map to the string property created above. Here is the relevant method of the inspector:
        public void AfterReceiveReply(ref Message reply, object correlationState)
        {
            // Read reply XML 
            var replyAsXml = new XmlDocument();
            var ms = new MemoryStream();
            var writer = XmlWriter.Create(ms);
            reply.WriteMessage(writer);
            writer.Flush();
            ms.Position = 0;
            replyAsXml.Load(ms);

            // Find any <extensionAttributes> elements and escape the XML so it can be deserialised as a single string property
            // The contents of <extensionAttributes> are customisable in Magento, so we cannot know the type they map to
            foreach (XmlNode extensionAttributesElement in replyAsXml.GetElementsByTagName("extensionAttributes"))
            {
                var elementXml = extensionAttributesElement.OuterXml;
                foreach (XmlNode childNode in extensionAttributesElement.ChildNodes)
                    extensionAttributesElement.RemoveChild(childNode);

                extensionAttributesElement.InnerText = string.Format("<![CDATA[{0}]]>", elementXml);
            }

            // Create the modified reply
            ms.SetLength(0);
            writer = XmlWriter.Create(ms);
            replyAsXml.WriteTo(writer);
            writer.Flush();
            ms.Position = 0;
            var reader = XmlReader.Create(ms);
            reply = Message.CreateMessage(reader, int.MaxValue, reply.Version);
        }

Consuming applications can un-wrap and deserialise the string as needed.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.