Schreiben einer ASP.NET-MVC-Web-App zum Abrufen von Outlook-Mail, -Kalender und -Kontakten

In diesem Leitfaden werden Sie schrittweise durch den Prozess des Erstellens einer einfachen ASP.NET-MVC-C#-App zum Abrufen von Nachrichten in Office 365 oder Outlook.com geführt. Wenn Sie die hier beschriebenen Schritte ausführen, sollte der Quellcode in diesem Repository das Ergebnis sein.

In diesem Lernprogramm wird die Microsoft-Authentifizierungsbibliothek (MSAL) für OAuth2-Aufrufe und die Microsoft Graph-Clientbibliothek für Aufrufe der Mail-API verwendet. Microsoft empfiehlt die Verwendung von Microsoft Graph für den Zugriff auf Outlook-Mail, -Kalender und -Kontakte. Verwenden Sie die Outlook-APIs nur dann direkt (über https://outlook.office.com/api), wenn Sie ein Feature benötigen, das auf den Graph-Endpunkten nicht verfügbar ist. Eine Version dieses Beispiels mit Verwendung der Outlook-APIs finden Sie in dieser Verzweigung.

In diesem Leitfaden wird davon ausgegangen, dass Sie bereits Visual Studio 2013 oder Visual Studio 2015 installiert haben und auf Ihrem Entwicklungscomputer ausführen.

Erstellen der App

Lassen Sie uns direkt loslegen. Erstellen Sie in Visual Studio eine neue Visual C#-ASP.NET-Webanwendung mithilfe von .NET Framework 4.5. Nennen Sie die Anwendung dotnet-tutorial.

Das Fenster „Neues Projekt“ in Visual Studio.

Wählen Sie die Vorlage MVC aus. Klicken Sie auf die Schaltfläche Authentifizierung ändern, und wählen Sie „Keine Authentifizierung“ aus. Deaktivieren Sie das Kontrollkästchen „Host in der Cloud“. Das Dialogfeld sollte wie folgt aussehen:

Das Fenster zur Vorlagenauswahl in Visual Studio.

Klicken Sie auf „OK“, damit Visual Studio das Projekt erstellt. Führen Sie danach das Projekt aus, um sicherzustellen, dass alles ordnungsgemäß funktioniert, indem Sie F5 drücken oder Debuggen starten aus dem Menü Debuggen auswählen. Es sollte ein geöffneter Browser angezeigt werden, in dem die vordefinierte ASP.NET-Startseite angezeigt wird. Schließen Sie den Browser.

Da wir nun bestätigt haben, dass die App funktioniert, können wir uns an die echte Arbeit machen.

Entwerfen der App

Unsere App ist sehr einfach. Wenn ein Benutzer die Website besucht, sieht er eine Schaltfläche zum Anmelden und Anzeigen seiner E-Mails. Beim Klicken auf die Schaltfläche gelangt er zur Azure-Anmeldeseite, wo er sich mit seinem Office 365- oder Outlook.com-Konto anmelden kann und Zugriff auf unsere App erhält. Schließlich wird er zurück zu unserer App geleitet, die eine Liste der neuen E-Mails im Posteingang des Benutzers anzeigt.

Beginnen wir nun, indem wir die vordefinierte Startseite durch eine einfachere ersetzen. Öffnen Sie die Datei ./Views/Home/Index.cshtml. Ersetzen Sie den vorhandenen Code durch den folgenden Code.

Inhalte der Datei ./Views/Home/Index.cshtml

@{
    ViewBag.Title = "Home Page";
}

<div class="jumbotron">
    <h1>ASP.NET MVC Tutorial</h1>
    <p class="lead">This sample app uses the Mail API to read messages in your inbox.</p>
    <p><a href="#" class="btn btn-primary btn-lg">Click here to login</a></p>
</div>

Dies ist im Wesentlichen eine Umfunktionierung des jumbotron-Element aus der vordefinierten Startseite; alle anderen Elemente werden entfernt. Die Schaltfläche hat noch keine Funktion, die Startseite sollte aber folgendermaßen aussehen.

Startseite der Beispiel-App.

Wir werden jetzt auch die vordefinierte Fehlerseite ändern, damit eine Fehlermeldung übergeben und angezeigt werden kann. Ersetzen Sie den Inhalt von ./Views/Shared/Error.cshtml durch den folgenden Code.

Inhalte der Datei ./Views/Shared/Error.cshtml

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Error</title>
</head>
<body>
    <hgroup>
        <h1>Error.</h1>
        <h2>An error occurred while processing your request.</h2>
    </hgroup>
    <div class="alert alert-danger">@ViewBag.Message</div>
    @if (!string.IsNullOrEmpty(ViewBag.Debug))
    {
    <pre><code>@ViewBag.Debug</code></pre>
    }
</body>
</html>

Fügen Sie schließlich eine Aktion zu der HomeController-Klasse hinzu, um die Fehleransicht aufzurufen.

Die Error-Aktion in ./Controllers/HomeController.cs

public ActionResult Error(string message, string debug)
{
    ViewBag.Message = message;
    ViewBag.Debug = debug;
    return View("Error");
}

Registrieren der App

Wichtig

Neue App-Registrierungen sollten im Anwendungsregistrierungsportal erstellt und verwaltet werden, damit sie mit Outlook.com kompatibel sind. Erstellen Sie neue App-Registrierungen nur dann im Azure-Verwaltungsportal Folgendes auf Ihre App zutrifft:

  • Sie verwendet den OAuth2 Client Credentials Grant-Fluss oder
  • Sie muss neben Outlook auf andere Office 365-Arbeitslasten zugreifen (z. B. OneDrive for Business oder SharePoint)

Beachten Sie, dass mithilfe des Azure-Verwaltungsportals registrierte Apps nicht mit Outlook.com kompatibel sind und dass Berechtigungsbereiche nicht dynamisch angefordert werden können. Vorhandene App-Registrierungen, die im Azure-Verwaltungsportal erstellt wurden, funktionieren weiterhin nur für Office 365. Diese Registrierungen werden nicht im Anwendungsregistrierungsportal angezeigt und müssen im Azure-Verwaltungsportal verwaltet werden.

Kontoanforderungen

Um das Anwendungsregistrierungsportal zu verwenden, benötigen Sie entweder ein Office 365-Geschäfts- oder Schulkonto oder ein Microsoft-Konto. Wenn Sie nicht über eines dieser Konten verfügen, haben Sie verschiedene Möglichkeiten:

  • Registrieren Sie sich hier für ein neues Microsoft-Konto.
  • Sie haben mehrere Möglichkeiten, ein Office 365-Abonnement zu erhalten:

REST-API-Verfügbarkeit

Die REST-API ist derzeit auf allen Office 365-Konten, die über Exchange Online verfügen, sowie auf allen Outlook.com-Konten aktiviert.

Wechseln Sie zum App-Registrierungsportal, um schnell eine App-ID und einen geheimen Schlüssel abzurufen.

  1. Melden Sie sich über den Link Anmelden mit Ihrem Microsoft-Konto (Outlook.com) oder Ihrem Geschäfts-, Schul- oder Unikonto (Office 365) an.
  2. Klicken Sie auf die Schaltfläche App hinzufügen. Geben Sie dotnet-tutorial für den Namen ein, und klicken Sie auf Anwendung erstellen.
  3. Suchen Sie den Abschnitt Anwendungsgeheimnisse, und klicken Sie auf die Schaltfläche Neues Kennwort generieren. Kopieren Sie nun das Kennwort, und speichern Sie es an einem sicheren Ort. Nachdem Sie das Kennwort kopiert haben, klicken Sie auf Ok.
  4. Suchen Sie den Abschnitt Plattformen, und klicken Sie auf Plattform hinzufügen. Wählen Sie Web aus und geben Sie dann http://localhost:<PORT> ein; ersetzen Sie dabei <PORT> durch die Portnummer, die Ihr Projekt verwendet, unter Umleitungs-URIs.

    Tipp

    Die Portnummer, die Ihr Projekt verwendet, finden Sie, indem Sie das Projekt im Projektmappen-Explorer auswählen und dann den Wert der URL unter Entwicklungsserver im Fenster Eigenschaften überprüfen.

  5. Klicken Sie auf Speichern, um die Registrierung abzuschließen. Kopieren Sie die App-ID, und speichern Sie sie zusammen mit dem Kennwort, das Sie zuvor kopiert haben. Wir benötigen diese Werte bald.

So sollten die Details Ihrer App-Registrierung aussehen, wenn Sie fertig sind.

Screenshot der abgeschlossenen App-Registrierung im App-Registrierungsportal

Implementieren von OAuth2

Unser Ziel in diesem Abschnitt ist es, den Link auf unserer Homepage so einzurichten, dass der OAuth2-Autorisierungscodegenehmigungs-Fluss mit Azure AD initiiert wird. Der Einfachheit halber verwenden wir die Microsoft-Authentifizierungsbibliothek (MSAL), um unsere OAuth-Anforderungen zu verarbeiten.

Öffnen Sie die Datei Web.config, und fügen Sie die folgenden Schlüssel innerhalb des <appSettings>-Elements hinzu:

<add key="ida:AppID" value="YOUR APP ID" />
<add key="ida:AppPassword" value="YOUR APP PASSWORD" />
<add key="ida:RedirectUri" value="http://localhost:10800" />
<add key="ida:AppScopes" value="User.Read Mail.Read" />

Ersetzen Sie den Wert des ida:AppID-Schlüssels durch die oben generierte Anwendungs-ID, und ersetzen Sie den Wert des ida:AppPassword-Schlüssels durch das oben generierte Kennwort. Wenn der Wert der URI-Umleitung anders lautet, müssen Sie den Wert von ida:RedirectUri aktualisieren.

Der nächste Schritt besteht darin, die OWIN-Middleware, die MSAL und die Graph-Bibliotheken von NuGet zu installieren. Wählen Sie im Visual Studio-Menü Tools den NuGet-Paket-Manager und dann die Paket-Manager-Konsole aus. Geben Sie die folgenden Befehle in der Paket-Manager-Konsole ein, um die OWIN Middleware-Bibliotheken zu installieren:

Install-Package Microsoft.Owin.Security.OpenIdConnect
Install-Package Microsoft.Owin.Security.Cookies
Install-Package Microsoft.Owin.Host.SystemWeb

Installieren Sie als Nächstes die Microsoft-Authentifizierungsbibliothek mit dem folgenden Befehl:

Install-Package Microsoft.Identity.Client -Pre

Installieren Sie schließlich die Microsoft Graph-Clientbibliothek mit dem folgenden Befehl:

Install-Package Microsoft.Graph

Zurück zur Codierung

Jetzt sind wir bereit, um uns um die Anmeldung zu kümmern. Beginnen wir mit dem Vebinden der OWIN-Middleware mit unserer App. Klicken Sie mit der rechten Maustaste auf den Ordner App_Start im Projekt-Explorer, und wählen Sie Hinzufügen und dann Neues Element aus. Wählen Sie die Vorlage für die OWIN-Startklasse aus, geben Sie der Datei den Namen Startup.cs, und klicken Sie auf Hinzufügen. Ersetzen Sie den gesamten Inhalt dieser Datei durch den folgenden Code.

using System;
using System.Configuration;
using System.IdentityModel.Claims;
using System.IdentityModel.Tokens;
using System.Threading.Tasks;
using System.Web;

using Microsoft.IdentityModel.Protocols;
using Microsoft.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.Notifications;
using Microsoft.Owin.Security.OpenIdConnect;

using Owin;


[assembly: OwinStartup(typeof(dotnet_tutorial.App_Start.Startup))]

namespace dotnet_tutorial.App_Start
{
    public class Startup
    {
        public static string appId = ConfigurationManager.AppSettings["ida:AppId"];
        public static string appPassword = ConfigurationManager.AppSettings["ida:AppPassword"];
        public static string redirectUri = ConfigurationManager.AppSettings["ida:RedirectUri"];
        public static string[] scopes = ConfigurationManager.AppSettings["ida:AppScopes"]
          .Replace(' ', ',').Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);

        public void Configuration(IAppBuilder app)
        {
            app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

            app.UseCookieAuthentication(new CookieAuthenticationOptions());

            app.UseOpenIdConnectAuthentication(
              new OpenIdConnectAuthenticationOptions
              {
                  ClientId = appId,
                  Authority = "https://login.microsoftonline.com/common/v2.0",
                  Scope = "openid offline_access profile email " + string.Join(" ", scopes),
                  RedirectUri = redirectUri,
                  PostLogoutRedirectUri = "/",
                  TokenValidationParameters = new TokenValidationParameters
                  {
                      // For demo purposes only, see below
                      ValidateIssuer = false

                      // In a real multitenant app, you would add logic to determine whether the
                      // issuer was from an authorized tenant
                      //ValidateIssuer = true,
                      //IssuerValidator = (issuer, token, tvp) =>
                      //{
                      //  if (MyCustomTenantValidation(issuer))
                      //  {
                      //    return issuer;
                      //  }
                      //  else
                      //  {
                      //    throw new SecurityTokenInvalidIssuerException("Invalid issuer");
                      //  }
                      //}
                  },
                  Notifications = new OpenIdConnectAuthenticationNotifications
                  {
                      AuthenticationFailed = OnAuthenticationFailed,
                      AuthorizationCodeReceived = OnAuthorizationCodeReceived
                  }
              }
            );
        }

        private Task OnAuthenticationFailed(AuthenticationFailedNotification<OpenIdConnectMessage,
          OpenIdConnectAuthenticationOptions> notification)
        {
            notification.HandleResponse();
            string redirect = "/Home/Error?message=" + notification.Exception.Message;
            if (notification.ProtocolMessage != null && !string.IsNullOrEmpty(notification.ProtocolMessage.ErrorDescription))
            {
                redirect += "&debug=" + notification.ProtocolMessage.ErrorDescription;
            }
            notification.Response.Redirect(redirect);
            return Task.FromResult(0);
        }

        private Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification notification)
        {
            notification.HandleResponse();
            notification.Response.Redirect("/Home/Error?message=See Auth Code Below&debug=" + notification.Code);
            return Task.FromResult(0);
        }
    }
}

