Working with Time Zones in the Exchange Web Services GetUserAvailability Operation

This content is no longer actively maintained. It is provided as is, for anyone who may still be using these technologies, with no warranties or claims of accuracy with regard to the most recent product version or service release.

Topic Last Modified: 2007-08-27

By Benjamin Spain

Time zones can be tricky, as any developer who has tried to create a calendaring application for clients on opposite ends of the world can tell you. Unfortunately, the GetUserAvailability Web service method in Microsoft Exchange Server 2007 is not immune to time zone–related issues. Let me give you an overview of the problem.

Exchange Web Services uses XML schema types to transfer date and time information. For the definition of this format, see XML Schema Part 2: Datatypes Second Edition.

Note

The third-party Web site information in this topic is provided to help you find the technical information that you need. The URLs are subject to change without notice.

The xs:date, xs:datetime, and xs:time string format allows for an optional time zone offset. The presence or omission of a time zone offset will cause these strings to be classified in one of two ways:

  • Synchronized with the local time zone; for example: 02-14-2006T08:30:00Z, 04-01-1996T07:45:00-08:00, 05-16-1978Z, 12-10-2032T13:45:14+05:30
  • Not synchronized with the local time zone; for example: 11-15-2007T04:15:00, 07-02-2015

Coordinated Universal Time (UTC)–offset strings (except for xs:time) specify a specific point in time, but not the set of rules that govern the time change information for a time zone. Non-UTC-offset strings must specify time zone information elsewhere in context in order to be understood by an application.

In Exchange 2007, a request for free/busy information always returns UTC-offset strings in the response. However, the server should, in fact, return strings that do not have a UTC offset, and instead the context of the time zone should come from the TimeZone element in the request. When the client and the server are in the same time zone, this is not an issue. When the client and server are in different time zones, however, the server calculates the time section of a CalendarEvent correctly, but stamps the wrong time zone offset onto it.

Confused yet? I don’t blame you. Let’s look at an example that will help clarify the issue.

Working with Time Zones in XML Requests

In this example, I will show you how to work with time zones and the GetUserAvailability Web method when you are using XML over HTTP. Later in this article I’ll provide examples that use the autogenerated proxy classes in C#.

The following figure shows you the Outlook Web Access calendar for Andy Jacobs. Andy has two appointments: a morning seminar, and afternoon golf.

Outlook Web Access calendar with two appointments
Outlook Web Access calendar

Now, let’s assume that both the client and the server are in the same time zone, which in this case is Pacific Standard Time (PST), or UTC -07:00. As part of the GetUserAvailabilty request, we have to supply this information in our TimeZone structure. The following code example shows you such a request.

<soap:Envelope 
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
     xmlns:soap="https://schemas.xmlsoap.org/soap/envelope/" 
     xmlns:t="http://.../types" 
     xmlns:m="http://.../messages">
  <soap:Body>
    <m:GetUserAvailabilityRequest>
      <t:TimeZone>
        <t:Bias>480</t:Bias>
        <t:StandardTime>
          <t:Bias>0</t:Bias>
          <t:Time>02:00:00</t:Time>
          <t:DayOrder>1</t:DayOrder>
          <t:Month>11</t:Month>
          <t:DayOfWeek>Sunday</t:DayOfWeek>
        </t:StandardTime>
        <t:DaylightTime>
          <t:Bias>-60</t:Bias>
          <t:Time>02:00:00</t:Time>
          <t:DayOrder>2</t:DayOrder>
          <t:Month>3</t:Month>
          <t:DayOfWeek>Sunday</t:DayOfWeek>
        </t:DaylightTime>
      </t:TimeZone>
      <m:MailboxDataArray>
        <t:MailboxData>
          <t:Email>
            <t:Address>andy@feperf9dom.extest.microsoft.com</t:Address>
          </t:Email>
          <t:AttendeeType>Organizer</t:AttendeeType>
          <t:ExcludeConflicts>true</t:ExcludeConflicts>
        </t:MailboxData>
      </m:MailboxDataArray>
      <t:FreeBusyViewOptions>
        <t:TimeWindow>
          <t:StartTime>2007-08-01T08:00:00</t:StartTime>
          <t:EndTime>2007-08-01T17:00:00</t:EndTime>
        </t:TimeWindow>
        <t:MergedFreeBusyIntervalInMinutes>
          30
        </t:MergedFreeBusyIntervalInMinutes>
        <t:RequestedView>Detailed</t:RequestedView>
      </t:FreeBusyViewOptions>
    </m:GetUserAvailabilityRequest>
  </soap:Body>
</soap:Envelope>

