使用 Microsoft Graph 构建 Blazor WebAssembly Graph
本教程指导你如何构建客户端 Blazor WebAssembly 应用,该应用使用 Microsoft Graph API 检索用户的日历信息。
提示
如果只想下载已完成的教程,可以下载或克隆GitHub存储库。 有关使用应用 ID 和密码配置应用的说明,请参阅演示文件夹中的自述文件。
先决条件
在开始本教程之前,应在开发计算机上安装 .NET SDK 。 如果没有 SDK,请访问上一链接,查看下载选项。
您还应该有一个在 Outlook.com 上拥有邮箱的个人 Microsoft 帐户,或者一个 Microsoft 工作或学校帐户。 如果你没有 Microsoft 帐户,则有几个选项可以获取免费帐户:
- 你可以 注册新的个人 Microsoft 帐户。
- 你可以注册开发人员计划Microsoft 365免费订阅Microsoft 365订阅。
备注
本教程是使用 .NET SDK 版本 5.0.302 编写的。 本指南中的步骤可能与其他版本一起运行,但该版本尚未经过测试。
反馈
Please provide any feedback on this tutorial in the GitHub repository.
创建 Blazor WebAssembly 应用程序
首先创建一个 Blazor WebAssembly 应用程序。
在要创建项目的 (CLI) 打开命令行接口。 运行以下命令:
dotnet new blazorwasm --auth SingleOrg -o GraphTutorial
参数
--auth SingleOrg
使生成的项目包含用于对项目进行身份验证Microsoft 标识平台。创建项目后,验证其是否正常工作,方法为将当前目录更改为 GraphTu一l 目录,并运行 CLI 中的以下命令。
dotnet watch run
打开浏览器并浏览到
https://localhost:5001
。 如果一切正常,你应该会看到"Hello, world!" 消息。
重要
如果收到 localhost 证书不受信任的警告,可以使用 .NET Core CLI 安装和信任开发证书。 有关特定操作系统的说明,请参阅 ASP.NET Core中的强制 HTTPS。
添加 Nuget 程序包
在继续之前,请安装一些NuGet程序包,你稍后会使用它。
- Microsoft.Graph 用来呼叫 Microsoft Graph。
- TimeZoneConverter,用于Windows时区标识符转换为 IANA 标识符。
在 CLI 中运行以下命令以安装依赖项。
dotnet add package Microsoft.Graph --version 4.1.0 dotnet add package TimeZoneConverter
设计应用
在此部分中,你将创建应用程序的基本 UI 结构。
删除模板生成的示例页面。 删除以下文件。
- ./Pages/Counter.kit
- ./Pages/FetchData.fetch
- ./Shared/SurveyPrompt.作为
- ./wwwroot/sample-data/weather.json
打开 "./wwwroot/index.html ",在结束标记前 添加 以下
</body>
代码。<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js" integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV" crossorigin="anonymous"></script>
这将添加 Bootstrap javascript 文件。
打开 ./wwwroot/css/app.css 并添加以下代码。
.nav-profile-photo { width: 36px; }
打开 ./Shared/NavMenu.nav ,并将其内容替换为以下内容。
<div class="top-row pl-4 navbar navbar-dark"> <a class="navbar-brand" href="">GraphTutorial</a> <button class="navbar-toggler" @onclick="ToggleNavMenu"> <span class="navbar-toggler-icon"></span> </button> </div> <div class="@NavMenuCssClass" @onclick="ToggleNavMenu"> <ul class="nav flex-column"> <li class="nav-item px-3"> <NavLink class="nav-link" href="" Match="NavLinkMatch.All"> <span class="oi oi-home" aria-hidden="true"></span> Home </NavLink> </li> <li class="nav-item px-3"> <NavLink class="nav-link" href="calendar"> <span class="oi oi-calendar" aria-hidden="true"></span> Calendar </NavLink> </li> </ul> </div> @code { private bool collapseNavMenu = true; private string NavMenuCssClass => collapseNavMenu ? "collapse" : null; private void ToggleNavMenu() { collapseNavMenu = !collapseNavMenu; } }
打开 "./Pages/Index.its" ,并将其内容替换为以下内容。
@page "/" <div class="jumbotron"> <h1>Blazor Client-side Graph Tutorial</h1> <p class="lead">This sample app shows how to use the Microsoft Graph API to access a user's data from a Blazor client-side app</p> <AuthorizeView> <Authorized> <h4>Welcome @context.User.Identity.Name!</h4> <p>Use the navigation bar on the left to get started.</p> </Authorized> <NotAuthorized> <a class="btn btn-primary btn-large" href="authentication/login">Click here to sign in</a> </NotAuthorized> </AuthorizeView> </div>
打开 "./Shared/LoginDisplay.设置 ",并将其内容替换为以下内容。
@using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.WebAssembly.Authentication @inject NavigationManager Navigation @inject SignOutSessionStateManager SignOutManager <AuthorizeView> <Authorized> <a class="text-decoration-none" data-toggle="dropdown" href="#" role="button"> <img src="/img/no-profile-photo.png" 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">@context.User.Identity.Name</h5> <p class="dropdown-item-text text-muted mb-0">placeholder@contoso.com</p> <div class="dropdown-divider"></div> <button class="dropdown-item" @onclick="BeginLogout">Log out</button> </div> </Authorized> <NotAuthorized> <a href="authentication/login">Log in</a> </NotAuthorized> </AuthorizeView> @code{ private async Task BeginLogout(MouseEventArgs args) { await SignOutManager.SetSignOutState(); Navigation.NavigateTo("authentication/logout"); } }
在 . /wwwroot 目录中新建一个名为 img 的目录。 在此目录中添加你选择的名为 no-profile-photo.png 的图像文件。 当用户在 Microsoft Graph 中没有照片时,此图像将用作用户Graph。
提示
你可以从用户下载这些屏幕截图中使用的GitHub。
保存所有更改并刷新页面。
在门户中注册该应用
在此练习中,你将使用 Azure AD 管理中心创建新的 Azure Active Directory Web 应用程序注册。
打开浏览器,并转到 Azure Active Directory 管理中心。 使用 个人帐户(亦称为“Microsoft 帐户”)或 工作或学校帐户 登录。
选择左侧导航栏中的“Azure Active Directory”,再选择“管理”下的“应用注册”。
选择“新注册”。 在“注册应用”页上,按如下方式设置值。
- 将“名称”设置为“
Blazor Graph Tutorial
”。 - 将“受支持的帐户类型”设置为“任何组织目录中的帐户和个人 Microsoft 帐户”。
- 在 "重定向 URI"下,将第一个下拉列表设置为"SPA (单页 ) ,将 值设置为
https://localhost:5001/authentication/login-callback
。
- 将“名称”设置为“
选择“注册”。 在 "Blazor Graph 教程"页上,复制 Application (客户端) ID 的值并 保存它,下一步中将需要该值。
添加 Azure AD 身份验证
在此练习中,你将扩展上一练习中的应用程序,以支持使用 Azure AD。 需要获得所需的 OAuth 访问令牌才能调用 Microsoft Graph API。
打开 ./wwwroot/appsettings.json。 添加属性
GraphScopes
并更新Authority
和ClientId
值,以匹配以下内容。{ "AzureAd": { "Authority": "https://login.microsoftonline.com/common", "ClientId": "YOUR_APP_ID_HERE", "ValidateAuthority": true }, "GraphScopes": "User.Read;MailboxSettings.Read;Calendars.ReadWrite" }
将
YOUR_APP_ID_HERE
替换为应用注册中的应用程序 ID。重要
如果你使用的是源代码管理(如 git),那么现在应该从源代码管理 中排除 appsettings.json 文件,以避免意外泄露应用 ID。
查看值中包含的
GraphScopes
范围。- User.Read 允许应用程序获取用户的个人资料和照片。
- MailboxSettings.Read 允许应用程序获取邮箱设置,其中包括用户的首选时区。
- Calendars.ReadWrite 允许应用程序读取和写入用户的日历。
实施登录
此时,.NET Core 项目模板已添加代码以启用登录。 但是,在此部分中,你将添加其他代码,以通过将 Microsoft Graph添加到用户标识来改进体验。
打开 "./Pages/Authentication.its" ,并将其内容替换为以下内容。
@page "/authentication/{action}" @using Microsoft.AspNetCore.Components.WebAssembly.Authentication <RemoteAuthenticatorView Action="@Action" LogInFailed="LogInFailedFragment" /> @code{ [Parameter] public string Action { get; set; } private static RenderFragment LogInFailedFragment(string message) { return builder => { builder.OpenElement(0, "div"); builder.AddAttribute(1, "class", "alert alert-danger"); builder.AddAttribute(2, "role", "alert"); builder.OpenElement(3, "p"); builder.AddContent(4, "There was an error trying to log you in."); builder.CloseElement(); if (!string.IsNullOrEmpty(message)) { builder.OpenElement(5, "p"); builder.AddContent(6, $"Error: {message}"); builder.CloseElement(); } builder.CloseElement(); }; } }
当登录无法显示身份验证过程返回的任何错误消息时,这将替换默认错误消息。
在名为 Graph 的项目的 根目录中Graph。
在名为 GraphUserAccountFactory.cs 的 ./Graph 目录中创建新文件并添加以下代码。
using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Components.WebAssembly.Authentication; using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal; using Microsoft.Extensions.Logging; using Microsoft.Graph; namespace GraphTutorial.Graph { // Extends the AccountClaimsPrincipalFactory that builds // a user identity from the identity token. // This class adds additional claims to the user's ClaimPrincipal // that hold values from Microsoft Graph public class GraphUserAccountFactory : AccountClaimsPrincipalFactory<RemoteUserAccount> { private readonly IAccessTokenProviderAccessor accessor; private readonly ILogger<GraphUserAccountFactory> logger; public GraphUserAccountFactory(IAccessTokenProviderAccessor accessor, ILogger<GraphUserAccountFactory> logger) : base(accessor) { this.accessor = accessor; this.logger = logger; } public async override ValueTask<ClaimsPrincipal> CreateUserAsync( RemoteUserAccount account, RemoteAuthenticationUserOptions options) { // Create the base user var initialUser = await base.CreateUserAsync(account, options); // If authenticated, we can call Microsoft Graph if (initialUser.Identity.IsAuthenticated) { try { // Add additional info from Graph to the identity await AddGraphInfoToClaims(accessor, initialUser); } catch (AccessTokenNotAvailableException exception) { logger.LogError($"Graph API access token failure: {exception.Message}"); } catch (ServiceException exception) { logger.LogError($"Graph API error: {exception.Message}"); logger.LogError($"Response body: {exception.RawResponseBody}"); } } return initialUser; } private async Task AddGraphInfoToClaims( IAccessTokenProviderAccessor accessor, ClaimsPrincipal claimsPrincipal) { // TEMPORARY: Get the token and log it var result = await accessor.TokenProvider.RequestAccessToken(); if (result.TryGetToken(out var token)) { logger.LogInformation($"Access token: {token.Value}"); } } } }
此类扩展 AccountClaimsPrincipalFactory 类并重写
CreateUserAsync
方法。 目前,此方法仅记录用于调试的访问令牌。 你将在此练习的Graph Microsoft 调用。打开 ./Program.cs ,在
using
文件顶部添加以下语句。using Microsoft.AspNetCore.Components.WebAssembly.Authentication; using GraphTutorial.Graph;
在
Main
中,将现有builder.Services.AddMsalAuthentication
调用替换为以下内容。builder.Services.AddMsalAuthentication<RemoteAuthenticationState, RemoteUserAccount>(options => { var scopes = builder.Configuration.GetValue<string>("GraphScopes"); if (string.IsNullOrEmpty(scopes)) { Console.WriteLine("WARNING: No permission scopes were found in the GraphScopes app setting. Using default User.Read."); scopes = "User.Read"; } foreach(var scope in scopes.Split(';')) { Console.WriteLine($"Adding {scope} to requested permissions"); options.ProviderOptions.DefaultAccessTokenScopes.Add(scope); } builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication); }) .AddAccountClaimsPrincipalFactory<RemoteAuthenticationState, RemoteUserAccount, GraphUserAccountFactory>();
考虑此代码执行哪些功能。
- 它从
GraphScopes
appsettings.json 加载 的值,并将每个范围添加到 MSAL 提供程序使用的默认作用域。 - 它将现有帐户工厂替换为 GraphUserAccountFactory 类。
- 它从
保存更改并重新启动该应用。 使用 "登录" 链接进行登录。 查看并接受请求的权限。
应用刷新时显示欢迎消息。 访问浏览器的开发人员工具并查看"控制台 " 选项卡。应用程序记录访问令牌。
获取用户详细信息
登录用户后,可以从 Microsoft Graph 获取其信息。 在此部分中,你将使用 Microsoft Graph向用户的 ClaimsPrincipal 添加其他声明。
在名为 GraphClaimsPrincipalExtensions.cs 的 ./Graph 目录中创建新文件并添加以下代码。
using Microsoft.Graph; using System; using System.IO; using System.Security.Claims; namespace GraphTutorial { public static class GraphClaimTypes { public const string DateFormat = "graph_dateformat"; 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 GetUserGraphDateFormat(this ClaimsPrincipal claimsPrincipal) { var claim = claimsPrincipal.FindFirst(GraphClaimTypes.DateFormat); return claim == null ? null : claim.Value; } public static string GetUserGraphEmail(this ClaimsPrincipal claimsPrincipal) { var claim = claimsPrincipal.FindFirst(GraphClaimTypes.Email); return claim == null ? null : claim.Value; } public static string GetUserGraphPhoto(this ClaimsPrincipal claimsPrincipal) { var claim = claimsPrincipal.FindFirst(GraphClaimTypes.Photo); return claim == null ? null : claim.Value; } public static string GetUserGraphTimeZone(this ClaimsPrincipal claimsPrincipal) { var claim = claimsPrincipal.FindFirst(GraphClaimTypes.TimeZone); return claim == null ? null : claim.Value; } public static string GetUserGraphTimeFormat(this ClaimsPrincipal claimsPrincipal) { var claim = claimsPrincipal.FindFirst(GraphClaimTypes.TimeFormat); return claim == null ? null : claim.Value; } // Adds claims from the provided User object public static void AddUserGraphInfo(this ClaimsPrincipal claimsPrincipal, User user) { var identity = claimsPrincipal.Identity as ClaimsIdentity; identity.AddClaim( new Claim(GraphClaimTypes.DateFormat, user.MailboxSettings.DateFormat)); 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)); } // Converts a photo Stream to a Data URI and stores it in a claim public static void AddUserGraphPhoto(this ClaimsPrincipal claimsPrincipal, Stream photoStream) { var identity = claimsPrincipal.Identity as ClaimsIdentity; if (photoStream != null) { // 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 photoUri = $"data:image/png;base64,{Convert.ToBase64String(photoBytes)}"; identity.AddClaim( new Claim(GraphClaimTypes.Photo, photoUri)); } } } }
此代码实现 ClaimsPrincipal 类的扩展方法,允许您使用 Microsoft Graph 对象获取和设置声明。
在名为 BlazorAuthProvider.cs 的 ./Graph 目录中创建新文件并添加以下代码。
using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal; using Microsoft.Graph; namespace GraphTutorial.Graph { public class BlazorAuthProvider : IAuthenticationProvider { private readonly IAccessTokenProviderAccessor accessor; public BlazorAuthProvider(IAccessTokenProviderAccessor accessor) { this.accessor = accessor; } // Function called every time the GraphServiceClient makes a call public async Task AuthenticateRequestAsync(HttpRequestMessage request) { // Request the token from the accessor var result = await accessor.TokenProvider.RequestAccessToken(); if (result.TryGetToken(out var token)) { // Add the token to the Authorization header request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Value); } } } }
此代码实现 Microsoft Graph SDK 的身份验证提供程序,该 SDK 使用 Microsoft.AspNetCore.Components.WebAssembly.Authentication 包提供的 IAccessTokenProviderAccessor 获取访问令牌。
在名为 GraphClientFactory.cs 的 ./Graph 目录中创建新文件并添加以下代码。
using System.Net.Http; using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal; using Microsoft.Extensions.Logging; using Microsoft.Graph; namespace GraphTutorial.Graph { public class GraphClientFactory { private readonly IAccessTokenProviderAccessor accessor; private readonly HttpClient httpClient; private readonly ILogger<GraphClientFactory> logger; private GraphServiceClient graphClient; public GraphClientFactory(IAccessTokenProviderAccessor accessor, HttpClient httpClient, ILogger<GraphClientFactory> logger) { this.accessor = accessor; this.httpClient = httpClient; this.logger = logger; } public GraphServiceClient GetAuthenticatedClient() { // Use the existing one if it's there if (graphClient == null) { // Create a GraphServiceClient using a scoped // HttpClient graphClient = new GraphServiceClient(httpClient); // Configure the auth provider graphClient.AuthenticationProvider = new BlazorAuthProvider(accessor); } return graphClient; } } }
此类创建使用 BlazorAuthProvider 配置的 GraphServiceClient。
打开 ./Program.cs,将新 HttpClient 的 BaseAddress 更改为
"https://graph.microsoft.com"
。// HttpClient for passing into GraphServiceClient constructor builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri("https://graph.microsoft.com") });
在 行前添加以下
await builder.Build().RunAsync();
代码。builder.Services.AddScoped<GraphClientFactory>();
这会将 GraphClientFactory 添加为作用域服务,我们可以通过依赖关系注入提供此服务。
打开 ./Graph/GraphUserAccountFactory.cs,将以下属性添加到 类。
private readonly GraphClientFactory clientFactory;
更新构造函数以使用 GraphClientFactory 参数并将其分配给
clientFactory
属性。public GraphUserAccountFactory(IAccessTokenProviderAccessor accessor, GraphClientFactory clientFactory, ILogger<GraphUserAccountFactory> logger) : base(accessor) { this.accessor = accessor; this.clientFactory = clientFactory; this.logger = logger; }
将现有的
AddGraphInfoToClaims
函数替换为以下内容。private async Task AddGraphInfoToClaims( IAccessTokenProviderAccessor accessor, ClaimsPrincipal claimsPrincipal) { var graphClient = clientFactory.GetAuthenticatedClient(); // Get user profile including mailbox settings // GET /me?$select=displayName,mail,mailboxSettings,userPrincipalName var user = await graphClient.Me .Request() // Request only the properties used to // set claims .Select(u => new { u.DisplayName, u.Mail, u.MailboxSettings, u.UserPrincipalName }) .GetAsync(); logger.LogInformation($"Got user: {user.DisplayName}"); claimsPrincipal.AddUserGraphInfo(user); // Get user's photo // GET /me/photos/48x48/$value var photo = await graphClient.Me .Photos["48x48"] // Smallest standard size .Content .Request() .GetAsync(); claimsPrincipal.AddUserGraphPhoto(photo); }
考虑此代码执行哪些功能。
- 它获取用户的配置文件。
- 它用于
Select
限制返回的属性。
- 它用于
- 它获取用户的照片。
- 它专门请求 48x48 像素版本的用户照片。
- 它将信息添加到 ClaimsPrincipal。
- 它获取用户的配置文件。
打开 "./Shared/LoginDisplay.设置 ",然后进行以下更改。
- 将
/img/no-profile-photo.png
替换为@(context.User.GetUserGraphPhoto() ?? "/img/no-profile-photo.png")
。 - 将
placeholder@contoso.com
替换为@context.User.GetUserGraphEmail()
。
... <img src="@(context.User.GetUserGraphPhoto() ?? "/img/no-profile-photo.png")" class="nav-profile-photo rounded-circle align-self-center mr-2"> ... <p class="dropdown-item-text text-muted mb-0">@context.User.GetUserGraphEmail()</p> ...
- 将
保存全部更改并重新启动该应用程序。 登录到应用。 应用将更新,以在顶部菜单中显示用户的照片。 选择用户的照片将打开一个下拉菜单,该下拉菜单包含用户名、电子邮件地址和 "注销" 按钮。
获取日历视图
在此部分中,你将进一步将 Microsoft Graph合并到应用程序中,以便查看当前星期的用户日历。
获取日历视图
在 ./Pages 目录中创建一个名为 Calendar.如果为 calendar.如果为 .****/Pages 的新文件,请添加以下代码。
@page "/calendar" @using Microsoft.Graph @using TimeZoneConverter @inject GraphTutorial.Graph.GraphClientFactory clientFactory <AuthorizeView> <Authorized> <!-- Temporary JSON dump of events --> <code>@graphClient.HttpProvider.Serializer.SerializeObject(events)</code> </Authorized> <NotAuthorized> <RedirectToLogin /> </NotAuthorized> </AuthorizeView> @code{ [CascadingParameter] private Task<AuthenticationState> authenticationStateTask { get; set; } private GraphServiceClient graphClient; private IList<Event> events = new List<Event>(); private string dateTimeFormat; }
在 部分中添加以下
@code{}
代码。protected override async Task OnInitializedAsync() { // Get the user var user = (await authenticationStateTask).User; var graphTimeZone = user.GetUserGraphTimeZone(); dateTimeFormat = $"{user.GetUserGraphDateFormat()} {user.GetUserGraphTimeFormat()}"; // Calculate the start and end of the current week in user's time zone var startOfWeek = GetUtcStartOfWeekInTimeZone(DateTime.Today, graphTimeZone); var endOfWeek = startOfWeek.AddDays(7); graphClient = clientFactory.GetAuthenticatedClient(); // Specifies the start and end of the view on the calendar // Translates to: ?startDateTime=""&endDateTime="" var viewOptions = new List<QueryOption> { new QueryOption("startDateTime", startOfWeek.ToString("o")), new QueryOption("endDateTime", endOfWeek.ToString("o")) }; var eventPage = 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=\"{graphTimeZone}\"") // 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(); events = eventPage.CurrentPage; } private DateTime GetUtcStartOfWeekInTimeZone(DateTime today, string timeZoneId) { // Time zone returned by Graph could be Windows or IANA style // TimeZoneConverter can take either TimeZoneInfo userTimeZone = TZConvert.GetTimeZoneInfo(timeZoneId); // 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, userTimeZone); }
考虑此代码执行哪些功能。
- 它从添加到用户的自定义声明获取当前用户的时区、日期格式和时间格式。
- 它计算用户的首选时区中本周的开始时间和结束时间。
- 它从 Microsoft Graph获取当前星期的日历视图。
- 它包含标头
Prefer: outlook.timezone
,使 Microsoft Graphstart
返回指定end
时区中的 和 属性。 - 它用于
Top(50)
请求响应中最多 50 个事件。 - 它使用
Select(u => new {})
仅请求应用使用的属性。 - 它使用
OrderBy("start/dateTime")
按开始时间对结果进行排序。
- 它包含标头
保存全部更改并重新启动该应用程序。 选择" 日历" 导航项。 应用显示从 Microsoft Graph 返回的事件的 JSON 表示形式。
显示结果
现在,可以将 JSON 转储替换为更用户友好的内容。
在 部分中添加以下
@code{}
函数。private string FormatIso8601DateTime(string iso8601DateTime) { // Load into a DateTime var dateTime = DateTime.Parse(iso8601DateTime); if (!string.IsNullOrWhiteSpace(dateTimeFormat)) { // Format it using the user's settings return dateTime.ToString(dateTimeFormat); } // Fallback to return original value return iso8601DateTime; }
此代码采用 ISO 8601 日期字符串,并将其转换为用户的首选日期和时间格式。
<code>
将 元素内的<Authorized>
元素替换为以下内容。<Authorized> <h1 class="mb-3">Calendar</h1> <a href="/newevent" class="btn btn-light btn-sm mb-3">New event</a> <table class="table"> <thead> <tr> <th>Organizer</th> <th>Subject</th> <th>Start</th> <th>End</th> </tr> </thead> <tbody> @foreach(var calendarEvent in events) { <tr> <td>@calendarEvent.Organizer.EmailAddress.Name</td> <td>@calendarEvent.Subject</td> <td>@FormatIso8601DateTime(calendarEvent.Start.DateTime)</td> <td>@FormatIso8601DateTime(calendarEvent.End.DateTime)</td> </tr> } </tbody> </table> </Authorized>
这将创建 Microsoft 事件中心返回的事件Graph。
保存更改并重新启动该应用。 现在 ," 日历"页呈现一个事件表。
创建新事件
在此部分中,您将添加在用户日历上创建事件的能力。
在 . /Pages 目录中新建一个名为 NewEvent.作为扩展 名的文件,并添加以下代码。
@page "/newevent" @using Microsoft.Graph @inject GraphTutorial.Graph.GraphClientFactory clientFactory <AuthorizeView> <Authorized> @if (!string.IsNullOrEmpty(status)) { <div class="alert @(isError ? "alert-danger" : "alert-success")">@status</div> } <form> <div class="form-group"> <label>Subject</label> <input @bind="subject" class="form-control" /> </div> <div class="form-group"> <label>Attendees</label> <input @bind="attendees" class="form-control" /> </div> <div class="form-row"> <div class="col"> <div class="form-group"> <label>Start</label> <input @bind="start" type="datetime" class="form-control" /> </div> </div> <div class="col"> <div class="form-group"> <label>End</label> <input @bind="end" type="datetime" class="form-control" /> </div> </div> </div> <div class="form-group"> <label>Body</label> <textarea @bind="body" class="form-control"></textarea> </div> </form> <button class="btn btn-primary mr-2" @onclick="CreateEvent">Create</button> <a href="/calendar" class="btn btn-secondrary">Cancel</a> </Authorized> <NotAuthorized> <RedirectToLogin /> </NotAuthorized> </AuthorizeView>
这会向页面添加一个窗体,以便用户可以输入新事件的值。
将以下代码添加到文件的末尾。
@code{ [CascadingParameter] private Task<AuthenticationState> authenticationStateTask { get; set; } private string status; private bool isError; private string userTimeZone; private string subject; private string attendees; private DateTime start = new DateTime(DateTime.Today.Ticks); private DateTime end = new DateTime(DateTime.Today.Ticks); private string body; protected override async Task OnInitializedAsync() { // Get the user's time zone var user = (await authenticationStateTask).User; userTimeZone = user.GetUserGraphTimeZone() ?? "UTC"; } private async Task CreateEvent() { // Initalize an Event object // with user input var newEvent = new Event { Subject = subject, Start = new DateTimeTimeZone { DateTime = start.ToString("o"), TimeZone = userTimeZone }, End = new DateTimeTimeZone { DateTime = end.ToString("o"), TimeZone = userTimeZone }, Body = new ItemBody { Content = body, ContentType = BodyType.Text } }; // If the user provided attendees (semicolon-delimited // list of email addresses) add them if (!string.IsNullOrEmpty(attendees)) { var attendeeList = new List<Attendee>(); var attendeeArray = attendees.Split(";"); foreach (var email in attendeeArray) { Console.WriteLine($"Adding {email}"); attendeeList.Add(new Attendee { // Set to required attendee Type = AttendeeType.Required, EmailAddress = new EmailAddress { Address = email } }); } newEvent.Attendees = attendeeList; } var graphClient = clientFactory.GetAuthenticatedClient(); try { // POST /me/events await graphClient.Me .Events .Request() .AddAsync(newEvent); isError = false; status = "Event created"; } catch (ServiceException exception) { isError = true; status = exception.Message; } } }
考虑此代码执行哪些功能。
- In
OnInitializedAsync
it gets the authenticated user's time zone. - In
CreateEvent
it initializes a new Event object using the values from the form. - 它使用 Graph SDK 将事件添加到用户的日历。
- In
保存全部更改并重新启动该应用程序。 在" 日历" 页上,选择" 新建事件"。 填写表单并选择"创建 "。
恭喜!
已完成 Blazor WebAssembly Microsoft Graph教程。 现在,你已经拥有一个调用 Microsoft Graph,可以试验并添加新功能。 请访问 Microsoft Graph概述,查看可以使用 Microsoft Graph 访问的所有数据。
反馈
Please provide any feedback on this tutorial in the GitHub repository.
你有关于此部分的问题? 如果有,请向我们提供反馈,以便我们对此部分作出改进。