Wir fügen nun eine SignIn-Aktion zu der HomeController-Klasse hinzu. Öffnen Sie die Datei .\Controllers\HomeController.cs. Fügen Sie die folgenden Zeilen im oberen Bereich der Datei hinzu:

using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.Identity.Client;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using Microsoft.Graph;

Fügen Sie jetzt eine neue Methode namens SignIn zu der HomeController-Klasse hinzu.

Die SignIn-Aktion in ./Controllers/HomeController.cs

public void SignIn()
{
    if (!Request.IsAuthenticated)
    {
        // Signal OWIN to send an authorization request to Azure
        HttpContext.GetOwinContext().Authentication.Challenge(
            new AuthenticationProperties { RedirectUri = "/" },
            OpenIdConnectAuthenticationDefaults.AuthenticationType);
    }
}

Nun aktualisieren wir schließlich die Startseite so, dass die Anmeldeschaltfläche die SignIn-Aktion aufruft.

Aktualisierte Inhalte der Datei ./Views/Home/Index.cshtml

@{
    ViewBag.Title = "Home Page";
}

<div class="jumbotron">
    <h1>ASP.NET MVC Tutorial</h1>
    <p class="lead">This sample app uses the Mail API to read messages in your inbox.</p>
    <p><a href="@Url.Action("SignIn", "Home", null, Request.Url.Scheme)" class="btn btn-primary btn-lg">Click here to login</a></p>
