Easily handle long-running operations using middleware in the Microsoft Graph .NET SDK


If you use the Microsoft Graph .NET SDK and need to handle long-running operations, build it as a middleware. Here’s how.

Why you should consider to use a Microsoft Graph SDK

Microsoft Graph .NET SDK offers you a convenient way to connect to Microsoft Graph - the API to data and insights on Microsoft 365. The SDK takes care of authentication and other request-related plumbing helping you to focus on building your app.

Long-running operations on Microsoft Graph

While most operations on Microsoft Graph are instantaneous, there are some exceptions. One example is creating a Microsoft Graph connector schema which can take several minutes. After you submit the schema, you get back a 202 Accepted response, with a Location header with the URL that you can call to check the status of the schema creation operation.

TIP: If you’re building code to handle the long-running operation of creating the Microsoft Graph connector schema, use the Microsoft 365 Developer Proxy mock to simulate the API behavior. Using the mock you’ll be able to easily test your code, without having to wait for the schema to be provisioned each time. It’ll save you a lot of time!

POST https://graph.microsoft.com/v1.0/external/connections/contosohr/schema
content-type: application/json

{
  "baseType": "microsoft.graph.externalItem",
  "properties": [
    {
      "name": "ticketTitle",
      "type": "String",
      "isSearchable": "true",
      "isRetrievable": "true",
      "labels": [
        "title"
      ]
    },
    {
      "name": "priority",
      "type": "String",
      "isQueryable": "true",
      "isRetrievable": "true",
      "isSearchable": "false"
    },
    {
      "name": "assignee",
      "type": "String",
      "isRetrievable": "true"
    }
  ]
}

202 Accepted
Location: https://graph.microsoft.com/v1.0/external/connections/contosohr/operations/616bfeed-666f-4ce0-8cd9-058939010bfc

Calling the URL gives you the current status of the creation operation:

GET https://graph.microsoft.com/v1.0/external/connections/contosohr/operations/616bfeed-666f-4ce0-8cd9-058939010bfc

200 OK
content-type: application/json

{
  "id": "1.neu.0278337E599FC8DBF5607ED12CF463E4.6410CCF8F6DB8758539FB58EB56BF8DC",
  "status": "inprogress",
  "error": null
}

If you want to wait until the operation completes, you need to poll the URL at regular intervals, until the status changes from inprogress to either completed or failed.

TIP: For checking the status of creating Microsoft Graph connector schema, Microsoft recommends polling the status every 1 minute.

As you can imagine, handling this flow adds a lot of code. A lot of code that makes it harder to understand what your app is doing exactly, but which you need nonetheless. Luckily, you don’t need to put it in the main flow of your app. By implementing it as a middleware, you can make it readily available to any piece of your code that needs it without having to invoke it explicitly.

What’s middleware

Microsoft Graph SDKs come with the concept of middleware, also known as handlers. Middleware are pieces of code that handle outgoing requests and incoming responses. They’re applied in a predefined order and run on every request you issue using the Microsoft Graph SDK.

Microsoft Graph SDKs ship with several middleware handlers and you can add your own to the pipeline too. What’s neat about middleware, is that it’s automatically applied to every request and doesn’t clutter your main code, making it easier to follow.

Handle long-running operations using middleware

The following is C# code that you can use to handle the long-running operation of creating Microsoft Graph connector schema when using the Microsoft Graph .NET SDK:

TIP: To see this middleware in action, check out the Microsoft Graph connector sample built using .NET that imports markdown content to Microsoft 365.

using Microsoft.Graph.Models.ExternalConnectors;
using Microsoft.Kiota.Abstractions.Serialization;

class CompleteJobWithDelayHandler : DelegatingHandler
{
  int delayMs;

  public CompleteJobWithDelayHandler(int delayMs = 60000)
  {
    this.delayMs = delayMs;
  }

  protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
  {
    var response = await base.SendAsync(request, cancellationToken);

    var location = response.Headers.FirstOrDefault(h => h.Key == "Location").Value?.FirstOrDefault();
    if (location is not null)
    {
      if (location.IndexOf("/operations/") < 0)
      {
        // not a job URL we should follow
        return response;
      }

      Console.WriteLine(string.Format("Location: {0}", location));

      // string interpolation causes NullReferenceException on macOS x64
      // for some reason here, so need to use String.Format instead
      Console.WriteLine(string.Format("Waiting {0}ms before following location {1}...", delayMs, location));
      await Task.Delay(delayMs);

      request.RequestUri = new Uri(location);
      request.Method = HttpMethod.Get;
      request.Content = null;

      var cts = new CancellationTokenSource();
      cts.CancelAfter(TimeSpan.FromMinutes(25));
      return await SendAsync(request, cts.Token);
    }

    if (!response.IsSuccessStatusCode)
    {
      Console.WriteLine(string.Format("Response is not successful: {0}", response.StatusCode));
      try
      {
        var errorBody = await response.Content.ReadAsStringAsync();
        Console.WriteLine(errorBody);
      }
      catch { }
      return response;
    }

    if (request.RequestUri?.AbsolutePath.IndexOf("/operations/") < 0)
    {
      // not a job
      return response;
    }

    var body = await response.Content.ReadAsStringAsync();

    // deserialize the response
    using var ms = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(body));
    var parseNode = ParseNodeFactoryRegistry.DefaultInstance.GetRootParseNode("application/json", ms);
    var operation = parseNode.GetObjectValue(ConnectionOperation.CreateFromDiscriminatorValue);

    if (operation?.Status == ConnectionOperationStatus.Inprogress)
    {
      // string interpolation causes NullReferenceException on macOS x64
      // for some reason here, so need to use String.Format instead
      Console.WriteLine(string.Format("Waiting {0}ms before retrying {1}...", delayMs, request.RequestUri));
      await Task.Delay(delayMs);
      return await SendAsync(request, cancellationToken);
    }
    else
    {
      return response;
    }
  }
}

The middleware starts with executing the request and capturing the response. Then, it checks if the response contains the Location header.

If it does, it checks if the URL in the Location header contains the /operations/ segment. Remember, the middleware runs on every request so before processing it, you need to be sure that you’re looking at the right request/response.

If the URL in the Location header doesn’t contain the /operations/ segment, the middleware stops its execution.

If it does contain the /operations/ segment, the middleware waits for the specified amount of time. Then, it updates the information about the request, mapping it to the URL from the Location header, updating its method to GET, and clearing its body. Additionally, it defines a new cancellation token set to 25 minutes to avoid the request timing out in 100 seconds which is the default behavior. With these updates in place, the middleware re-issues the request.

Going back to the main code flow, if the request doesn’t return a successful response, the middleware will return the response to the caller. It’ll also return the response if the request doesn’t have the /operations/ segment.

If the request URL does contain the /operations/ segment, then the middleware reads the body to check the status of the operation. Here, it uses Kiota’s (technology that underpins the Microsoft Graph .NET SDK) deserialization capabilities to convert the response body to a Microsoft Graph entity that represents the connection operation.

If the operation is in progress, the middleware will wait and call the status URL again. If the status is different, it will return the response to the caller.

It’s a lot of code, isn’t it? What’s really cool about it though, is that because it’s implemented in the middleware, it’s invisible in your main code flow, where the only thing you’ll see is:

// GraphService.Client is an instance of GraphServiceClient
await GraphService.Client.External
  .Connections[ConnectionConfiguration.ExternalConnection.Id]
  .Schema
  .PatchAsync(ConnectionConfiguration.Schema);

See how simple to understand this code is? It’s only possible because the complexity of handling the long-running operation has been put in middleware.

To use this middleware, add it to the code where you instantiate GraphClient:

using Azure.Identity;
using Microsoft.Graph;

// get default middleware
var handlers = GraphClientFactory.CreateDefaultHandlers();
// add the long-running operation middleware as first
handlers.Insert(0, new CompleteJobWithDelayHandler(60000));

var httpClient = GraphClientFactory.Create(handlers);
var credential = new ClientSecretCredential(tenantId, clientId, clientSecret);
var client = new GraphServiceClient(httpClient, credential);

In the Microsoft Graph .NET SDK, auth is handled separately, so you can add the long-running operation middleware as the first middleware in the collection to ensure that it runs first in the pipeline.

Summary

Using the Microsoft Graph .NET SDK is an easy way for you to get the data and insights from Microsoft 365 in your app. The SDK simplifies authentication and handles complex API scenarios for you. If you need to handle long-running operations, like creating Microsoft Graph connector schema, implement it as middleware to keep your main code flow clean and make the logic available throughout your app without repeating the code.

Others found also helpful: