Tales from the Evil Empire

Bertrand Le Roy's blog

News


Bertrand Le Roy

BoudinFatal's Gamercard

Tales from the Evil Empire - Blogged

Blogs I read

My other stuff

Archives

A C# helper to read and write XML from and to objects

I really like jQuery’s pattern of attribute getters and setters. They are fluent and work really well with HTML and XML DOMs. If you specify a value in addition to the name, it’s setting, otherwise it’s getting. In C#, we have an OK API for XML, XElement, but it’s not as easy to use as jQuery’s attr methods. It is also missing the flexibility of Javascript with regards to parameter types. To recreate the simplicity of attr in C#, I built a set of extension methods for the most common simple types:

var el = new XElement("node");
el.Attr("foo", "bar")
  .Attr("baz", 42)
  .Attr("really", true);
var answer = el.Attr("baz");

The element built by this code looks like this:

<node foo="bar" baz="42" really="true"/>

And the answer variable will contain “42”.

Even with this API, there is still a fair amount of repetition in code that reads and writes XML from and to objects. You could rely on serialization in those cases, of course, but when you need a little more control, and the types are not necessarily serializable, or if you just want to do it manually, you need something more. This is why I also built ToAttr and FromAttr. Both are extension methods that take an object and an expression for what property of the object to get or set from XML attributes. The methods will infer the type and name from the property.

This is especially useful when writing the import and export methods in an Orchard part driver:

protected override void Importing(
UspsShippingMethodPart part,
ImportContentContext context) {
var el = context.Data.Element(typeof(UspsShippingMethodPart).Name); if (el == null) return; el.FromAttr(part, p => p.Name) .FromAttr(part, p => p.Size) .FromAttr(part, p => p.WidthInInches) .FromAttr(part, p => p.LengthInInches) .FromAttr(part, p => p.HeightInInches) .FromAttr(part, p => p.MaximumWeightInOunces) .FromAttr(part, p => p.Priority) .FromAttr(part, p => p.International) .FromAttr(part, p => p.RegisteredMail) .FromAttr(part, p => p.Insurance) .FromAttr(part, p => p.ReturnReceipt) .FromAttr(part, p => p.CertificateOfMailing) .FromAttr(part, p => p.ElectronicConfirmation); } protected override void Exporting(
UspsShippingMethodPart part, ExportContentContext context) {
context.Element(typeof (UspsShippingMethodPart).Name) .ToAttr(part, p => p.Name) .ToAttr(part, p => p.Size) .ToAttr(part, p => p.WidthInInches) .ToAttr(part, p => p.LengthInInches) .ToAttr(part, p => p.HeightInInches) .ToAttr(part, p => p.MaximumWeightInOunces) .ToAttr(part, p => p.Priority) .ToAttr(part, p => p.International) .ToAttr(part, p => p.RegisteredMail) .ToAttr(part, p => p.Insurance) .ToAttr(part, p => p.ReturnReceipt) .ToAttr(part, p => p.CertificateOfMailing) .ToAttr(part, p => p.ElectronicConfirmation); }

There is no need to specify attribute names or types here, everything is inferred from the expression. Both methods manipulate XML looking like this:

<UspsShippingMethodPart
        Name="Foo"
        Size="L"
        WidthInInches="10"
        LengthInInches="11"
        HeightInInches="12"
        MaximumWeightInOunces="1.3"
        Priority="14"
        International="true"
        RegisteredMail="true"
        Insurance="false"
        ReturnReceipt="true"
        CertificateOfMailing="true"
        ElectronicConfirmation="true"/>

UPDATE: You may notice that there is still quite some repetition of the part parameter in the import/export code above. In order to remove this repetition, I’ve added a small class that aggregates the XML element with a context and has simpler ToAttr and From Attr methods. With this new helper class, we can rewrite the driver’s import/export code to be even more concise:

