How to track changes in personal OneDrive with Microsoft Graph .NET SDK

Introduction

Delta query (also called change tracking) for driveItems enables to discover newly created, updated, or deleted driveItems without performing a full read of all items in OneDrive.

Using delta query helps you avoid constantly polling Microsoft Graph, as the app requests only data that changed since the last request. This pattern allows the application to reduce the amount of data it requests, thereby reducing the cost of the request and as such, likely limit the chances of the requests being throttled.

Implementing change tracking in OneDrive with the Microsoft Graph .NET SDK is not straightforward, but once you understand the concept, it is easy to implement not only for personal OneDrive.

Register Entra application

To access the Microsoft Graph API, you need to register an application in the Entra admin portal. The application will be used to authenticate the user and access personal OneDrive of signed-in user.

  1. Login to the Entra admin portal and register a new application.

  1. Add a new platform and select Mobile and desktop applications.

  1. Add the redirect URI http://localhost.

  1. To be able to track changes in personal OneDrive, you need to add and grant delegated permission Files.Read:

Go to the Overview page and copy the Application (client) ID and Directory (tenant) ID.

C# console application

Create a new C# console application (latest .NET) and install the following NuGet packages:

  • Microsoft.Graph - Access the Microsoft Graph API
  • Azure.Identity - Provides Entra token authentication support

Implementation

Let's say, we want to track changes in the personal OneDrive of the signed-in user for 1 hour. Create a cancellation token source and schedule cancellation after 1 hour.

var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromHours(1));

Instantiate the GraphServiceClient class and authenticate the user with the InteractiveBrowserCredential class.

var credential = new InteractiveBrowserCredential(new InteractiveBrowserCredentialOptions
{
    ClientId = "<client_id_from_app_registration>",
    TenantId = "<tenant_id>",
    LoginHint = "<username>" // optional
});

var graphClient = new GraphServiceClient(credential);

Use clientId and tenantId from the app registration. LoginHint is an optional parameter that specifies the username of the user to sign in. If you omit this parameter, the user will be prompted to enter their username.

Get the latest delta token

var latestTokenResponse = await graphClient.Drives["Me"].Items["Root"].DeltaWithToken("latest").GetAsDeltaWithTokenGetResponseAsync(cancellationToken: cts.Token);
var deltaLink = latestTokenResponse.OdataDeltaLink;
var timestamp = DateTime.UtcNow;

Here I'm using DeltaWithToken method to get the latest delta token. The OdataDeltaLink property contains the delta link that we will use to get the next set of changes.

Now, create a Task with a while loop that will run until the cancellation token is canceled. Inside the loop, we will retrieve changes using the delta link every 10 minutes.

var trackingTask = Task.Run(async () =>
{
    while (true)
    {
        // change delay if you want to track changes more frequently
        await Task.Delay(TimeSpan.FromMinutes(10), cts.Token);
        var response = await graphClient.Drives["Me"].Items["Root"].Delta.WithUrl(deltaLink).GetAsDeltaGetResponseAsync(rc =>
        {
            rc.Headers.Add("Prefer", "deltaExcludeParent");
        }, cts.Token);

        var pageIterator = PageIterator<DriveItem, DeltaGetResponse>.CreatePageIterator(graphClient, response, (item) =>
        {
            // ignore root item
            if (item.Name == "root")
            {
                return true;
            }

            if (item.Deleted != null)
            {
                Console.WriteLine($"Deleted item: {item.Id}, state: {item.Deleted.State}");
                return true;
            }

            if (item.CreatedDateTime > timestamp)
            {
                Console.WriteLine($"New item: {item.Id}, created: {item.CreatedDateTime}, name: {item.Name}");
                return true;
            }

            Console.WriteLine($"Modified item: {item.Id}, modified: {item.LastModifiedDateTime}, name: {item.Name}");
            return true;
        },
        rc =>
        {
            rc.Headers.Add("Prefer", "deltaExcludeParent");
            return rc;
        });

        await pageIterator.IterateAsync(cts.Token);
        // store the latest delta link and timestamp for the next iteration
        deltaLink = pageIterator.Deltalink;
        timestamp = DateTime.UtcNow;
    }
});

If you run the console application, you will see the changes in the OneDrive of the signed-in user like this:

To apply deltaLink, use the WithUrl(deltaLink) method. The Prefer header with the value deltaExcludeParent is used to exclude the information about parent folders of changed files. If you want to include parent folders, you can remove this header. By default, the delta endpoint includes information root of the drive, even if you specify the Prefer: deltaExcludeParent header.

The PageIterator class is a helper class from the Microsoft Graph .NET SDK that helps you iterate over the pages of driveItems. The callback inside the CreatePageIterator method is called for each driveItem in the response. Inside the callback, you can check if the item was deleted, created, or modified. For deleted items, the Deleted property is not null. For new items, the CreatedDateTime property is greater than the timestamp. It's up to you to make a check for created and modified items more robust.

The IterateAsync method is used to iterate over all pages of driveItems.

If you want to track changes in another drive where the user has access, you can replace Me (Drives["Me"]) with the drive ID. In Entra admin center, you need to add and grant delegated permission Files.Read.All to be able to track changes in other drives.

The sample application can be found in the GitHub repo

1
Buy Me a Coffee at ko-fi.com
An error has occurred. This application may no longer respond until reloaded. Reload x