The Graph API provides a single access point to wide variety of services offered by Microsoft including Azure Active Directory. Quite a few projects I've been working on recently necessitated direct integration to Azure Active Directory so I thought it would be helpful to provide some guidance on how to do this using the Microsoft.Graph SDK offered by Microsoft.

For our example today, we're going to build a .NET Core 3 console application that will query the graph to obtain the Object ID's for both an Azure Active Directory User and Security Group.

To get started, install the Graph SDK into your .NET core application by executing the following command at the command line in your application:

dotnet add package Microsoft.Graph

The main class we'll be interacting with is the GraphServiceClient. As you review the constructors, one of the the first thing you'll notice with the SDK is you are responsible for injecting your own class for Authentication by implementing the IAuthenticationProvider interface provided in the library. To add the proper the authentication mechanism, let's install the ADAL library using the following command:

dotnet add package Microsoft.IdentityModel.Clients.ActiveDirectory

In addition to the ADAL library, we're responsible for setting up the HttpClient for dependency injection. I'd recommend that we add the following two packages:

dotnet add package Microsoft.Extensions.Hosting
dotnet add package Microsoft.Extensions.Http.Polly

The hosting package is used to setup dependency injection for our application and Polly will be used to add resiliency to our HttpRequests.

The first thing we will want to do is create a class that implements the IAuthenticationProvider:

    public class GraphAuthenticationProvider : IAuthenticationProvider
    {
        public const string GRAPH_URI = "https://graph.microsoft.com/";
        private string _tenantId { get; set; }
        private string _clientId { get; set; }
        private string _clientSecret { get; set; }

        public GraphAuthenticationProvider(IConfiguration configuration)
        {
            _tenantId = configuration.GetValue<string>("TenantId");
            _clientId = configuration.GetValue<string>("ClientId");
            _clientSecret = configuration.GetValue<string>("ClientSecret");
        }

        public async Task AuthenticateRequestAsync(HttpRequestMessage request)
        {
            AuthenticationContext authContext = new AuthenticationContext($"https://login.microsoftonline.com/{_tenantId}");

            ClientCredential creds = new ClientCredential(_clientId, _clientSecret);

            AuthenticationResult authResult = await authContext.AcquireTokenAsync(GRAPH_URI, creds);

            request.Headers.Add("Authorization", "Bearer " + authResult.AccessToken);
        }
    }

This code is added as a piece of middleware to GraphServiceClient calls to ensure each call is properly authenticated. This code will authenticate against Azure Active Directory using a Client ID and Client Secret and acquire a bearer token that will be added to the Graph API request. For this class, I've taken advantage of the configuration available when you use the Generic Host for your console application. This code expects the following 3 parameters to be configured within your application:

  1. TenantId - This is the Azure Active Directory - Directory ID
  2. ClientId - See the Client ID of your app registration (see below)
  3. ClientSecret - Secret Value created for your App Registration (see below)

Create Your Azure AD App Registration

In order for this code to work you will have to perform the following steps:

  • Created an App Registration in Azure AD
  • Created a secret in Azure AD
  • Added an Application API Permission for Directory.ReadAll to the App Registration you created

Implement Graph Service Client Provider

To execute calls against the Microsoft Graph API, let's build a provider class that uses dependency injection to inject the IGraphServiceClient (which we will register later on in the article). This class will allow us to query the Users/Security Groups that we have setup in Azure AD.

public interface IGraphProvider
    {
        Task<string> GetIdByEmail(string email);
        Task<string> GetIdByGroupName(string groupName);
    }

    public class MicrosoftGraphProvider : IGraphProvider
    {
        private readonly IGraphServiceClient _graphServiceClient;

        public MicrosoftGraphProvider(IGraphServiceClient graphServiceClient)
        {
            _graphServiceClient = graphServiceClient;
        }

        public async Task<string> GetIdByEmail(string email)
        {
            var user = await _graphServiceClient.Users[email]
                            .Request()
                            .GetAsync();

            if (user == null || string.IsNullOrEmpty(user.Id))
            {
                return string.Empty;
            }

            return user.Id;
        }

        public async Task<string> GetIdByGroupName(string groupName)
        {
            var group = await _graphServiceClient.Groups.Request()
                            .Filter($"displayName eq '{groupName}'")
                            .Select("displayName,description,id").GetAsync();

            if (group == null || group.Count == 0 || string.IsNullOrEmpty(group[0].Id))
            {
                return string.Empty;
            }

            return group[0].Id;
        }
    }

