How to create custom delegating handler for Graph API in C#

When using the Microsoft Graph Client Library for .NET, you can face a situation that requires reading and editing either a response message sent to the Graph API or a response received from the Graph API.

The GraphServiceClient allows you to add a custom message handler and handle all outgoing requests and incoming responses.

Message handlers

A message handler is a class that receives an HTTP request and returns an HTTP response.

A series of message handlers can be chained together.

  • the first handler receives an HTTP request
  • does some processing
  • gives the request to the next handler

At some point, the response is created and goes back up the chain.

On the client side, the HttpClient class uses a message handler to process requests.

Default handlers in the Graph API SDK

The GraphServiceClient internally creates a new instance of HttpClient with a default set of handlers.

The collection of default handlers contains the following items:

  • CompressionHandler

    • handles compression
    • adds Accept-encoding: gzip header to incoming request
    • decompresses response content when Content-Encoding: gzip header is present
  • RetryHandler

    • retries sending the request
    • handles HTTP statuses:
      • 503 - service unavailable
      • 504 - gateway timeout
      • 429 - too many requests
  • RedirectHandler

    • handles redirection of requests
    • sends request to a new address defined in Location header
    • handles HTTP statuses:
      • 301 - move permanently
      • 302 - found
      • 303 - see other
      • 307 - temporary redirect
      • 308 - permanent redirect
  • ParametersNameDecodingHandler

    • decodes special characters in the request query parameters that had to be encoded due to RFC 6570 restrictions names before executing the request
  • UserAgentHandler

    • appends the current library version to the User-Agent header
  • HeadersInspectionHandler

    • allows the developer to inspect the headers of the request and response
  • GraphTelemetryHandler

    • adds some telemetry data:
      • version of the SDK library
      • host current platform identifier
      • host platform architecture
      • host .NET installation
      • client-request-id header with unique GUID of each request to help Microsoft investigate any errors that may occur

Custom handlers

To create a custom message handler, create a class that inherits from System.Net.Http.DelegatingHandler and override SendAsync method.

In the implementation of the SendAsync method:

  • process the request message
  • call base.SendAsync to send the request to the inner handler and to ensure that the request is forwarded through the request pipeline
  • the inner handler returns a response message
  • process the response and return it to the caller

Create custom handler

I will create a custom handler to log the method and absolute path, including the query of a request, to the console. After a response is received, I will log the status code of the response and the client-request-id header in case the response is not successful.

Let's start with the custom message handler:

public class ConsoleLoggingHandler : DelegatingHandler
{
    private readonly ILogger _logger;

    public ConsoleLoggingHandler(ILogger logger)
    {
        _logger = logger;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        _logger.LogInformation($">>> {request.Method} {request.RequestUri.PathAndQuery}");

        // send request through the pipeline
        var response = await base.SendAsync(request, cancellationToken);

        // check response status code
        if (response.IsSuccessStatusCode)
        {
            _logger.LogInformation($"<<< {request.Method} {request.RequestUri.PathAndQuery}: {response.StatusCode}");
        }
        else
        {
            // log client-request-id header
            var requestId = response.Headers.GetValues(CoreConstants.Headers.ClientRequestId).FirstOrDefault();
            _logger.LogError($"<<< {request.Method} {request.RequestUri.PathAndQuery}: {response.StatusCode}, {CoreConstants.Headers.ClientRequestId}: {requestId}");
        }
        return response;
    }
}

The instance of logger is passed through the constructor of the class.

In the next step, use the GraphClientFactory class to create a default set of middleware for calling the Graph API:

var handlers = GraphClientFactory.CreateDefaultHandlers();

Create a console logger (NuGet Microsoft.Extensions.Logging.Console):

var loggerFactory = LoggerFactory.Create(x => x.AddSimpleConsole(opt =>
{
    opt.TimestampFormat = "s";
    opt.UseUtcTimestamp = true;
    opt.SingleLine = true;
}));
var logger = loggerFactory.CreateLogger<Program>();

Create a new instance of the ConsoleLoggingHandler and add it to the collection of default handlers:

handlers.Add(new ConsoleLoggingHandler(logger));

Create a new instance of the HttpClient configured with the provided handlers:

var httpClient = GraphClientFactory.Create(handlers);

Create a new instance of the GraphServiceClient with configured HttpClient:

var authenticationProvider = new BaseBearerTokenAuthenticationProvider(new TokenProvider());
var graphServiceClient = new GraphServiceClient(httpClient, authenticationProvider);

Call several endpoints and check the output in the console:

await graphServiceClient.Me.GetAsync();
await graphServiceClient.Me.Drive.GetAsync();
await graphServiceClient.Admin.ServiceAnnouncement.HealthOverviews.GetAsync();

The whole code of the console application:

static async Task Main(string[] args)
{
    var handlers = GraphClientFactory.CreateDefaultHandlers();

    var loggerFactory = LoggerFactory.Create(x => x.AddSimpleConsole(opt =>
    {
        opt.TimestampFormat = "s";
        opt.UseUtcTimestamp = true;
        opt.SingleLine = true;
    }));
    var logger = loggerFactory.CreateLogger<Program>();

    handlers.Add(new ConsoleLoggingHandler(logger));

    var httpClient = GraphClientFactory.Create(handlers);

    var authenticationProvider = new BaseBearerTokenAuthenticationProvider(new TokenProvider());
    var graphServiceClient = new GraphServiceClient(httpClient, authenticationProvider);

    await graphServiceClient.Me.GetAsync();
    await graphServiceClient.Me.Drive.GetAsync();
    await graphServiceClient.Admin.ServiceAnnouncement.HealthOverviews.GetAsync();
}

The output:

Conclusion

I've explained what a message handler is, what a default set of message handlers used by the GraphServiceClient is and I've demonstrated how easy it is to create a new delegating handler and integrate it with the GraphServiceClient.

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