Создание основных приложений MVC ASP.NET с помощью Microsoft Graph
В этом руководстве рассказывается о создании веб ASP.NET Core приложения, которое использует API microsoft Graph для получения сведений о календаре для пользователя.
Совет
Если вы предпочитаете просто скачать завершенный учебник, вы можете скачать или клонировать GitHub репозиторий. Инструкции по настройке приложения с помощью ID-приложения и секрета см. в файле README в демо-папке.
Предварительные требования
Прежде чем приступить к этому учебнику, необходимо установить SDK.NET Core на компьютере разработки. Если у вас нет SDK, посетите предыдущую ссылку для скачать параметры.
Вы также должны иметь личную учетную запись Майкрософт с почтовым ящиком на Outlook.com или учетную запись Microsoft work или school. Если у вас нет учетной записи Майкрософт, существует несколько вариантов получения бесплатной учетной записи:
- Вы можете зарегистрироваться на новую личную учетную запись Майкрософт.
- Вы можете зарегистрироваться в программе Microsoft 365 разработчика, чтобы получить бесплатную Office 365 подписку.
Примечание
Это руководство было написано с .NET Core SDK версии 5.0.102. Действия в этом руководстве могут работать с другими версиями, но они не были проверены.
Отзывы
Обратите внимание на этот учебник в репозитории GitHub.
Создание ASP.NET основного веб-приложения MVC
Начните с создания ASP.NET Core веб-приложения.
Откройте интерфейс командной строки (CLI) в каталоге, в котором необходимо создать проект. Выполните следующую команду.
dotnet new mvc -o GraphTutorial
После создания проекта убедитесь, что он работает путем изменения текущего каталога в каталог GraphTutorial и запуска следующей команды в CLI.
dotnet run
Откройте браузер и просмотрите .
https://localhost:5001
Если все работает, вы должны увидеть страницу ASP.NET Core по умолчанию.
Важно!
Если вы получаете предупреждение о том, что сертификат localhost не доверяется, вы можете использовать CLI core .NET для установки сертификата разработки и доверия к этому сертификату. Инструкции по конкретным операционным системам см. в ASP.NET Core https.ru.
Добавление пакетов NuGet
Прежде чем двигаться дальше, установите дополнительные NuGet пакеты, которые вы будете использовать позже.
- Microsoft.Identity.Web для запроса и управления маркерами доступа.
- Microsoft.Identity.Web.MicrosoftGraph для добавления Graph SDK с помощью инъекции зависимостей.
- Пользовательский интерфейс Microsoft.Identity.Web.UI для пользовательского интерфейса для регистрации и регистрации.
- TimeZoneConverter для обработки идентификаторов часового пояса поперек платформы.
Запустите следующие команды в CLI для установки зависимостей.
dotnet add package Microsoft.Identity.Web --version 1.5.1 dotnet add package Microsoft.Identity.Web.MicrosoftGraph --version 1.5.1 dotnet add package Microsoft.Identity.Web.UI --version 1.5.1 dotnet add package TimeZoneConverter
Проектирование приложения
В этом разделе будет создаваться базовая структура пользовательского интерфейса приложения.
Реализация методов расширения оповещений
В этом разделе будут создаваться методы расширения для типа IActionResult
, возвращаемого представлениями контроллера. Это расширение позволит передать представление временные сообщения об ошибке или успехе.
Совет
Для редактирования исходных файлов для этого учебника можно использовать любой текстовый редактор. Однако Visual Studio Code дополнительные функции, такие как отладка и Intellisense.
Создайте новый каталог в каталоге GraphTutorial с именем Alerts.
Создайте новый файл с именем WithAlertResult.cs в каталоге ./Alerts и добавьте следующий код.
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.Extensions.DependencyInjection; using System.Threading.Tasks; namespace GraphTutorial { // WithAlertResult adds temporary error/info/success // messages to the result of a controller action. // This data is read and displayed by the _AlertPartial view public class WithAlertResult : IActionResult { public IActionResult Result { get; } public string Type { get; } public string Message { get; } public string DebugInfo { get; } public WithAlertResult(IActionResult result, string type, string message, string debugInfo) { Result = result; Type = type; Message = message; DebugInfo = debugInfo; } public async Task ExecuteResultAsync(ActionContext context) { var factory = context.HttpContext.RequestServices .GetService<ITempDataDictionaryFactory>(); var tempData = factory.GetTempData(context.HttpContext); tempData["_alertType"] = Type; tempData["_alertMessage"] = Message; tempData["_alertDebugInfo"] = DebugInfo; await Result.ExecuteResultAsync(context); } } }
Создайте новый файл с именем AlertExtensions.cs в каталоге ./Alerts и добавьте следующий код.
using Microsoft.AspNetCore.Mvc; namespace GraphTutorial { public static class AlertExtensions { public static IActionResult WithError(this IActionResult result, string message, string debugInfo = null) { return Alert(result, "danger", message, debugInfo); } public static IActionResult WithSuccess(this IActionResult result, string message, string debugInfo = null) { return Alert(result, "success", message, debugInfo); } public static IActionResult WithInfo(this IActionResult result, string message, string debugInfo = null) { return Alert(result, "info", message, debugInfo); } private static IActionResult Alert(IActionResult result, string type, string message, string debugInfo) { return new WithAlertResult(result, type, message, debugInfo); } } }
Реализация методов расширения пользовательских данных
В этом разделе будут созданы методы расширения для объекта ClaimsPrincipal
, генерируемого платформой Microsoft Identity. Это позволит расширить существующую идентификацию пользователя с помощью данных microsoft Graph.
Примечание
Этот код является только заполнителям на данный момент, вы завершите его в более позднем разделе.
Создайте новый каталог в каталоге GraphTutorial с именем Graph.
Создайте новый файл с именем GraphClaimsPrincipalExtensions.cs и добавьте следующий код.
using System.Security.Claims; namespace GraphTutorial { public static class GraphClaimTypes { public const string DisplayName ="graph_name"; public const string Email = "graph_email"; public const string Photo = "graph_photo"; public const string TimeZone = "graph_timezone"; public const string DateTimeFormat = "graph_datetimeformat"; } // Helper methods to access Graph user data stored in // the claims principal public static class GraphClaimsPrincipalExtensions { public static string GetUserGraphDisplayName(this ClaimsPrincipal claimsPrincipal) { return "Adele Vance"; } public static string GetUserGraphEmail(this ClaimsPrincipal claimsPrincipal) { return "adelev@contoso.com"; } public static string GetUserGraphPhoto(this ClaimsPrincipal claimsPrincipal) { return "/img/no-profile-photo.png"; } } }
Создание представлений
В этом разделе будут реализованы представления Razor для приложения.
Добавьте новый файл с именем _LoginPartial.cshtml в каталоге ./Views/Shared и добавьте следующий код.
@using GraphTutorial <ul class="nav navbar-nav"> @if (User.Identity.IsAuthenticated) { <li class="nav-item dropdown"> <a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" role="button"> <img src="@User.GetUserGraphPhoto()" class="nav-profile-photo rounded-circle align-self-center mr-2"> </a> <div class="dropdown-menu dropdown-menu-right"> <h5 class="dropdown-item-text mb-0">@User.GetUserGraphDisplayName()</h5> <p class="dropdown-item-text text-muted mb-0">@User.GetUserGraphEmail()</p> <div class="dropdown-divider"></div> <a class="dropdown-item" asp-area="MicrosoftIdentity" asp-controller="Account" asp-action="SignOut">Sign out</a> </div> </li> } else { <li class="nav-item"> <a class="nav-link" asp-area="MicrosoftIdentity" asp-controller="Account" asp-action="SignIn">Sign in</a> </li> } </ul>
Добавьте новый файл с именем _AlertPartial.cshtml в каталоге ./Views/Shared и добавьте следующий код.
@{ var type = $"{TempData["_alertType"]}"; var message = $"{TempData["_alertMessage"]}"; var debugInfo = $"{TempData["_alertDebugInfo"]}"; } @if (!string.IsNullOrEmpty(type)) { <div class="alert alert-@type" role="alert"> @if (string.IsNullOrEmpty(debugInfo)) { <p class="mb-0">@message</p> } else { <p class="mb-3">@message</p> <pre class="alert-pre border bg-light p-2"><code>@debugInfo</code></pre> } </div> }
Откройте файл ./Views/Shared/_Layout.cshtml и замените все его содержимое следующим кодом, чтобы обновить глобальный макет приложения.
@{ string controller = $"{ViewContext.RouteData.Values["controller"]}"; } <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>@ViewData["Title"] - GraphTutorial</title> <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" /> <link rel="stylesheet" href="~/css/site.css" /> </head> <body> <header> <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-dark bg-dark border-bottom box-shadow mb-3"> <div class="container"> <a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">GraphTutorial</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="navbar-collapse collapse mr-auto"> <ul class="navbar-nav flex-grow-1"> <li class="@(controller == "Home" ? "nav-item active" : "nav-item")"> <a class="nav-link" asp-area="" asp-controller="Home" asp-action="Index">Home</a> </li> @if (User.Identity.IsAuthenticated) { <li class="@(controller == "Calendar" ? "nav-item active" : "nav-item")"> <a class="nav-link" asp-area="" asp-controller="Calendar" asp-action="Index">Calendar</a> </li> } </ul> <partial name="_LoginPartial"/> </div> </div> </nav> </header> <div class="container"> <main role="main" class="pb-3"> <partial name="_AlertPartial"/> @RenderBody() </main> </div> <footer class="border-top footer text-muted"> <div class="container"> © 2020 - GraphTutorial - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a> </div> </footer> <script src="~/lib/jquery/dist/jquery.min.js"></script> <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script> <script src="~/js/site.js" asp-append-version="true"></script> @RenderSection("Scripts", required: false) </body> </html>
Откройте ./wwwroot/css/site.css и добавьте следующий код в нижней части файла.
.nav-profile-photo { width: 32px; } .alert-pre { word-wrap: break-word; word-break: break-all; white-space: pre-wrap; } .calendar-view-date-cell { width: 150px; } .calendar-view-date { width: 40px; font-size: 36px; line-height: 36px; margin-right: 10px; } .calendar-view-month { font-size: 0.75em; } .calendar-view-timespan { width: 200px; } .calendar-view-subject { font-size: 1.25em; } .calendar-view-organizer { font-size: .75em; } .calendar-view-date-diff { font-size: .75em }
Откройте файл ./Views/Home/index.cshtml и замените его содержимое следующим.
@{ ViewData["Title"] = "Home Page"; } @using GraphTutorial <div class="jumbotron"> <h1>ASP.NET Core Graph Tutorial</h1> <p class="lead">This sample app shows how to use the Microsoft Graph API to access a user's data from ASP.NET Core</p> @if (User.Identity.IsAuthenticated) { <h4>Welcome @User.GetUserGraphDisplayName()!</h4> <p>Use the navigation bar at the top of the page to get started.</p> } else { <a class="btn btn-primary btn-large" asp-area="MicrosoftIdentity" asp-controller="Account" asp-action="SignIn">Click here to sign in</a> } </div>
Создайте новый каталог в каталоге ./wwwroot с именем img. Добавьте файл изображений выбора с именем no-profile-photo.png в этом каталоге. Это изображение будет использоваться в качестве фотографии пользователя, если у пользователя нет фотографии в Microsoft Graph.
Совет
Вы можете скачать изображение, используемую на этих скриншотах из GitHub.
Сохраните все изменения и перезапустите сервер (
dotnet run
). Теперь приложение должно выглядеть совсем по-другому.
Регистрация приложения на портале
В этом упражнении будет создаваться новая регистрация веб-приложений Azure AD с помощью центра администрирования Azure Active Directory администратора.
Откройте браузер и перейдите в Центр администрирования Azure Active Directory. Войдите с помощью личной учетной записи (т.е. учетной записи Microsoft) или рабочей (учебной) учетной записи.
Выберите Azure Active Directory на панели навигации слева, затем выберите Регистрация приложений в разделе Управление.
Выберите Новая регистрация. На странице Зарегистрировать приложение задайте необходимые значения следующим образом.
- Введите имя
ASP.NET Core Graph Tutorial
. - Введите поддерживаемые типы учетных записей для учетных записей в любом каталоге организаций и личных учетных записей Microsoft.
- В разделе URI адрес перенаправления введите значение в первом раскрывающемся списке
Web
и задайте значениеhttps://localhost:5001/
.
- Введите имя
Нажмите Зарегистрировать. На странице ASP.NET Core Graph учебника скопируйте значение ID приложения (клиента) и сохраните его, оно потребуется на следующем шаге.
Выберите пункт Проверка подлинности в разделе Управление. В url-адресов перенаправления добавьте URI со значением
https://localhost:5001/signin-oidc
.Установите URL-адрес журнала
https://localhost:5001/signout-oidc
.Найдите раздел Неявное представление и включите Маркеры идентификации. Нажмите Сохранить.
Выберите Сертификаты и секреты в разделе Управление. Нажмите кнопку Новый секрет клиента. Введите значение в поле Описание, выберите один из параметров Срок действия и нажмите Добавить.
Прежде чем покинуть страницу, скопируйте значение секрета клиента. Оно вам понадобится на следующем шаге.
Важно!
Система никогда не покажет секрет клиента повторно, поэтому убедитесь, что вы скопировали его.
Добавление проверки подлинности с помощью Azure AD
В этом упражнении вы расширит приложение от предыдущего упражнения для поддержки проверки подлинности с помощью Azure AD. Это необходимо для получения маркера доступа OAuth, который нужен для вызова API Microsoft Graph. На этом шаге будет настроена библиотека Microsoft.Identity.Web .
Важно!
Чтобы не хранить код приложения и секрет в источнике, для хранения этих значений используется диспетчер секрета .NET . Секретный менеджер предназначен только для целей разработки, для хранения секретов в производственных приложениях должен использовать доверенный секретный менеджер.
Откройте ./appsettings.json и замените его содержимое следующим.
{ "AzureAd": { "Instance": "https://login.microsoftonline.com/", "TenantId": "common", "CallbackPath": "/signin-oidc" }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*" }
Откройте свой CLI в каталоге, в котором расположен GraphTutorial.csproj , и запустите следующие команды,
YOUR_APP_ID
заменив свой ID приложения на портале Azure иYOUR_APP_SECRET
секрет приложения.dotnet user-secrets init dotnet user-secrets set "AzureAd:ClientId" "YOUR_APP_ID" dotnet user-secrets set "AzureAd:ClientSecret" "YOUR_APP_SECRET"
Реализация входа в систему
Начните с добавления служб платформы microsoft Identity в приложение.
Создайте новый файл с именем GraphConstants.cs в каталоге ./Graph и добавьте следующий код.
namespace GraphTutorial { public static class GraphConstants { // Defines the permission scopes used by the app public readonly static string[] Scopes = { "User.Read", "MailboxSettings.Read", "Calendars.ReadWrite" }; } }
Откройте файл ./Startup.cs и добавьте
using
следующие утверждения в верхнюю часть файла.using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc.Authorization; using Microsoft.Identity.Web; using Microsoft.Identity.Web.UI; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.Graph; using System.Net; using System.Net.Http.Headers;
Замените имеющуюся функцию
ConfigureServices
указанным ниже кодом.public void ConfigureServices(IServiceCollection services) { services // Use OpenId authentication .AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) // Specify this is a web app and needs auth code flow .AddMicrosoftIdentityWebApp(Configuration) // Add ability to call web API (Graph) // and get access tokens .EnableTokenAcquisitionToCallDownstreamApi(options => { Configuration.Bind("AzureAd", options); }, GraphConstants.Scopes) // Use in-memory token cache // See https://github.com/AzureAD/microsoft-identity-web/wiki/token-cache-serialization .AddInMemoryTokenCaches(); // Require authentication services.AddControllersWithViews(options => { var policy = new AuthorizationPolicyBuilder() .RequireAuthenticatedUser() .Build(); options.Filters.Add(new AuthorizeFilter(policy)); }) // Add the Microsoft Identity UI pages for signin/out .AddMicrosoftIdentityUI(); }
В функции
Configure
добавьте следующую строку выше строкиapp.UseAuthorization();
.app.UseAuthentication();
Откройте ./Controllers/HomeController.cs и замените его содержимое следующим.
using GraphTutorial.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.Identity.Web; using System.Diagnostics; using System.Threading.Tasks; namespace GraphTutorial.Controllers { public class HomeController : Controller { ITokenAcquisition _tokenAcquisition; private readonly ILogger<HomeController> _logger; // Get the ITokenAcquisition interface via // dependency injection public HomeController( ITokenAcquisition tokenAcquisition, ILogger<HomeController> logger) { _tokenAcquisition = tokenAcquisition; _logger = logger; } public async Task<IActionResult> Index() { // TEMPORARY // Get the token and display it try { string token = await _tokenAcquisition .GetAccessTokenForUserAsync(GraphConstants.Scopes); return View().WithInfo("Token acquired", token); } catch (MicrosoftIdentityWebChallengeUserException) { return Challenge(); } } public IActionResult Privacy() { return View(); } [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] public IActionResult Error() { return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); } [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] [AllowAnonymous] public IActionResult ErrorWithMessage(string message, string debug) { return View("Index").WithError(message, debug); } } }
Сохраните изменения и запустите проект. Вход в учетную запись Майкрософт.
Изучите запрос на согласие. Список разрешений соответствует списку областей разрешений, настроенных в ./Graph/GraphConstants.cs.
- Поддержание доступа к данным, которые вы предоставили ему доступ: (
offline_access
) Это разрешение запрашивается MSAL для получения маркеров обновления. - Во входе и прочтите свой профиль: (
User.Read
) Это разрешение позволяет приложению получать в журнале профиль пользователя и фотографию профиля. - Ознакомьтесь с настройками почтовых ящиков. (
MailboxSettings.Read
) Это разрешение позволяет приложению читать параметры почтовых ящиков пользователя, включая часовой пояс и формат времени. - Полный доступ к календарям: (
Calendars.ReadWrite
) Это разрешение позволяет приложению читать события в календаре пользователя, добавлять новые события и изменять существующие.
Дополнительные сведения о согласии см. в приложении Understanding Azure AD application consent experiences.
- Поддержание доступа к данным, которые вы предоставили ему доступ: (
Согласие на запрашиваемую разрешения. Браузер перенаправит вас в приложение, показывая маркер.
Получение сведений о пользователе
Как только пользователь вошел в систему, вы можете получить сведения о нем из Microsoft Graph.
Откройте ./Graph/GraphClaimsPrincipalExtensions.cs и замените все содержимое на следующее.
using Microsoft.Graph; using System; using System.IO; using System.Security.Claims; namespace GraphTutorial { public static class GraphClaimTypes { public const string DisplayName ="graph_name"; public const string Email = "graph_email"; public const string Photo = "graph_photo"; public const string TimeZone = "graph_timezone"; public const string TimeFormat = "graph_timeformat"; } // Helper methods to access Graph user data stored in // the claims principal public static class GraphClaimsPrincipalExtensions { public static string GetUserGraphDisplayName(this ClaimsPrincipal claimsPrincipal) { return claimsPrincipal.FindFirstValue(GraphClaimTypes.DisplayName); } public static string GetUserGraphEmail(this ClaimsPrincipal claimsPrincipal) { return claimsPrincipal.FindFirstValue(GraphClaimTypes.Email); } public static string GetUserGraphPhoto(this ClaimsPrincipal claimsPrincipal) { return claimsPrincipal.FindFirstValue(GraphClaimTypes.Photo); } public static string GetUserGraphTimeZone(this ClaimsPrincipal claimsPrincipal) { return claimsPrincipal.FindFirstValue(GraphClaimTypes.TimeZone); } public static string GetUserGraphTimeFormat(this ClaimsPrincipal claimsPrincipal) { return claimsPrincipal.FindFirstValue(GraphClaimTypes.TimeFormat); } public static void AddUserGraphInfo(this ClaimsPrincipal claimsPrincipal, User user) { var identity = claimsPrincipal.Identity as ClaimsIdentity; identity.AddClaim( new Claim(GraphClaimTypes.DisplayName, user.DisplayName)); identity.AddClaim( new Claim(GraphClaimTypes.Email, user.Mail ?? user.UserPrincipalName)); identity.AddClaim( new Claim(GraphClaimTypes.TimeZone, user.MailboxSettings.TimeZone)); identity.AddClaim( new Claim(GraphClaimTypes.TimeFormat, user.MailboxSettings.TimeFormat)); } public static void AddUserGraphPhoto(this ClaimsPrincipal claimsPrincipal, Stream photoStream) { var identity = claimsPrincipal.Identity as ClaimsIdentity; if (photoStream == null) { // Add the default profile photo identity.AddClaim( new Claim(GraphClaimTypes.Photo, "/img/no-profile-photo.png")); return; } // Copy the photo stream to a memory stream // to get the bytes out of it var memoryStream = new MemoryStream(); photoStream.CopyTo(memoryStream); var photoBytes = memoryStream.ToArray(); // Generate a date URI for the photo var photoUrl = $"data:image/png;base64,{Convert.ToBase64String(photoBytes)}"; identity.AddClaim( new Claim(GraphClaimTypes.Photo, photoUrl)); } } }
Откройте ./Startup.cs и замените существующую строку
.AddMicrosoftIdentityWebApp(Configuration)
следующим кодом.// Specify this is a web app and needs auth code flow .AddMicrosoftIdentityWebApp(options => { Configuration.Bind("AzureAd", options); options.Prompt = "select_account"; options.Events.OnTokenValidated = async context => { var tokenAcquisition = context.HttpContext.RequestServices .GetRequiredService<ITokenAcquisition>(); var graphClient = new GraphServiceClient( new DelegateAuthenticationProvider(async (request) => { var token = await tokenAcquisition .GetAccessTokenForUserAsync(GraphConstants.Scopes, user:context.Principal); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); }) ); // Get user information from Graph var user = await graphClient.Me.Request() .Select(u => new { u.DisplayName, u.Mail, u.UserPrincipalName, u.MailboxSettings }) .GetAsync(); context.Principal.AddUserGraphInfo(user); // Get the user's photo // If the user doesn't have a photo, this throws try { var photo = await graphClient.Me .Photos["48x48"] .Content .Request() .GetAsync(); context.Principal.AddUserGraphPhoto(photo); } catch (ServiceException ex) { if (ex.IsMatch("ErrorItemNotFound") || ex.IsMatch("ConsumerPhotoIsNotSupported")) { context.Principal.AddUserGraphPhoto(null); } else { throw; } } }; options.Events.OnAuthenticationFailed = context => { var error = WebUtility.UrlEncode(context.Exception.Message); context.Response .Redirect($"/Home/ErrorWithMessage?message=Authentication+error&debug={error}"); context.HandleResponse(); return Task.FromResult(0); }; options.Events.OnRemoteFailure = context => { if (context.Failure is OpenIdConnectProtocolException) { var error = WebUtility.UrlEncode(context.Failure.Message); context.Response .Redirect($"/Home/ErrorWithMessage?message=Sign+in+error&debug={error}"); context.HandleResponse(); } return Task.FromResult(0); }; })
Рассмотрим, что делает этот код.
- Он добавляет обработник событий для
OnTokenValidated
события.- Он использует интерфейс
ITokenAcquisition
для получения маркера доступа. - Он вызывает microsoft Graph, чтобы получить профиль пользователя и фотографию.
- Он добавляет Graph в удостоверение пользователя.
- Он использует интерфейс
- Он добавляет обработник событий для
Добавьте следующий вызов функции после вызова
EnableTokenAcquisitionToCallDownstreamApi
и перед вызовомAddInMemoryTokenCaches
.// Add a GraphServiceClient via dependency injection .AddMicrosoftGraph(options => { options.Scopes = string.Join(' ', GraphConstants.Scopes); })
Это сделает проверку подлинности GraphServiceClient доступной для контроллеров с помощью инъекции зависимостей.
Откройте ./Controllers/HomeController.cs и замените
Index
функцию на следующую.public IActionResult Index() { return View(); }
Удалите все ссылки
ITokenAcquisition
на класс HomeController .Сохраните изменения, запустите приложение и пройдите процедуру регистрации. Вы должны вернуться на домашняя страница, но пользовательский интерфейс должен измениться, чтобы указать, что вы подписаны.
Щелкните аватар пользователя в правом верхнем углу, чтобы получить доступ к ссылке Sign Out . Щелкнув кнопку "Выйти", вы сбросит сеанс и возвращает вас на домашнюю страницу.
Совет
Если вы не видите имя пользователя на домашней странице, а при отсеве аватара не хватает имени и электронной почты после внесения этих изменений, выпишитесь и войте обратно.
Хранение и обновление маркеров
На этом этапе у приложения есть маркер доступа, который отправляется Authorization
в заголовке вызовов API. Это маркер, который позволяет приложению получать доступ к Microsoft Graph от имени пользователя.
Однако этот маркер недолговечен. Срок действия маркера истекает через час после его выпуска. Вот здесь и пригодится маркер обновления. Маркер обновления позволяет приложению запрашивать новый маркер доступа, не требуя от пользователя повторного входа в систему.
Поскольку приложение использует библиотеку Microsoft.Identity.Web, не нужно внедрять логику хранения маркеров или обновления.
Приложение использует кэш маркеров в памяти, который является достаточным для приложений, которым не нужно сохранять маркеры при перезапуске приложения. Производственные приложения могут вместо этого использовать параметры распределенного кэша в библиотеке Microsoft.Identity.Web.
Метод GetAccessTokenForUserAsync
обрабатывает срок действия маркера и обновляется для вас. Сначала он проверяет кэш-маркер, и если срок его действия не истек, он возвращает его. Если срок действия истек, он использует кэшный маркер обновления для получения нового.
GraphServiceClient, который контроллеры получают с помощью инъекции зависимостей, будет предварительно настроен с поставщиком проверки подлинности, который использует GetAccessTokenForUserAsync
для вас.
Получение представления календаря
В этом разделе вы будете включать microsoft Graph в приложение. Для этого приложения вы будете использовать клиентскую библиотеку Microsoft Graph для .NET для звонков в Microsoft Graph.
Получение событий календаря из Outlook
Начните с создания нового контроллера представлений календаря.
Добавьте новый файл с именем CalendarController.cs в каталоге ./Controllers и добавьте следующий код.
using GraphTutorial.Models; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.Identity.Web; using Microsoft.Graph; using System; using System.Collections.Generic; using System.Threading.Tasks; using TimeZoneConverter; namespace GraphTutorial.Controllers { public class CalendarController : Controller { private readonly GraphServiceClient _graphClient; private readonly ILogger<HomeController> _logger; public CalendarController( GraphServiceClient graphClient, ILogger<HomeController> logger) { _graphClient = graphClient; _logger = logger; } } }
Добавьте в класс
CalendarController
следующие функции, чтобы получить представление календаря пользователя.private async Task<IList<Event>> GetUserWeekCalendar(DateTime startOfWeekUtc) { // Configure a calendar view for the current week var endOfWeekUtc = startOfWeekUtc.AddDays(7); var viewOptions = new List<QueryOption> { new QueryOption("startDateTime", startOfWeekUtc.ToString("o")), new QueryOption("endDateTime", endOfWeekUtc.ToString("o")) }; var events = await _graphClient.Me .CalendarView .Request(viewOptions) // Send user time zone in request so date/time in // response will be in preferred time zone .Header("Prefer", $"outlook.timezone=\"{User.GetUserGraphTimeZone()}\"") // Get max 50 per request .Top(50) // Only return fields app will use .Select(e => new { e.Subject, e.Organizer, e.Start, e.End }) // Order results chronologically .OrderBy("start/dateTime") .GetAsync(); IList<Event> allEvents; // Handle case where there are more than 50 if (events.NextPageRequest != null) { allEvents = new List<Event>(); // Create a page iterator to iterate over subsequent pages // of results. Build a list from the results var pageIterator = PageIterator<Event>.CreatePageIterator( _graphClient, events, (e) => { allEvents.Add(e); return true; } ); await pageIterator.IterateAsync(); } else { // If only one page, just use the result allEvents = events.CurrentPage; } return allEvents; } private static DateTime GetUtcStartOfWeekInTimeZone(DateTime today, TimeZoneInfo timeZone) { // Assumes Sunday as first day of week int diff = System.DayOfWeek.Sunday - today.DayOfWeek; // create date as unspecified kind var unspecifiedStart = DateTime.SpecifyKind(today.AddDays(diff), DateTimeKind.Unspecified); // convert to UTC return TimeZoneInfo.ConvertTimeToUtc(unspecifiedStart, timeZone); }
Рассмотрим, что делает код
GetUserWeekCalendar
.- Он использует часовой пояс пользователя для получения значений даты начала и окончания UTC за неделю.
- Он запрашивает представление календаря пользователя, чтобы получить все события, которые попадают между датой начала и конца/временем. Использование представления календаря вместо перечисления событий расширяет повторяющиеся события, возвращая все события, которые происходят в указанном окне времени.
- Для получения
Prefer: outlook.timezone
результатов в часовом поясе пользователя используется загона. - Он использует
Select
для ограничения полей, возвращаясь только к полям, используемым приложением. - Он использует для
OrderBy
сортировки результатов в хронологическом порядке. - Он использует страницу
PageIterator
через коллекцию событий. Это обрабатывает тот случай, когда у пользователя в календаре больше событий, чем запрашиваемой страницы.
Добавьте в класс
CalendarController
следующую функцию, чтобы реализовать временное представление возвращенных данных.// Minimum permission scope needed for this view [AuthorizeForScopes(Scopes = new[] { "Calendars.Read" })] public async Task<IActionResult> Index() { try { var userTimeZone = TZConvert.GetTimeZoneInfo( User.GetUserGraphTimeZone()); var startOfWeek = CalendarController.GetUtcStartOfWeekInTimeZone( DateTime.Today, userTimeZone); var events = await GetUserWeekCalendar(startOfWeek); // Return a JSON dump of events return new ContentResult { Content = _graphClient.HttpProvider.Serializer.SerializeObject(events), ContentType = "application/json" }; } catch (ServiceException ex) { if (ex.InnerException is MicrosoftIdentityWebChallengeUserException) { throw; } return new ContentResult { Content = $"Error getting calendar view: {ex.Message}", ContentType = "text/plain" }; } }
Запустите приложение, войдите и нажмите ссылку Календарь в панели nav. Если все работает надлежащим образом, в календаре пользователя должен появиться дамп событий в формате JSON.
Отображение результатов
Теперь вы можете добавить представление для отображения результатов в более понятной пользователям форме.
Создание моделей представлений
Создайте новый файл с именем CalendarViewEvent.cs в каталоге ./Models и добавьте следующий код.
using Microsoft.Graph; using System; namespace GraphTutorial.Models { public class CalendarViewEvent { public string Subject { get; private set; } public string Organizer { get; private set; } public DateTime Start { get; private set; } public DateTime End { get; private set; } public CalendarViewEvent(Event graphEvent) { Subject = graphEvent.Subject; Organizer = graphEvent.Organizer.EmailAddress.Name; Start = DateTime.Parse(graphEvent.Start.DateTime); End = DateTime.Parse(graphEvent.End.DateTime); } } }
Создайте новый файл с именем DailyViewModel.cs в каталоге ./Models и добавьте следующий код.
using System; using System.Collections.Generic; namespace GraphTutorial.Models { public class DailyViewModel { // Day the view is for public DateTime Day { get; private set; } // Events on this day public IEnumerable<CalendarViewEvent> Events { get; private set; } public DailyViewModel(DateTime day, IEnumerable<CalendarViewEvent> events) { Day = day; Events = events; } } }
Создайте новый файл с именем CalendarViewModel.cs в каталоге ./Models и добавьте следующий код.
using Microsoft.Graph; using System; using System.Collections.Generic; using System.Linq; namespace GraphTutorial.Models { public class CalendarViewModel { private DateTime _startOfWeek; private DateTime _endOfWeek; private List<CalendarViewEvent> _events; public CalendarViewModel() { _startOfWeek = DateTime.MinValue; _events = new List<CalendarViewEvent>(); } public CalendarViewModel(DateTime startOfWeek, IEnumerable<Event> events) { _startOfWeek = startOfWeek; _endOfWeek = startOfWeek.AddDays(7); _events = new List<CalendarViewEvent>(); if (events != null) { foreach (var item in events) { _events.Add(new CalendarViewEvent(item)); } } } // Get the start - end dates of the week public string TimeSpan() { return $"{_startOfWeek.ToString("MMMM d, yyyy")} - {_startOfWeek.AddDays(6).ToString("MMMM d, yyyy")}"; } // Property accessors to pass to the daily view partial // These properties get all events on the specific day public DailyViewModel Sunday { get { return new DailyViewModel( _startOfWeek, GetEventsForDay(System.DayOfWeek.Sunday)); } } public DailyViewModel Monday { get { return new DailyViewModel( _startOfWeek.AddDays(1), GetEventsForDay(System.DayOfWeek.Monday)); } } public DailyViewModel Tuesday { get { return new DailyViewModel( _startOfWeek.AddDays(2), GetEventsForDay(System.DayOfWeek.Tuesday)); } } public DailyViewModel Wednesday { get { return new DailyViewModel( _startOfWeek.AddDays(3), GetEventsForDay(System.DayOfWeek.Wednesday)); } } public DailyViewModel Thursday { get { return new DailyViewModel( _startOfWeek.AddDays(4), GetEventsForDay(System.DayOfWeek.Thursday)); } } public DailyViewModel Friday { get { return new DailyViewModel( _startOfWeek.AddDays(5), GetEventsForDay(System.DayOfWeek.Friday)); } } public DailyViewModel Saturday { get { return new DailyViewModel( _startOfWeek.AddDays(6), GetEventsForDay(System.DayOfWeek.Saturday)); } } private IEnumerable<CalendarViewEvent> GetEventsForDay(System.DayOfWeek day) { return _events.Where(e => (e.End > _startOfWeek && ((e.Start.DayOfWeek.Equals(day) && e.Start >= _startOfWeek) || (e.End.DayOfWeek.Equals(day) && e.End < _endOfWeek)))); } } }
Создание представлений
Создайте новый каталог с именем Calendar в каталоге ./Views .
Создайте новый файл _DailyEventsPartial.cshtml в каталоге ./Views/Calendar и добавьте следующий код.
@model DailyViewModel @{ bool dateCellAdded = false; var timeFormat = User.GetUserGraphTimeFormat(); var rowClass = Model.Day.Date.Equals(DateTime.Today.Date) ? "table-warning" : ""; } @if (Model.Events.Count() <= 0) { // Render an empty row for the day <tr> <td class="calendar-view-date-cell"> <div class="calendar-view-date float-left text-right">@Model.Day.Day</div> <div class="calendar-view-day">@Model.Day.ToString("dddd")</div> <div class="calendar-view-month text-muted">@Model.Day.ToString("MMMM, yyyy")</div> </td> <td></td> <td></td> </tr> } @foreach(var item in Model.Events) { <tr class="@rowClass"> @if (!dateCellAdded) { // Only add the day cell once dateCellAdded = true; <td class="calendar-view-date-cell" rowspan="@Model.Events.Count()"> <div class="calendar-view-date float-left text-right">@Model.Day.Day</div> <div class="calendar-view-day">@Model.Day.ToString("dddd")</div> <div class="calendar-view-month text-muted">@Model.Day.ToString("MMMM, yyyy")</div> </td> } <td class="calendar-view-timespan"> <div>@item.Start.ToString(timeFormat) - @item.End.ToString(timeFormat)</div> @if (item.Start.Date != Model.Day.Date) { <div class="calendar-view-date-diff">Start date: @item.Start.Date.ToShortDateString()</div> } @if (item.End.Date != Model.Day.Date) { <div class="calendar-view-date-diff">End date: @item.End.Date.ToShortDateString()</div> } </td> <td> <div class="calendar-view-subject">@item.Subject</div> <div class="calendar-view-organizer">@item.Organizer</div> </td> </tr> }
Создайте новый файл с именем Index.cshtml в каталоге ./Views/Calendar и добавьте следующий код.
@model CalendarViewModel @{ ViewData["Title"] = "Calendar"; } <div class="mb-3"> <h1 class="mb-3">@Model.TimeSpan()</h1> <a class="btn btn-light btn-sm" asp-controller="Calendar" asp-action="New">New event</a> </div> <div class="calendar-week"> <div class="table-responsive"> <table class="table table-sm"> <thead> <tr> <th>Date</th> <th>Time</th> <th>Event</th> </tr> </thead> <tbody> <partial name="_DailyEventsPartial" for="Sunday" /> <partial name="_DailyEventsPartial" for="Monday" /> <partial name="_DailyEventsPartial" for="Tuesday" /> <partial name="_DailyEventsPartial" for="Wednesday" /> <partial name="_DailyEventsPartial" for="Thursday" /> <partial name="_DailyEventsPartial" for="Friday" /> <partial name="_DailyEventsPartial" for="Saturday" /> </tbody> </table> </div> </div>
Обновление контроллера календаря
Откройте ./Controllers/CalendarController.cs и замените существующую
Index
функцию на следующую.// Minimum permission scope needed for this view [AuthorizeForScopes(Scopes = new[] { "Calendars.Read" })] public async Task<IActionResult> Index() { try { var userTimeZone = TZConvert.GetTimeZoneInfo( User.GetUserGraphTimeZone()); var startOfWeekUtc = CalendarController.GetUtcStartOfWeekInTimeZone( DateTime.Today, userTimeZone); var events = await GetUserWeekCalendar(startOfWeekUtc); // Convert UTC start of week to user's time zone for // proper display var startOfWeekInTz = TimeZoneInfo.ConvertTimeFromUtc(startOfWeekUtc, userTimeZone); var model = new CalendarViewModel(startOfWeekInTz, events); return View(model); } catch (ServiceException ex) { if (ex.InnerException is MicrosoftIdentityWebChallengeUserException) { throw; } return View(new CalendarViewModel()) .WithError("Error getting calendar view", ex.Message); } }
Запустите приложение, войдите и нажмите ссылку Календарь . Теперь в приложении должна появиться таблица событий.
Создание нового события
В этом разделе вы добавим возможность создания событий в календаре пользователя.
Создание модели
Создайте новый файл с именем NewEvent.cs в каталоге ./Models и добавьте следующий код.
using System; using System.ComponentModel.DataAnnotations; namespace GraphTutorial.Models { public class NewEvent { [Required] public string Subject { get; set; } public DateTime Start { get; set; } public DateTime End { get; set; } [DataType(DataType.MultilineText)] public string Body { get; set; } [RegularExpression(@"((\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*)*([;])*)*", ErrorMessage="Please enter one or more email addresses separated by a semi-colon (;)")] public string Attendees { get; set; } } }
Создание представления
Создайте новый файл с именем New.cshtml в каталоге ./Views/Calendar и добавьте следующий код.
@model NewEvent @{ ViewData["Title"] = "New event"; } <form asp-action="New"> <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="form-group"> <label asp-for="Subject" class="control-label"></label> <input asp-for="Subject" class="form-control" /> <span asp-validation-for="Subject" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="Attendees" class="control-label"></label> <input asp-for="Attendees" class="form-control" /> <span asp-validation-for="Attendees" class="text-danger"></span> </div> <div class="form-row"> <div class="col"> <div class="form-group"> <label asp-for="Start" class="control-label"></label> <input asp-for="Start" class="form-control" /> <span asp-validation-for="Start" class="text-danger"></span> </div> </div> <div class="col"> <div class="form-group"> <label asp-for="End" class="control-label"></label> <input asp-for="End" class="form-control" /> <span asp-validation-for="End" class="text-danger"></span> </div> </div> </div> <div class="form-group"> <label asp-for="Body" class="control-label"></label> <textarea asp-for="Body" class="form-control"></textarea> <span asp-validation-for="Body" class="text-danger"></span> </div> <div class="form-group"> <input type="submit" value="Save" class="btn btn-primary" /> </div> </form> @section Scripts { @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} }
Добавление действий контроллера
Откройте ./Controllers/CalendarController.cs
CalendarController
и добавьте в класс следующее действие, чтобы отрисовывать новую форму события.// Minimum permission scope needed for this view [AuthorizeForScopes(Scopes = new[] { "Calendars.ReadWrite" })] public IActionResult New() { return View(); }
Добавьте следующее
CalendarController
действие в класс, чтобы получить новое событие из формы, когда пользователь щелкает Сохранить и использовать Microsoft Graph, чтобы добавить событие в календарь пользователя.[HttpPost] [ValidateAntiForgeryToken] [AuthorizeForScopes(Scopes = new[] { "Calendars.ReadWrite" })] public async Task<IActionResult> New([Bind("Subject,Attendees,Start,End,Body")] NewEvent newEvent) { var timeZone = User.GetUserGraphTimeZone(); // Create a Graph event with the required fields var graphEvent = new Event { Subject = newEvent.Subject, Start = new DateTimeTimeZone { DateTime = newEvent.Start.ToString("o"), // Use the user's time zone TimeZone = timeZone }, End = new DateTimeTimeZone { DateTime = newEvent.End.ToString("o"), // Use the user's time zone TimeZone = timeZone } }; // Add body if present if (!string.IsNullOrEmpty(newEvent.Body)) { graphEvent.Body = new ItemBody { ContentType = BodyType.Text, Content = newEvent.Body }; } // Add attendees if present if (!string.IsNullOrEmpty(newEvent.Attendees)) { var attendees = newEvent.Attendees.Split(';', StringSplitOptions.RemoveEmptyEntries); if (attendees.Length > 0) { var attendeeList = new List<Attendee>(); foreach (var attendee in attendees) { attendeeList.Add(new Attendee{ EmailAddress = new EmailAddress { Address = attendee }, Type = AttendeeType.Required }); } graphEvent.Attendees = attendeeList; } } try { // Add the event await _graphClient.Me.Events .Request() .AddAsync(graphEvent); // Redirect to the calendar view with a success message return RedirectToAction("Index").WithSuccess("Event created"); } catch (ServiceException ex) { // Redirect to the calendar view with an error message return RedirectToAction("Index") .WithError("Error creating event", ex.Error.Message); } }
Запустите приложение, войдите и нажмите ссылку Календарь . Нажмите кнопку "Новое событие ", заполните форму и нажмите кнопку Сохранить.
Поздравляем!
Вы завершили учебный ASP.NET Core microsoft Graph. Теперь, когда у вас есть рабочее приложение, которое вызывает Microsoft Graph, вы можете экспериментировать и добавлять новые функции. В обзоре microsoft Graph все данные, к которые можно получить доступ с помощью Microsoft Graph.
Отзывы
Обратите внимание на этот учебник в репозитории GitHub.
Возникла проблема с этим разделом? Если это так, отправьте нам отзыв, чтобы мы исправили этот раздел.