Here we have two simple functions that take either an email (for user) or a security group display name (for a security group) and will return the Object Id. This is just a small sample of the object information you can retrieve via the API. But the concept of querying the Graph remains the same and can be applied to any other call you might need to make.

Hosted Service

When the Host.Start method is called during the main execution call, the .NET Core Generic Host will automatically run any class registered via the AddHostedService method call when dependency injection is configured. For our purposes, let's implement a class the run the main logic of our application.

public class GetGraphItemsWorker : IHostedService
    {
        private readonly IGraphProvider _microsoftGraphProvider;

        public GetGraphItemsWorker(IGraphProvider microsoftGraphProvider)
        {
            _microsoftGraphProvider = microsoftGraphProvider;
        }

        public async Task StartAsync(CancellationToken cancellationToken)
        {
            var emailObjectId = await _microsoftGraphProvider.GetIdByEmail("YOUR_EMAIL");
            Console.WriteLine(emailObjectId);

            var groupObjectId = await _microsoftGraphProvider.GetIdByGroupName("SECURITY_GROUP");
            Console.WriteLine(groupObjectId);

            Console.ReadLine();
        }

        public async Task StopAsync(CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();
        }

    }

The main entry point is the StartAsync method, which will use our provider class to query for the Object ID of a user and a security group. Note: You will need to replace the strings for the email and security group in order for this code to work.

Setup your Program

Now, let's modify our Program.cs file to put this all together.

    class Program
    {
        static void Main(string[] args)
        {
            using (var host = CreateHostBuilder(args).Build())
            {
                host.Start();
            }
        }

        public static IHostBuilder CreateHostBuilder(string[] args)
        {
            var host = Host.CreateDefaultBuilder(args)
                .ConfigureServices((hostContext, services) =>
                {
                    services.AddLogging();
                    services.AddTransient<IAuthenticationProvider, GraphAuthenticationProvider>();
                    services.AddTransient<IGraphServiceClient, GraphServiceClient>();
                    services.AddTransient<IGraphProvider, MicrosoftGraphProvider>();
                    services.AddHostedService<GetGraphItemsWorker>();

                    services.AddHttpClient("RestClient")
                    .AddPolicyHandler((services, request) => HttpPolicyExtensions.HandleTransientHttpError()
                            .WaitAndRetryAsync(new[]
                            {
                                TimeSpan.FromSeconds(1),
                                TimeSpan.FromSeconds(5),
                                TimeSpan.FromSeconds(10),
                                TimeSpan.FromSeconds(20),
                                TimeSpan.FromSeconds(30)
                            }
                        ));
                });

            return host;
        }
    }

The code above will setup the Generic Host for the console application which will allow you to do the following:

  • Add Logging (Which you will see in the console window when running)
  • Setup Dependency Injection
  • Add your hosted service which will execute the main logic of your application
  • Setup your HTTP Client Factory complete with transient error handling thanks to Polly.

In order to save you the effort, here is the complete Program.cs file for our demo:

using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Graph;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using Polly;
using Polly.Extensions.Http;

