tig
tig

Reputation: 3483

Updating an Appointment causes it to change to a Meeting in EWS 1.1

Here's what I'm trying to do:

I get the items with:

FindItemsResults<Appointment> findResults = calendar.FindAppointments(new CalendarView(startDate, endDate));

This query works fine. But whenever I call Update to save the item I get an exception:

Microsoft.Exchange.WebServices.Data.ServiceResponseException: One or more recipients are invalid.

Even though I get an exception, the item is saved and gets changed to have IsMeeting set to true! Now the updated item is a meeting with an organizer etc... This is, effectively, data corruption for me.

Here's the code. It is no more complicated than this. I've tested it by just changing Location or Subject and both cause the problem.

Appointment a = Appointment.Bind(_service, new ItemId(id));
a.Location = newLocation
a.Update(ConflictResolutionMode.AlwaysOverwrite);

Am I missing some concept or something? This seems like a pretty egregious problem.

FWIW, this is EWS 1.1 against an Office 365 server.

Upvotes: 11

Views: 7087

Answers (4)

Gavin G
Gavin G

Reputation: 886

To answer this bit of the question

"Even though I get an exception, the item is saved and gets changed to have IsMeeting set to true! Now the updated item is a meeting with an organizer etc... This is, effectively, data corruption for me."

The Microsoft documentation states, in the small print, "A meeting request is just an appointment that has attendees. You can convert an appointment into a meeting request by adding required attendees, optional attendees, or resources to the appointment" - as seen here http://msdn.microsoft.com/en-us/library/office/dd633641%28v=exchg.80%29.aspx

In other words, as soon as you have any attendees, Exchange converts it to a meeting automatically.

Upvotes: 1

donovan
donovan

Reputation: 1472

So tig's answer works when you never want to send out appointment updates to the other attendees. However to answer this properly you actually need to get the attendee state loaded.

By default it is trying to send appointment updates to the attendees, however your appointment object doesn't have the attendee state loaded and is hence blowing up. When you do the bind you should load the attendee properties. You should probably also load the organizer as well to cover another edge case:

  • AppointmentSchema.RequiredAttendees
  • AppointmentSchema.OptionalAttendees
  • AppointmentSchema.Resources
  • AppointmentSchema.Organizer

This will get the attendees populated if you want to do an update that sends out updates to the attendees.

However there is then another edge case that you have to worry about. If you have an appointment with no attendees added to it (just the organizer), then EWS may still complain and throw this error. It will actually work for appointments in some states, but fail in other states.

So the most complete solution is a combination of:

  1. Loading the attendee state.
  2. Inspecting the attendee state to see if there are any attendees other than the organizer (depending on how the appointment was created the organizer may or may not appear in the RequiredAttendees collection). If there are not then you must use SendInvitationsOrCancellationsMode.SendToNone.

So the full sample would look something like:

Appointment a = Appointment.Bind(_service, new ItemId(id), new PropertySet(AppointmentSchema.RequiredAttendees, AppointmentSchema.OptionalAttendees, AppointmentSchema.Resources, AppointmentSchema.Organizer));
a.Location = newLocation

// Check if the appointment has attendees other than the organizer. The organizer may
// or may not appear in the required attendees list.
if (HasNoOtherAttendee(a.Organizer.Address, a.RequiredAttendees) &&
    (a.OptionalAttendees.Count == 0) && (a.Resources.Count == 0))
{
    a.Update(ConflictResolutionMode.AlwaysOverwrite, SendInvitationsOrCancellationsMode.SendToNone);
}
else
{
    // We have other attendees in the appointment, so we can use SendToAllAndSaveCopy so
    // they will see the update.
    a.Update(ConflictResolutionMode.AlwaysOverwrite, SendInvitationsOrCancellationsMode.SendToAllAndSaveCopy);
}


bool HasNoOtherAttendee(string email, AttendeeCollection attendees)
{
    bool emptyOrOnlyMe = true;
    foreach (var a in attendees)
    {
        if (!string.Equals(email, a.Address, StringComparison.OrdinalIgnoreCase))
        {
            emptyOrOnlyMe = false;
            break;
        }
    }
    return emptyOrOnlyMe;
}

Upvotes: 5

guyBami_W
guyBami_W

Reputation: 1

public static bool UpdateAppointment(ExchangeCredential credentials,
           ItemId appointmentId, string newLocation, string newSubject, 
DateTime startTime,
            DateTime endTime)
        {
            ExchangeService service = GetExchangeService(credentials);
            try
            {
                Appointment appt = Appointment.Bind(service, appointmentId,
                    new PropertySet(BasePropertySet.IdOnly, AppointmentSchema.Start,
                        AppointmentSchema.ReminderDueBy, AppointmentSchema.End,   AppointmentSchema.StartTimeZone,
                        AppointmentSchema.TimeZone));

                appt.Location = newLocation;
                appt.Start = startTime;
                appt.End = endTime;
                appt.Subject = newSubject;
                // very important! you must load the new timeZone
                appt.StartTimeZone = TimeZoneInfo.Local;
                //appt.Body.Text = newBody; //if needed
                appt.Update(ConflictResolutionMode.AlwaysOverwrite);
            }
            catch (Exception ex)
            {
                throw ex;
            }

            return true;
        }

Upvotes: -1

tig
tig

Reputation: 3483

I figured it out with help from this Question:

Exchange Appointment Types

The key is the Update method needs to be called with the SendInvitationsOrCancellationsMode.SendToNone flag set in the 2nd parameter.

Like this:

a.Update(ConflictResolutionMode.AlwaysOverwrite, SendInvitationsOrCancellationsMode.SendToNone);

Upvotes: 16

Related Questions