Azure Cloud
.NET Core Azure AD C# MS Graph Office 365 REST Web API

How to use an MS Graph API Calendar with .NET Core

Welcome to today’s blog.

In today’s post I will be showing how we can use .NET Core and the Microsoft Graph API libraries to retrieve calendar events from our Outlook 365 Calendar Events.

What is MS Graph?

MS Graph is a rest-based Web API technology that allows developers to access cloud-based resources in MS Azure. There are many MS Graph Applications that can be accessed and modified. Some of these include:

  • MS Outlook Mail 365
  • MS Outlook Calendar 365
  • MS OneDrive
  • MS Excel

More details and overview can be obtained from the Microsoft Graph site

To be able to use MS Graph API requires us to connect using an existing Microsoft account.

After we connect, we can run Graph API HTTP GET, POST, PUT etc queries to retrieve, post or update changes to the target MS Graph resource.

Running an MS Graph API request from within an application requires the application to be registered within an MS Azure tenant. The application id and scopes are then required for us to be able to allow MS Graph to delegate access from the application to our MS account Graph resources. This level of delegation uses OAuth2 to authorise the application to use the scopes to access the Graph resources under the account.

Once we have approved access, we will then be issued with an access token. The authentication uses Microsoft Identity to provide us with an access token, which we can then use as a JWT Bearer token to call the MS Graph API and retrieve a client connection to the API.

The code to retrieve the calendar events is an adaptation of the application on the Microsoft Graph tutorials site.

The implementation first involves creating an implementation provider. The provider’s main purpose is to generate and return an access token, then attach the token as a bearer token when calling an MS Graph API HTTP request. Obtaining an access token is done using OAuth2 from the user MS account via a browser login. Following the login, the user is presented with a dialog that confirms if the user wishes to allow MS Graph to access the user MS account based on the specified scopes. Following this confirmation, the application will be permitted to delegate access to the MS resources though MS Graph API.

The authentication provider implementation is shown below:

using Microsoft.Graph;
using Microsoft.Identity.Client;
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;

namespace Graph.Authentication
{
    public class DeviceCodeAuthProvider : IAuthenticationProvider
    {
        private readonly IPublicClientApplication _msalClient;
        private readonly string[] _scopes;
        private IAccount _userAccount;

        public DeviceCodeAuthProvider(string appId, string[] scopes)
        {
            _scopes = scopes;

            _msalClient = PublicClientApplicationBuilder
                .Create(appId)
                .WithAuthority(AadAuthorityAudience
                    .AzureAdAndPersonalMicrosoftAccount, true)
                .Build();
        }

        public async Task<string> GetAccessToken()
        {
            // If there is no saved user account, the user must sign-in
            if (_userAccount == null)
            {
                try
                {
                    // Invoke device code flow so user can sign-in with a browser
                    var result = await _msalClient.AcquireTokenWithDeviceCode(
                        _scopes, callback => {
                            Console.WriteLine(callback.Message);
                            return Task.FromResult(0);
                        }).ExecuteAsync().ConfigureAwait(false);

                    _userAccount = result.Account;
                    return result.AccessToken;
                }
                catch (Exception exception)
                {
                    Console.WriteLine($"Error getting access token: {exception.Message}");
                    return null;
                }
            }
            else
            {
                // If there is an account, call AcquireTokenSilent
                // By doing this, MSAL will refresh the token automatically if
                // it is expired. Otherwise it returns the cached token.

                var result = await _msalClient
                    .AcquireTokenSilent(_scopes, _userAccount)
                    .ExecuteAsync();

                return result.AccessToken;
            }
        }

        // This is the required function to implement IAuthenticationProvider
        // The Graph SDK will call this function each time it makes a Graph
        // call.
        public async Task AuthenticateRequestAsync(HttpRequestMessage requestMessage)
        {
            requestMessage.Headers.Authorization =
                new AuthenticationHeaderValue("bearer", await GetAccessToken());
        }
    }
}

You can also use a personal Microsoft Account for this exercise, however that is not recommended. The following allows you to create an identity client for a personal account. Only do this for POC purposes, then later amend the application registration that is within an AD Tenant:

_msalClient = PublicClientApplicationBuilder
    .Create(appId)
    .WithAuthority(AadAuthorityAudience.PersonalMicrosoftAccount, true)
    .Build();