The TimeZone element is a fully qualified description of Pacific Standard Time. In this time zone, August 1 is daylight saving time, but we do not have to worry about this detail. Because we have supplied a fully qualified time zone definition to the Exchange server, the server will use that information to resolve all dates and times. Now let’s take a look at the response from the server. The following code example shows you the response.

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope
      xmlns:soap="https://schemas.xmlsoap.org/soap/envelope/" 
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
      xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <soap:Header>
    <t:ServerVersionInfo MajorVersion="8" MinorVersion="0" 
        MajorBuildNumber="685" MinorBuildNumber="24" 
        xmlns:t="http://.../types"/>
  </soap:Header>
  <soap:Body>
    <GetUserAvailabilityResponse 
        xmlns="http://.../messages">
      <FreeBusyResponseArray>
        <FreeBusyResponse>
          <ResponseMessage ResponseClass="Success">
            <ResponseCode>NoError</ResponseCode>
          </ResponseMessage>
          <FreeBusyView>
            <FreeBusyViewType 
                xmlns="http://.../types">Detailed</FreeBusyViewType>
            <CalendarEventArray xmlns="http://.../types">
              <CalendarEvent>
                <StartTime>2007-08-01T08:00:00-07:00</StartTime>
                <EndTime>2007-08-01T12:00:00-07:00</EndTime>
                <BusyType>Busy</BusyType>
                <CalendarEventDetails>
                  <ID>000000...</ID>
                  <Subject>Morning Seminar</Subject>
                  <Location/>
                  <IsMeeting>false</IsMeeting>
                  <IsRecurring>false</IsRecurring>
                  <IsException>false</IsException>
                  <IsReminderSet>true</IsReminderSet>
                  <IsPrivate>false</IsPrivate>
                </CalendarEventDetails>
              </CalendarEvent>
              <CalendarEvent>
                <StartTime>2007-08-01T13:00:00-07:00</StartTime>
                <EndTime>2007-08-01T17:00:00-07:00</EndTime>
                <BusyType>Busy</BusyType>
                <CalendarEventDetails>
                  <ID>0000000...</ID>
                  <Subject>Afternoon Golf</Subject>
                  <Location/>
                  <IsMeeting>false</IsMeeting>
                  <IsRecurring>false</IsRecurring>
                  <IsException>false</IsException>
                  <IsReminderSet>true</IsReminderSet>
                  <IsPrivate>false</IsPrivate>
                </CalendarEventDetails>
              </CalendarEvent>
            </CalendarEventArray>
            <WorkingHours xmlns="http://.../types">
              <TimeZone>
                <Bias>480</Bias>
                <StandardTime>
                  <Bias>0</Bias>
                  <Time>02:00:00</Time>
                  <DayOrder>1</DayOrder>
                  <Month>11</Month>
                  <DayOfWeek>Sunday</DayOfWeek>
                </StandardTime>
                <DaylightTime>
                  <Bias>-60</Bias>
                  <Time>02:00:00</Time>
                  <DayOrder>2</DayOrder>
                  <Month>3</Month>
                  <DayOfWeek>Sunday</DayOfWeek>
                </DaylightTime>
              </TimeZone>
              <WorkingPeriodArray>
                <WorkingPeriod>
                  <DayOfWeek>
                      Monday Tuesday Wednesday Thursday Friday
                  </DayOfWeek>
                  <StartTimeInMinutes>480</StartTimeInMinutes>
                  <EndTimeInMinutes>1020</EndTimeInMinutes>
                </WorkingPeriod>
              </WorkingPeriodArray>
            </WorkingHours>
          </FreeBusyView>
        </FreeBusyResponse>
      </FreeBusyResponseArray>
    </GetUserAvailabilityResponse>
  </soap:Body>
</soap:Envelope>

The response also contains a TimeZone structure. This will be important to remember later. For now, let’s focus on the CalendarEvent for Andy’s morning seminar and afternoon golf. These appointments are shown in the following code block.

</CalendarEventArray>
  <CalendarEvent>
    <StartTime>2007-08-01T08:00:00-07:00</StartTime>
    <EndTime>2007-08-01T12:00:00-07:00</EndTime>
    <BusyType>Busy</BusyType>
    <CalendarEventDetails ... />
  </CalendarEvent>
  <CalendarEvent>
    <StartTime>2007-08-01T13:00:00-07:00</StartTime>
    <EndTime>2007-08-01T17:00:00-07:00</EndTime>
    <BusyType>Busy</BusyType>
    <CalendarEventDetails ... />
  </CalendarEvent>
</CalendarEventArray>

Notice that the times that appear in the StartTime and EndTime elements include the correct time zone offset, and this corresponds to the appointments that appear on Andy's calendar.

Now let's look at the CalendarEvent elements in the response when we change the time zone of the Client Access server from PST to Eastern Standard Time (EST).

Changing the time zone in the Date and Time Properties dialog box
Date and Time Properties Box

The following code example shows you the response when the time zone is adjusted.

