Click here to Skip to main content
15,868,141 members
Articles / Hosted Services / Azure

Consuming Outlook Streaming Notifications in C#

Rate me:
Please Sign up or sign in to vote.
4.40/5 (5 votes)
14 Aug 2017CPOL10 min read 12.4K   219   5   2
Sketch application capable of consuming Office365 Oultook Streaming Notificaion API
This article is a good starting point for those who want to consume Outlook streaming notification API from dotNet application.

Introduction

Wouldn't it be nice to be able to react from code instantly to events arising in applications external to your solution? Of course. Well, in some cases, you simply can't do that, in others, you have to hack that system somehow to catch that event and notify your application. Fortunately, some - more and more - platforms provide more or less standard solutions to interact with them - but backward communication is not that widespread. Even Azure APIs have limited support for that. For example - besides polling - at the time this article was written, Office 365 APIs provide two somewhat limited subscription options:

For the first option to work, you need to have an open http(s) endpoint. This is not possible in some cases, especially in the case of on-premise solutions, mostly because of security reasons; but it is also not usable if fixed IP is not available.

Thus getting notified over a stream channel opened by the client seems a reasonable option, and even a more responsive one than the callback based. To have a general overview, please take a look at the documentation in bold link above.

Please note, that the API used is officially in Beta (preview) stage!

Background

For my recent project, the customer asked for a demonstration application that subscribes to the calendar events of a specific Azure AD user. I have prepared a LinqPad 5 sketch for this purpose. This is why you won't find attached any library or complete application to download, just this sketch. In this article, I will present the particularities encountered during the development of this proof-of-concept solution.

I have used NuGet packages, which are supported only in the registered version of LinqPad. If you are using the free one, you will have to download the packages yourself.

Using the Code

Before you begin, you need to have an Azure AD tenant and a linked Office 365 user - whose events you will be watching. If you don't have these, you will have to proceed for a trial account.