</div>

Speichern Sie Ihre Arbeit, und führen Sie die App aus. Klicken Sie auf die Schaltfläche zum Anmelden. Nach der Anmeldung sollten Sie auf die Fehlerseite umgeleitet werden, auf der ein Autorisierungscode angezeigt wird. Führen wir nun eine Aktion damit aus.

Austauschen des Codes durch ein Token

Aktualisieren wir nun die OnAuthorizationCodeReceived-Funktion zum Abrufen eines Tokens. Fügen Sie in ./App_Start/Startup.cs die folgenden Zeilen im oberen Bereich der Datei hinzu:

using Microsoft.Identity.Client;

Ersetzen Sie die aktuelle OnAuthorizationCodeReceived-Funktion durch Folgendes.

Aktualisierte OnAuthorizationCodeReceived-Aktion in ./App_Start/Startup.cs

// Note the function signature is changed!
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification notification)
{
    ConfidentialClientApplication cca = new ConfidentialClientApplication(
        appId, redirectUri, new ClientCredential(appPassword), null, null);

    string message;
    string debug;

    try
    {
        var result = await cca.AcquireTokenByAuthorizationCodeAsync(notification.Code, scopes);
        message = "See access token below";
        debug = result.AccessToken;
    }
    catch (MsalException ex)
    {
        message = "AcquireTokenByAuthorizationCodeAsync threw an exception";
        debug = ex.Message;
    }

    notification.HandleResponse();
    notification.Response.Redirect("/Home/Error?message=" + message + "&debug=" + debug);
}