<CalendarEventArray xmlns="http://.../types">
  <CalendarEvent>
    <StartTime>2007-08-01T08:00:00-04:00</StartTime>
    <EndTime>2007-08-01T12:00:00-04:00</EndTime>
    <BusyType>Busy</BusyType>
    <CalendarEventDetails ... />
  </CalendarEvent>
  <CalendarEvent>
    <StartTime>2007-08-01T13:00:00-04:00</StartTime>
    <EndTime>2007-08-01T17:00:00-04:00</EndTime>
    <BusyType>Busy</BusyType>
    <CalendarEventDetails ... />
  </CalendarEvent>
</CalendarEventArray>

You can see here that the time section of the strings is correct, but the time zone portion has changed. It now reflects EST, the time zone of the Client Access server. A client application that must adhere to the time zone that the user is working in (PST) would have to convert this EST time string into PST; as a result, the morning seminar would have a start time of 5 A.M. (too early for any seminar, in my opinion.)

In Exchange Server 2007 Service Pack 1 (SP1), the time zone offset will be removed from the strings, and this means that in your applications, you should effectively do the same thing. In Exchange Server 2007 SP1, the server will continue to ensure that the time section of the strings is correct, but the time zone portion will be omitted. In Exchange 2007, however, your application should ignore the time zone information in any date/time string in a CalendarEvent.

If the server returns no time zone information in date/time strings, which time zone should be used for the CalendarEvent: the time zone that is specified in the TimeZone element in the request, or the time zone that is specified in the TimeZone element that is returned from the server? The answer is the time zone that is specified in the request. The TimeZone element in the response scopes the WorkingHours information only. All dates and times within CalendarEvents reflect the time zone that is specified in the request.

Working with Time Zones in Proxy Code

Whereas applications that work directly with XML can easily ignore the time zone specification in strings, applications that consume proxy code don’t have that luxury. This is because the Microsoft.NET Framework must deserialize the UTC-offset string into a System.DateTime structure. Unfortunately, during this deserialization process, any of the time zone information that is contained within the string is lost and cannot be ‘undone’ in the resulting DateTime instance.

Let me demonstrate. Consider the following C# code example, which is akin to what the autogenerated proxy classes do when they encounter a StartTime on a CalendarEvent. This example shows you the code that is used to deserialize a date/time string that has a UTC offset.

public static void DeSerializeTimeZonedString()
{
    string dateTimeString = "<dateTime>2007-07-01T08:00:00-04:00</dateTime>";

    Console.WriteLine("This method will 'deserialize' the following date time string\r\n'" 
        + dateTimeString + "'\r\n");

    // Convert the string into a DateTime structure.
    System.Xml.Serialization.XmlSerializer xmls = 
        new System.Xml.Serialization.XmlSerializer(typeof(DateTime));
    DateTime deszDateTime = (DateTime)xmls.Deserialize(
        new System.IO.StringReader(dateTimeString));

    // Reconvert the string back into a date/time string.
    string reSerializedDT = System.Xml.XmlConvert.ToString(
        deszDateTime,
        System.Xml.XmlDateTimeSerializationMode.RoundtripKind);

    Console.WriteLine("Back To Date Time String:    '{0}'", reSerializedDT);
}

This code, when it is run on a PST client, produces the output that is shown in the following figure.

Sample code output
Sample code output

The problem, you will notice, is that when the DateTime instance is converted back into a UTC-offset string, it has the offset of the client time zone, not the time zone from the original string. Whereas from a purely technical standpoint, we could apply some math against the resulting DateTime instance, in effect canceling the bad offset to compensate for this time zone problem, I will not describe how to do this for two very good reasons:

  1. There is no supported Web service method for getting the time zone of a Client Access server, so the time zone should always be treated as an unknown.
  2. When this problem is fixed, any mathematical calculations would show up as a problem in the client application.

To make this work correctly, we need apply a solution that enables our autogenerated proxy code to ignore the offset, and to do so in such a way that any future fixes will not cause us to have to rebuild our applications. It turns out that we can use the IXmlSerializable interface that is provided by the.NET Framework to achieve this. Unfortunately, implementing IXmlSerializable is no small task; it requires us to do four things:

  1. Find the System.Xml.Serialization.XmlTypeAttribute on our autogenerated proxy class and remove it.
  2. Create a new partial class for the type that can return the full schema of the type in a GetSchema() method.
  3. Provide a ReadXml() method that is capable of parsing all of the sibling elements of the type from the raw XML stream.
  4. Provide a WriteXml() method this is capable of turning all member properties into their raw XML form.

We must do all four of these for the CalendarEventType in our autogenerated proxy class.

Removing the System.Xml.Serialization.XmlTypeAttribute from CalendarEventType

To remove the System.Xml.Serialization.XmlTypeAttribute, find the .cs file that contains all the autogenerated proxy code. CTRL+F is your friend here. In my proxy code it exists at line 13187. The following example shows you how to comment out the XmlTypeAttribute for the CalendarEvent type in proxy code.

    /// <remarks/>
    [System.CodeDom.Compiler.GeneratedCodeAttribute("wsdl", "2.0.50727.42")]
    [System.SerializableAttribute()]
    [System.Diagnostics.DebuggerStepThroughAttribute()]
    [System.ComponentModel.DesignerCategoryAttribute("code")]
    //[System.Xml.Serialization.XmlTypeAttribute(Namespace="http://.../types")]
    public partial class CalendarEvent { ... }