The client ID used in this article is registered (but I don't know for how long) as multi tenant native application, thus you might be able to run directly with your tenant. If not, you will need to register for yourself following this article. Please note that the application is using ADAL, thus endpoint V1 is addressed. The application will request calendar.read delegated scope.

You will absolutely need to change the tenant to yours. Both the tenant and client ID can be found in the Query Properties, on the App.Config page. And of course, you will need to specify your tenant's UPN and authorize this application for it. You will notice that device profile flow is used for authorization. This is because of the final solution targeted. And you will also notice some simplifications too (like missing ConfigureAwait-s, no DI/IoC, etc..), but as I stated before, the intention was not to provide a full-featured library, just a proof-of-concept with a specific goal in mind.

Even if this code is focusing on calendar events, email, contact, and task events need to be handled similarly.

Some Words about the API

Briefly, there are two steps the client has to complete to get streaming notifications:

  1. Initiate a subscription by posting a request.
  2. Start accessing the stream using the subscription ID returned by the server in the previous step.

Quite simple, isn't it?

Yes indeed, but with one annoying limitation: even if the "impersonated" user has access to other user's calendar (shared calendar), you can subscribe to the events of the current user only. So if you need to watch multiple calendars at once, you do it either impersonating those users parallelly or, you have to poll with Graph API. I hope this will change in the future...

Following OData format, you can request simple and rich subscriptions: in the latter case, the notifications will contain portions of the object that triggered the notification itself. Details of the event in my case. And of course, you can specify some filtering if needed.

The classes in the code representing the subscription request are the following:

C#
public abstract class StreamingSubscriptionBase
{
    [JsonProperty(PropertyName = "@odata.type")]
    public string ODataType {get; protected set;}
    
    public string Resource {get; protected set;}
    
    public string ChangeType {get; protected set;}
}

public class SimpleStreamingEventSubscription : StreamingSubscriptionBase
{
    public SimpleStreamingEventSubscription()
    {
        this.ODataType = "#Microsoft.OutlookServices.StreamingSubscription";
        this.Resource = "https://outlook.office.com/api/beta/me/events";
        this.ChangeType = "Acknowledgment, Created, Updated, Deleted, Missed";
    }
}

Note that I have subscribed for all possible changes. The expected response will be deserialized in this class:

C#
public class StreamingSubscriptionResponse
{
    [JsonProperty(PropertyName = "@odata.context")]
    public string ODataContext { get; set; }
    
    [JsonProperty(PropertyName = "@odata.type")]
    public string ODataType { get; set; }

    [JsonProperty(PropertyName = "@odata.id")]
    public string ODataId { get; set; }

    public string Id { get; set; }

    public string Resource { get; set; }

    public string ChangeType { get; set; }
}

Most of this is not really useful, except the Id property, which is the reference of the subscription itself.

In the API documentation, you will find the notion of subscription expiration. You would expect to get the expiration timestamp at the moment your subscription is created. But, for some reason, you will first see it with the first actual event change notification. Which means, if nothing interesting happens, your subscription might get expired before you even get the deadline known. Fortunately, if you are trying to read the stream of an expired subscription, you will get http response code 404 "Not found" - which can be handled quite easily. Thus my code is reacting to this exception and ignoring the existence of an eventual expiration timestamp.

Once you have the Id, you can start to listen, by POSTing an other request. This request will last (almost) infinitely. Well actually, it has a maximum duration of 90 minutes, but from the processing point of view, it is much the same: you have to process it while reading the chunks.

The format of the stream you get is like this:

JavaScript
{
 "@odata.context":"https://outlook.office.com/api/beta/metadata#Notifications",
 "value": [
            // messages come here
          ]
}

This is a valid JSON object: the first three rows come as the stream is opened, the last two rows are sent when the connection timeout is reached (the server will end the connection after a maximum of 90 minutes). The content of the array arrives depending on the keealive period requested and the change events in the resource for which the notification was requested. Practically, it can happen that nothing is in the pipe for minutes. And this is a pitfall, as we will see later on.

Thus, you will get in the stream with any or other fragmentation and order keepalive and event notifications. Of course, these are also valid JSON objects, as valid elements of the values array.

You have to be prepared to handle not only connection termination by the server but also other network issues that might result in broken communication. The documentation is requesting that you should try to reestablish the stream using the same subscription Id, while it is still valid (see the paragraph above about the expiration issue). Thus this whole cycle should end only when you want it to end - you have to keep reopening the stream, renewing subscription as long as you need to, by respecting fair use policies.

Authentication

Before continuing with the actual API, I have to speak about the authentication process. Of course, we have OAuth2 at the base, but fortunately, there is a library we can use, called ADAL (Azure Active Directory Authentication Library), that manages most of the hard work related to this protocol (for the V2.0 endpoint, there is the MSAL library).

Graph API is the new, unified API for Office 365. Still, it is not yet complete. For example, streaming subscriptions are not (yet) implemented in Graph API. But, to be prepared for that, I have used and implemented the IAuthenticationProvider interface defined in the Graph API Library Core:

C#
class AzureAuthenticationProvider: IAuthenticationProvider
{
    private static string aadInstance = ConfigurationManager.AppSettings["ida:AADInstance"];
    private static string tenant = ConfigurationManager.AppSettings["ida:Tenant"];
    private static string clientId = ConfigurationManager.AppSettings["ida:ClientId"];

    private static string authority = 
            String.Format(CultureInfo.InvariantCulture, aadInstance, tenant);
    private static string resource = "https://outlook.office.com";

    TokenCache cache = new TokenCache();
    Action<DeviceCodeResult> signInFeedback = null;

    public AzureAuthenticationProvider(string userPrincipalName, 
                                       Action<DeviceCodeResult> signInFeedback)
    {
        this.signInFeedback = signInFeedback;
        
        string cachefile = Path.Combine(System.Environment.GetFolderPath
               (Environment.SpecialFolder.LocalApplicationData), userPrincipalName);
        cache.AfterAccess += (c) => File.WriteAllBytes(cachefile, cache.Serialize());

        if (File.Exists(cachefile))
        {
            cache.Deserialize(File.ReadAllBytes(cachefile));
        }
    }

    public async Task AuthenticateRequestAsync(HttpRequestMessage request)
    {
        AuthenticationContext authContext = new AuthenticationContext(authority, true, cache);
        AuthenticationResult result = null;

        bool signInNeeded = authContext.TokenCache.Count < 1;
        try
        {
            if (!signInNeeded)
            {
                result = await authContext.AcquireTokenSilentAsync(resource, clientId);
            }
        }
        catch (Exception ex)
        {
            var adalEx = ex.InnerException as AdalException;
            if ((adalEx != null) && (adalEx.ErrorCode == "failed_to_acquire_token_silently"))
            {
                signInNeeded = true;
            }
            else
            {
                throw ex;
            }
        }

        if (signInNeeded)
        {
            DeviceCodeResult codeResult = 
                  await authContext.AcquireDeviceCodeAsync(resource, clientId);
            this.signInFeedback?.Invoke(codeResult);
            result = await authContext.AcquireTokenByDeviceCodeAsync(codeResult);
        }

        request.Headers.Authorization = 
                new AuthenticationHeaderValue("Bearer", result.AccessToken);
    }
}

This provider is using quite a simple but efficient token cache: on every change, it stores the content in a file using the UPN as the file name. Feel free to use your implementation. As stated above, the solution is using the device profile flow, which means that if the cache is empty or the silent authentication fails, the application will prompt the user to open the browser, navigate to a specific location and to enter a specific code. The process will wait until the server confirms that the code was entered and to authorize the application for the user - or it will time out eventually. Because the refresh token will practically never expire, the application will be able to silently authenticate anytime later.

Subscribing

This is the easy part:

C#
public async Task<StreamingSubscriptionResponse> SubscribeToEventsAsync()
    {
        string requestUrl = "https://outlook.office.com/api/beta/me/subscriptions";
        var subscriptionRequestContent = 
            JsonConvert.SerializeObject(new SimpleStreamingEventSubscription());

        HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUrl);
        request.Content = new StringContent
                (subscriptionRequestContent, Encoding.UTF8, "application/json");
        await provider.AuthenticateRequestAsync(request);
        HttpResponseMessage response = await client.SendAsync(request);

        return await response.Content.ReadAsAsync<StreamingSubscriptionResponse>();
    }

