From f7a68d1de78bbe00446aa7f294f84c9498f6c119 Mon Sep 17 00:00:00 2001 From: Harikrishnan Rajandiran Date: Tue, 7 Jan 2025 13:32:03 +0530 Subject: [PATCH 1/3] Sample Code Optimization --- .../AdapterWithErrorHandler.cs | 16 +- .../BotSsoAdaptivecard/Bots/DialogBot.cs | 160 ++++++------------ .../BotSsoAdaptivecard/Bots/TeamsBot.cs | 15 +- .../Controllers/BotController.cs | 28 ++- .../BotSsoAdaptivecard/Dialogs/MainDialog.cs | 15 +- .../Helper/SimpleGraphClient.cs | 80 ++++++--- .../csharp/BotSsoAdaptivecard/Program.cs | 79 +++++---- 7 files changed, 196 insertions(+), 197 deletions(-) diff --git a/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/AdapterWithErrorHandler.cs b/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/AdapterWithErrorHandler.cs index 876617a418..2a57af38ee 100644 --- a/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/AdapterWithErrorHandler.cs +++ b/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/AdapterWithErrorHandler.cs @@ -12,23 +12,21 @@ namespace Microsoft.BotBuilderSamples { public class AdapterWithErrorHandler : CloudAdapter { - public AdapterWithErrorHandler(BotFrameworkAuthentication auth, ILogger logger, ConversationState conversationState = default) + public AdapterWithErrorHandler(BotFrameworkAuthentication auth, ILogger logger) : base(auth, logger) { + // Handle errors that occur during the bot's turn, logging details and sending a trace activity OnTurnError = async (turnContext, exception) => { - // Log any leaked exception from the application. - // NOTE: In production environment, you should consider logging this to - // Azure Application Insights. Visit https://aka.ms/bottelemetry to see how - // to add telemetry capture to your bot. - logger.LogError(exception, $"[OnTurnError] unhandled error : {exception.Message}"); + // Log unhandled exceptions to assist with debugging and error tracking + logger.LogError(exception, $"[OnTurnError] unhandled error: {exception.Message}"); - // Uncomment below commented line for local debugging. + // In development environment, you can uncomment below for local debugging // await turnContext.SendActivityAsync($"Sorry, it looks like something went wrong. Exception Caught: {exception.Message}"); - // Send a trace activity, which will be displayed in the Bot Framework Emulator + // Send a trace activity to the Bot Framework Emulator for further investigation await turnContext.TraceActivityAsync("OnTurnError Trace", exception.Message, "https://www.botframework.com/schemas/error", "TurnError"); }; } } -} \ No newline at end of file +} diff --git a/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Bots/DialogBot.cs b/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Bots/DialogBot.cs index 58dc3726a6..f707479f50 100644 --- a/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Bots/DialogBot.cs +++ b/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Bots/DialogBot.cs @@ -21,11 +21,10 @@ namespace Microsoft.BotBuilderSamples { - // This IBot implementation can run any type of Dialog. The use of type parameterization is to allows multiple different bots - // to be run at different endpoints within the same project. This can be achieved by defining distinct Controller types - // each with dependency on distinct IBot types, this way ASP Dependency Injection can glue everything together without ambiguity. - // The ConversationState is used by the Dialog system. The UserState isn't, however, it might have been used in a Dialog implementation, - // and the requirement is that all BotState objects are saved at the end of a turn. + // This IBot implementation can run any type of Dialog. + // The use of type parameterization allows multiple bots to run at different endpoints within the same project. + // ConversationState is used by the Dialog system, while UserState may or may not be used in a Dialog implementation. + // All BotState objects should be saved at the end of a turn. public class DialogBot : TeamsActivityHandler where T : Dialog { protected readonly BotState _conversationState; @@ -43,12 +42,7 @@ public DialogBot(ConversationState conversationState, UserState userState, T dia _connectionName = connectionName; } - /// - /// Get sign in link - /// - /// The context for the current turn. - /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. - /// A task that represents the work queued to execute. + // Get sign-in link private async Task GetSignInLinkAsync(ITurnContext turnContext, CancellationToken cancellationToken) { var userTokenClient = turnContext.TurnState.Get(); @@ -56,43 +50,40 @@ private async Task GetSignInLinkAsync(ITurnContext turnContext, Cancella return resource.SignInLink; } - /// - /// Add logic to apply after the type-specific logic after the call to the base class method. - /// - /// The context for the current turn. - /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. - /// A task that represents the work queued to execute. + // OnTurnAsync: Parallelize saving state changes public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken)) { await base.OnTurnAsync(turnContext, cancellationToken); - // Save any state changes that might have occurred during the turn. - await _conversationState.SaveChangesAsync(turnContext, false, cancellationToken); - await _userState.SaveChangesAsync(turnContext, false, cancellationToken); + // Save any state changes in parallel to improve performance + await Task.WhenAll( + _conversationState.SaveChangesAsync(turnContext, false, cancellationToken), + _userState.SaveChangesAsync(turnContext, false, cancellationToken) + ); } - /// - /// Override this in a derived class to provide logic specific to Message activities, such as the conversational logic. - /// - /// The context for the current turn. - /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. - /// A task that represents the work queued to execute. + // Simplified message activity handling using a helper function protected override async Task OnMessageActivityAsync(ITurnContext turnContext, CancellationToken cancellationToken) { var signInLink = await GetSignInLinkAsync(turnContext, cancellationToken).ConfigureAwait(false); - if (turnContext.Activity.Text.Contains("login")) + await HandleCommandAsync(turnContext.Activity.Text, turnContext, signInLink, cancellationToken); + } + + // New helper function to handle commands and send the appropriate adaptive card + private async Task HandleCommandAsync(string command, ITurnContext turnContext, string signInLink, CancellationToken cancellationToken) + { + var commandToFileMap = new Dictionary { - string[] path = { ".", "Resources", "options.json" }; - var member = await TeamsInfo.GetMemberAsync(turnContext, turnContext.Activity.From.Id, cancellationToken); - var initialAdaptiveCard = GetAdaptiveCardFromFileName(path, signInLink, turnContext.Activity.From.Name, member.Id); - await turnContext.SendActivityAsync(MessageFactory.Attachment(initialAdaptiveCard), cancellationToken); - } - else if (turnContext.Activity.Text.Contains("PerformSSO")) + { "login", "options.json" }, + { "PerformSSO", "AdaptiveCardWithSSOInRefresh.json" } + }; + + if (commandToFileMap.ContainsKey(command)) { - string[] path = { ".", "Resources", "AdaptiveCardWithSSOInRefresh.json" }; + string[] path = { ".", "Resources", commandToFileMap[command] }; var member = await TeamsInfo.GetMemberAsync(turnContext, turnContext.Activity.From.Id, cancellationToken); - var initialAdaptiveCard = GetAdaptiveCardFromFileName(path, signInLink, turnContext.Activity.From.Name, member.Id); - await turnContext.SendActivityAsync(MessageFactory.Attachment(initialAdaptiveCard), cancellationToken); + var adaptiveCard = GetAdaptiveCardFromFileName(path, signInLink, turnContext.Activity.From.Name, member.Id); + await turnContext.SendActivityAsync(MessageFactory.Attachment(adaptiveCard), cancellationToken); } else { @@ -100,12 +91,7 @@ protected override async Task OnMessageActivityAsync(ITurnContext - /// The OAuth Prompt needs to see the Invoke Activity in order to complete the login process. - /// - /// The context for the current turn. - /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. - /// A task that represents the work queued to execute. + // Override to handle invoke activities, such as OAuth and adaptive card actions protected override async Task OnInvokeActivityAsync(ITurnContext turnContext, CancellationToken cancellationToken) { if (turnContext.Activity.Name == "adaptiveCard/action") @@ -118,104 +104,77 @@ protected override async Task OnInvokeActivityAsync(ITurnContext if (value["action"] == null) return null; - JObject actiondata = JsonConvert.DeserializeObject(value["action"].ToString()); + JObject actionData = JsonConvert.DeserializeObject(value["action"].ToString()); - if (actiondata["verb"] == null) + if (actionData["verb"] == null) return null; - - string verb = actiondata["verb"].ToString(); + + string verb = actionData["verb"].ToString(); JObject authentication = null; + string state = null; - // When adaptiveCard/action invoke activity from teams contains token in response to sso flow from earlier invoke. + // Check for authentication token or state if (value["authentication"] != null) { authentication = JsonConvert.DeserializeObject(value["authentication"].ToString()); } - // When adaptiveCard/action invoke activity from teams contains 6 digit state in response to nominal sign in flow from bot. - string state = null; if (value["state"] != null) { state = value["state"].ToString(); } - // authToken and state are absent, handle verb + // Token and state are absent, initiate SSO if (authentication == null && state == null) { - switch (verb) - { // when token is absent in the invoke. We can initiate SSO in response to the invoke - case "initiateSSO": - return await initiateSSOAsync(turnContext, cancellationToken); + if (verb == "initiateSSO") + { + return await InitiateSSOAsync(turnContext, cancellationToken); } } else { - return createAdaptiveCardInvokeResponseAsync(authentication, state); + return CreateAdaptiveCardInvokeResponseAsync(authentication, state); } } return null; } - /// - /// Authentication success. - /// AuthToken or state is present. Verify token/state in invoke payload and return AC response - /// - /// authToken are absent, handle verb - /// state are absent, handle verb - /// The context for the current turn. - /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. - /// Refresh type - /// AdaptiveCardResponse.json - /// A task that represents the work queued to execute. - private InvokeResponse createAdaptiveCardInvokeResponseAsync(JObject authentication, string state, bool isBasicRefresh = false, string fileName = "AdaptiveCardResponse.json") + // Method to create adaptive card invoke response + private InvokeResponse CreateAdaptiveCardInvokeResponseAsync(JObject authentication, string state, bool isBasicRefresh = false, string fileName = "AdaptiveCardResponse.json") { - // Verify token is present or not. - - bool isTokenPresent = authentication != null ? true : false; - bool isStatePresent = state != null && state != "" ? true : false; - - string[] filepath = { ".", "Resources", fileName }; + string authResultData = (authentication != null) ? "SSO success" : (state != null && state != "") ? "OAuth success" : "SSO/OAuth failed"; - var adaptiveCardJson = File.ReadAllText(Path.Combine(filepath)); - AdaptiveCardTemplate template = new AdaptiveCardTemplate(adaptiveCardJson); - var authResultData = isTokenPresent ? "SSO success" : isStatePresent ? "OAuth success" : "SSO/OAuth failed"; - if (isBasicRefresh) { authResultData = "Refresh done"; } - - var payloadData = new - { - authResult = authResultData, - }; - var cardJsonstring = template.Expand(payloadData); + string[] filePath = { ".", "Resources", fileName }; + var adaptiveCardJson = File.ReadAllText(Path.Combine(filePath)); + AdaptiveCardTemplate template = new AdaptiveCardTemplate(adaptiveCardJson); + var payloadData = new { authResult = authResultData }; + var cardJsonString = template.Expand(payloadData); var adaptiveCardResponse = new AdaptiveCardInvokeResponse() { StatusCode = 200, Type = AdaptiveCard.ContentType, - Value = JsonConvert.DeserializeObject(cardJsonstring) + Value = JsonConvert.DeserializeObject(cardJsonString) }; return CreateInvokeResponse(adaptiveCardResponse); } - /// - /// when token is absent in the invoke. We can initiate SSO in response to the invoke - /// - /// The context for the current turn. - /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. - /// A task that represents the work queued to execute. - private async Task initiateSSOAsync(ITurnContext turnContext, CancellationToken cancellationToken) + // Method to initiate SSO flow by sending OAuth card + private async Task InitiateSSOAsync(ITurnContext turnContext, CancellationToken cancellationToken) { var signInLink = await GetSignInLinkAsync(turnContext, cancellationToken).ConfigureAwait(false); var oAuthCard = new OAuthCard { - Text = "Signin Text", + Text = "Please sign in", ConnectionName = _connectionName, TokenExchangeResource = new TokenExchangeResource { @@ -227,7 +186,7 @@ private async Task initiateSSOAsync(ITurnContext initiateSSOAsync(ITurnContext - /// Get Adaptive Card - /// - /// json path - /// Get sign in link - /// createdBy - /// createdById - /// + // Method to retrieve adaptive card from a file and expand with dynamic data private Attachment GetAdaptiveCardFromFileName(string[] filepath, string signInLink, string name = null, string userMRI = null) { var adaptiveCardJson = File.ReadAllText(Path.Combine(filepath)); @@ -259,9 +211,9 @@ private Attachment GetAdaptiveCardFromFileName(string[] filepath, string signInL createdById = userMRI, createdBy = name }; - - var cardJsonstring = template.Expand(payloadData); - var card = JsonConvert.DeserializeObject(cardJsonstring); + + var cardJsonString = template.Expand(payloadData); + var card = JsonConvert.DeserializeObject(cardJsonString); var adaptiveCardAttachment = new Attachment() { ContentType = "application/vnd.microsoft.card.adaptive", diff --git a/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Bots/TeamsBot.cs b/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Bots/TeamsBot.cs index 0d1b9fab0e..c2e72c5eb1 100644 --- a/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Bots/TeamsBot.cs +++ b/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Bots/TeamsBot.cs @@ -18,24 +18,29 @@ public class TeamsBot : DialogBot public TeamsBot(ConversationState conversationState, UserState userState, MainDialog dialog, ILogger> logger, IConfiguration configuration) : base(conversationState, userState, dialog, logger, configuration["ConnectionName"]) { + if (string.IsNullOrEmpty(configuration["ConnectionName"])) + { + logger.LogError("ConnectionName is missing from configuration."); + } } /// - /// Override this in a derived class to provide logic for when members other than the bot join the conversation, such as your bot's welcome logic. + /// Override this in a derived class to provide logic for when members, except the bot, join the conversation, such as your bot's welcome logic. /// - /// A list of all the members added to the conversation, as described by the conversation update activity. /// A strongly-typed context object for this turn. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. /// A task that represents the work queued to execute. protected override async Task OnMembersAddedAsync(IList membersAdded, ITurnContext turnContext, CancellationToken cancellationToken) { - foreach (var member in turnContext.Activity.MembersAdded) + // Iterate over all members added to the conversation. + foreach (var member in membersAdded) { if (member.Id != turnContext.Activity.Recipient.Id) { - await turnContext.SendActivityAsync(MessageFactory.Text("Welcome to Universal Adaptive Cards. Type 'login' to get sign in universal sso."), cancellationToken); + // Send a welcome message to new members. + await turnContext.SendActivityAsync(MessageFactory.Text("Welcome to Universal Adaptive Cards. Type 'login' to sign in using Universal SSO."), cancellationToken); } } } } -} \ No newline at end of file +} diff --git a/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Controllers/BotController.cs b/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Controllers/BotController.cs index fee03d9bf5..5725551145 100644 --- a/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Controllers/BotController.cs +++ b/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Controllers/BotController.cs @@ -2,35 +2,47 @@ // Copyright (c) Microsoft. All rights reserved. // +using System; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Integration.AspNet.Core; namespace Microsoft.BotBuilderSamples { - // This ASP Controller is created to handle a request. Dependency Injection will provide the Adapter and IBot - // implementation at runtime. Multiple different IBot implementations running at different endpoints can be - // achieved by specifying a more specific type for the bot constructor argument. + // This ASP Controller is created to handle requests to the bot. + // Dependency Injection provides the Adapter and IBot at runtime. + // Multiple IBot implementations can be run at different endpoints. [Route("api/messages")] [ApiController] public class BotController : ControllerBase { private readonly IBotFrameworkHttpAdapter _adapter; - private readonly IBot _bot; + private readonly IBot _botInstance; + // Constructor to inject the adapter and bot instance public BotController(IBotFrameworkHttpAdapter adapter, IBot bot) { _adapter = adapter; - _bot = bot; + _botInstance = bot; } [HttpPost] public async Task PostAsync() { - // Delegate the processing of the HTTP POST to the adapter. - // The adapter will invoke the bot. - await _adapter.ProcessAsync(Request, Response, _bot); + try + { + // Delegate HTTP request processing to the bot adapter. + await _adapter.ProcessAsync(Request, Response, _botInstance); + } + catch (Exception ex) + { + // Log exception and return 500 Internal Server Error + // You can replace this with proper logging mechanism. + Response.StatusCode = 500; + await Response.WriteAsync($"Error processing request: {ex.Message}"); + } } } } diff --git a/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Dialogs/MainDialog.cs b/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Dialogs/MainDialog.cs index 97e8fd6db5..36f712f180 100644 --- a/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Dialogs/MainDialog.cs +++ b/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Dialogs/MainDialog.cs @@ -1,23 +1,12 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; +using System; using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Dialogs; -using Microsoft.Bot.Schema; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace Microsoft.BotBuilderSamples { public class MainDialog : ComponentDialog { - protected readonly ILogger _logger; - public MainDialog(ILogger logger) : base(nameof(MainDialog)) { @@ -25,4 +14,4 @@ public MainDialog(ILogger logger) InitialDialogId = nameof(WaterfallDialog); } } -} \ No newline at end of file +} diff --git a/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Helper/SimpleGraphClient.cs b/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Helper/SimpleGraphClient.cs index fe4af4a112..92fe84e69b 100644 --- a/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Helper/SimpleGraphClient.cs +++ b/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Helper/SimpleGraphClient.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) Microsoft. All rights reserved. // @@ -14,11 +14,11 @@ namespace BotSsoAdaptivecard.Helper { // This class is a wrapper for the Microsoft Graph API // See: https://developer.microsoft.com/en-us/graph - public class SimpleGraphClient + public class GraphServiceClientHelper { private readonly string _token; - public SimpleGraphClient(string token) + public GraphServiceClientHelper(string token) { if (string.IsNullOrWhiteSpace(token)) { @@ -29,11 +29,11 @@ public SimpleGraphClient(string token) } /// - /// Sends an email on the users behalf using the Microsoft Graph API + /// Sends an email on the user's behalf using the Microsoft Graph API. /// - /// to address - /// mail subject - /// body content + /// Recipient email address. + /// Subject of the email. + /// Body content of the email. /// The task object representing the asynchronous operation. /// public async Task SendMailAsync(string toAddress, string subject, string content) @@ -54,7 +54,7 @@ public async Task SendMailAsync(string toAddress, string subject, string content } var graphClient = GetAuthenticatedClient(); - var recipients = new List + var recipients = new[] { new Recipient { @@ -77,51 +77,79 @@ public async Task SendMailAsync(string toAddress, string subject, string content ToRecipients = recipients, }; - // Send the message. - await graphClient.Me.SendMail(email, true).Request().PostAsync(); + try + { + // Send the message. + await graphClient.Me.SendMail(email, true).Request().PostAsync().ConfigureAwait(false); + } + catch (ServiceException ex) + { + // Handle exception + Console.WriteLine($"Error sending mail: {ex.Message}"); + } } - // Gets mail for the user using the Microsoft Graph API + /// + /// Gets the most recent mail for the user. + /// + /// A list of the 5 most recent messages. public async Task GetRecentMailAsync() { var graphClient = GetAuthenticatedClient(); - var messages = await graphClient.Me.MailFolders.Inbox.Messages.Request().GetAsync(); + var messages = await graphClient.Me.MailFolders.Inbox.Messages.Request().GetAsync().ConfigureAwait(false); return messages.Take(5).ToArray(); } - // Get information about the user. + /// + /// Gets information about the current authenticated user. + /// + /// User information. public async Task GetMeAsync() { var graphClient = GetAuthenticatedClient(); - var me = await graphClient.Me.Request().GetAsync(); + var me = await graphClient.Me.Request().GetAsync().ConfigureAwait(false); return me; } - // Gets the user's photo + /// + /// Gets the user's photo in base64 format. + /// + /// The base64-encoded photo string, or an empty string if no photo is available. public async Task GetPhotoAsync() { var graphClient = GetAuthenticatedClient(); - var photo = await graphClient.Me.Photo.Content.Request().GetAsync(); - if (photo != null) + try { - MemoryStream ms = new MemoryStream(); - photo.CopyTo(ms); - byte[] buffers = ms.ToArray(); - string imgDataURL = string.Format("data:image/png;base64,{0}", Convert.ToBase64String(buffers)); - return imgDataURL; + var photo = await graphClient.Me.Photo.Content.Request().GetAsync(); + if (photo != null) + { + MemoryStream ms = new MemoryStream(); + photo.CopyTo(ms); + byte[] buffers = ms.ToArray(); + string imgDataURL = string.Format("data:image/png;base64,{0}", Convert.ToBase64String(buffers)); + return imgDataURL; + } + else + { + return ""; // Return empty string if no photo is available. + } } - else + catch (ServiceException ex) { + Console.WriteLine($"Error fetching photo: {ex.Message}"); return ""; } } - // Get an Authenticated Microsoft Graph client using the token issued to the user. + /// + /// Returns an authenticated Microsoft Graph client using the token issued to the user. + /// + /// An authenticated instance of GraphServiceClient. private GraphServiceClient GetAuthenticatedClient() { var graphClient = new GraphServiceClient( new DelegateAuthenticationProvider( - requestMessage => + async requestMessage => { // Append the access token to the request. requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", _token); @@ -129,7 +157,7 @@ private GraphServiceClient GetAuthenticatedClient() // Get event times in the current time zone. requestMessage.Headers.Add("Prefer", "outlook.timezone=\"" + TimeZoneInfo.Local.Id + "\""); - return Task.CompletedTask; + await Task.CompletedTask.ConfigureAwait(false); })); return graphClient; diff --git a/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Program.cs b/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Program.cs index e925479f35..9d7858eeec 100644 --- a/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Program.cs +++ b/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Program.cs @@ -13,47 +13,62 @@ var builder = WebApplication.CreateBuilder(args); -builder.Services.AddHttpClient().AddControllers().AddNewtonsoftJson(); +// Add necessary services to the container. +ConfigureServices(builder.Services); -// Create the Bot Framework Adapter with error handling enabled. -builder.Services.AddSingleton(); +var app = builder.Build(); -// Create the Bot Framework Authentication to be used with the Bot Adapter. -builder.Services.AddSingleton(); +// Configure the HTTP request pipeline. +ConfigureApp(app); -// Create the storage we'll be using for User and Conversation state. (Memory is great for testing purposes.) -builder.Services.AddSingleton(); +app.Run(); -// Create the User state. (Used in this bot's Dialog implementation.) -builder.Services.AddSingleton(); +void ConfigureServices(IServiceCollection services) +{ + // Add HTTP client and JSON configuration. + services.AddHttpClient().AddControllers().AddNewtonsoftJson(); -// Create the Conversation state. (Used by the Dialog system itself.) -builder.Services.AddSingleton(); + // Register the Bot Framework Adapter with error handling. + services.AddSingleton(); -// The Dialog that will be run by the bot. -builder.Services.AddSingleton(); + // Register Bot Framework Authentication. + services.AddSingleton(); -// Create the bot as a transient. In this case the ASP Controller is expecting an IBot. -builder.Services.AddTransient(); + // Register state management services. Consider using Scoped for better isolation. + services.AddScoped(); // Consider replacing MemoryStorage with persistent storage for production. + services.AddScoped(); + services.AddScoped(); -var app = builder.Build(); + // Register the dialog to be used by the bot. + services.AddSingleton(); -// Configure the HTTP request pipeline.l -if (!app.Environment.IsDevelopment()) -{ - app.UseHsts(); + // Register the bot as a transient service. + services.AddTransient(); } -else + +void ConfigureApp(WebApplication app) { - app.UseDeveloperExceptionPage(); -} + // Environment-specific configurations. + if (!app.Environment.IsDevelopment()) + { + // Enabling HTTPS Strict Transport Security (HSTS) in production environments. + app.UseHsts(); + } + else + { + // Enabling developer exception page during development. + app.UseDeveloperExceptionPage(); + } -app.UseDefaultFiles() - .UseStaticFiles() - .UseRouting() - .UseAuthorization() - .UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); -app.Run(); \ No newline at end of file + // Serving static files and configuring routing. + app.UseDefaultFiles() + .UseStaticFiles() + .UseRouting() + .UseAuthorization(); + + // Configure endpoints to handle incoming requests. + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); // Mapping controller routes. + }); +} From d4d885a7be2a54e676b9cadc5e2a971aff31d993 Mon Sep 17 00:00:00 2001 From: Harikrishnan Rajandiran Date: Tue, 7 Jan 2025 13:42:17 +0530 Subject: [PATCH 2/3] Update Code Description --- .../AdapterWithErrorHandler.cs | 15 ++++++------- .../BotSsoAdaptivecard/Bots/DialogBot.cs | 21 ++++++++++--------- .../BotSsoAdaptivecard/Bots/TeamsBot.cs | 3 +++ .../BotSsoAdaptivecard/Dialogs/MainDialog.cs | 7 +++++-- 4 files changed, 25 insertions(+), 21 deletions(-) diff --git a/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/AdapterWithErrorHandler.cs b/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/AdapterWithErrorHandler.cs index 2a57af38ee..8f3fa7e04b 100644 --- a/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/AdapterWithErrorHandler.cs +++ b/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/AdapterWithErrorHandler.cs @@ -1,8 +1,4 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// - -using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Integration.AspNet.Core; using Microsoft.Bot.Builder.TraceExtensions; using Microsoft.Bot.Connector.Authentication; @@ -12,19 +8,20 @@ namespace Microsoft.BotBuilderSamples { public class AdapterWithErrorHandler : CloudAdapter { + // Constructor that initializes the bot framework authentication and logger. public AdapterWithErrorHandler(BotFrameworkAuthentication auth, ILogger logger) : base(auth, logger) { - // Handle errors that occur during the bot's turn, logging details and sending a trace activity + // Define the error handling behavior during the bot's turn. OnTurnError = async (turnContext, exception) => { - // Log unhandled exceptions to assist with debugging and error tracking + // Log the exception details for debugging and tracking errors. logger.LogError(exception, $"[OnTurnError] unhandled error: {exception.Message}"); - // In development environment, you can uncomment below for local debugging + // For development purposes, uncomment to provide a custom error message to users locally. // await turnContext.SendActivityAsync($"Sorry, it looks like something went wrong. Exception Caught: {exception.Message}"); - // Send a trace activity to the Bot Framework Emulator for further investigation + // Send a trace activity to the Bot Framework Emulator for deeper debugging. await turnContext.TraceActivityAsync("OnTurnError Trace", exception.Message, "https://www.botframework.com/schemas/error", "TurnError"); }; } diff --git a/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Bots/DialogBot.cs b/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Bots/DialogBot.cs index f707479f50..232864573e 100644 --- a/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Bots/DialogBot.cs +++ b/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Bots/DialogBot.cs @@ -27,12 +27,13 @@ namespace Microsoft.BotBuilderSamples // All BotState objects should be saved at the end of a turn. public class DialogBot : TeamsActivityHandler where T : Dialog { - protected readonly BotState _conversationState; - protected readonly Dialog _dialog; - protected readonly ILogger _logger; - protected readonly BotState _userState; - protected string _connectionName { get; } + protected readonly BotState _conversationState; // Represents the conversation state + protected readonly Dialog _dialog; // The dialog logic to run + protected readonly ILogger _logger; // Logger for debugging and tracing + protected readonly BotState _userState; // Represents the user state + protected string _connectionName { get; } // Connection name for OAuth + // Constructor to initialize the bot with necessary dependencies public DialogBot(ConversationState conversationState, UserState userState, T dialog, ILogger> logger, string connectionName) { _conversationState = conversationState; @@ -42,7 +43,7 @@ public DialogBot(ConversationState conversationState, UserState userState, T dia _connectionName = connectionName; } - // Get sign-in link + // Get the sign-in link for OAuth private async Task GetSignInLinkAsync(ITurnContext turnContext, CancellationToken cancellationToken) { var userTokenClient = turnContext.TurnState.Get(); @@ -50,7 +51,7 @@ private async Task GetSignInLinkAsync(ITurnContext turnContext, Cancella return resource.SignInLink; } - // OnTurnAsync: Parallelize saving state changes + // OnTurnAsync: Handles parallel saving of conversation and user state changes public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken)) { await base.OnTurnAsync(turnContext, cancellationToken); @@ -62,14 +63,14 @@ await Task.WhenAll( ); } - // Simplified message activity handling using a helper function + // Simplified message activity handling to trigger appropriate adaptive card based on the message command protected override async Task OnMessageActivityAsync(ITurnContext turnContext, CancellationToken cancellationToken) { var signInLink = await GetSignInLinkAsync(turnContext, cancellationToken).ConfigureAwait(false); await HandleCommandAsync(turnContext.Activity.Text, turnContext, signInLink, cancellationToken); } - // New helper function to handle commands and send the appropriate adaptive card + // Helper function to handle commands and send the appropriate adaptive card private async Task HandleCommandAsync(string command, ITurnContext turnContext, string signInLink, CancellationToken cancellationToken) { var commandToFileMap = new Dictionary @@ -142,7 +143,7 @@ protected override async Task OnInvokeActivityAsync(ITurnContext return null; } - // Method to create adaptive card invoke response + // Method to create adaptive card invoke response with dynamic data private InvokeResponse CreateAdaptiveCardInvokeResponseAsync(JObject authentication, string state, bool isBasicRefresh = false, string fileName = "AdaptiveCardResponse.json") { string authResultData = (authentication != null) ? "SSO success" : (state != null && state != "") ? "OAuth success" : "SSO/OAuth failed"; diff --git a/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Bots/TeamsBot.cs b/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Bots/TeamsBot.cs index c2e72c5eb1..f7588c1eaf 100644 --- a/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Bots/TeamsBot.cs +++ b/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Bots/TeamsBot.cs @@ -15,9 +15,11 @@ namespace Microsoft.BotBuilderSamples // This bot is derived (view DialogBot) from the TeamsActivityHandler class currently included as part of this sample. public class TeamsBot : DialogBot { + // Constructor to initialize the bot with necessary dependencies public TeamsBot(ConversationState conversationState, UserState userState, MainDialog dialog, ILogger> logger, IConfiguration configuration) : base(conversationState, userState, dialog, logger, configuration["ConnectionName"]) { + // Check if the ConnectionName exists in the configuration if (string.IsNullOrEmpty(configuration["ConnectionName"])) { logger.LogError("ConnectionName is missing from configuration."); @@ -35,6 +37,7 @@ protected override async Task OnMembersAddedAsync(IList membersA // Iterate over all members added to the conversation. foreach (var member in membersAdded) { + // Ensure that the bot doesn't greet itself if (member.Id != turnContext.Activity.Recipient.Id) { // Send a welcome message to new members. diff --git a/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Dialogs/MainDialog.cs b/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Dialogs/MainDialog.cs index 36f712f180..6b52bb58a0 100644 --- a/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Dialogs/MainDialog.cs +++ b/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Dialogs/MainDialog.cs @@ -5,13 +5,16 @@ namespace Microsoft.BotBuilderSamples { + // MainDialog is a class that represents the main dialog of the bot. + // It inherits from ComponentDialog which allows adding and managing multiple dialogs. public class MainDialog : ComponentDialog { + // Constructor that accepts a logger for logging purposes. public MainDialog(ILogger logger) - : base(nameof(MainDialog)) + : base(nameof(MainDialog)) // Pass the dialog's name to the base constructor { // The initial child Dialog to run. - InitialDialogId = nameof(WaterfallDialog); + InitialDialogId = nameof(WaterfallDialog); // Specifies that the first dialog to run is a WaterfallDialog. } } } From 5f697da41468269545206b2d1265b87de75ba07c Mon Sep 17 00:00:00 2001 From: Harikrishnan Rajandiran Date: Thu, 16 Jan 2025 11:49:28 +0530 Subject: [PATCH 3/3] Updated Code Comments --- .../AdapterWithErrorHandler.cs | 6 +- .../BotSsoAdaptivecard/Bots/DialogBot.cs | 60 +++++++++++++++++-- .../BotSsoAdaptivecard/Bots/TeamsBot.cs | 1 + .../Controllers/BotController.cs | 6 +- .../BotSsoAdaptivecard/Dialogs/MainDialog.cs | 6 +- .../Helper/SimpleGraphClient.cs | 2 +- 6 files changed, 69 insertions(+), 12 deletions(-) diff --git a/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/AdapterWithErrorHandler.cs b/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/AdapterWithErrorHandler.cs index 8f3fa7e04b..dec2c1ce26 100644 --- a/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/AdapterWithErrorHandler.cs +++ b/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/AdapterWithErrorHandler.cs @@ -1,4 +1,8 @@ -using Microsoft.Bot.Builder; +// +// Copyright (c) Microsoft. All rights reserved. +// + +using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Integration.AspNet.Core; using Microsoft.Bot.Builder.TraceExtensions; using Microsoft.Bot.Connector.Authentication; diff --git a/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Bots/DialogBot.cs b/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Bots/DialogBot.cs index 232864573e..7b6c43527e 100644 --- a/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Bots/DialogBot.cs +++ b/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Bots/DialogBot.cs @@ -21,10 +21,11 @@ namespace Microsoft.BotBuilderSamples { - // This IBot implementation can run any type of Dialog. - // The use of type parameterization allows multiple bots to run at different endpoints within the same project. - // ConversationState is used by the Dialog system, while UserState may or may not be used in a Dialog implementation. - // All BotState objects should be saved at the end of a turn. + // This IBot implementation can run any type of Dialog. The use of type parameterization is to allows multiple different bots + // to be run at different endpoints within the same project. This can be achieved by defining distinct Controller types + // each with dependency on distinct IBot types, this way ASP Dependency Injection can glue everything together without ambiguity. + // The ConversationState is used by the Dialog system. The UserState isn't, however, it might have been used in a Dialog implementation, + // and the requirement is that all BotState objects are saved at the end of a turn. public class DialogBot : TeamsActivityHandler where T : Dialog { protected readonly BotState _conversationState; // Represents the conversation state @@ -43,6 +44,12 @@ public DialogBot(ConversationState conversationState, UserState userState, T dia _connectionName = connectionName; } + /// + /// Get sign in link + /// + /// The context for the current turn. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// A task that represents the work queued to execute. // Get the sign-in link for OAuth private async Task GetSignInLinkAsync(ITurnContext turnContext, CancellationToken cancellationToken) { @@ -51,6 +58,12 @@ private async Task GetSignInLinkAsync(ITurnContext turnContext, Cancella return resource.SignInLink; } + /// + /// Add logic to apply after the type-specific logic after the call to the base class method. + /// + /// The context for the current turn. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// A task that represents the work queued to execute. // OnTurnAsync: Handles parallel saving of conversation and user state changes public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken)) { @@ -63,6 +76,12 @@ await Task.WhenAll( ); } + /// + /// Override this in a derived class to provide logic specific to Message activities, such as the conversational logic. + /// + /// The context for the current turn. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// A task that represents the work queued to execute. // Simplified message activity handling to trigger appropriate adaptive card based on the message command protected override async Task OnMessageActivityAsync(ITurnContext turnContext, CancellationToken cancellationToken) { @@ -92,6 +111,12 @@ private async Task HandleCommandAsync(string command, ITurnContext + /// The OAuth Prompt needs to see the Invoke Activity in order to complete the login process. + /// + /// The context for the current turn. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// A task that represents the work queued to execute. // Override to handle invoke activities, such as OAuth and adaptive card actions protected override async Task OnInvokeActivityAsync(ITurnContext turnContext, CancellationToken cancellationToken) { @@ -143,7 +168,17 @@ protected override async Task OnInvokeActivityAsync(ITurnContext return null; } - // Method to create adaptive card invoke response with dynamic data + /// + /// Authentication success. + /// AuthToken or state is present. Verify token/state in invoke payload and return AC response + /// + /// authToken are absent, handle verb + /// state are absent, handle verb + /// The context for the current turn. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// Refresh type + /// AdaptiveCardResponse.json + /// A task that represents the work queued to execute. private InvokeResponse CreateAdaptiveCardInvokeResponseAsync(JObject authentication, string state, bool isBasicRefresh = false, string fileName = "AdaptiveCardResponse.json") { string authResultData = (authentication != null) ? "SSO success" : (state != null && state != "") ? "OAuth success" : "SSO/OAuth failed"; @@ -169,7 +204,12 @@ private InvokeResponse CreateAdaptiveCardInvokeResponseAsync(JObject authenticat return CreateInvokeResponse(adaptiveCardResponse); } - // Method to initiate SSO flow by sending OAuth card + /// + /// when token is absent in the invoke. We can initiate SSO in response to the invoke + /// + /// The context for the current turn. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// A task that represents the work queued to execute. private async Task InitiateSSOAsync(ITurnContext turnContext, CancellationToken cancellationToken) { var signInLink = await GetSignInLinkAsync(turnContext, cancellationToken).ConfigureAwait(false); @@ -202,6 +242,14 @@ private async Task InitiateSSOAsync(ITurnContext + /// Get Adaptive Card + /// + /// json path + /// Get sign in link + /// createdBy + /// createdById + /// // Method to retrieve adaptive card from a file and expand with dynamic data private Attachment GetAdaptiveCardFromFileName(string[] filepath, string signInLink, string name = null, string userMRI = null) { diff --git a/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Bots/TeamsBot.cs b/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Bots/TeamsBot.cs index f7588c1eaf..741c0eef48 100644 --- a/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Bots/TeamsBot.cs +++ b/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Bots/TeamsBot.cs @@ -29,6 +29,7 @@ public TeamsBot(ConversationState conversationState, UserState userState, MainDi /// /// Override this in a derived class to provide logic for when members, except the bot, join the conversation, such as your bot's welcome logic. /// + /// A list of all the members added to the conversation, as described by the conversation update activity. /// A strongly-typed context object for this turn. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. /// A task that represents the work queued to execute. diff --git a/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Controllers/BotController.cs b/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Controllers/BotController.cs index 5725551145..3ae911a6a6 100644 --- a/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Controllers/BotController.cs +++ b/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Controllers/BotController.cs @@ -11,9 +11,9 @@ namespace Microsoft.BotBuilderSamples { - // This ASP Controller is created to handle requests to the bot. - // Dependency Injection provides the Adapter and IBot at runtime. - // Multiple IBot implementations can be run at different endpoints. + // This ASP Controller is created to handle a request. Dependency Injection will provide the Adapter and IBot + // implementation at runtime. Multiple different IBot implementations running at different endpoints can be + // achieved by specifying a more specific type for the bot constructor argument. [Route("api/messages")] [ApiController] public class BotController : ControllerBase diff --git a/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Dialogs/MainDialog.cs b/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Dialogs/MainDialog.cs index 6b52bb58a0..ffbb29f3ea 100644 --- a/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Dialogs/MainDialog.cs +++ b/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Dialogs/MainDialog.cs @@ -1,4 +1,8 @@ -using System; +// +// Copyright (c) Microsoft. All rights reserved. +// + +using System; using Microsoft.Bot.Builder; using Microsoft.Bot.Builder.Dialogs; using Microsoft.Extensions.Logging; diff --git a/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Helper/SimpleGraphClient.cs b/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Helper/SimpleGraphClient.cs index 92fe84e69b..e459be362e 100644 --- a/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Helper/SimpleGraphClient.cs +++ b/samples/bot-sso-adaptivecard/csharp/BotSsoAdaptivecard/Helper/SimpleGraphClient.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) Microsoft. All rights reserved. //