Every now and then I see questions popping up in the newsgroup where the author posts source code where they create WebDAV requests by string concatenation or using a StringBuilder. While this may work with simple PROPFIND requests, this becomes more a hassle when creating PROPPATCH requests. Apart from that, it is prone to error. But while .NET has some nice XML handling, this can become a problem with Exchange because some properties have local names which start with a zero, like the property for the start date of a task: http://schemas.microsoft.com/mapi/id/{00062003-0000-0000-C000-000000000046}/0x8104.
If you try to use that local name 0x8104 with one of Xml classes you'll get an error stating that this is not a valid local name. Consequently, parsing a WebDAV response containing one of these properties into a XmlDocument or XmlReader will fail with the same error. So a little extra work is needed here. You'll see that working with Exchange gets much easier once you have some helper methods. To create a PROPFIND request, you can use these methods:
1 publicstaticbyte[] CreatePropFindRequest(paramsstring[] properties)
2 {
3 XmlWriterSettings settings;
4
5 settings = newXmlWriterSettings();
6 settings.Encoding = Encoding.UTF8;
7
8 using (MemoryStream stream = newMemoryStream())
9 using (XmlWriter writer = XmlWriter.Create(stream, settings))
10 {
11 writer.WriteStartElement("propfind", "DAV:");
12
13 writer.WriteStartElement("prop", "DAV:");
14 foreach (string p in properties)
15 {
16 string ns;
17 string localName;
18
19 ParsePropertyName(p, out ns, out localName);
20
21 writer.WriteElementString("a", XmlConvert.EncodeLocalName(localName), ns, null);
22 }
23 writer.WriteEndElement();
24
25 writer.WriteEndElement();
26 writer.WriteEndDocument();
27
28 writer.Flush();
29 return stream.ToArray();
30 }
31 }
32
33 privatestaticvoid ParsePropertyName(string property, outstring ns, outstring localName)
34 {
35 int index;
36
37 if (string.IsNullOrEmpty(property)) thrownewArgumentNullException("property");
38
39 // Custom Outlook properties don't have any namespaces,
40 // so LastIndexOf maybe -1
41 index = Math.Max(property.LastIndexOfAny(newchar[] { '/', ':', '#' }) + 1, 0);
42
43 ns = property.Substring(0, index);
44 localName = property.Substring(index);
45 }
The handling of those special properties like the one I mentioned above is located in line 21. The XmlConvert.EncodeLocalName() method is used here: The local name 0x8104 is converted to _x0030_x8104, which is now a perfectly valid name.
The other method used here is the ParsePropertyName method. It takes a full qualified property name like DAV:href and splits it into its segments: "DAV:" and "href".
Unfortunately, the response returned by Exchange contains the properties unencoded. Before we can load the response into a XmlDocument or XmlReader, we'll have to encode those property names. A regular expression is suitable for this job:
1 privatestaticreadonlyRegex _TagNameRegexFinder = newRegex(@"(?<=\</?\w+\:)\w+", RegexOptions.Compiled);
The method to encode local names in the response is this:
1 staticXmlReader ParsePropfindResponse(string response)
2 {
3 if(string.IsNullOrEmpty(response)) thrownewArgumentNullException("response");
4
5 response = _TagNameRegexFinder.Replace(response, delegate(Match match) { returnXmlConvert.EncodeLocalName(match.Value); });
6
7 returnXmlReader.Create(newStringReader(response));
8 }
The local names of all tags in the response are encoded using the XmlConvert.EncodeLocalName() method.
Now that we have all helper methods in place we can actually perform a request:
1 privatestaticvoid PropFind()
2 {
3 byte[] buffer;
4 XmlReader reader;
5 HttpWebRequest request;
6
7 buffer = CreatePropFindRequest("http://schemas.microsoft.com/mapi/id/{00062003-0000-0000-C000-000000000046}/0x8104");
8
9 request = (HttpWebRequest)WebRequest.Create("http://w2k3srv.contoso.local/exchange/administrator/tasks/test.eml");
10 request.Method = "PROPFIND";
11 request.ContentType = "text/xml";
12 request.Credentials = CredentialCache.DefaultCredentials;
13 request.Headers.Add("Translate", "f");
14 request.Headers.Add("Depth", "0");
15 request.SendChunked = true;
16
17 using (Stream stream = request.GetRequestStream())
18 {
19 stream.Write(buffer, 0, buffer.Length);
20 }
21
22 using (WebResponse response = request.GetResponse())
23 {
24 string content = newStreamReader(response.GetResponseStream()).ReadToEnd();
25 reader = ParsePropfindResponse(content);
26 XmlNamespaceManager nsmgr = newXmlNamespaceManager(reader.NameTable);
27
28 nsmgr.AddNamespace("dav", "DAV:");
29 XPathDocument doc = newXPathDocument(reader);
30
31 foreach (XPathNavigator element in doc.CreateNavigator().Select("//dav:propstat[dav:status = 'HTTP/1.1 200 OK']/dav:prop/*", nsmgr))
32 {
33 Console.WriteLine("{0}{1} ({2}): {3}", element.NamespaceURI, XmlConvert.DecodeName(element.LocalName), element.GetAttribute("dt", DataTypeNamespace), element.Value);
34 }
35 }
36 }
The real interesting parts here are in the lines 7, 25 and 33. In line 7, a buffer created using the CreatePropFindRequest method. Since the parameter is a params array, there is no need to create an array manually. The property names can just be passed as a comma separated list. In line 25, the request from the server is parsed into an XmlReader. In line 31, all properties, which were successfully returned are selected in the reader, and line 33 shows how the various parts of each property can be accessed:
- The namespace of the property: element.NamespaceURI
- The local name of the property: XmlConvert.DecodeName(element.LocalName)
- The data type of the property: element.GetAttribute("dt", DataTypeNamespace)
- The value of the property: element.Value
Of course, the value is only accessible if it's not a multi valued property. To check this, you can examine the data type of the property: If it starts with the letters "mv", it's a multi valued property and you must parse it accordingly.
The next article will show how to construct PROPPATCH requests using this schema.