Creating the IXmlSerializable partial class

The following C# code example shows you how to create a new partial class that implements IXmlSerializable to control the formatting of StartTime and EndTime and successfully returns a valid schema, and also handles the reading and writing of XML whenever it is requested.

///<summary>
/// Proxy extension for Suggestion that implements IXmlSerializable.  The purpose of this extension
/// is to control the XML for the StartTime and EndTime properties during deserialization due
/// to a problem in the Exchange server where the CalendarEvent is a UTC-offset string that is 
/// incorrectly stamped with the server time zone (it should be a non-UTC-offset string).
///</summary>
///<remarks>
/// For this to work, the XmlTypeAttribute that WSDL puts on this type in the autogenerated .cs file must be removed.
/// For example:
///<code> 
///     [System.CodeDom.Compiler.GeneratedCodeAttribute("wsdl", "2.0.50727.42")]
///     [System.SerializableAttribute()]
///     [System.Diagnostics.DebuggerStepThroughAttribute()]
///     [System.ComponentModel.DesignerCategoryAttribute("code")]
///     //[System.Xml.Serialization.XmlTypeAttribute(Namespace="http://.../types")]
///     public partial class CalendarEvent {...}
///</code>   
///</remarks>
public partial class CalendarEvent : IXmlSerializable
{
    public CalendarEvent()
    {

    }


    public System.Xml.Schema.XmlSchema GetSchema()
    {
        // Schema to return
        //  
        //     <?xml version="1.0" encoding="utf-8" ?>
        //<xs:schema 
        //        id="types"
        //        elementFormDefault="qualified"
        //        version="Exchange2007" 
        //        xmlns:t="https://schemas.microsoft.com/exchange/services/2006/types"
        //        targetNamespace="https://schemas.microsoft.com/exchange/services/2006/types"
        //        xmlns:tns="https://schemas.microsoft.com/exchange/services/2006/types"
        //        xmlns:xs="http://www.w3.org/2001/XMLSchema">
        //  <xs:complexType name="CalendarEvent">
        //    <xs:sequence>
        //      <xs:element minOccurs="1" maxOccurs="1" name="StartTime" type="xs:dateTime" />
        //      <xs:element minOccurs="1" maxOccurs="1" name="EndTime" type="xs:dateTime" />
        //      <xs:element minOccurs="1" maxOccurs="1" name="BusyType"
        //                  type="t:LegacyFreeBusyType" />
        //      <xs:element minOccurs="0" maxOccurs="1" name="CalendarEventDetails" 
        //                  type="t:CalendarEventDetails" />
        //    </xs:sequence>
        //  </xs:complexType>
        string xsTypes = "https://schemas.microsoft.com/exchange/services/2006/types";
        string xsSchema = "http://www.w3.org/2001/XMLSchema";

        XmlSchema schema = new XmlSchema();
        schema.Id = "types";
        schema.ElementFormDefault = XmlSchemaForm.Qualified;
        schema.Version = "Exchange2007";
        schema.TargetNamespace = xsTypes;

        // <xs:complexType ... >
        XmlSchemaComplexType xmlct1 = new XmlSchemaComplexType();
        schema.Items.Add(xmlct1);
        xmlct1.Name = "CalendarEvent";

        //  <xs:sequence ... >
        XmlSchemaSequence xmlsq1 = new XmlSchemaSequence();
        xmlct1.Particle = xmlsq1;

        //    <xs:element minOccurs="1" maxOccurs="1" name="StartTime" type="xs:dateTime" />
        XmlSchemaElement xmle1 = new XmlSchemaElement();
        xmlsq1.Items.Add(xmle1);
        xmle1.Name = "StartTime";
        xmle1.MinOccurs = 1;
        xmle1.MaxOccurs = 1;
        xmle1.SchemaTypeName = new XmlQualifiedName("dateTime", xsSchema);

        //    <xs:element minOccurs="1" maxOccurs="1" name="EndTime" type="xs:dateTime" />
        XmlSchemaElement xmle2 = new XmlSchemaElement();
        xmlsq1.Items.Add(xmle2);
        xmle2.Name = "EndTime";
        xmle2.MinOccurs = 1;
        xmle2.MaxOccurs = 1;
        xmle2.SchemaTypeName = new XmlQualifiedName("dateTime", xsSchema);

        //    <xs:element minOccurs="1" maxOccurs="1" name="BusyType" type="t:Le..." />
        XmlSchemaElement xmle3 = new XmlSchemaElement();
        xmlsq1.Items.Add(xmle3);
        xmle3.Name = "BusyType";
        xmle3.MinOccurs = 1;
        xmle3.MaxOccurs = 1;
        xmle3.SchemaTypeName = new XmlQualifiedName("LegacyFreeBusyType", xsTypes);

        //    <xs:element minOccurs="0" maxOccurs="1" name="Calenda..." type="t:Cal..." />
        XmlSchemaElement xmle4 = new XmlSchemaElement();
        xmlsq1.Items.Add(xmle4);
        xmle4.Name = "CalendarEventDetails";
        xmle4.MinOccurs = 0;
        xmle4.MaxOccurs = 1;
        xmle4.SchemaTypeName = new XmlQualifiedName("CalendarEventDetails", xsTypes);

        return schema;
    }
    public void ReadXml(System.Xml.XmlReader reader)
    {
        string xsTypes = "https://schemas.microsoft.com/exchange/services/2006/types";

        // Grab the LocalName of the element that we are currently at. It should be
        // CalendarEvent.  When we reach an EndElement with this name, we
        // are finished with our section of the XmlStream.
        //
        string toplevelElementName = reader.LocalName;
        reader.Read();

        while (1 == 1)
        {
            // Determine whether processing is finished.
            if (reader.NodeType == XmlNodeType.EndElement && 0 == 
                String.Compare(reader.LocalName, toplevelElementName))
            {
                // Processing is complete; consume this EndElement and stop processing.
                reader.Read();
                break;
            }

            if (reader.NodeType == XmlNodeType.EndElement)
            {
                // This means that we are at the closing tag of </CalendarEventDetails>.
                // No data here to process.
                reader.Read();
                continue;
            }

            // Consume StartTime or EndTime.
            if ((0 == String.Compare(reader.LocalName, "StartTime")) ||
                (0 == String.Compare(reader.LocalName, "EndTime")))
            {
                // Save the LocalName - we'll need this to determine whether this is the 
                // StartTime or EndTime field later.
                string localName = reader.LocalName;

                // StartTime or EndTime is the primary reason we needed to implement 
                // IXmlSerializable. The server will always append a time zone offset to the 
                // suggestion. This offset cannot be trusted, and is incorrect. The time of 
                // the event is always valid if it is treated as local time.
                //
                // Use a Regular Expression to extract whatever was supplied as a local 
                // time only.
                //
                string timeValue = reader.ReadElementContentAsString();
                System.Text.RegularExpressions.Regex regex = new System.Text.RegularExpressions.Regex(
                        @"(?<untimezoned>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})",
                        System.Text.RegularExpressions.RegexOptions.Compiled);

                string unZonedTimeValue = regex.Match(timeValue).Result("${untimezoned}");
                DateTime parsedDateTime = DateTime.Parse(unZonedTimeValue);

                // Set to the appropriate field.
                if (0 == String.Compare(localName, "StartTime"))
                {
                    this.startTimeField = parsedDateTime;
                }
                else
                {
                    this.endTimeField = parsedDateTime;
                }
            }

            // Consume BusyType.
            if (0 == String.Compare(reader.LocalName, "BusyType"))
            {
                string value = reader.ReadElementContentAsString();
                this.busyTypeField =
                    (LegacyFreeBusyType)Enum.Parse(typeof(LegacyFreeBusyType), value);
            }

            // Consume CalendarEventDetails. We will roll an XmlSerializer for 
            // that to allow the default serialization process of the type to do the 
            // heavy lifting.
            if (0 == String.Compare(reader.LocalName, "CalendarEventDetails"))
            {
                using (System.IO.StringReader strdr = new System.IO.StringReader(reader.ReadOuterXml()))
                {
                    XmlSerializer xmls = new XmlSerializer(typeof(CalendarEventDetails), xsTypes);
                    this.calendarEventDetailsField = (CalendarEventDetails)xmls.Deserialize(strdr);
                }
            }
        }
    }

    public void WriteXml(System.Xml.XmlWriter writer)
    {
        string xsTypes = "https://schemas.microsoft.com/exchange/services/2006/types";

        // Our position in the writer already includes a StartElement for our type;
        // therefore, our job is to pick up writing to the stream all our content
        // starting with the attributes of the StartElement.

        // Write StartTime.
        writer.WriteElementString("StartTime",
            System.Xml.XmlConvert.ToString((DateTime)this.startTimeField, 
                XmlDateTimeSerializationMode.RoundtripKind));

        // Write EndTime.
        writer.WriteElementString("EndTime",
            System.Xml.XmlConvert.ToString((DateTime)this.endTimeField, 
                XmlDateTimeSerializationMode.RoundtripKind));

        // Write BusyType.
        writer.WriteElementString("BusyType", xsTypes, this.busyTypeField.ToString());

        // Write CalendarEventDetails.
        XmlSerializer xmls = new XmlSerializer(typeof(CalendarEventDetails), xsTypes);
        xmls.Serialize(writer, this.calendarEventDetailsField);
    }
}

