The Help Desk demo, Part 4–Microsoft Graph

Admittedly, this is the section of the application that I’m most excited about, since it’s brand spanking new technology.  Sure, it’s been around for a while as the Unified API, but now it’s officially official and it’s called the Microsoft Graph.  The Microsoft Graph is a REST based service that provides a single endpoint to access things like Users, Groups, Mail, Calendars, etc.  For this application, we’re going to use three primary functions:  messages, mailFolders, and groups.

I fully expect that at some point a nice Graph Client will be available, but since it’s not here yet, we’ll build our own wrapper to make it a little simpler to use.  To start, we’ll add another helper class to the Helpers folder called ‘GraphHelper.cs’.  In that class, we’ll build a function called ‘QueryGraph’.  This is the function that does the majority of the heavy lifting.  It probably doesn’t meet all of the needs of the complete Graph service, but it works well enough for our needs.  The function looks like this:

private async Task<JObject> QueryGraph(string endpoint, HttpMethod method = null, StringContent content = null)
{
    if (method == null)
        method = HttpMethod.Get;

    JObject json = null;

    using (HttpClient client = new HttpClient())
    {
        Uri requestUri = new Uri(new Uri(SettingsHelper.GraphUrl), endpoint);
        using (HttpRequestMessage request = new HttpRequestMessage(method, requestUri))
        {
            request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", (await GetAccessToken()).AccessToken);

            if (content != null)
            {
                content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
                request.Content = content;
            }

            using (HttpResponseMessage response = await client.SendAsync(request))
            {
                if (response.StatusCode == HttpStatusCode.OK)
                {
                    json = JObject.Parse(await response.Content.ReadAsStringAsync());
                }
            }
        }
    }

    return json;
}

At it’s very basic, it accepts an endpoint that points to the particular bit of data that we’re interested in retrieving.  If the endpoint is the only parameter that’s passed, then it assumes we’re going to make a GET request.  It then calls out to the Graph service, setting the necessary Authorization header (which is ultimately retrieved from the AuthenticationHelper.cs class that we created earlier), and then parses the results to JSON.  Certainly in a production environment, we’d want to add some error handling and such to make this a bit more robust, but this suits us for now.

To call into the QueryGraph function is very straightforward and can be accomplished with a call like this:

JObject results = await QueryGraph("users/opshelpdesk@jonhussdev.onmicrosoft.com/mailFolders/Inbox/messages");

The results variable will contain all of the messages that are in the Inbox of the ‘opshelpdesk@jonhussdev.onmicrosoft.com’ mailbox or null if the call fails.  Painfully easy, right?  This particular function is documented here: https://graph.microsoft.io/docs/api-reference/v1.0/api/mailfolder_list_messages.  In our case, we’d rather work with business objects than JSON, so we’ll parse the resulting JSON into our business objects.  This particular function also filters out any e-mails that have a category set (I’ll explain later why that is).  The GetNewEmailMessages function looks like this:

public async Task<IEnumerable<EmailMessage>> GetNewEmailMessages()
{
    JObject results = await QueryGraph("users/" + SettingsHelper.HelpDeskEmailAddress + "/mailFolders/Inbox/messages");

    List<EmailMessage> emailMessages = new List<EmailMessage>();

    foreach(JToken result in results["value"])
    {
        if (!result["categories"].Any())
        {
            EmailMessage emailMessage = new EmailMessage();
            emailMessage.MessageID = result["id"].ToString();
            emailMessage.Sender = result["sender"]["emailAddress"]["name"].ToString();
            emailMessage.SentTimestamp = result["sentDateTime"].ToString();
            emailMessage.Subject = result["subject"].ToString();
            emailMessage.Body = result["body"]["content"].ToString();

            emailMessages.Add(emailMessage);
        }
    }

    return emailMessages;
}

In our case, the function does the parsing directly in an effort to display the process without complicating things too much.  For a production setting, we’d absolutely put that parsing inside of the constructor of the EmailMessage class.

