Criar aplicativos do ASP.NET Core MVC com o Microsoft Graph
Este tutorial ensina como criar um aplicativo web ASP.NET Core que usa a API do Microsoft Graph para recuperar informações de calendário para um usuário.
Dica
Se você preferir apenas baixar o tutorial concluído, poderá baixar ou clonar o GitHub repositório. Consulte o arquivo README na pasta de demonstração para obter instruções sobre como configurar o aplicativo com uma ID do aplicativo e segredo.
Pré-requisitos
Antes de iniciar este tutorial, você deve ter o SDK do .NET Core instalado em sua máquina de desenvolvimento. Se você não tiver o SDK, visite o link anterior para opções de download.
Você também deve ter uma conta pessoal da Microsoft com uma caixa de correio em Outlook.com, ou uma conta de trabalho ou de estudante da Microsoft. Se você não tiver uma conta da Microsoft, há algumas opções para obter uma conta gratuita:
- Você pode se inscrever em uma nova conta pessoal da Microsoft.
- Você pode se inscrever no programa de desenvolvedor Microsoft 365 para obter uma assinatura Office 365 gratuita.
Observação
Este tutorial foi escrito com o .NET Core SDK versão 5.0.102. As etapas neste guia podem funcionar com outras versões, mas que não foram testadas.
Comentários
Forneça qualquer comentário sobre este tutorial no repositório GitHub.
Criar um aplicativo Web do ASP.NET Core MVC
Comece criando um aplicativo ASP.NET Core Web.
Abra sua interface de linha de comando (CLI) em um diretório onde você deseja criar o projeto. Execute o seguinte comando:
dotnet new mvc -o GraphTutorial
Depois que o projeto for criado, verifique se ele funciona alterando o diretório atual para o diretório GraphTutorial e executando o seguinte comando em sua CLI.
dotnet run
Abra seu navegador e navegue até
https://localhost:5001
. Se tudo estiver funcionando, você deverá ver uma página ASP.NET Core padrão.
Importante
Se você receber um aviso de que o certificado para localhost não é confiável, você pode usar a CLI do .NET Core para instalar e confiar no certificado de desenvolvimento. Consulte Enforce HTTPS in ASP.NET Core para obter instruções para sistemas operacionais específicos.
Adicionar pacotes NuGet
Antes de continuar, instale alguns pacotes NuGet que você usará posteriormente.
- Microsoft.Identity.Web para solicitar e gerenciar tokens de acesso.
- Microsoft.Identity.Web.MicrosoftGraph para adicionar o Microsoft Graph SDK por meio da injeção de dependência.
- Microsoft.Identity.Web.UI para entrar e sair da interface do usuário.
- TimeZoneConverter para manipular identificadores de fuso horário entre plataformas.
Execute os seguintes comandos em sua CLI para instalar as dependências.
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
Design do aplicativo
Nesta seção, você criará a estrutura básica da interface do usuário do aplicativo.
Implementar métodos de extensão de alerta
Nesta seção, você criará métodos de extensão para o tipo IActionResult
retornado por exibições do controlador. Essa extensão permitirá passar mensagens de erro temporário ou de sucesso para o modo de exibição.
Dica
Você pode usar qualquer editor de texto para editar os arquivos de origem deste tutorial. No entanto, Visual Studio Code fornece recursos adicionais, como depuração e Intellisense.
Crie um novo diretório no diretório GraphTutorial chamado Alertas.
Crie um novo arquivo chamado WithAlertResult.cs no diretório ./Alerts e adicione o código a seguir.
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); } } }
Crie um novo arquivo chamado AlertExtensions.cs no diretório ./Alerts e adicione o código a seguir.
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); } } }
Implementar métodos de extensão de dados do usuário
Nesta seção, você criará métodos de extensão para o ClaimsPrincipal
objeto gerado pela plataforma Microsoft Identity. Isso permitirá que você estenda a identidade de usuário existente com dados da Microsoft Graph.
Observação
Este código é apenas um espaço reservado por enquanto, você o concluirá em uma seção posterior.
Crie um novo diretório no diretório GraphTutorial chamado Graph.
Crie um novo arquivo chamado GraphClaimsPrincipalExtensions.cs e adicione o código a seguir.
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"; } } }
Criar exibições
Nesta seção, você implementará os pontos de exibição de lâmina para o aplicativo.
Adicione um novo arquivo chamado _LoginPartial.cshtml no diretório ./Views/Shared e adicione o código a seguir.
@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>
Adicione um novo arquivo chamado _AlertPartial.cshtml no diretório ./Views/Shared e adicione o código a seguir.
@{ 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> }
Abra o arquivo ./Views/Shared/_Layout.cshtml e substitua todo o seu conteúdo pelo código a seguir para atualizar o layout global do aplicativo.
@{ 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>
Abra ./wwwroot/css/site.css e adicione o seguinte código na parte inferior do arquivo.
.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 }
Abra o arquivo ./Views/Home/index.cshtml e substitua seu conteúdo pelo seguinte.
@{ 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>
Crie um novo diretório no diretório ./wwwroot chamado img. Adicione um arquivo de imagem de sua escolha chamada no-profile-photo.png neste diretório. Essa imagem será usada como a foto do usuário quando o usuário não tiver nenhuma foto no Microsoft Graph.
Dica
Você pode baixar a imagem usada nessas capturas de tela GitHub.
Salve todas as alterações e reinicie o servidor (
dotnet run
). Agora, o aplicativo deve ter uma aparência muito diferente.
Registrar o aplicativo no portal
Neste exercício, você criará um novo registro de aplicativo Web do Azure AD usando o Azure Active Directory de administração.
Abra um navegador e navegue até o centro de administração do Azure Active Directory. Faça logon usando uma conta pessoal (também conhecida como Conta da Microsoft) ou Conta Corporativa ou de Estudante.
Selecione Azure Active Directory na navegação esquerda e selecione Registros de aplicativos em Gerenciar.
Selecione Novo registro. Na página Registrar um aplicativo, defina os valores da seguinte forma.
- Defina Nome para
ASP.NET Core Graph Tutorial
. - Defina Tipos de conta com suporte para Contas em qualquer diretório organizacional e contas pessoais da Microsoft.
- Em URI de Redirecionamento, defina o primeiro menu suspenso para
Web
e defina o valor comohttps://localhost:5001/
.
- Defina Nome para
Selecione Registrar. Na página ASP.NET Core Graph Tutorial, copie o valor da ID do Aplicativo (cliente) e salve-a, você precisará dela na próxima etapa.
Selecione Autenticação em Gerenciar. Em URIs de redirecionamento , adicione um URI com o valor
https://localhost:5001/signin-oidc
.De definir a URL de Logout como
https://localhost:5001/signout-oidc
.Localize a seção Concessão Implícita e habilite tokens de ID. Selecione Salvar.
Selecione Certificados e segredos sob Gerenciar. Selecione o botão Novo segredo do cliente. Insira um valor em Descrição e selecione uma das opções para Expira em e selecione Adicionar.
Copie o valor de segredo do cliente antes de sair desta página. Você precisará dele na próxima etapa.
Importante
Este segredo do cliente nunca é mostrado novamente, portanto, certifique-se de copiá-lo agora.
Adicionar autenticação do Azure AD
Neste exercício, você estenderá o aplicativo do exercício anterior para dar suporte à autenticação com o Azure AD. Isso é necessário para obter o token de acesso OAuth necessário para chamar a API do Microsoft Graph. Nesta etapa, você configurará a biblioteca Microsoft.Identity.Web .
Importante
Para evitar armazenar a ID do aplicativo e o segredo na origem, você usará o .NET Secret Manager para armazenar esses valores. O Gerenciador Secreto é apenas para fins de desenvolvimento, os aplicativos de produção devem usar um gerente secreto confiável para armazenar segredos.
Abra ./appsettings.json e substitua seu conteúdo pelo seguinte.
{ "AzureAd": { "Instance": "https://login.microsoftonline.com/", "TenantId": "common", "CallbackPath": "/signin-oidc" }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*" }
Abra sua CLI no diretório onde GraphTutorial.csproj está localizado e execute os comandos a seguir,
YOUR_APP_ID
substituindo com sua ID de aplicativo do portalYOUR_APP_SECRET
do Azure e com seu segredo do aplicativo.dotnet user-secrets init dotnet user-secrets set "AzureAd:ClientId" "YOUR_APP_ID" dotnet user-secrets set "AzureAd:ClientSecret" "YOUR_APP_SECRET"
Implementar login
Comece adicionando os serviços da plataforma Microsoft Identity ao aplicativo.
Crie um novo arquivo chamado GraphConstants.cs no diretório ./Graph e adicione o código a seguir.
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" }; } }
Abra o arquivo ./Startup.cs e adicione as instruções
using
a seguir à parte superior do arquivo.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;
Substitua a função
ConfigureServices
existente pela seguinte.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(); }
Na função
Configure
, adicione a seguinte linha acima daapp.UseAuthorization();
linha.app.UseAuthentication();
Abra ./Controllers/HomeController.cs e substitua seu conteúdo pelo seguinte.
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); } } }
Salvar suas alterações e iniciar o projeto. Faça logon com sua conta da Microsoft.
Examine o prompt de consentimento. A lista de permissões corresponde à lista de escopos de permissões configurados em ./Graph/GraphConstants.cs.
- Mantenha acesso aos dados aos que você lhe deu acesso: (
offline_access
) Essa permissão é solicitada pelo MSAL para recuperar tokens de atualização. - Entre e leia seu perfil: (
User.Read
) Essa permissão permite que o aplicativo receba o perfil e a foto de perfil do usuário conectado. - Leia as configurações da caixa de correio: (
MailboxSettings.Read
) Essa permissão permite que o aplicativo leia as configurações de caixa de correio do usuário, incluindo fuso horário e formato de hora. - Tenha acesso total aos seus calendários: (
Calendars.ReadWrite
) Essa permissão permite que o aplicativo leia eventos no calendário do usuário, adicione novos eventos e modifique os existentes.
Para obter mais informações sobre consentimento, consulte Understanding Azure AD application consent experiences.
- Mantenha acesso aos dados aos que você lhe deu acesso: (
Consentimento para as permissões solicitadas. O navegador redireciona para o aplicativo, mostrando o token.
Obter detalhes do usuário
Depois que o usuário estiver conectado, você pode obter suas informações no Microsoft Graph.
Abra ./Graph/GraphClaimsPrincipalExtensions.cs e substitua todo o conteúdo pelo seguinte.
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)); } } }
Abra ./Startup.cs e substitua a linha existente
.AddMicrosoftIdentityWebApp(Configuration)
pelo código a seguir.// 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); }; })
Considere o que esse código faz.
- Ele adiciona um manipulador de eventos para o
OnTokenValidated
evento.- Ele usa a
ITokenAcquisition
interface para obter um token de acesso. - Ele chama a Microsoft Graph para obter o perfil e a foto do usuário.
- Ele adiciona as Graph à identidade do usuário.
- Ele usa a
- Ele adiciona um manipulador de eventos para o
Adicione a seguinte chamada de função após a
EnableTokenAcquisitionToCallDownstreamApi
chamada e antes daAddInMemoryTokenCaches
chamada.// Add a GraphServiceClient via dependency injection .AddMicrosoftGraph(options => { options.Scopes = string.Join(' ', GraphConstants.Scopes); })
Isso disponibiliza um GraphServiceClient autenticado aos controladores por meio da injeção de dependência.
Abra ./Controllers/HomeController.cs e substitua a
Index
função pelo seguinte.public IActionResult Index() { return View(); }
Remova todas as referências à
ITokenAcquisition
classe HomeController .Salve suas alterações, inicie o aplicativo e vá pelo processo de login. Você deve terminar de volta na home page, mas a interface do usuário deve mudar para indicar que você está inscreveu.
Clique no avatar do usuário no canto superior direito para acessar o link Sair . Clicar em Sair redefine a sessão e retorna você para a home page.
Dica
Se você não vir seu nome de usuário na home page e o menu suspenso usar avatar estiver faltando nome e email após fazer essas alterações, saia e entre novamente.
Armazenar e atualizar tokens
Neste ponto, seu aplicativo tem um token de acesso, que é enviado no Authorization
header de chamadas da API. Este é o token que permite que o aplicativo acesse o Microsoft Graph em nome do usuário.
No entanto, esse token tem vida curta. O token expira uma hora após a emissão. É aqui que o token de atualização se torna útil. O token de atualização permite que o aplicativo solicite um novo token de acesso sem exigir que o usuário faça login novamente.
Como o aplicativo está usando a biblioteca Microsoft.Identity.Web, você não precisa implementar nenhum armazenamento de token ou lógica de atualização.
O aplicativo usa o cache de token na memória, o que é suficiente para aplicativos que não precisam persistir tokens quando o aplicativo é reiniciado. Em vez disso, os aplicativos de produção podem usar as opções de cache distribuído na biblioteca Microsoft.Identity.Web.
O GetAccessTokenForUserAsync
método lida com a expiração do token e a atualização para você. Ele primeiro verifica o token armazenado em cache e, se não estiver expirado, ele o retornará. Se expirar, ele usará o token de atualização em cache para obter um novo.
O GraphServiceClient que os controladores obterem por injeção de dependência será pré-configurado com um provedor de autenticação que usa GetAccessTokenForUserAsync
para você.
Obter um modo de exibição de calendário
Nesta seção, você incorporará o Microsoft Graph no aplicativo. Para este aplicativo, você usará a Biblioteca de Clientes do Microsoft Graph para .NET para fazer chamadas para o Microsoft Graph.
Obtenha eventos de calendário do Outlook
Comece criando um novo controlador para exibições de calendário.
Adicione um novo arquivo chamado CalendarController.cs no diretório ./Controllers e adicione o código a seguir.
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; } } }
Adicione as seguintes funções à classe
CalendarController
para obter a exibição de calendário do usuário.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); }
Considere o que o código em
GetUserWeekCalendar
faz.- Ele usa o fuso horário do usuário para obter valores de data/hora utc inicial e final da semana.
- Ele consulta a exibição de calendário do usuário para obter todos os eventos que se enquadram entre a data/hora inicial e final. Usar uma exibição de calendário em vez de listar eventos expande eventos recorrentes, retornando quaisquer ocorrências que ocorram na janela de tempo especificada.
- Ele usa o
Prefer: outlook.timezone
header para obter resultados de volta no timezone do usuário. - Ele usa
Select
para limitar os campos que voltam apenas para aqueles usados pelo aplicativo. - Ele usa
OrderBy
para classificar os resultados cronologicamente. - Ele usa um para
PageIterator
página através da coleção de eventos. Isso lida com o caso em que o usuário tem mais eventos em seu calendário do que o tamanho da página solicitada.
Adicione a seguinte função à classe
CalendarController
para implementar uma exibição temporária dos dados retornados.// 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" }; } }
Inicie o aplicativo, entre e clique no link Calendário na barra de nav. Se tudo funcionar, você deverá ver um despejo JSON de eventos no calendário do usuário.
Exibir os resultados
Agora você pode adicionar um modo de exibição para exibir os resultados de uma maneira mais amigável.
Criar modelos de exibição
Crie um novo arquivo chamado CalendarViewEvent.cs no diretório ./Models e adicione o código a seguir.
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); } } }
Crie um novo arquivo chamado DailyViewModel.cs no diretório ./Models e adicione o código a seguir.
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; } } }
Crie um novo arquivo chamado CalendarViewModel.cs no diretório ./Models e adicione o código a seguir.
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)))); } } }
Criar exibições
Crie um novo diretório chamado Calendário no diretório ./Views .
Crie um novo arquivo chamado _DailyEventsPartial.cshtml no diretório ./Views/Calendar e adicione o código a seguir.
@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> }
Crie um novo arquivo chamado Index.cshtml no diretório ./Views/Calendar e adicione o código a seguir.
@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>
Atualizar controlador de calendário
Abra ./Controllers/CalendarController.cs e substitua a função existente
Index
pelo seguinte.// 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); } }
Inicie o aplicativo, entre e clique no link Calendário . O aplicativo agora deve renderizar uma tabela de eventos.
Criar um novo evento
Nesta seção, você adicionará a capacidade de criar eventos no calendário do usuário.
Criar um modelo
Crie um novo arquivo chamado NewEvent.cs no diretório ./Models e adicione o código a seguir.
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; } } }
Criar exibição
Crie um novo arquivo chamado New.cshtml no diretório ./Views/Calendar e adicione o código a seguir.
@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");} }
Adicionar ações do controlador
Abra ./Controllers/CalendarController.cs
CalendarController
e adicione a ação a seguir à classe para renderizar o novo formulário de evento.// Minimum permission scope needed for this view [AuthorizeForScopes(Scopes = new[] { "Calendars.ReadWrite" })] public IActionResult New() { return View(); }
Adicione a ação a
CalendarController
seguir à classe para receber o novo evento do formulário quando o usuário clicar em Salvar e usar o Microsoft Graph adicionar o evento ao calendário do usuário.[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); } }
Inicie o aplicativo, entre e clique no link Calendário . Clique no botão Novo evento, preencha o formulário e clique em Salvar.
Parabéns!
Você concluiu o tutorial ASP.NET Core microsoft Graph. Agora que você tem um aplicativo de trabalho que chama a Microsoft Graph, você pode experimentar e adicionar novos recursos. Visite a visão geral do microsoft Graph para ver todos os dados que você pode acessar com o Microsoft Graph.
Comentários
Forneça qualquer comentário sobre este tutorial no repositório GitHub.
Tem algum problema com essa seção? Se tiver, envie seus comentários para que possamos melhorar esta seção.