Speichern Sie die Änderungen, und starten Sie die App neu. Dieses Mal sollte nach der Anmeldung ein Zugriffstoken angezeigt werden. Da wir nun ein Zugriffstoken abrufen können, benötigen wir eine Möglichkeit, um das Token zu speichern, damit die App es verwenden kann. Die MSAL-Bibliothek definiert eine TokenCache-Schnittstelle, die wir zum Speichern der Token verwenden können. Da es sich um ein Lernprogramm handelt, implementieren wir nun einen grundlegenden Cache, der in der Sitzung gespeichert wird.

Erstellen Sie einen neuen Ordner im Projekts namens TokenStorage. Fügen Sie eine Klasse in diesem Ordner mit dem Namen SessionTokenCache hinzu, und ersetzen Sie den Inhalt dieser Datei durch den folgenden Code (geliehen von https://github.com/Azure-Samples/active-directory-dotnet-webapp-openidconnect-v2).

Inhalte der Datei ./TokenCache/SessionTokenCache.cs

using System.Threading;
using System.Web;

using Microsoft.Identity.Client;

namespace dotnet_tutorial.TokenStorage
{
    // Adapted from https://github.com/Azure-Samples/active-directory-dotnet-webapp-openidconnect-v2
    public class SessionTokenCache
    {
        private static ReaderWriterLockSlim sessionLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
        string userId = string.Empty;
        string cacheId = string.Empty;
        HttpContextBase httpContext = null;

        TokenCache tokenCache = new TokenCache();

        public SessionTokenCache(string userId, HttpContextBase httpContext)
        {
            this.userId = userId;
            cacheId = userId + "_TokenCache";
            this.httpContext = httpContext;
            Load();
        }

        public TokenCache GetMsalCacheInstance()
        {
            tokenCache.SetBeforeAccess(BeforeAccessNotification);
            tokenCache.SetAfterAccess(AfterAccessNotification);
            Load();
            return tokenCache;
        }

        public bool HasData()
        {
            return (httpContext.Session[cacheId] != null && ((byte[])httpContext.Session[cacheId]).Length > 0);
        }

        public void Clear()
        {
            httpContext.Session.Remove(cacheId);
        }

        private void Load()
        {
            sessionLock.EnterReadLock();
            tokenCache.Deserialize((byte[])httpContext.Session[cacheId]);
            sessionLock.ExitReadLock();
        }

        private void Persist()
        {
            sessionLock.EnterReadLock();

            // Optimistically set HasStateChanged to false. 
            // We need to do it early to avoid losing changes made by a concurrent thread.
            tokenCache.HasStateChanged = false;

            httpContext.Session[cacheId] = tokenCache.Serialize();
            sessionLock.ExitReadLock();
        }

        // Triggered right before ADAL needs to access the cache. 
        private void BeforeAccessNotification(TokenCacheNotificationArgs args)
        {
            // Reload the cache from the persistent store in case it changed since the last access. 
            Load();
        }

        // Triggered right after ADAL accessed the cache.
        private void AfterAccessNotification(TokenCacheNotificationArgs args)
        {
            // if the access operation resulted in a cache update
            if (tokenCache.HasStateChanged)
            {
                Persist();
            }
        }
    }
}

Aktualisieren wir nun die OnAuthorizationCodeReceived-Funktion, um den neuen Cache zu verwenden.

Fügen Sie die folgenden Zeilen im oberen Bereich der Datei Startup.cs hinzu:

using dotnet_tutorial.TokenStorage;

Ersetzen Sie die aktuelle OnAuthorizationCodeReceived-Funktion durch die folgende.

Neue OnAuthorizationCodeReceived in ./App_Start/Startup.cs

private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification notification)
{
    // Get the signed in user's id and create a token cache
    string signedInUserId = notification.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;
    SessionTokenCache tokenCache = new SessionTokenCache(signedInUserId, 
        notification.OwinContext.Environment["System.Web.HttpContextBase"] as HttpContextBase);

    ConfidentialClientApplication cca = new ConfidentialClientApplication(
        appId, redirectUri, new ClientCredential(appPassword), tokenCache.GetMsalCacheInstance(), null);

    try
    {
        var result = await cca.AcquireTokenByAuthorizationCodeAsync(notification.Code, scopes);
    }
    catch (MsalException ex)
    {
        string message = "AcquireTokenByAuthorizationCodeAsync threw an exception";
        string debug = ex.Message;
        notification.HandleResponse();
        notification.Response.Redirect("/Home/Error?message=" + message + "&debug=" + debug);
    }
}