Please note the spot when authorization occurs using the provider from above. The Graph API client library would perform these steps transparently, but as said before, this feature is not in the Graph API yet.

Consuming Notifications

And now, the interesting part.

C#
public async Task ListenForSubscriptionStreamAsync(CancellationToken ct)
    {
        string requestUrl = "https://outlook.office.com/api/beta/Me/GetNotifications";

        var requestContent = new ListenRequest 
        { ConnectionTimeoutInMinutes = 60, KeepAliveNotificationIntervalInSeconds = 15 };

        var handler = new WinHttpHandler();
        handler.ReceiveDataTimeout = TimeSpan.FromSeconds
                        (requestContent.KeepAliveNotificationIntervalInSeconds + 5);
        HttpClient listeningClient = new HttpClient(handler);

As this is a long running async operation, collaborative cancellation is essential.

When the operation is starting, you can specify the timeout requested in minutes and the keepalive notification interval. If any is missing, the defaults are used. The request is not yet complete, though.

As I highlighted before, it can happen that no any byte will arrive in the socket between these keepalive notifications. The connection might be broken in the meantime and neither the HttpClient object nor the stream reader will notify it - and even cancellation won't work in such a case. Fortunately, we can use the WinHttpHandler "plugin", that has a ReceiveDataTimeout property. You should set its value a little bit higher than the keepalive interval.

In my code, each listener is using its own HttpClient instance. Until further tests are performed, I can't tell how a shared one can handle this long-running-request scenario.

C#
var subscription = await SubscribeToEventsAsync();

try
{
    while (!ct.IsCancellationRequested)
    {
        try
        {
            requestContent.SubscriptionIds = new string[] { subscription.Id };
            HttpRequestMessage request =
                       new HttpRequestMessage(HttpMethod.Post, requestUrl);
            request.Content = new ObjectContent<ListenRequest>
                              (requestContent, new JsonMediaTypeFormatter());
            await provider.AuthenticateRequestAsync(request);

Now we create the initial subscription. Let's suppose for now that this won't fail, but you will probably want to wait a longer bit and try again.

The request is completed with the initial subscription Id, and then authenticated. Everything is part of a large cycle, which is only interrupted by the cancellation initiated from outside.

C#
using (var response = await listeningClient.SendAsync
      (request, HttpCompletionOption.ResponseHeadersRead, ct))
                    {
                        if (!response.IsSuccessStatusCode)
                        {
                            if (response.StatusCode != HttpStatusCode.NotFound)
                            {
                                await Task.Delay(TimeSpan.FromMinutes(1));
                            }
                            subscription = 
                                await SubscribeToEventsAsync(); // renew subscription
                            
                            continue;
                        }

Now we try to initiate the long-running-request. As mentioned before, this is the spot, when the expired request situation is handled: if "not found" is returned, we go to get a new one. In case of any other problem, we do the same, but after a little delay.

Please note the HttpCompletionOption.ResponseHeadersRead option when opening the stream: without this, the client would wait until the whole content arrived, which means, for an hour in my case. This is not what we want. To be able to process the notifications as they arrive, we have to proceed after the header arrived.

And here comes the code that processes the stream:

C#
using (var stream = await response.Content.ReadAsStreamAsync())
using (var streamReader = new StreamReader(stream))
using (var jsonReader = new JsonTextReader(streamReader))
{
   bool inValuesArray = false;
   while (await jsonReader.ReadAsync(ct))
   {
      ct.ThrowIfCancellationRequested();
      if (!inValuesArray && jsonReader.TokenType == 
          JsonToken.PropertyName && jsonReader.Value.ToString() == "value")
      {
         inValuesArray = true;
      }
      if (inValuesArray && jsonReader.TokenType == JsonToken.StartObject)
      {
        JObject obj = await JObject.LoadAsync(jsonReader, ct);
        if (obj["@odata.type"].ToString() == 
            "#Microsoft.OutlookServices.KeepAliveNotification")
        {
          $"{DateTime.Now} keepalive".Dump();
        }
        else
        {
          // This is where you do your job...
          obj.ToString(Newtonsoft.Json.Formatting.Indented, null).Dump();
        }
      }
   }

   $"{DateTime.Now} Stream ended".Dump();
}

As mentioned before, the stream will contain a big, valid JSON object. But to be able to react on the notification, we need to process the elements of the value array - that means we will need to deserialize portions of the stream. Fortunately, the great Json.NET package contains a JsonTextReader class that is capable of processing JSON tokens as they arrive. With one problem: jsonReader.ReadAsync is blocked waiting for the next chars in the stream. If the socket is not closed but the communication is failing, we would be stuck without return... even the cancellation token passed has no effect. But lucky us, we have WinHttpHandler.ReceiveDataTimeout that won't let it wait infinitely.

The stream will contain only keepalive notifications and event change notifications. We are simply reading them into JObject instances, which can easily be converted to our custom objects (not declared in my code).

At the end of this method, you will find some catch blocks, but practically that's it.

Recap

Although this code is not complete and not optimal either, I think it is a good starting point for everybody who wants to consume Outlook streaming notification API from dotNet application.

History

  • 8th August, 2017: Version 1.0

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Technical Lead
Hungary Hungary
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionI am getting 403 forbidden error Pin
ashishbhulani6-Mar-20 3:01
ashishbhulani6-Mar-20 3:01 
QuestionUpdate to code Pin
Member 1369241816-Jun-18 6:28
Member 1369241816-Jun-18 6:28 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.