article-bg

GraphAPI delta request

Jul. 2024
24

Requesting a resource delta via the REST GraphAPI isn't as straightforward as it should be. As a matter of fact, it was easily possible with the NuGet version 4.x, but delta-request support has been removed with the major release of version 5.0. However, despite not offering any official library delta request support, it still is possible with a cumbersome workaround.

Extension methods

internal static class RequestInformationExtensions
{
    public static RequestInformation EnableDelta(this RequestInformation reqInfo, string skipToken = null, string deltaToken = null)
    {
        StringBuilder queryBuilder = new StringBuilder();

        if (!string.IsNullOrEmpty(skipToken))
            queryBuilder.Append(",%24skiptoken");
        if (!string.IsNullOrEmpty(deltaToken))
            queryBuilder.Append(",%24deltatoken");

        string query = queryBuilder.ToString();

        reqInfo.UrlTemplate = reqInfo.UrlTemplate.Insert(reqInfo.UrlTemplate.Length - 1, query);

        if (!string.IsNullOrEmpty(skipToken))
            reqInfo.QueryParameters.Add("%24skiptoken", skipToken);
        if (!string.IsNullOrEmpty(deltaToken))
            reqInfo.QueryParameters.Add("%24deltatoken", deltaToken);

        return reqInfo;
    }
}
internal static class IRequestAdapterExtensions
{
    private const string SKIP_TOKEN_IDENTIFIER = "$skiptoken=";
    private const string DELTA_TOKEN_IDENTIFIER = "$deltatoken=";

    public static async Task<DeltaResponse<ResponseCollection>> SendDeltaAsync<ResponseCollection>(this IRequestAdapter requestAdapter, 
        RequestInformation requestInformation, 
        ParsableFactory<ResponseCollection> parsable) 
        where ResponseCollection : IParsable
    {
        return await requestAdapter.SendAsync(requestInformation, (node) =>
        {
            var deltaTokenNode = node.GetChildNode("@odata.deltaLink");
            var skipTokenNode = node.GetChildNode("@odata.nextLink");

            string skipToken = null;
            string deltaToken = null;

            if (!string.IsNullOrEmpty(skipTokenNode?.GetStringValue()))
            {
                string tokenIdentifier = SKIP_TOKEN_IDENTIFIER;
                int tokenBegin = skipTokenNode.GetStringValue().IndexOf(tokenIdentifier) + tokenIdentifier.Length;
                skipToken = skipTokenNode.GetStringValue().Substring(tokenBegin);
            }

            if (!string.IsNullOrEmpty(deltaTokenNode?.GetStringValue()))
            {
                string tokenIdentifier = DELTA_TOKEN_IDENTIFIER;
                int tokenBegin = deltaTokenNode.GetStringValue().IndexOf(tokenIdentifier) + tokenIdentifier.Length;
                deltaToken = deltaTokenNode.GetStringValue().Substring(tokenBegin);
            }

            return new DeltaResponse<ResponseCollection>(skipToken, deltaToken, parsable(node));
        });
    }
}

Delta response

DeltaResponse is a response wrapper, solely providing the SkipToken and DeltaToken for follow-up requests. The GraphResponse contains the actual response object.

internal class DeltaResponse<ResponseCollection> : IParsable where ResponseCollection : IParsable
{
    public DeltaResponse(string skipToken, string deltaToken, ResponseCollection responseCollection) =>
        (GraphResponse, SkipToken, DeltaToken) = (responseCollection, skipToken, deltaToken);

    public IDictionary<string, Action<IParseNode>> GetFieldDeserializers() => GraphResponse.GetFieldDeserializers();

    public void Serialize(ISerializationWriter writer) => GraphResponse.Serialize(writer);

    public ResponseCollection GraphResponse { get; private set; }

    public string SkipToken { get; private set; }

    public string DeltaToken { get; private set; }

    public bool HasDeltaToken => !string.IsNullOrEmpty(DeltaToken);
}

Delta request

public async Task<Tuple<string, IEnumerable<Contact>>> GetDeltaAsync(string deltaToken = null, CancellationToken cancellationToken = default)
{
    List<Contact> contacts = new List<Contact>();

    async Task<DeltaResponse<ContactCollectionResponse>> BuildDeltaRequest(string skip = null, string delta = null)
    {
        var requestInformation = Client.Users["{mailbox}"].Contacts.Delta.ToGetRequestInformation().EnableDelta(skip, delta);
        return await Client.RequestAdapter.SendDeltaAsync(requestInformation, ContactCollectionResponse.CreateFromDiscriminatorValue);
    }

    var response = await BuildDeltaRequest(null, deltaToken);
            
    while (!cancellationToken.IsCancellationRequested)
    {
        foreach (var contact in response.GraphResponse.Value)
            contacts.Add(contact);

        if (response.HasDeltaToken)
        {
            return new Tuple<string, IEnumerable<Contact>>(response.DeltaToken, contacts);
        }
        else
        {
            response = await BuildDeltaRequest(response.SkipToken, deltaToken);
        }
    }

    return new Tuple<string, IEnumerable<Contact>>(null, contacts);
}