Da es in dieser Sitzung ums Speichern geht, fügen wir auch eine SignOut-Aktion zu der HomeController-Klasse hinzu. Fügen Sie die folgenden Zeilen im oberen Bereich der Datei HomeController.cs hinzu:

using dotnet_tutorial.TokenStorage;

Fügen Sie dann die SignOut-Aktion hinzu.

Die SignOut-Aktion in ./Controllers/HomeController.cs

public void SignOut()
{
    if (Request.IsAuthenticated)
    {
        string userId = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;

        if (!string.IsNullOrEmpty(userId))
        {
            // Get the user's token cache and clear it
            SessionTokenCache tokenCache = new SessionTokenCache(userId, HttpContext);
            tokenCache.Clear();
        }
    }
    // Send an OpenID Connect sign-out request. 
    HttpContext.GetOwinContext().Authentication.SignOut(
        CookieAuthenticationDefaults.AuthenticationType);
    Response.Redirect("/");
}

Wenn Sie nun die App neu starten und sich anmelden, werden Sie bemerken, dass Sie die App zurück zur Startseite leitet, ohne sichtbares Ergebnis. Aktualisieren wir nun die Startseite so, dass diese sich in Abhängigkeit davon ändert, ob der Benutzer angemeldet ist oder nicht.

Zuerst wollen wir die Index-Aktion in HomeController.cs aktualisieren, um den Anzeigenamen des Benutzers anzuzeigen. Ersetzen Sie die vorhandene Index-Funktion durch den folgenden Code.

Aktualisierte Index-Aktion in ./Controllers/HomeController.cs

public ActionResult Index()
{
    if (Request.IsAuthenticated)
    {
        string userName = ClaimsPrincipal.Current.FindFirst("name").Value;
        string userId = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
        if (string.IsNullOrEmpty(userName) || string.IsNullOrEmpty(userId))
        {
            // Invalid principal, sign out
            return RedirectToAction("SignOut");
        }

        // Since we cache tokens in the session, if the server restarts
        // but the browser still has a cached cookie, we may be
        // authenticated but not have a valid token cache. Check for this
        // and force signout.
        SessionTokenCache tokenCache = new SessionTokenCache(userId, HttpContext);
        if (!tokenCache.HasData())
        {
            // Cache is empty, sign out
            return RedirectToAction("SignOut");
        }

        ViewBag.UserName = userName;
    }
    return View();
}

Ersetzen Sie als Nächstes den vorhandenen Index.cshtml-Code durch den folgenden Code.

Aktualisierte Inhalte der Datei ./Views/Home/Index.cshtml

@{
    ViewBag.Title = "Home Page";
}

<div class="jumbotron">
    <h1>ASP.NET MVC Tutorial</h1>
    <p class="lead">This sample app uses the Mail API to read messages in your inbox.</p>
    @if (Request.IsAuthenticated)
    {
        <p>Welcome, @(ViewBag.UserName)!</p>
    }
    else
    {
        <p><a href="@Url.Action("SignIn", "Home", null, Request.Url.Scheme)" class="btn btn-primary btn-lg">Click here to login</a></p>
    }
</div>

Zum Schluss fügen wir nun einige Menüelemente zur Navigationsleiste hinzu, wenn der Benutzer angemeldet ist. Öffnen Sie die Datei ./Views/Shared/_Layout.cshtml. Suchen Sie die folgenden Codezeilen:

<ul class="nav navbar-nav">
    <li>@Html.ActionLink("Home", "Index", "Home")</li>
    <li>@Html.ActionLink("About", "About", "Home")</li>
    <li>@Html.ActionLink("Contact", "Contact", "Home")</li>
</ul>

Ersetzen Sie diese Zeilen durch den folgenden Code:

<ul class="nav navbar-nav">
    <li>@Html.ActionLink("Home", "Index", "Home")</li>
    @if (Request.IsAuthenticated)
    {
        <li>@Html.ActionLink("Inbox", "Inbox", "Home")</li>
        <li>@Html.ActionLink("Sign Out", "SignOut", "Home")</li>
    }
</ul>

Wenn Sie jetzt Ihre Änderungen speichern und die App neu starten, sollten auf der Startseite der Name des angemeldeten Benutzers und die beiden neuen Menüelemente in der oberen Navigationsleiste angezeigt werden, nachdem Sie sich angemeldet haben. Da wir nun den angemeldeten Benutzer und ein Zugriffstoken haben, können wir die Mail-API aufrufen.

Verwenden der Mail-API

Beginnen wir mit dem Hinzufügen einer neuen Funktion zur HomeController-Klasse, um das Zugriffstoken des Benutzers zu erhalten. In dieser Funktion verwenden wir MSAL und unseren Tokencache. Wenn ein gültiges nicht abgelaufenes Token im Cache vorhanden ist, gibt MSAL dieses zurück. Falls dieses abgelaufen ist, wird das Token von MSAL aktualisiert.