Here's an overview of what is occurring. First, in the GetSchema() method, we are returning an System.Xml.Schema.XmlSchema instance as it appears in the .xsd definition file of our service. Second, in the ReadXml() method, we are given an XmlReader ‘stream’ which is already positioned at the point in the XML where our CalendarEvent begins. Our job then is to read this XML and populate the CalendarEvent properties accordingly.

You can see that we are using a RegularExpression to extract only the data from the date/time string that we care about. This pattern allows the date/time string to contain a time zone offset; we just will ignore it. In addition to handling the StartTime and EndTime properties, you can see that we must also handle the BusyType and CalendarEventDetails properties. Because we know that BusyType is just an enumeration value from LegacyFreeBusyType, that process is straightforward. The CalendarEventDetails, however, is an instance of another nested type, and if we were to parse it, we would likely have to include additional error-prone XML parsing (and we are injecting an awful lot of code for such a contained error already), so to expedite this, we will create an instance of an XmlSerializer and let it do the work for us.

Finally, in the WriteXml() method, we write out to an XmlWriter all the properties of our type. Similar to what we did in ReadXml(), we handle the properties at our level, and create an XmlSerializer to handle the CalendarEventDetails node.

Let's take a look at how this works. Consider again Andy’s morning seminar CalendarEvent in raw XML from the server.

<CalendarEvent>
  <StartTime>2007-08-01T08:00:00-04:00</StartTime>
  <EndTime>2007-08-01T12:00:00-04:00</EndTime>
  <BusyType>Busy</BusyType>
  <CalendarEventDetails ... />
</CalendarEvent>

The following figure shows the same CalendarEvent after it has been deserialized into a CalendarEvent type and viewed in the Visual Studio debugger on a PST client before our partial CalendarEvent class was compiled into the project.

CalendarEvent StartTime/EndTime without custom XML serialization
CalendarEvent StartTime/EndTime

The following figure shows the CalendarEvent after our partial CalendarEvent class was compiled into the project. In this figure the StartTime and EndTime structures are set to the values that we expected.

CalendarEvent StartTime/EndTime with custom XML serialization
CalendarEvent StartTime/EndTime

Working with Time Zones in Suggestions

The GetUserAvailability Web service method allows for callers to get a listing of potential meeting suggestions for a group of participants. Each suggestion includes a MeetingTime element that holds the suggested time for a potential open slot on all participants’ calendars. The following code example shows a meeting suggestion.

<Suggestion>
  <MeetingTime>2007-08-01T12:00:00-04:00</MeetingTime>
  <IsWorkTime>true</IsWorkTime>
  <SuggestionQuality>Excellent</SuggestionQuality>
  <AttendeeConflictDataArray>
    <IndividualAttendeeConflictData>
      <BusyType>Free</BusyType>
    </IndividualAttendeeConflictData>
  </AttendeeConflictDataArray>
</Suggestion>

As you can see, the server appends its own time zone offset to the date/time string within the MeetingTime element of the Suggestion.

For applications that deal directly with XML, the solution is the same as that for the CalendarEvent: to ignore any time zone offset in the MeetingTime element and treat the date/time string as a non-UTC-offset string. The time zone context comes from the TimeZone element of the request.

If you are working with proxy code, the solution for Suggestions is the same: remove the System.Xml.Serialization.XmlTypeAttribute from the autogenerated proxy code, and create a partial Suggestion class that implements IXmlSerializable, as shown in the following code example.

///<summary>
/// Proxy extension for Suggestion that implements IXmlSerializable.  The purpose of this extension
/// is to control the XML for the MeetingTime propery during deserialization because of a problem in
/// the Exchange server where the offset of the MeetingTime is a  string that is incorrectly 
/// stamped with the time zone of the server (it should be a non-UTC-offset string).
///</summary>
///<remarks>
/// For this to work, the XmlTypeAttribute that WSDL puts on this type in the autogenerated .cs file must be removed.
/// For example:
///<code> 
///     [System.CodeDom.Compiler.GeneratedCodeAttribute("wsdl", "2.0.50727.42")]
///     [System.SerializableAttribute()]
///     [System.Diagnostics.DebuggerStepThroughAttribute()]
///     [System.ComponentModel.DesignerCategoryAttribute("code")]
///     //[System.Xml.Serialization.XmlTypeAttribute(Namespace="http://.../types")]
///     public partial class Suggestion {...}
///</code>   
///</remarks>
public partial class Suggestion : IXmlSerializable
{
    public Suggestion()
    {

    }