namespace GraphCli
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var host = CreateHostBuilder(args).Build())
            {
                host.Start();
            }
        }

        public static IHostBuilder CreateHostBuilder(string[] args)
        {
            var host = Host.CreateDefaultBuilder(args)
                .ConfigureServices((hostContext, services) =>
                {
                    services.AddLogging();
                    services.AddTransient<IAuthenticationProvider, GraphAuthenticationProvider>();
                    services.AddTransient<IGraphServiceClient, GraphServiceClient>();
                    services.AddTransient<IGraphProvider, MicrosoftGraphProvider>();
                    services.AddHostedService<GetGraphItemsWorker>();

                    services.AddHttpClient("RestClient")
                    .AddPolicyHandler((services, request) => HttpPolicyExtensions.HandleTransientHttpError()
                            .WaitAndRetryAsync(new[]
                            {
                                TimeSpan.FromSeconds(1),
                                TimeSpan.FromSeconds(5),
                                TimeSpan.FromSeconds(10),
                                TimeSpan.FromSeconds(20),
                                TimeSpan.FromSeconds(30)
                            },
                            onRetry: (outcome, timespan, retryAttempt, context) =>
                            {
                                int LOG_THRESHOLD = 1000;
                                if (timespan.TotalMilliseconds > LOG_THRESHOLD)
                                {
                                    services.GetService<ILogger>()
                                        .LogWarning("Delaying for {delay}ms, then making retry {retry}.", timespan.TotalMilliseconds, retryAttempt);
                                }
                            }
                        ));
                });

            return host;
        }
    }

    public class GraphAuthenticationProvider : IAuthenticationProvider
    {
        public const string GRAPH_URI = "https://graph.microsoft.com/";
        private string _tenantId { get; set; }
        private string _clientId { get; set; }
        private string _clientSecret { get; set; }

        public GraphAuthenticationProvider(IConfiguration configuration)
        {
            _tenantId = configuration.GetValue<string>("TenantId");
            _clientId = configuration.GetValue<string>("ClientId");
            _clientSecret = configuration.GetValue<string>("ClientSecret");
        }

        public async Task AuthenticateRequestAsync(HttpRequestMessage request)
        {
            AuthenticationContext authContext = new AuthenticationContext($"https://login.microsoftonline.com/{_tenantId}");

            ClientCredential creds = new ClientCredential(_clientId, _clientSecret);

            AuthenticationResult authResult = await authContext.AcquireTokenAsync(GRAPH_URI, creds);

            request.Headers.Add("Authorization", "Bearer " + authResult.AccessToken);
        }
    }

    public interface IGraphProvider
    {
        Task<string> GetIdByEmail(string email);
        Task<string> GetIdByGroupName(string groupName);
    }

    public class MicrosoftGraphProvider : IGraphProvider
    {
        private readonly IGraphServiceClient _graphServiceClient;

        public MicrosoftGraphProvider(IGraphServiceClient graphServiceClient)
        {
            _graphServiceClient = graphServiceClient;
        }

        public async Task<string> GetIdByEmail(string email)
        {
            var user = await _graphServiceClient.Users[email]
                            .Request()
                            .GetAsync();

            if (user == null || string.IsNullOrEmpty(user.Id))
            {
                return string.Empty;
            }

            return user.Id;
        }

        public async Task<string> GetIdByGroupName(string groupName)
        {
            var group = await _graphServiceClient.Groups.Request()
                            .Filter($"displayName eq '{groupName}'")
                            .Select("displayName,description,id").GetAsync();

            if (group == null || group.Count == 0 || string.IsNullOrEmpty(group[0].Id))
            {
                return string.Empty;
            }

            return group[0].Id;
        }
    }

    public class GetGraphItemsWorker : IHostedService
    {
        private readonly IGraphProvider _microsoftGraphProvider;

        public GetGraphItemsWorker(IGraphProvider microsoftGraphProvider)
        {
            _microsoftGraphProvider = microsoftGraphProvider;
        }

        public async Task StartAsync(CancellationToken cancellationToken)
        {
            var emailObjectId = await _microsoftGraphProvider.GetIdByEmail("YOUR_EMAIL");
            Console.WriteLine(emailObjectId);

            var groupObjectId = await _microsoftGraphProvider.GetIdByGroupName("SECURITY_GROUP");
            Console.WriteLine(groupObjectId);

            Console.ReadLine();
        }

        public async Task StopAsync(CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();
        }

    }
}

At this point, we're ready to test our application. There are a number of ways to add your configuration parameters, but to test locally you can use the following command line arguments:

TenantId={Your AD Directory ID} ClientId={Your App Registration Client ID} ClientSecret={App Registration Client Secret}

If all goes as planned, your application should return the Object Id's for the email and security groups you've registered. You can now use this foundation to leverage whatever functionality you need to access out of the Microsoft Graph API.

Summary

In this article, we've seen how to:

  • Query the Microsoft Graph API in our .NET Core 3 Console Application
  • Setup the Generic Host to include out of the box dependency injection and logging
  • Configure an Azure Active Directory App Registration for our Application
  • Authenticate Microsoft Graph API calls using ADAL
  • Setup an HTTP Client Factory with Resiliency

Image by Pete Linforth from Pixabay