Fügen Sie die folgenden using-Anweisungen in ./Controllers/HomeController.cs hinzu.

using System.Configuration;
using System.Net.Http.Headers;

GetAccessToken-Funktion in ./Controllers/HomeController.cs

public async Task<string> GetAccessToken()
{
    string accessToken = null;

    // Load the app config from web.config
    string appId = ConfigurationManager.AppSettings["ida:AppId"];
    string appPassword = ConfigurationManager.AppSettings["ida:AppPassword"];
    string redirectUri = ConfigurationManager.AppSettings["ida:RedirectUri"];
    string[] scopes = ConfigurationManager.AppSettings["ida:AppScopes"]
        .Replace(' ', ',').Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);

    // Get the current user's ID
    string userId = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;

    if (!string.IsNullOrEmpty(userId))
    {
        // Get the user's token cache
        SessionTokenCache tokenCache = new SessionTokenCache(userId, HttpContext);

        ConfidentialClientApplication cca = new ConfidentialClientApplication(
            appId, redirectUri, new ClientCredential(appPassword), tokenCache.GetMsalCacheInstance(), null);

        // Call AcquireTokenSilentAsync, which will return the cached
        // access token if it has not expired. If it has expired, it will
        // handle using the refresh token to get a new one.
        AuthenticationResult result = await cca.AcquireTokenSilentAsync(scopes, cca.Users.First());

        accessToken = result.AccessToken;
    }

    return accessToken;
}

Jetzt testen wir unsere neue Funktion. Fügen Sie eine neue Funktion zur HomeController-Klasse mit dem Namen Inbox hinzu.

Die Inbox-Aktion in ./Controllers/HomeController.cs

public async Task<ActionResult> Inbox()
{
    string token = await GetAccessToken();
    if (string.IsNullOrEmpty(token))
    {
        // If there's no token in the session, redirect to Home
        return Redirect("/");
    }

    return Content(string.Format("Token: {0}", token));
}

Wenn Sie die App jetzt ausführen und nach der Anmeldung auf das Menüelement Posteingang klicken, wird das Zugriffstoken im Browser angezeigt. Dieses wollen wir jetzt verwenden.

Unsere erste Aufgabe mit der Mail-API besteht darin, die E-Mail-Adresse des Benutzers abzurufen. Sie werden bald sehen, warum wir das tun. Fügen Sie eine weitere Funktion zu der HomeController-Klasse mit dem Namen GetUserEmail hinzu.

GetUserEmail-Funktion in ./Controllers/HomeController.cs

public async Task<string> GetUserEmail()
{
    GraphServiceClient client = new GraphServiceClient(
        new DelegateAuthenticationProvider(
            async (requestMessage) =>
            {
                string accessToken = await GetAccessToken();
                requestMessage.Headers.Authorization =
                    new AuthenticationHeaderValue("Bearer", accessToken);
            }));

    // Get the user's email address
    try
    {
        Microsoft.Graph.User user = await client.Me.Request().GetAsync();
        return user.Mail;
    }
    catch (ServiceException ex)
    {
        return string.Format("#ERROR#: Could not get user's email address. {0}", ex.Message);
    }
}

Aktualisieren Sie die Inbox-Funktion durch den folgenden Code.

Aktualisierte Inbox-Aktion in ./Controllers/HomeController.cs

public async Task<ActionResult> Inbox()
{
    string token = await GetAccessToken();
    if (string.IsNullOrEmpty(token))
    {
        // If there's no token in the session, redirect to Home
        return Redirect("/");
    }

    string userEmail = await GetUserEmail();

    GraphServiceClient client = new GraphServiceClient(
        new DelegateAuthenticationProvider(
            (requestMessage) =>
            {
                requestMessage.Headers.Authorization =
                    new AuthenticationHeaderValue("Bearer", token);

                requestMessage.Headers.Add("X-AnchorMailbox", userEmail);

                return Task.FromResult(0);
            }));

    try
    {
        var mailResults = await client.Me.MailFolders.Inbox.Messages.Request()
                            .OrderBy("receivedDateTime DESC")
                            .Select(m => new { m.Subject, m.ReceivedDateTime, m.From})
                            .Top(10)
                            .GetAsync();

        string content = "";

        foreach (var msg in mailResults.CurrentPage)
        {
            content += string.Format("Subject: {0}<br/>", msg.Subject);
        }

        return Content(content);
    }
    catch (ServiceException ex)
    {
        return RedirectToAction("Error", "Home", new { message = "ERROR retrieving messages", debug = ex.Message });
    }
}

Zusammenfassung des neuen Codes in der Inbox-Funktion:

  • Er erstellt ein GraphServiceClient-Objekt.
  • Er ändert die Header der ausgehenden Anforderung in der DelegateAuthenticationProvider, indem das Zugriffstoken als Authorization-Header und die E-Mail-Adresse des Benutzers als X-AnchorMailbox-Header hinzugefügt wird. Jetzt zahlt sich unsere Arbeit zum Abrufen der E-Mail-Adresse des Benutzers aus. Durch Festlegen dieser Kopfzeile auf das Postfach des Benutzers kann der API-Endpunkt API-Aufrufe effizienter an den entsprechenden Back-End-Postfachserver weiterleiten.
  • Er gibt eine GET-Anforderung an die URL für die Nachrichten im Posteingang mit den folgenden Merkmalen aus:
    • Er verwendet die OrderBy()-Funktion mit einem Wert von receivedDateTime DESC, um die Ergebnisse nach ReceivedDateTime zu sortieren.
    • Er verwendet die Select()-Funktion mit einem LINQ-Ausdruck, um die zurückgegebenen Felder auf diejenigen zu beschränken, die wir benötigen.
  • Er durchläuft die Ergebnisse und druckt den Betreff.