    public System.Xml.Schema.XmlSchema GetSchema()
    {
        // Schema to return.
        //  
        //     <?xml version="1.0" encoding="utf-8" ?>
        //<xs:schema 
        //        id="types"
        //        elementFormDefault="qualified"
        //        version="Exchange2007" 
        //        xmlns:t="https://schemas.microsoft.com/exchange/services/2006/types"
        //        targetNamespace="https://schemas.microsoft.com/exchange/services/2006/types"
        //        xmlns:tns="https://schemas.microsoft.com/exchange/services/2006/types"
        //        xmlns:xs="http://www.w3.org/2001/XMLSchema">
        // <xs:complexType name="Suggestion">
        //  <xs:sequence>
        //    <xs:element minOccurs="1" maxOccurs="1" name="MeetingTime" type="xs:dateTime" />
        //    <xs:element minOccurs="1" maxOccurs="1" name="IsWorkTime" type="xs:boolean" />
        //    <xs:element minOccurs="1" maxOccurs="1" name="SuggestionQuality" 
        //                type="t:SuggestionQuality" />
        //    <xs:element minOccurs="0" maxOccurs="1" name="AttendeeConflictDataArray" 
        //                type="t:ArrayOfAttendeeConflictData" />
        //  </xs:sequence>
        // </xs:complexType>

        string xsTypes = "https://schemas.microsoft.com/exchange/services/2006/types";
        string xsSchema = "http://www.w3.org/2001/XMLSchema";

        XmlSchema schema = new XmlSchema();
        schema.Id = "types";
        schema.ElementFormDefault = XmlSchemaForm.Qualified;
        schema.Version = "Exchange2007";
        schema.TargetNamespace = xsTypes;

        // <xs:complexType ... >
        XmlSchemaComplexType xmlct1 = new XmlSchemaComplexType();
        schema.Items.Add(xmlct1);
        xmlct1.Name = "Suggestion";

        //  <xs:sequence ... >
        XmlSchemaSequence xmlsq1 = new XmlSchemaSequence();
        xmlct1.Particle = xmlsq1;

        //    <xs:element minOccurs="1" maxOccurs="1" name="MeetingTime" type="xs:dateTime" />
        XmlSchemaElement xmle1 = new XmlSchemaElement();
        xmlsq1.Items.Add(xmle1);
        xmle1.Name = "MeetingTime";
        xmle1.MinOccurs = 1;
        xmle1.MaxOccurs = 1;
        xmle1.SchemaTypeName = new XmlQualifiedName("dateTime", xsSchema);

        //    <xs:element minOccurs="1" maxOccurs="1" name="IsWorkTime" type="xs:boolean" />
        XmlSchemaElement xmle2 = new XmlSchemaElement();
        xmlsq1.Items.Add(xmle2);
        xmle2.Name = "IsWorkTime";
        xmle2.MinOccurs = 1;
        xmle2.MaxOccurs = 1;
        xmle2.SchemaTypeName = new XmlQualifiedName("boolean", xsSchema);

        //    <xs:element minOccurs="1" maxOccurs="1" name="SuggestionQuality" type="t:Sug..." />
        XmlSchemaElement xmle3 = new XmlSchemaElement();
        xmlsq1.Items.Add(xmle3);
        xmle3.Name = "SuggestionQuality";
        xmle3.MinOccurs = 1;
        xmle3.MaxOccurs = 1;
        xmle3.SchemaTypeName = new XmlQualifiedName("SuggestionQuality", xsTypes);

        //    <xs:element minOccurs="0" maxOccurs="1" name="Atten..." type="t:ArrayOfAtte..." />
        XmlSchemaElement xmle4 = new XmlSchemaElement();
        xmlsq1.Items.Add(xmle4);
        xmle4.Name = "AttendeeConflictDataArray";
        xmle4.MinOccurs = 0;
        xmle4.MaxOccurs = 1;
        xmle4.SchemaTypeName = new XmlQualifiedName("ArrayOfAttendeeConflictData", xsTypes);

        return schema;
    }
    public void ReadXml(System.Xml.XmlReader reader)
    {
        string xsTypes = "https://schemas.microsoft.com/exchange/services/2006/types";

        // Grab the LocalName of the element that we are currently at. It should be
        // Suggestion. When we reach an EndElement with this name, we
        // are finished with our section of the XmlStream.
        //
        string toplevelElementName = reader.LocalName;
        reader.Read();

        while (1 == 1)
        {
            // Determine whether processing is finished.
            if (reader.NodeType == XmlNodeType.EndElement && 0 == String.Compare(reader.LocalName, toplevelElementName))
            {
                // Processing is finished. Consume this EndElement and stop processing.
                reader.Read();
                break;
            }

            if (reader.NodeType == XmlNodeType.EndElement)
            {
                // This means that we are at the closing tag of </AttendeeConflictDataArray>.
                // No data here to process.
                reader.Read();
                continue;
            }

            // Consume MeetingTime.
            if (0 == String.Compare(reader.LocalName, "MeetingTime"))
            {
                // MeetingTime is the primary reason we needed to implement IXmlSerializable. The
                // server will always append a time zone offset to the suggestion. This offset
                // cannot be trusted, and is incorrect. The time of the suggestion is
                // always valid if it is treated as local time.
                //
                // Use a Regular Expression to extract whatever was supplied as a local time only.
                //
                string meetingTimeValue = reader.ReadElementContentAsString();
                System.Text.RegularExpressions.Regex regex = new System.Text.RegularExpressions.Regex(
                        @"(?<untimezoned>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})",
                        System.Text.RegularExpressions.RegexOptions.Compiled);

                string unZonedTimeValue = regex.Match(meetingTimeValue).Result("${untimezoned}");
                this.meetingTimeField = DateTime.Parse(unZonedTimeValue);
            }

            // Consume IsWorkTime.
            if (0 == String.Compare(reader.LocalName, "IsWorkTime"))
            {
                this.isWorkTimeField = reader.ReadElementContentAsBoolean();
            }

            // Consume SuggestionQuality.
            if (0 == String.Compare(reader.LocalName, "SuggestionQuality"))
            {
                string value = reader.ReadElementContentAsString();
                this.suggestionQualityField =
                    (SuggestionQuality)Enum.Parse(typeof(SuggestionQuality), value);
            }

            // Consume AttendeeConflictDataArray.
            if (0 == String.Compare(reader.LocalName, "AttendeeConflictDataArray"))
            {
                // Unfortunately, the XmlSerializer can't just deserialize an array of items;
                // therefore we have to look at the types of each element of the array 
                // and deserialize them based on their type.
                XmlDocument xmld = new XmlDocument();
                string outerXml = reader.ReadOuterXml();
                xmld.LoadXml(outerXml);

                if (!xmld.HasChildNodes)
                {
                    // This an an empty AttendeeConflictDataArray, so we are finished.
                    this.attendeeConflictDataArrayField = new AttendeeConflictData[0];
                    continue;
                }

                XmlNodeList attendeeConflictNodes = xmld.FirstChild.ChildNodes;
                List<AttendeeConflictData> attendeeConflictDataList = new List<AttendeeConflictData>(attendeeConflictNodes.Count);

                foreach (XmlNode xmln in attendeeConflictNodes)
                {
                    if (0 == String.Compare(xmln.Name, "IndividualAttendeeConflictData"))
                    {
                        using (System.IO.StringReader strdr = new System.IO.StringReader(xmln.OuterXml))
                        {
                            XmlSerializer xmls = new XmlSerializer(typeof(IndividualAttendeeConflictData), xsTypes);
                            attendeeConflictDataList.Add( (IndividualAttendeeConflictData)xmls.Deserialize(strdr));
                        }
                    }
                    if (0 == String.Compare(xmln.Name, "GroupAttendeeConflictData"))
                    {
                        using (System.IO.StringReader strdr = new System.IO.StringReader(xmln.OuterXml))
                        {
                            XmlSerializer xmls = new XmlSerializer(typeof(GroupAttendeeConflictData), xsTypes);
                            attendeeConflictDataList.Add((GroupAttendeeConflictData)xmls.Deserialize(strdr));
                        }
                    }
                    if (0 == String.Compare(xmln.Name, "UnknownAttendeeConflictData"))
                    {
                        using (System.IO.StringReader strdr = new System.IO.StringReader(xmln.OuterXml))
                        {
                            XmlSerializer xmls = new XmlSerializer(typeof(UnknownAttendeeConflictData), xsTypes);
                            attendeeConflictDataList.Add((UnknownAttendeeConflictData)xmls.Deserialize(strdr));
                        }
                    }
                    if (0 == String.Compare(xmln.Name, "TooBigGroupAttendeeConflictData"))
                    {
                        using (System.IO.StringReader strdr = new System.IO.StringReader(xmln.OuterXml))
                        {
                            XmlSerializer xmls = new XmlSerializer(typeof(TooBigGroupAttendeeConflictData), xsTypes);
                            attendeeConflictDataList.Add((TooBigGroupAttendeeConflictData)xmls.Deserialize(strdr));
                        }
                    }
                }

                // Convert our list of AttendeeConflictData to an array.
                this.attendeeConflictDataArrayField = attendeeConflictDataList.ToArray();
            }
        }
    }

    public void WriteXml(System.Xml.XmlWriter writer)
    {
        string xsTypes = "https://schemas.microsoft.com/exchange/services/2006/types";

        // Our position in the writer already includes a StartElement for our type;
        // therefore, our job is to pick up writing to the stream all our content
        // starting with the attributes of the StartElement.

        // Write MeetingTime.
        writer.WriteElementString("MeetingTime",
            System.Xml.XmlConvert.ToString((DateTime)this.meetingTimeField, XmlDateTimeSerializationMode.RoundtripKind));

        // Write IsWorkTime.
        writer.WriteElementString("IsWorkTime", xsTypes, this.isWorkTimeField.ToString());

        // Write SuggestionQuality.
        writer.WriteElementString("SuggestionQuality", xsTypes, this.suggestionQualityField.ToString());

        // Write AttendeeConflictDataArray.
        XmlSerializer xmls = new XmlSerializer(typeof(AttendeeConflictData[]), xsTypes);
        xmls.Serialize(writer, this.attendeeConflictDataArrayField);
    }
}