We next implement a calendar helper to retrieve the event entries for the next N days.

This is shown below:

using Microsoft.Graph;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using TimeZoneConverter;

namespace GraphHelper
{
    public class GraphCalendarHelper
    {
        private static GraphServiceClient graphClient;
        public static void Initialize(IAuthenticationProvider authProvider)
        {
            graphClient = new GraphServiceClient(authProvider);
        }

        public static async Task<Microsoft.Graph.User> GetMeAsync()
        {
            try
            {
                // GET /me
                return await graphClient.Me
                    .Request()
                    .Select(u => new{
                        u.DisplayName,
                        u.MailboxSettings
                    })
                    .GetAsync();
            }
            catch (ServiceException ex)
            {
                Console.WriteLine($"Error getting signed-in user: {ex.Message}");
                return null;
            }
        }

        public static async Task<IEnumerable<Event>> GetCalendarViewAsync(
            DateTime today,
            string timeZone,
            int numberOfDays)
        {
            if (numberOfDays > 31)
                return null;

            var startOfWeek = GetUtcStartOfWeekInTimeZone(today, timeZone);
            var endOfWeek = startOfWeek.AddDays(numberOfDays);

            var viewOptions = new List<QueryOption>
            {
                new QueryOption("startDateTime", startOfWeek.ToString("o")),
                new QueryOption("endDateTime", endOfWeek.ToString("o"))
            };

            try
            {
                var events = await graphClient.Me
                    .CalendarView
                    .Request(viewOptions)
                    .Header("Prefer", $"outlook.timezone=\"{timeZone}\"")
                    .Top(50)
                    .Select(e => new
                    {
                        e.Subject,
                        e.Organizer,
                        e.Start,
                        e.End
                    })
                    .OrderBy("start/dateTime")
                    .GetAsync();

                return events.CurrentPage;
            }
            catch (ServiceException ex)
            {
                Console.WriteLine($"Error getting events: {ex.Message}");
                return null;
            }
        }

        private static DateTime GetUtcStartOfWeekInTimeZone(DateTime today, 
            string timeZoneId)
        {
            TimeZoneInfo userTimeZone = TZConvert.GetTimeZoneInfo(timeZoneId);
            int diff = System.DayOfWeek.Sunday - today.DayOfWeek;
            var unspecifiedStart = DateTime.SpecifyKind(
                today.AddDays(diff), DateTimeKind.Unspecified);
            return TimeZoneInfo.ConvertTimeToUtc(unspecifiedStart, userTimeZone);
        }
    }
}

We then construct a service class to wrap the calendar helper.

This implementation is shown below:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using User.Calendar.API.Models;
using Microsoft.Extensions.Options;
using Graph.Authentication;

namespace User.Calendar.API.Services
{
    public class OutlookCalendar: IOutlookCalendar
    {
        private readonly string[] _scopes;
        private readonly string _appId;
        private readonly string _numberCalendarDays;
        private readonly IOptions<AppConfiguration> _appConfiguration;

        public OutlookCalendar(IOptions<AppConfiguration> appConfiguration)
        {
            string scopes = appConfiguration.Value.Scopes;
            _scopes = scopes.Split(";");
            _appId = appConfiguration.Value.AppId;
            _numberCalendarDays = appConfiguration.Value.NumberCalendarDays;
        }

        public async Task<List<OutlookCalendarEvent>> GetCalendarEvents()
        {
            var authProvider = new DeviceCodeAuthProvider(_appId, _scopes);

            var accessToken = authProvider.GetAccessToken().Result;
            Console.WriteLine($"Access token: {accessToken}\n");

            GraphHelper.GraphCalendarHelper.Initialize(authProvider);

            var user = GraphHelper.GraphCalendarHelper.GetMeAsync().Result;

            var events = ListCalendarEvents(
                user.MailboxSettings.TimeZone,
                $"{user.MailboxSettings.DateFormat} {user.MailboxSettings.TimeFormat}",
                System.Convert.ToInt16(_numberCalendarDays));

            List<OutlookCalendarEvent> outlookCalendarEvents = new 
                List<OutlookCalendarEvent>();

            foreach (Microsoft.Graph.Event item in events)
            {
                outlookCalendarEvents.Add(new OutlookCalendarEvent()
                {
                    EventOrganizer = item.Organizer.EmailAddress.Name,
                    Subject = item.Subject,
                    Start = item.Start,
                    End = item.End
                });
            }
            return outlookCalendarEvents;
        }

