使用 Microsoft Graph 生成 ASP.NET Core MVC 应用
本教程指导你构建一个 ASP.NET Core Web 应用,该应用使用 Microsoft Graph API 检索用户的日历信息。
提示
如果只想下载已完成的教程,可以下载或克隆GitHub存储库。 有关使用应用 ID 和密码配置应用的说明,请参阅演示文件夹中的自述文件。
先决条件
在开始本教程之前,应在开发计算机上安装 .NET Core SDK 。 如果没有 SDK,请访问上一链接,查看下载选项。
您还应该有一个在 Outlook.com 上拥有邮箱的个人 Microsoft 帐户,或者一个 Microsoft 工作或学校帐户。 如果你没有 Microsoft 帐户,则有几个选项可以获取免费帐户:
- 你可以 注册新的个人 Microsoft 帐户。
- 你可以注册开发人员计划Microsoft 365免费订阅Office 365订阅。
备注
本教程是使用 .NET Core SDK 版本 5.0.102 编写的。 本指南中的步骤可能与其他版本一起运行,但该版本尚未经过测试。
反馈
Please provide any feedback on this tutorial in the GitHub repository.
创建 ASP.NET Core MVC web 应用程序
首先创建一个 ASP.NET Core Web 应用。
在要创建项目的 (CLI) 打开命令行接口。 运行以下命令:
dotnet new mvc -o GraphTutorial
创建项目后,验证其是否正常工作,方法为将当前目录更改为 GraphTu一l 目录,并运行 CLI 中的以下命令。
dotnet run
打开浏览器并浏览到
https://localhost:5001
。 如果一切正常,则应该会看到默认 ASP.NET Core页面。
重要
如果收到 localhost 证书不受信任的警告,可以使用 .NET Core CLI 安装和信任开发证书。 有关特定操作系统的说明,请参阅 ASP.NET Core中的强制 HTTPS。
添加 Nuget 程序包
在继续之前,请安装一些NuGet程序包,你稍后会使用它。
- 用于请求和管理访问令牌的 Microsoft.Identity.Web。
- 用于通过依赖关系注入添加 Microsoft Graph SDK 的 Microsoft.Identity.Web.MicrosoftGraph。
- 用于登录和注销 UI 的 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
设计应用
在此部分中,你将创建应用程序的基本 UI 结构。
实现警报扩展方法
在此部分中,你将为控制器视图 IActionResult
返回的类型创建扩展方法。 此扩展将允许向视图传递临时错误或成功消息。
提示
可以使用任何文本编辑器编辑本教程的源文件。 但是,Visual Studio Code提供其他功能,如调试和 Intellisense。
在 GraphTu一l 目录中新建一个名为 Alerts 的目录。
在 ./Alerts 目录中创建一个名为 WithAlertResult.cs 的新文件并添加以下代码。
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); } } }
在 ./Alerts 目录中创建一个名为 AlertExtensions.cs 的新文件,并添加以下代码。
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); } } }
实现用户数据扩展方法
在此部分中,你将为 Microsoft Identity 平台 ClaimsPrincipal
生成的对象创建扩展方法。 这将允许你使用 Microsoft Graph 数据扩展现有用户Graph。
备注
目前,此代码只是一个占位符,将在稍后部分完成。
在 GraphTu一 l 目录中新建一个名为 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"; } } }
创建视图
在此部分中,你将实现应用程序的"百万"视图。
在 ./Views/Shared 目录中添加一个名为 _LoginPartial.cshtml 的新文件,并添加以下代码。
@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>
在 ./Views/Shared 目录中添加一个名为 _AlertPartial.cshtml 的新文件,并添加以下代码。
@{ 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 中没有照片时,此图像将用作用户Graph。
提示
你可以从用户下载这些屏幕截图中使用的GitHub。
保存所有更改并重新启动服务器 (
dotnet run
) 。 现在,应用看起来应该非常不同。
在门户中注册该应用
在此练习中,你将使用 Azure AD 管理中心创建新的 Azure Active Directory Web 应用程序注册。
打开浏览器,并转到 Azure Active Directory 管理中心。 使用 个人帐户(亦称为“Microsoft 帐户”)或 工作或学校帐户 登录。
选择左侧导航栏中的“Azure Active Directory”,再选择“管理”下的“应用注册”。
选择“新注册”。 在“注册应用”页上,按如下方式设置值。
- 将“名称”设置为“
ASP.NET Core Graph Tutorial
”。 - 将“受支持的帐户类型”设置为“任何组织目录中的帐户和个人 Microsoft 帐户”。
- 在“重定向 URI”下,将第一个下拉列表设置为“
Web
”,并将值设置为“https://localhost:5001/
”。
- 将“名称”设置为“
选择“注册”。 在 ASP.NET Core Graph 教程"页上,复制"应用程序 (客户端) ID"的值并保存它,下一步中将需要该值。
选择“管理”下的“身份验证”。 在 "重定向 URI" 下,添加值为 的 URI
https://localhost:5001/signin-oidc
。将 "注销 URL"设置为
https://localhost:5001/signout-oidc
。找到“隐式授予”部分,并启用“ID 令牌”。 选择“保存”。
选择“管理”下的“证书和密码”。 选择“新客户端密码”按钮。 在“说明”中输入值,并选择“过期”下的一个选项,再选择“添加”。
离开此页前,先复制客户端密码值。 将在下一步中用到它。
重要
此客户端密码不会再次显示,所以请务必现在就复制它。
添加 Azure AD 身份验证
在此练习中,你将扩展上一练习中的应用程序,以支持使用 Azure AD。 需要获得所需的 OAuth 访问令牌才能调用 Microsoft Graph API。 在此步骤中,您将配置 Microsoft.Identity.Web 库。
重要
为了避免在源中存储应用程序 ID 和密码,你将使用 .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": "*" }
在 GraphTu一l.csproj
YOUR_APP_ID
所在的目录中打开 CLI,然后运行以下命令,用 AzureYOUR_APP_SECRET
门户中的应用程序 ID 和应用程序密码进行代用。dotnet user-secrets init dotnet user-secrets set "AzureAd:ClientId" "YOUR_APP_ID" dotnet user-secrets set "AzureAd:ClientSecret" "YOUR_APP_SECRET"
实施登录
首先,将 Microsoft Identity 平台服务添加到应用程序。
在 ./Graph 目录中创建一个名为 GraphConstants.cs 的新 文件,并添加以下代码。
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); } } }
保存更改并开始项目。 使用 Microsoft 帐户登录。
检查同意提示。 权限列表对应于在 ./Graph/GraphConstants.cs 中配置的权限范围列表。
- 保持对已授予 其访问权限的数据的访问权限:
offline_access
() MSAL 请求获取此权限,以便检索刷新令牌。 - 登录并读取个人资料 : (
User.Read
) 此权限允许应用获取登录用户的个人资料和个人资料照片。 - 读取邮箱设置: (
MailboxSettings.Read
) 此权限允许应用读取用户的邮箱设置,包括时区和时间格式。 - 具有对
Calendars.ReadWrite
日历的完全访问权限: () 此权限允许应用读取用户日历上的事件、添加新事件和修改现有事件。
有关同意详细信息,请参阅了解Azure AD许可体验。
- 保持对已授予 其访问权限的数据的访问权限:
同意请求的权限。 浏览器将重新定向到应用程序,显示令牌。
获取用户详细信息
登录用户后,可以从 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 类的所有 引用。保存更改、启动应用并完成登录过程。 你最终应返回主页,但 UI 应更改以指示你已登录。
单击右上角的用户头像以访问 "注销" 链接。 单击 "注销 "将重置会话,并返回到主页。
提示
如果在主页上未看到用户名,并且"使用头像"下拉列表在进行更改后缺少名称和电子邮件,请注销并重新登录。
存储和刷新令牌
此时,应用程序具有访问令牌,该令牌在 Authorization
API 调用标头中发送。 该令牌允许应用程序代表用户访问 Microsoft Graph。
但是,此令牌期很短。 令牌在颁发后一小时过期。 此时刷新令牌将变得有效。 刷新令牌允许应用程序请求新的访问令牌,而无需用户再次登录。
由于应用使用的是 Microsoft.Identity.Web 库,因此不需要实现任何令牌存储或刷新逻辑。
应用使用内存中令牌缓存,这足以供应用在重新启动时无需保留令牌使用。 生产应用可能会改为使用 Microsoft.Identity.Web 库中 的分布式缓存选项。
方法 GetAccessTokenForUserAsync
将处理令牌过期并刷新。 它首先检查缓存的令牌,如果令牌未过期,则返回它。 如果已过期,它将使用缓存的刷新令牌获取新的刷新令牌。
控制器通过依赖关系注入获取的 GraphServiceClient 将使用 GetAccessTokenForUserAsync
你使用的身份验证提供程序进行预配置。
获取日历视图
在此部分中,你将 microsoft Graph集成到应用程序中。 对于此应用程序,你将使用适用于 .NET 的 Microsoft Graph 客户端库调用 Microsoft Graph。
从 Outlook 获取日历事件
首先为日历视图创建新的控制器。
在 ./Controllers 目录中添加名为 CalendarController.cs 的新文件,并添加以下代码。
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
执行哪些功能。将以下函数添加到 类
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" }; } }
启动应用、登录,然后单击导航 栏中 的"日历"链接。 如果一切正常,应在用户日历上看到事件被 JSON 卸载。
显示结果
现在可以添加一个视图,以对用户更加友好的方式显示结果。
创建视图模型
在 ./Models 目录中创建一个名为 CalendarViewEvent.cs 的新文件并添加以下代码。
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); } } }
在 ./Models 目录中创建一个名为 DailyViewModel.cs 的新文件,并添加以下代码。
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; } } }
在 ./Models 目录中创建一个名为 CalendarViewModel.cs 的新文件,并添加以下代码。
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)))); } } }
创建视图
在 ./Views 目录中新建一个名为 Calendar 的目录。
在 ./Views/Calendar 目录中创建一个名为 _DailyEventsPartial.cshtml 的新文件并添加以下代码。
@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> }
在 ./Views/Calendar 目录中创建一个名为 Index.cshtml 的新文件并添加以下代码。
@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); } }
启动应用、登录,然后单击" 日历" 链接。 现在,应用应呈现事件表。
创建新事件
在此部分中,您将添加在用户日历上创建事件的能力。
创建模型
在 ./Models 目录中创建一个名为 NewEvent.cs 的新文件并添加以下代码。
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; } } }
创建视图
在 ./Views/Calendar 目录中创建一个名为 New.cshtml 的新文件,并添加以下代码。
@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); } }
启动应用、登录,然后单击" 日历" 链接。 单击" 新建事件 "按钮,填写表单,然后单击"保存 "。
恭喜!
已完成 Microsoft ASP.NET Core Graph教程。 现在,你已经拥有一个调用 Microsoft Graph,你可以试验和添加新功能。 请访问 Microsoft Graph概述,查看可以使用 Microsoft Graph 访问的所有数据。
反馈
Please provide any feedback on this tutorial in the GitHub repository.
你有关于此部分的问题? 如果有,请向我们提供反馈,以便我们对此部分作出改进。