Showing posts with label XML. Show all posts
Showing posts with label XML. Show all posts

Thursday, 9 April 2009

Using ShouldSerialize for conditional omission of properties in the XmlSerializer

Wow

I've just found out about the undocumented ShouldSerialize technique for XmlSerializers in .NET. Possibly the most useful System.Xml discovery I've made for years. Basically, while it's not very OO (but when is codegen particularly OO?) it allows you to provide programattic control of when a property should be serialized to the XML result stream inside the generated XmlSerializer for that type.




Here's a link to the only MSDN article I've seen on the subject http://msdn.microsoft.com/en-us/library/53b8022e(VS.71).aspx

Why

You have to comply with an existing XML consumer (probably bespoke and third party) that isn't tolerant of fully schema-bound XML, and you want to use the built-in XmlSerializer in .NET. And why wouldn't you? it's extremely powerful and configurable - and there's a huge sense of satisfaction to be gained from getting it working with some esoteric consumer - done well, your code is clean, readable and functional - far better than messing around with StringBuilders and XmlWriters.




Now I've gotten quite good at manipulating the XmlSerializer over the years, so it tends to be my first port of call when I need to generate some XML for some reason. Who wouldn't want to replace gobs and gobs of String concatenation code with three lines of a call to a serializer. It's clear what you're doing and far far easier to maintain. The point being that XML is structured data, it's not just text, just because it can be represented that way.

The Scenario

I'm trying to automatically Serialize some CAML to send to SharePoint. Specifically a Query which looks something like this:

<Query xmlns="http://schemas.microsoft.com/sharepoint/soap/">
  <OrderBy>
    <FieldRef Name="Title" />
  </OrderBy>
</Query>

(1) Required XML from the serializer

I have a Query class which contains a List<FieldRef> called OrderBy. So far so good. Now, I also added a subclass of List<FieldRef> called GroupBy, adding the extra property Collapse, which you can see the schema requires. Now consider how I get the XML above from the serializer. My API looks something like this:


Query query = new Query();
query.OrderBy.Add(
new FieldRef ("Title"));

(2) API to build the XML in (1) above.

so I default the GroupBy and OrderBy properties to the Query to new instances, but since I haven't added a GroupBy, when I run I'll get this XML back.

<Query xmlns="http://schemas.microsoft.com/sharepoint/soap/">
  <OrderBy>
    <FieldRef Name="Title" />
  </OrderBy>
  <GroupBy />
</Query>

(3) XML generated by the serializer under normal circumstances.


See the extra GroupBy? That's no good. So how do we get rid of this empty element? Ok, I could annotate the GroupBy property with [DefaultValue(null)] and have the GroupBy property instantiate lazily. That's all well and good, and would work... until we want to remove a FieldRef from the list, leaving it empty. Same problem, the list isn't null although it's empty, so it serializes and we get the XML (3) above.




The problem is that DefaultValue doesn't allow conditional evaluation. What we need is something that behaves like the DefaultValueAttribute which tells the Serializer to skip the property when it has that value, but that does allow conditional evaluation. Of course we could performs some weird hacks, checking for an empty list in the property getter and returning null... that would keep the serializer happy, but it would break the API, forcing us to explicitly create the list from client code, and that's something easily forgotten leading to potential errors.



Enter ShouldSerialize. It turns out that creating a public boolean method called ShouldSerialize[PropertyName] in the serializable class tells the generated XmlSerializer to call that method to determine whether it should try to serialize the property or not. So, I create a method called ShouldSerializeGroupBy in my Query class, which checks for null or empty, returning false in that case, and BAM! tests pass, and I am happy. So my Query class now looks like this below.


[
XmlRoot("Query", Namespace = Namespaces.SharePointSoap),
XmlType("Query", Namespace = Namespaces.SharePointSoap),
Serializable
]
public class Query
{
private List orderBy = new List();
private GroupBy groupBy = new GroupBy();

[XmlArray("OrderBy")]
public List OrderBy
{
get { return orderBy; }
set { orderBy = value; }
}

public bool ShouldSerializeGroupBy()
{
return groupBy != null && groupBy.Count > 0;
}

[XmlArray("GroupBy"), DefaultValue(null)]
public GroupBy GroupBy
{
get { return groupBy; }
set { groupBy = value; }
}
}


Hope this helps you. It's saved me a lot of trouble. TTFN :)

Wednesday, 17 January 2007

Copying folder contents with BCOPY through WebDAV


I'm working on an Exchange project at the moment, and have chosen to use WebDAV for the operations required. One of the subsidiary requirements is to copy contacts from one folder on a mailbox to another, then synchronise those contacts with a public folder on the Exchange server too. I discovered the WebDAV BCOPY method, which allows you to copy a whole load of items from one folder to another. Perfect! It works like this.

I make a WebDAV SEARCH request to the exchange server like so


<?xml version="1.0" ?>
<D:searchrequest xmlns:D="DAV:">
  <D:sql>
    select "DAV:href","DAV:contentclass"
    FROM SCOPE ('SHALLOW TRAVERSAL OF "/exchange/<mailbox>/Contacts"')
    WHERE "DAV:ishidden" = false
  </D:sql>
</D:searchrequest>


it returns a result set like this:


<?xml version="1.0" encoding="utf-16"?>
<a:multistatus xmlns:b="urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/" xmlns:c="xml:" xmlns:a="DAV:">
  <a:response>
    <a:href>http://<exchsrvr>/exchange/<mailbox>/Contacts/BobLog.EML</a:href>
        <a:propstat>
          <a:status>HTTP/1.1 200 OK</a:status>
          <a:prop>
            <a:href>http://<exchsrvr>/exchange/<mailbox>/Contacts/BobLog.EML</a:href>
            <a:contentclass>urn:content-classes:person</a:contentclass>
          </a:prop>
        </a:propstat>
      </a:response>
      <a:response>
        <a:href>...


and so on.

then you iterate through your <a:response>s, extract the hrefs and build this request


<?xml version="1.0" ?>
<D:copy xmlns:D="DAV:">
  <D:target>
    <D:href>http://<exchsrvr>/exchange/<mailbox>/Contacts/BobLog.EML</D:href>
    <D:href>http://<exchsrvr>/exchange/<mailbox>/Contacts/JudahBauer.EML</D:href>
    ... etc
  </D:target>
</D:copy>


add an Http header called Destination, with the value of the destination folder, then fire it off as a BCOPY command. Bingo, all your specified contacts have been copied, with changes from the source overwriting the destination object.

It's a bit long winded though. Not to mention the hassle of removing old entries. Anyway, it worked. So I kept experimenting, and tried adding a subfolder to the items collection to see what would happen if I tried to copy it. And whaddyaknow, it only bloomin created the destination folder, and all the contacts in it!

!!BRAIN WAVE!!

That means the entire operation can be condensed into this:


<?xml version="1.0" ?>
<D:copy xmlns:D="DAV:">
  <D:target>
    <D:href>http://<exchsrvr>/exchange/<mailbox>/Contacts</D:href>
    <D:dest>http://<exchsrvr>/exchange/<mailbox>/ContactsCopy</D:dest>
  </D:target>
</D:copy>


fired off at the mailbox root. And it cascades item deletes and everything!

Laaaaahvley!