Skip to content

Conversation

@MIchaelMainer
Copy link
Collaborator

@MIchaelMainer MIchaelMainer commented Oct 29, 2019

Delta query provides a way to track changes. You begin to track changes with a request like this:

GET https://graph.microsoft.com/v1.0/users/delta?$select=displayName,givenName,surname

var userDeltaCollectionPage = await graphClient.Users.Delta().Request().Select("displayName,givenName,surname").GetAsync();

Currently, a developer has to inspect the selected properties on each object in the page. The inspection requires first that they check that the selected properties are not null before getting the changed value. If the selected property is a primitive, then they can capture that value. If the selected property is an object, they must also inspect all of its property with a null check before capturing the values. This may mean unnecessarily checking many properties that have not changed. This PR should reduce the amount of null checking that has to occur when a delta query payload is received by providing a manifest of changes. This is especially useful when a property has been set to null as previously, there was no way to determine whether a property has been set to null, or was not set and so it defaulted to null.

The change manifest is set on each item in the CollectionPage. It is found in the AdditionalData dictionary with a a key called changes. This functionality is accessed via:

Microsoft.Graph.Core

var deltaServiceLibResponse = await deltaResponseHandler.HandleResponse<JObject>(httpresponsemessage);
var changesOnTheFirstItem = deltaJObjectResponse["value"][0]["changes"] as JArray;

// This is not an important scenario as the developer can query whether a value is set null using JMESPath or using Newtonsoft.

Microsoft.Graph

This enables developers to discover values set to null when using the service library.

var eventsDeltaCollectionPage = await graphClient.Me.CalendarView
                                               .Delta()
                                               .Request()
                                               .WithResponseHandler(new DeltaResponseHandler())
                                               .Select("subject")
                                               .GetAsync();

// Accesses the changes on each page item.
eventsDeltaCollectionPage[0].AdditionalData.TryGetValue("changes", out object changes);
var changeList = (changes as JArray).ToObject<List<string>>();

// Updating a non-schematized property on a model such as instance annotations, open types,
// and schema extensions. Can also be used to discover deleted items.
if (changeList.Exists(x => x.Equals("@removed.reason")))
{
    eventsDeltaCollectionPage[0].AdditionalData.TryGetValue("@removed", out object odataEtag);
    Console.Writeline("This item was removed.")
    // TODO: myModel.Delete();
}

// Core scenario - update schematized property regardless of it is set to null.
// This property has been set to null in the response. We can be confident that
// whatever the value set is correct, regardless whether it is null.
if (changeList.Exists(x => x.Equals("subject")))
{
    myModel.Subject = eventsDeltaCollectionPage[0].Subject;
}

// Update the value on a complex type property's value. Developer can't just replace the body
// as that could result in overwriting other unchanged property. Essentially, they need to inspect
// every leaf node in the selected property set.
if (changeList.Exists(x => x.Equals("body.content"))) // 
{
    if (myModel.Body == null)
    {
        myModel.Body = new ItemBody();
    }

    myModel.Body.Content = eventsDeltaCollectionPage[0].Body.Content;
}

// Update complex type property's value when the value is a collection of objects.
// We don't know whether this is an update or add without querying the client model.
// We will need to check each object in the model.
var attendeesChangelist = changeList.FindAll(x => x.Contains("attendees"));
if (attendeesChangelist.Count > 0)
{
    // This is where if we provided the delta response as a JSON object, 
    // we could let the developer use JMESPath to query the changes.
    if (changeList.Exists(x => x.Equals("attendees[0].emailAddress.name")))
    {
        if (myModel.Attendees == null) // Attendees are being added for the first time.
        {
            var attendees = new List<Attendee>();
            attendees.AddRange(eventsDeltaCollectionPage[0].Attendees);
            myModel.Attendees = attendees;
        }
        else // Attendees list is being updated.
        {
            // We need to inspect each object, and determine which objects and properties 
            // need to be initialized and/or updated.
        }
    }
}

The raw JSON on the object after adding the change list looks like:

{
    "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#Collection(event)",
    "@odata.nextLink": "https://graph.microsoft.com/v1.0/me/calendarView/delta?$skiptoken=R0usmci39OQxqJrxK4",
    "value": [
        {
            "@odata.type": "#microsoft.graph.event",
            "@odata.etag": "EZ9r3czxY0m2jz8c45czkwAAFXcvIw==",
            "subject": null,
            "id": "AAMkADVxTAAA=",
            "changes" : ["@odata.type", "@odata.etag", "subject", "id"]
        }
    ]
}

This also now allows developers to add their own response handler to any request builder. This enables new ways to process the contents of a request builder based response.

Generally, a change results in the returning the entire item with the original property set.

The delta feed shows the latest state for each item, not each change. If an item were renamed twice, it would only show up once, with its latest name.

https://docs.microsoft.com/en-us/graph/api/driveitem-delta?view=graph-rest-1.0&tabs=http

  • Put changes on the object, not the page collection. Will need to JObject, not JsonTextReader
  • Should work with only core and also with service libraries.
  • support open properties in the change list.

modified:   src/Microsoft.Graph.Core/Extensions/BaseRequestExtensions.cs

Added WithResponseHandler<T>

modified:   src/Microsoft.Graph.Core/Requests/BaseRequest.cs

Added property to make the response handler accessible.

new file:   src/Microsoft.Graph.Core/Requests/DeltaResponseHandler.cs

Added DeltaResponseHandler to capture changed property names
and add them to a list that is added to a prpoerty named 'changes'.

modified:   src/Microsoft.Graph.Core/Requests/IBaseRequest.cs

Added ResponseHandler property.

new file:   src/Microsoft.Graph.Core/Requests/IResponseHandler.cs

Added a common interface for creating response handlers.

modified:   src/Microsoft.Graph.Core/Requests/ResponseHandler.cs

Now implements the common interface.

modified:   tests/Microsoft.Graph.DotnetCore.Core.Test/Requests/ResponseHandlerTests.cs
@andrueastman
Copy link
Contributor

As suggested, I agree it would be a better experience to provide a way to return an instance of List<List<string>> with the first level being the list of change objects and the second level being the list of strings of changed properties in dot notation.

I think this may also make it easier for the user to correlate the changes property list to the actual values in the DeltaCollectionPage. The user could possibly use the index in the 'changes' list to retrieve the corresponding actual value using the same index in the DeltaCollectionPage.

/// deserializer can't express changes to null so you can now discover if a property
/// has been set to null. This is intended for use with a Delta query scenario.
/// </summary>
public class DeltaResponseHandler : IResponseHandler
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add docs on how to use this. Ideally, this is only used after an initial sync, as we have to inspect the contents of the response twice to determine the change list.

@MIchaelMainer
Copy link
Collaborator Author

@andtu for future review

@MIchaelMainer MIchaelMainer added this to the 1.20.0 milestone Dec 6, 2019
Copy link
Contributor

@andrueastman andrueastman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works as described in the description. 👍

andrueastman
andrueastman previously approved these changes Jan 3, 2020
Copy link
Contributor

@andrueastman andrueastman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants