tags:

views:

59

answers:

3

OK, a challenge to the deep thinkers out there:

My soap server sends me XML that looks sort of like this:

<?xml version='1.0' encoding='utf-8'?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"&gt;
  <SOAP-ENV:Header xmlns:xxxxxx="...">
    <...bunch of soap header stuff.../>
  </SOAP-ENV:Header>
  <SOAP-ENV:Body>
    <queryResponse xmlns="...">
      <resultSet seqNo="0">
        <Types>
          <name>varchar</name>
          <value>varchar</value>
        </Types>
        <row>
          <name>field1</name>
          <value>0</value>
        </row>
        <row>
          <name>field2</name>
          <value>some string value</value>
        </row>
        <row>
          <name>field3</name>
          <value>false</value>
        </row>
        <... repeats for many more rows... />
      </resultSet>
    </queryResponse>
  </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

How can I take the <row> nodes and populate the following class:

public class SessionProperties
{
  public int IntField {get;set;}
  public string StringField {get;set;}
  public bool BooleanField {get;set;}

  // more fields...
}

I want to avoid manually populating the SessionProperties instance, e.g.,

var myProps = new SessionProperties();
myProps.IntField = XElement("...").Value;  // I don't want to do this!

I'd like to write a "generic" set of code which can populate the SessionProperties instance without having to hardcode the class properties to a specific XML node.

Also I don't want to write an IXmlSerializable implementation, I think that will only make the code more complex. Let me know if I'm wrong though.

Finally, the soap server may send me additional row nodes in the future, and all I'd like to do is just update the SessionProperties class if at all possible. Is there some generic approach (perhaps using custom attributes, etc.) which can accomplish this?

Thanks in advance!

A: 

Well, that XML is kind of not good.

If it were me, I'd create a wrapper type for your soap result which implements ICustomTypeDescriptor. The WPF binding system will, if it detects your type has a custom type descriptor, will use it when binding.

Custom type descriptors perform the same action as normal reflection, but allow type creators to handle the process of reflection for callers. The default implementation TypeDescriptor can work with any type. You, however, can create your own type descriptor wrapper for your soap message that, when queried for a property, can pick the correct value from the XML and return it.

The true benefit of this is that your bindings are exactly the same as they would be for normal POCO properties or DependencyProperties.

In other words, your binding may look like this:

<TextBox Text="{Binding Session.UserName}" /> 

even though your actual type does not have a UserName property. The binding system redirects the lookup logic to your custom type descriptor's GetProperty method, where you can return a MethodInfo instance that you control. When the value is requested, you can look into your soap XML for a matching element and return it.

You can gracefully handle situations where the soap message has changed and properties are now mapped elsewhere, or when no property exists (return a default value, for example). And in a situation where you know the message is in great flux, you could have your type descriptor even be a proxy that redirects to an instance of ICustomTypeDescriptor that is set at runtime using a Dependency Injection framework.

I'm not saying it will be easy, but it definitely is not that hard once you get the core concepts.

Will
+1  A: 