Wenn Sie die App jetzt neu starten, sollten Sie eine sehr einfache Auflistung der E-Mail-Betreffe erhalten. Für ein besseres Ergebnis können wir jedoch die MVC-Features verwenden.

Anzeigen der Ergebnisse

MVC kann Ansichten basierend auf einem Modell generieren. Beginnen wir daher mit dem Erstellen einer Ansicht basierend auf dem Microsoft.Graph.Message-Objekt. Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf den Ordner ./Views/Home, und wählen Sie Hinzufügen und dann Ansicht aus. Geben Sie Inbox als Ansichtsnamen ein. Ändern Sie das Feld Vorlage in Empty (without model). Belassen Sie für alles andere die Standardwerte, und klicken Sie auf Hinzufügen.

Das Dialogfeld „Ansicht hinzufügen“.

Öffnen Sie die Datei Inbox.cshtml, die erstellt wird, und ersetzen Sie den gesamten Inhalt der Datei durch den folgenden Code.

@model IEnumerable<Microsoft.Graph.Message>

@{
    ViewBag.Title = "Inbox";
}

<h2>Inbox</h2>

<table class="table">
    <tr>
        <th>
            Subject
        </th>
        <th>
            Received
        </th>
        <th>
            From
        </th>
    </tr>

    @foreach (var item in Model)
    {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Subject)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReceivedDateTime)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.From.EmailAddress.Name) &lt;@Html.DisplayFor(modelItem => item.From.EmailAddress.Address)&gt;
            </td>
        </tr>
    }

</table>

An diesem Punkt weist Visual Studio auf ein Problem mit item.ReceivedDateTime hin. Wir müssen einen Verweis auf System.Runtime in der Datei Web.config hinzufügen. Öffnen Sie Web.config, und suchen Sie die folgende Zeile:

<compilation debug="true" targetFramework="4.5" />

Ersetzen Sie diese Zeile durch den folgenden Code:

<compilation debug="true" targetFramework="4.5">
  <assemblies>
    <add assembly="System.Runtime, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
  </assemblies>
</compilation>

Jetzt müssen wir nur noch eine Sache erledigen. Und zwar wollen wir die Inbox-Funktion so aktualisieren, dass unsere neue Ansicht verwendet wird.

Aktualisierte Inbox-Aktion in ./Controllers/HomeController.cs

public async Task<ActionResult> Inbox()
{
    string token = await GetAccessToken();
    if (string.IsNullOrEmpty(token))
    {
        // If there's no token in the session, redirect to Home
        return Redirect("/");
    }

    string userEmail = await GetUserEmail();

    GraphServiceClient client = new GraphServiceClient(
        new DelegateAuthenticationProvider(
            (requestMessage) =>
            {
                requestMessage.Headers.Authorization =
                    new AuthenticationHeaderValue("Bearer", token);

                requestMessage.Headers.Add("X-AnchorMailbox", userEmail);

                return Task.FromResult(0);
            }));

    try
    {
        var mailResults = await client.Me.MailFolders.Inbox.Messages.Request()
                            .OrderBy("receivedDateTime DESC")
                            .Select("subject,receivedDateTime,from")
                            .Top(10)
                            .GetAsync();

        return View(mailResults.CurrentPage);
    }
    catch (ServiceException ex)
    {
        return RedirectToAction("Error", "Home", new { message = "ERROR retrieving messages", debug = ex.Message });
    }
}

Die Änderungen sind hier minimal. Anstatt eine Zeichenfolge mit den Ergebnissen zu erstellen, übergeben wir stattdessen die mailResults.CurrentPage-Auflistung an die View-Methode.

Speichern Sie die Änderungen, und führen Sie die App aus. Sie sollten jetzt eine Liste von Nachrichten erhalten, die etwa so aussieht.

Die Beispiel-App, die den Posteingang eines Benutzers anzeigt.

Hinzufügen von Kalender- und Kontakt-APIs

Da Sie nun das Aufrufen der Outlook-Mail-API gemeistert haben, dürfte es kein Problem mehr sein, das gleiche für Kalender- und Kontakte-APIs zu tun.

Tipp

Wenn Sie die Schritte in diesem Lernprogramm befolgt haben, haben Sie wahrscheinlich ein Zugriffstoken in Ihrer Sitzung gespeichert. Dieses Token ist nur für den Mail.Read-Bereich gültig. Um die Kalender- oder Kontakte-API aufzurufen, müssen wir neue Bereiche hinzufügen. Melden Sie sich unbedingt von der App ab, um die gespeicherten Token zu entfernen, damit Sie den Anmeldevorgang von vorne beginnen können, um ein neues Zugriffstoken zu erhalten.

