1

I am integrating a planner application with Microsoft Graph using the C# nuget package 4.36.

Sometimes when I change an event from the Outlook.com online calendar, my Delta query will return the event as deleted, with End and Start properties == null:

AdditionalData[0] = {[@removed, ValueKind = Object : "{"reason":"deleted"}"]}

This only happens some times, I am often allowed to move an event backwards beyond "today". But if it is moved forward again, moving it up to, or beyond "today", the delta will return a deleted event. Often moving it forwards in time will return a deleted event, as long as it is dropped beyond "today". If I omit providing a deltaToken to the query, all events are returned in valid state even though they have been altered. But trying again to add my deltaToken, will return some deleted events, that are in fact never deleted.

What is happening and how do I avoid it?

My steps to reproduce the issue:

  1. A test environment in Azure AD is needed and proper set up of App Only authentication (what I am using).
  2. Having this, I create a calendar class with an active AppClient wrapping the Delta query like this:
public async Task<IEventDeltaCollectionPage> GetChangedEventsByUserId(string userId, int maxPageCount = 50,
            string deltaToken = "")
        {
            var queryOptions = new List<QueryOption>();
            queryOptions.Add(new QueryOption("startdatetime", "2010-08-01T00:00:00"));
            queryOptions.Add(new QueryOption("enddatetime",
                DateTime.Now.ToString("o"))); 
            // Check for skipToken -> Start from where we left of last sync:
            if (!string.IsNullOrWhiteSpace(deltaToken))
                queryOptions.Add(new QueryOption("$deltatoken", deltaToken));
            try
            {
                return await AppClient.Users[userId].Calendar.CalendarView
                    .Delta()
                    .Request(queryOptions)
                    .Header("Prefer", "outlook.body-content-type='text'")
                    .Header("Prefer", "outlook.timezone=\"W. Europe Standard Time\"")
                    .Header("Prefer", $"odata.maxpagesize={maxPageCount}")
                    .GetAsync();
            }
            catch (Exception e)
            {
                if (!string.IsNullOrWhiteSpace(deltaToken))
                {
                    // If the request fails and has been given a deltaToken, try without the token:
                    return await GetChangedEventsByUserId(userId, maxPageCount);
                }
            }

            return null;
        }
  1. I then created an "Explicit" nUnit-test that will create 3 events for me in a given users calendar. Breaking in the middle of this test, I change one event in outlook.com and observe that the delta query returns a deleted event Start and End == null. If however, I break at the first delta query where no deltaToken is given, I recieve all events as expected without any null values for Start/End.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Graph;
using NUnit.Framework;

[TestFixture]
public class GraphClientTests
{
[TestCase, Explicit]
        public void CheckDeltaQueryOnManualUpdates()
        {
            var timeout = new TimeSpan(0, 2, 0);
            var idList = new List<string>();
            var userId = "8078e9b9-088e-45ad-8c0b-ccf2e78b32d9";// Insert your own user ID

            // Create new events:
            var appointment = new Event
                {
                    Subject = "Let's go for lunch",
                    Body = new ItemBody
                    {
                        ContentType = BodyType.Html,
                        Content = "Forget your worries!"
                    },
                    Start = new DateTimeTimeZone
                    {
                        DateTime = (DateTime.Now.Date + TimeSpan.FromHours(12) + TimeSpan.FromDays(1)).ToString("o"),
                        TimeZone = "W. Europe Standard Time"
                    },
                    End = new DateTimeTimeZone
                    {
                        DateTime = (DateTime.Now.Date + TimeSpan.FromHours(12+8) + TimeSpan.FromDays(1)).ToString("o"),
                        TimeZone = "W. Europe Standard Time"
                    },
                    Location = new Location
                    {
                        DisplayName = "Blue Diner"
                    },
                };
            for (int i = 0; i < 3; i++)
            {
                idList.Add(_calendar.AddEvent(userId, appointment).Result.Id);
            }
        

            /// BREAK or step to this place to test without DeltaToken - this works
            var newEvents = _calendar.GetChangedEventsByUserId(userId, 7).Result;
            calendarList.AddRange(newEvents.ToList());

            while (newEvents.NextPageRequest != null)
            {
                newEvents = newEvents.NextPageRequest.GetAsync().Result;
                calendarList.AddRange(newEvents.ToList());
            }

            foreach (var happening in calendarList)
            {
                Assert.NotNull(happening.Start);
                Assert.NotNull(happening.End);
            }

            string deltaTokenName = "deltatoken=";
            // Get delta link to be used on next call:
            var deltaToken = newEvents.AdditionalData["@odata.deltaLink"].ToString();
            deltaToken = deltaToken.Substring(deltaToken.IndexOf(deltaTokenName, StringComparison.Ordinal) +
                                              deltaTokenName.Length);
            Assert.IsFalse(string.IsNullOrWhiteSpace(deltaToken));
            
            /// BREAK or step to this place to test with DeltaToken - This fails
            newEvents = _calendar.GetChangedEventsByUserId(userId, 7, deltaToken).Result;
            
            foreach (var happening in newEvents)
            {
                Assert.NotNull(happening.Start);
                Assert.NotNull(happening.End);
            }

            deltaToken = newEvents.AdditionalData["@odata.deltaLink"].ToString();
            deltaToken = deltaToken.Substring(deltaToken.IndexOf(deltaTokenName, StringComparison.Ordinal) +
                                              deltaTokenName.Length);
            /// BREAK here to restart from earlier break-points.
            Assert.IsFalse(string.IsNullOrWhiteSpace(deltaToken));

            // Cleanup after test is done:
            foreach (var deletableId in idList)
            {
                Task.WaitAll(new Task[]{
                    _calendar.DeleteEvent(userId, deletableId)
                }, timeout);
            }
        }
}

Update:

This question points in the direction of a hack to get around the issue: Since the "Deleted" event contains an Id, it is possible to look up the failing event directly via:

return await AppClient.Users[userId].Calendar.Events[eventId]
                    .Request()
                    .Header("Prefer", "outlook.body-content-type='text'")
                    .Header("Prefer", "outlook.timezone=\"W. Europe Standard Time\"")
                    .GetAsync();

Hence, by looping over all events from the delta query and asking directly for those with Start == null the desired set can be obtained. This however, still keeps me wondering why the Delta query does not give me edited events correctly. Any thoughts?

Håkon Seljåsen
  • 589
  • 5
  • 18

0 Answers0