I think this should be self-explanatory:

    class Program
    {
        // map names in the XML to the names of SessionInfo properties;
        // you'll need to update this when you add a new property to
        // the SessionInfo class:
        private static Dictionary<string, string> PropertyMap = 
            new Dictionary<string, string>
        {
            {"field1", "StringProperty1"},
            {"field2", "IntProperty1"},
            {"field3", "BoolProperty1"},
        };

        // map CLR types to XmlConvert methods; you'll need one entry in
        // this map for every CLR type SessionInfo uses
        private static Dictionary<Type, Func<string, object>> TypeConverterMap = 
            new Dictionary<Type, Func<string, object>>
        {
            { typeof(bool), x => XmlConvert.ToBoolean(x)},
            { typeof(int), x => XmlConvert.ToInt32(x)},
            { typeof(string), x => x},
        };

        static void Main(string[] args)
        {
            // map SessionInfo's property names to their PropertyInfo objects
            Dictionary<string, PropertyInfo> properties = AppDomain.CurrentDomain.GetAssemblies()
                .SelectMany(x => x.GetExportedTypes())
                .Where(x => x.Name == "SessionInfo")
                .SelectMany(x => x.GetMembers())
                .Where(x => x.MemberType == MemberTypes.Property)
                .Cast<PropertyInfo>()
                .ToDictionary(x => x.Name);

            string xml =
                @"<example>
<row>
    <name>field1</name>
    <value>stringProperty</value>
</row>
<row>
    <name>field2</name>
    <value>123</value>
</row>
<row>
    <name>field3</name>
    <value>true</value>
</row>
</example>";
            XmlDocument d = new XmlDocument();
            d.LoadXml(xml);

            SessionInfo s = new SessionInfo();

            // populate the object's properties from the values in the XML
            foreach (XmlElement elm in d.SelectNodes("//row"))
            {
                string name = elm.SelectSingleNode("name").InnerText;
                string value = elm.SelectSingleNode("value").InnerText;
                // look up the property for the name in the XML and get its
                // PropertyInfo object
                PropertyInfo pi = properties[PropertyMap[name]];
                // set the property to the value in the XML, using the the converter for 
                // the property's type
                pi.SetValue(s, TypeConverterMap[pi.PropertyType](value), null);
            }

            // and the results:
            Console.WriteLine(s.StringProperty1);
            Console.WriteLine(s.IntProperty1);
            Console.WriteLine(s.BoolProperty1);
            Console.ReadKey();
        }
Robert Rossney
This is very good, I like the thought behind the code. However, is an alternative to using dictionaries (static or otherwise)? Otherwise I like the approach.
code4life
The map doesn't need to be static. You can populate that dictionary at runtime from an external XML document or whatever. You could define an attribute class and mark the properties in the SessionInfo class, and then construct the dictionary from those attributes. A `Dictionary<string, string>` is the best way to represent the mapping so that it's usable by the code.
Robert Rossney
I like the attributes based approach, simply because it would mean that I only have to add the property to one class and decorate it with the attribute, rather than having to modify both the dictionary and the class. The XmlConverter dictionary is a brilliant idea though.
code4life
That comes from my experience with Python, where it's a lot more idiomatic than it is in C# - understandably, since the syntax in C# is a little daunting. But it's one of those ideas that once you grasp it, you'll use it everywhere.
Robert Rossney
A: 

This solution demonstrates the usage of custom attributes, as Robert suggested.

First, the custom attribute class definition:

// match the properties to the xml dynamically using this attribute...
[AttributeUsage(AttributeTargets.Property)]
public class DBSPropAttribute : Attribute
{
    public string MappingField { get; set; }
    public DBSPropAttribute(string fieldName)
    {
        MappingField = fieldName;
    }
}

The custom attribute is then applied to the SessionProperties class like this:

[DBSProp("archiveDays")]
public int ArchiveDays { get; set; }

In the SessionProperties class, I'm also going to define this converter dictionary, a very elegant idea of Robert's:

// map CLR types to convert methods; you'll need one entry in
// this map for every CLR type
private static readonly Dictionary<Type, Func<string, object>> TypeConverterMap =
    new Dictionary<Type, Func<string, object>>
{
    { typeof(bool), x => Convert.ToBoolean(x)},
    { typeof(int), x => Convert.ToInt32(x)},
    { typeof(string), x => x},
    { typeof(double), x => Convert.ToDouble(x)}
};

Finally the class will implement the following method to bind the XML to the object properties:

public void SetPropertyValues(IEnumerable<XElement> elements)
{
    var propList = typeof(SessionProperties).GetProperties();

    foreach (var elm in elements)
    {
        var nm = elm.Element("name").Value;
        var val = elm.Element("value").Value;

        // MUST throw an exception if there are no matches...
        var pi = propList.First(c => c.GetCustomAttributes(true)
                   .OfType<DBSPropAttribute>()
                   .First()
                   .MappingField == nm);

        pi.SetValue(this, TypeConverterMap[pi.PropertyType](val), null);
    }
}

Thanks again to Robert for illustrating the key concepts which led to this answer. I think it's a nice alternative to his equally valid approach.

code4life