protected override void Importing(
UspsShippingMethodPart part, ImportContentContext context) {
var el = context.Data.Element(typeof (UspsShippingMethodPart).Name); if (el == null) return; el.With(part) .FromAttr(p => p.Name) .FromAttr(p => p.Size) .FromAttr(p => p.WidthInInches) .FromAttr(p => p.LengthInInches) .FromAttr(p => p.HeightInInches) .FromAttr(p => p.MaximumWeightInOunces) .FromAttr(p => p.Priority) .FromAttr(p => p.International) .FromAttr(p => p.RegisteredMail) .FromAttr(p => p.Insurance) .FromAttr(p => p.ReturnReceipt) .FromAttr(p => p.CertificateOfMailing) .FromAttr(p => p.ElectronicConfirmation); } protected override void Exporting(
UspsShippingMethodPart part, ExportContentContext context) {
context.Element(typeof (UspsShippingMethodPart).Name) .With(part) .ToAttr(p => p.Name) .ToAttr(p => p.Size) .ToAttr(p => p.WidthInInches) .ToAttr(p => p.LengthInInches) .ToAttr(p => p.HeightInInches) .ToAttr(p => p.MaximumWeightInOunces) .ToAttr(p => p.Priority) .ToAttr(p => p.International) .ToAttr(p => p.RegisteredMail) .ToAttr(p => p.Insurance) .ToAttr(p => p.ReturnReceipt) .ToAttr(p => p.CertificateOfMailing) .ToAttr(p => p.ElectronicConfirmation); }

You can find the code for this helper class here:
https://gist.github.com/bleroy/5384405

And I have a small test suite for the whole thing here:
https://gist.github.com/bleroy/5385284

Comments

harmony7 said:

Ah, this is nice.  I had written my own helpers to do this, but I think I like your fluent syntax.

# April 14, 2013 10:10 PM

pszmyd said:

Yeah, very useful stuff! It'd be even more awesome to have ToElement/FromElement (to use nested items instead of attributes, when needed) and fluent nesting :)

# April 15, 2013 3:00 PM

Bertrand Le Roy said:

@Piotr: yes, I'll implement that when I need it ;) Please feel free to fork and implement if you do.

# April 15, 2013 3:07 PM

Sipke Schoorstra said:

That's pretty slick!

And with two little Orchard specific extensions on ImportContentContext and ExportContentContext, it could be even more concise:

protected override void Importing(    UspsShippingMethodPart part, ImportContentContext context) {

   context.ImportPart(part,

     p => p.Name,

     p => p.Size,

     p => p.WidthInInches,

     p => p.LengthInInches,

     p => p.HeightInInches,

     p => p.MaximumWeightInOunces.

     p => p.Priority,

     p => p.International,

     p => p.RegisteredMail,

     p => p.Insurance,

     p => p.ReturnReceipt,

     p => p.CertificateOfMailing,

     p => p.ElectronicConfirmation);

}

protected override void Exporting(    UspsShippingMethodPart part, ExportContentContext context) {

   context.ExportPart(part,

     p => p.Name,

     p => p.Size,

     p => p.WidthInInches,

     p => p.LengthInInches,

     p => p.HeightInInches,

     p => p.MaximumWeightInOunces.

     p => p.Priority,

     p => p.International,

     p => p.RegisteredMail,

     p => p.Insurance,

     p => p.ReturnReceipt,

     p => p.CertificateOfMailing,

     p => p.ElectronicConfirmation);

}

The two extension classes would look like this:

public static class ImportContextExtensions {

       public static void ImportPart<TPart>(this ImportContentContext context, TPart part, params Expression<Func<TPart, object>>[] targetExpressions) where TPart: ContentPart {

           var element = context.Data.Element(part.PartDefinition.Name);

           if (element == null)

               return;

           foreach (var targetExpression in targetExpressions) {

               element.FromAttr(part, targetExpression);

           }

       }

   }

public static class ExportContextExtensions {

       public static void ExportPart<TPart>(this ExportContentContext context, TPart part, params Expression<Func<TPart, object>>[] targetExpressions) where TPart: ContentPart {

           var element = context.Data.Element(part.PartDefinition.Name);

           if (element == null)

               return;

           foreach (var targetExpression in targetExpressions) {

               element.ToAttr(part, targetExpression);

           }

       }

   }

# April 16, 2013 6:33 AM

Lelala said:

Spike, thanks for that additional info!

# April 20, 2013 1:35 PM