Für die Kalender-API:

  1. Aktualisieren Sie den Wert ida:AppScopes in Web.config, um den Calendars.Read-Bereich einzuschließen.

    <add key="ida:AppScopes" value="User.Read Mail.Read Calendars.Read" />
    
  2. Fügen Sie eine Ansicht für Ereignisse mit dem Namen Calendar hinzu, die die Empty (without model)-Vorlage verwendet. Fügen Sie Calendar.cshtml den folgenden Code hinzu:

    @model IEnumerable<Microsoft.Graph.Event>
    
    @{
        ViewBag.Title = "Calendar";
    }
    
    <h2>Calendar</h2>
    
    <table class="table">
        <tr>
            <th>
                Subject
            </th>
            <th>
                Start
            </th>
            <th>
                End
            </th>
        </tr>
    
    @foreach (var item in Model) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Subject)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Start.DateTime) (@Html.DisplayFor(modelItem => item.Start.TimeZone))
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.End.DateTime) (@Html.DisplayFor(modelItem => item.End.TimeZone))
            </td>
        </tr>
    }
    
    </table>
    
  3. Fügen Sie ein Nagivationselement für die Kalenderansicht zu ./Views/Shared/_Layout.cshtml hinzu.

    @if (Request.IsAuthenticated)
    {
        <li>@Html.ActionLink("Inbox", "Inbox", "Home")</li>
        <li>@Html.ActionLink("Calendar", "Calendar", "Home")</li>
        <li>@Html.ActionLink("Sign Out", "SignOut", "Home")</li>
    }
    
  4. Fügen Sie der HomeController-Klasse eine Calendar-Funktion hinzu.

    public async Task<ActionResult> Calendar()
    {
        string token = await GetAccessToken();
        if (string.IsNullOrEmpty(token))
        {
            // If there's no token in the session, redirect to Home
            return Redirect("/");
        }
    
        string userEmail = await GetUserEmail();
    
        GraphServiceClient client = new GraphServiceClient(
            new DelegateAuthenticationProvider(
                (requestMessage) =>
                {
                    requestMessage.Headers.Authorization =
                        new AuthenticationHeaderValue("Bearer", token);
    
                    requestMessage.Headers.Add("X-AnchorMailbox", userEmail);
    
                    return Task.FromResult(0);
                }));
    
        try
        {
            var eventResults = await client.Me.Events.Request()
                                .OrderBy("start/dateTime DESC")
                                .Select("subject,start,end")
                                .Top(10)
                                .GetAsync();
    
            return View(eventResults.CurrentPage);
        }
        catch (ServiceException ex)
        {
            return RedirectToAction("Error", "Home", new { message = "ERROR retrieving events", debug = ex.Message });
        }
    }
    
  5. Klicken Sie nach der Anmeldung bei der App auf das Element Kalender im oberen Nagivationsmenü.

Für die Kontakte-API:

  1. Aktualisieren Sie den Wert ida:AppScopes in Web.config, um den Contacts.Read-Bereich einzuschließen.

    <add key="ida:AppScopes" value="User.Read Mail.Read Contacts.Read" />
    
  2. Fügen Sie eine Ansicht für Ereignisse mit dem Namen Contacts hinzu, die die Empty (without model)-Vorlage verwendet. Fügen Sie Contacts.cshtml den folgenden Code hinzu:

    @model IEnumerable<Microsoft.Graph.Contact>
    
    @{
        ViewBag.Title = "Contacts";
    }
    
    <h2>Contacts</h2>
    
    <table class="table">
        <tr>
            <th>
                DisplayName
            </th>
            <th>
                Email Address
            </th>
            <th>
                Mobile Phone
            </th>
        </tr>
    
    @foreach (var item in Model) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.DisplayName)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.EmailAddresses.First().Address)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.MobilePhone)
            </td>
        </tr>
    }
    
    </table>
    
  3. Fügen Sie ein Nagivationselement für die Kontakteansicht zu ./Views/Shared/_Layout.cshtml hinzu.

    @if (Request.IsAuthenticated)
    {
        <li>@Html.ActionLink("Inbox", "Inbox", "Home")</li>
        <li>@Html.ActionLink("Contacts", "Contacts", "Home")</li>
        <li>@Html.ActionLink("Sign Out", "SignOut", "Home")</li>
    }
    
  4. Fügen Sie der HomeController-Klasse eine Contacts-Funktion hinzu.

    public async Task<ActionResult> Contacts()
    {
        string token = await GetAccessToken();
        if (string.IsNullOrEmpty(token))
        {
            // If there's no token in the session, redirect to Home
            return Redirect("/");
        }
    
        string userEmail = await GetUserEmail();
    
        GraphServiceClient client = new GraphServiceClient(
            new DelegateAuthenticationProvider(
                (requestMessage) =>
                {
                    requestMessage.Headers.Authorization =
                        new AuthenticationHeaderValue("Bearer", token);
    
                    requestMessage.Headers.Add("X-AnchorMailbox", userEmail);
    
                    return Task.FromResult(0);
                }));
    
        try
        {
            var contactResults = await client.Me.Contacts.Request()
                                .OrderBy("displayName")
                                .Select("displayName,emailAddresses,mobilePhone")
                                .Top(10)
                                .GetAsync();
    
            return View(contactResults.CurrentPage);
        }
        catch (ServiceException ex)
        {
            return RedirectToAction("Error", "Home", new { message = "ERROR retrieving contacts", debug = ex.Message });
        }
    }
    
  5. Klicken Sie nach der Anmeldung bei der App auf das Element Kontakte im oberen Nagivationsmenü.