        static List<Microsoft.Graph.Event> ListCalendarEvents(
            string userTimeZone, 
            string dateTimeFormat,
            int numberOfDays)
        {
            var events = GraphHelper
                .GraphCalendarHelper
                .GetCurrentWeekCalendarViewAsync(
                    DateTime.Today, 
                    userTimeZone, 
                    numberOfDays)
                .Result.ToList();
            return events;
        }
    }
}

The calendar wrapper service method:

public async Task<List<OutlookCalendarEvent>> GetCalendarEvents()

returns a List of custom type OutlookCalendarEvent.

namespace User.Calendar.API.Models
{
    public class OutlookCalendarEvent
    {
        public string Subject { get; set; }
        public string EventOrganizer { get; set; }
        public DateTimeTimeZone Start { get; set; }
        public DateTimeTimeZone End { get; set; }
    }
}

For our authorized permissions we set the following scopes:

User.Read

MailboxSettings

Read;Calendars.ReadWrite

We will also need to specify the application client id GUID from the Application registered within our Azure tenant:

If we have not already done so, we should create at least one event within Outlook calendar.

An example event is shown below:

Our API controller is one method to retrieve calendar events:

[ApiController]
public class OutlookCalendarController : ControllerBase
{
    private readonly IOutlookCalendar _outlookCalendar;
    private readonly ILogger<OutlookCalendarController> _logger;

    public OutlookCalendarController(
        ILogger<OutlookCalendarController> logger, 
        IOutlookCalendar outlookCalendar)
    {
        _logger = logger;
        _outlookCalendar = outlookCalendar;
    }

    [HttpGet("api/[controller]/GetCalendarEvents")]
    public async Task<IActionResult> GetCalendarEvents()
    {
        var calendarEvents = await _outlookCalendar.GetCalendarEvents();
        if (calendarEvents == null)
            return BadRequest("Calendar request failed!");
        return Ok(calendarEvents);
    }
}

After running our API application. Execute the API method GetCalendarMethods.

Once the API method runs, the request will appear to hang. However, the auth flow from the MSAL library call to AcquireTokenWithDeviceCode() returns a call back message from the issuing authority  (https://login.microsoftonline.com/common/ or https://login.microsoftonline.com/consumers/) which can be observed from the console.

var result = await _msalClient.AcquireTokenWithDeviceCode(_scopes, callback => {
    Console.WriteLine(callback.Message);
    return Task.FromResult(0);
}).ExecuteAsync().ConfigureAwait(false);

The console shows us the prompt that can be displayed in the calling client.

info: Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker[3]
      => RequestPath:/api/OutlookCalendar/GetCalendarEvents RequestId:80000027-0002-fb01-b63f-84710c7968bb, SpanId:|252fb84d-48eddde5c02dbafe., TraceId:252fb85c-48eddde4d02dbafe, ParentId: => User.Calendar.API.Controllers.OutlookCalendarController.GetCalendarEvents (User.Calendar.API)
…
To sign in, use a web browser to open the page https://www.microsoft.com/link and enter the code XXXXXXXX to authenticate.

You will then be prompted to enter a token within the browser as shown:

To sign in, use a web browser to open the page https://www.microsoft.com/link and enter the code XXXXXXXX to authenticate.

The browser token prompt is shown below:

The next browser dialog to open requests the password to enter for your MS Account:

The next dialog then requests your permission to accept the delegated scoped permissions that the application will be accessing your profile and Outlook calendars.

Once accepted, the sign in will be complete.

When the graph API has successfully returned the event data from the Outlook events, you should see the output from the API method in JSON format:

We have successfully retrieved our calendar event from MS Outlook using .NET Core from within a Web API method.

This is a basic scenario for calling MS Graph from a Web API, which can be used to test a Web API that connects to and accesses MS Graph resources.

A more realistic situation would be using the above in the context of a client UI (such as a desktop or client web application) which would allow us to connect to the API using Azure AD then using the token to call MS Graph recourses from the API method. This is something I will explore in a future post.

That is all for today’s post. I hope you found this post useful and informative.

Social media & sharing icons powered by UltimatelySocial