Another function that’s required allows the application to add a Category to an e-mail message (again, I’ll explain later).  To accomplish this, we just need to send a PATCH request to the messages function (documented here: https://graph.microsoft.io/docs/api-reference/v1.0/api/message_update).  An important note – when updating an e-mail message (and presumably other objects as well), we only need to send the updated properties in the request.  Sending an entire updated object will result in an error.  The function to update the category on a message looks like this:

public async Task MarkEmailMessageAssigned(string messageID)
{
    StringContent content = new StringContent("{ \"categories\" : [ \"Assigned\" ] }");

    await QueryGraph("users/" + SettingsHelper.HelpDeskEmailAddress + "/messages/" + messageID, new HttpMethod("PATCH"), content);
}

The entire GraphHelper class looks like this:

using BusinessApps.HelpDesk.Models.Email;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;

namespace BusinessApps.HelpDesk.Helpers
{
    public class GraphHelper
    {
        private AuthenticationResult _token;
      

        #region Email

public async Task<IEnumerable<EmailMessage>> GetNewEmailMessages()
{
    JObject results = await QueryGraph("users/" + SettingsHelper.HelpDeskEmailAddress + "/mailFolders/Inbox/messages");

    List<EmailMessage> emailMessages = new List<EmailMessage>();

    foreach(JToken result in results["value"])
    {
        if (!result["categories"].Any())
        {
            EmailMessage emailMessage = new EmailMessage();
            emailMessage.MessageID = result["id"].ToString();
            emailMessage.Sender = result["sender"]["emailAddress"]["name"].ToString();
            emailMessage.SentTimestamp = result["sentDateTime"].ToString();
            emailMessage.Subject = result["subject"].ToString();
            emailMessage.Body = result["body"]["content"].ToString();

            emailMessages.Add(emailMessage);
        }
    }

    return emailMessages;
}

        public async Task<EmailMessage> GetEmailMessage(string messageID)
        {
            JObject result = await QueryGraph("users/" + SettingsHelper.HelpDeskEmailAddress + "/messages/" + messageID);

            EmailMessage emailMessage = new EmailMessage();
            emailMessage.MessageID = result["id"].ToString();
            emailMessage.Sender = result["sender"]["emailAddress"]["name"].ToString();
            emailMessage.SentTimestamp = result["sentDateTime"].ToString();
            emailMessage.Subject = result["subject"].ToString();
            emailMessage.Body = result["body"]["content"].ToString();

            return emailMessage;
        }

        public async Task MarkEmailMessageAssigned(string messageID)
        {
            StringContent content = new StringContent("{ \"categories\" : [ \"Assigned\" ] }");

            await QueryGraph("users/" + SettingsHelper.HelpDeskEmailAddress + "/messages/" + messageID, new HttpMethod("PATCH"), content);
        }

        public async Task MarkEmailMessageClosed(string messageID)
        {
            StringContent content = new StringContent("{ \"categories\" : [ \"Closed\" ] }");

            await QueryGraph("users/" + SettingsHelper.HelpDeskEmailAddress + "/messages/" + messageID, new HttpMethod("PATCH"), content);
        }

        public async Task DeleteEmailMessage(string messageID)
        {
            JObject results = await QueryGraph("users/" + SettingsHelper.HelpDeskEmailAddress + "/mailFolders/DeletedItems");

            string json = "{ \"DestinationId\" : \"" + results["id"].ToString() + "\" }";
            StringContent content = new StringContent(json);
           
            await QueryGraph("users/" + SettingsHelper.HelpDeskEmailAddress + "/messages/" + messageID + "/microsoft.graph.move", HttpMethod.Post, content);
        }

        #endregion



        #region People

        public async Task<IEnumerable<HelpdeskOperator>> GetHelpdeskOperators()
        {
            JObject results = await QueryGraph("groups/9fbc4bf3-da97-489a-bf31-afd1707c4b39/members");  //  9fbc4bf3-da97-489a-bf31-afd1707c4b39 is the id of the Help Desk Operators Office 365 group

            List <HelpdeskOperator> helpdeskOperators = new List<HelpdeskOperator>();

            foreach (JToken result in results["value"])
            {
                HelpdeskOperator helpdeskOperator = new HelpdeskOperator();
                helpdeskOperator.EmailAddress = result["userPrincipalName"].ToString();
                helpdeskOperator.Name = result["displayName"].ToString();

                helpdeskOperators.Add(helpdeskOperator);
            }

            return helpdeskOperators;
        }
       
      
        #endregion

        private async Task<JObject> QueryGraph(string endpoint, HttpMethod method = null, StringContent content = null)
        {
            if (method == null)
                method = HttpMethod.Get;

            JObject json = null;

            using (HttpClient client = new HttpClient())
            {
                Uri requestUri = new Uri(new Uri(SettingsHelper.GraphUrl), endpoint);
                using (HttpRequestMessage request = new HttpRequestMessage(method, requestUri))
                {
                    request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                    request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", (await GetAccessToken()).AccessToken);

                    if (content != null)
                    {
                        content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
                        request.Content = content;
                    }

                    using (HttpResponseMessage response = await client.SendAsync(request))
                    {
                        if (response.StatusCode == HttpStatusCode.OK)
                        {
                            json = JObject.Parse(await response.Content.ReadAsStringAsync());
                        }
                    }
                }
            }

            return json;
        }

        private async Task SetAccessToken()
        {
            AuthenticationHelper authHelper = new AuthenticationHelper();
            _token = await authHelper.GetToken(SettingsHelper.GraphResource);
        }
       
        private async Task<AuthenticationResult> GetAccessToken()
        {
            if (_token == null)
                await SetAccessToken();

            if (_token.ExpiresOn < DateTime.Now)
                await SetAccessToken();

            return _token;
        }
    }
}

Again, in a production setting, we’d want to add a bunch of error handling and retry logic to ensure our application didn’t fall over in the event of an error.  However, it really doesn’t get much easier than this, folks.

The Help Desk Demo

The entire source code for the Help Desk demo can be found here https://github.com/OfficeDev/PnP/tree/dev/Solutions/BusinessApps.HelpDesk/, in the Office 365 Dev PnP GitHub repository.

Leave a Reply

Your email address will not be published. Required fields are marked *