From b99a8e2899d1a22401066dbfb736870992ad6d59 Mon Sep 17 00:00:00 2001 From: MrDave1999 Date: Mon, 23 Jan 2023 13:00:26 -0500 Subject: [PATCH 01/16] Updated dotenv package --- src/DentallApp.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DentallApp.csproj b/src/DentallApp.csproj index c4a3a438..082dcf94 100755 --- a/src/DentallApp.csproj +++ b/src/DentallApp.csproj @@ -29,7 +29,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all From 04fff5dd5863a8e3bf7e8a170ef8ad5577b9e023 Mon Sep 17 00:00:00 2001 From: MrDave1999 Date: Mon, 23 Jan 2023 13:02:01 -0500 Subject: [PATCH 02/16] Added the keys in .env.example for configuring Direct Line --- .env.example | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index ff5d3ed0..a3ee7721 100644 --- a/.env.example +++ b/.env.example @@ -39,4 +39,10 @@ REMINDER_CRON_EXPR=0 0 8 * * ? * # At 08:00:00am every day. MICROSOFT_APP_TYPE= MICROSOFT_APP_ID= MICROSOFT_APP_PASSWORD= -MICROSOFT_APP_TENANT_ID= \ No newline at end of file +MICROSOFT_APP_TENANT_ID= + +# +# Direct Line settings +# +DIRECT_LINE_SECRET= +DIRECT_LINE_BASE_URL=http://localhost:5000/ \ No newline at end of file From 6d924099edc6c4df09133b39d9128c4393150095 Mon Sep 17 00:00:00 2001 From: MrDave1999 Date: Mon, 23 Jan 2023 13:03:25 -0500 Subject: [PATCH 03/16] Added the class that represents the Direct Line configuration --- src/Configuration/DirectLineSettings.cs | 30 +++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/Configuration/DirectLineSettings.cs diff --git a/src/Configuration/DirectLineSettings.cs b/src/Configuration/DirectLineSettings.cs new file mode 100644 index 00000000..a44467bf --- /dev/null +++ b/src/Configuration/DirectLineSettings.cs @@ -0,0 +1,30 @@ +namespace DentallApp.Configuration; + +public class DirectLineSettings +{ + public const string DirectLineSecretSetting = "DIRECT_LINE_SECRET"; + public const string DirectLineBaseUrlSetting = "DIRECT_LINE_BASE_URL"; + public const string DefaultBaseUrl = "https://directline.botframework.com/"; + public const string DefaultProviderName = nameof(DirectLineAzureService); + + public string DirectLineSecret { get; set; } + private string DirectLineBaseUrl { get; set; } + + public string GetDirectLineBaseUrl() + => string.IsNullOrWhiteSpace(DirectLineBaseUrl) ? + DefaultBaseUrl : + DirectLineBaseUrl.TrimEnd('/') + "/"; + + public string GetProviderName() + { + var baseUrl = GetDirectLineBaseUrl(); + return string.IsNullOrWhiteSpace(baseUrl) ? + DefaultProviderName : + GetServiceName(baseUrl); + } + + private static string GetServiceName(string baseUrl) + => baseUrl.StartsWith(DefaultBaseUrl) ? + DefaultProviderName : + nameof(InDirectLineService); +} From 63e9f78e39f8d40a660ee7b50ba81752271398bb Mon Sep 17 00:00:00 2001 From: MrDave1999 Date: Mon, 23 Jan 2023 13:04:40 -0500 Subject: [PATCH 04/16] Added the error message in case the request to Direct Line fails --- src/Constants/ResponseMessages.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Constants/ResponseMessages.cs b/src/Constants/ResponseMessages.cs index 1f1f32de..94acd4cc 100644 --- a/src/Constants/ResponseMessages.cs +++ b/src/Constants/ResponseMessages.cs @@ -9,6 +9,7 @@ public class ResponseMessages public const string ResourceNotFoundMessage = "Recurso no encontrado."; public const string ResourceFromAnotherUserMessage = "El recurso es de otro usuario."; + public const string DirectLineTokenFailedMessage = "Direct Line token API call failed."; public const string InactiveUserAccountMessage = "Cuenta de usuario inactiva."; public const string EmailSuccessfullyVerifiedMessage = "Correo electrónico verificado con éxito."; public const string SuccessfulLoginMessage = "Ha iniciado la sesión con éxito."; From cc43152f1f08a1c7752ee2ad1e405de8aa8edc74 Mon Sep 17 00:00:00 2001 From: MrDave1999 Date: Mon, 23 Jan 2023 13:06:45 -0500 Subject: [PATCH 05/16] Added the class that represents the DirectLine response --- .../Chatbot/DirectLine/DTOs/DirectLineGetTokenDto.cs | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/Features/Chatbot/DirectLine/DTOs/DirectLineGetTokenDto.cs diff --git a/src/Features/Chatbot/DirectLine/DTOs/DirectLineGetTokenDto.cs b/src/Features/Chatbot/DirectLine/DTOs/DirectLineGetTokenDto.cs new file mode 100644 index 00000000..efd8943b --- /dev/null +++ b/src/Features/Chatbot/DirectLine/DTOs/DirectLineGetTokenDto.cs @@ -0,0 +1,9 @@ +namespace DentallApp.Features.Chatbot.DirectLine.DTOs; + +public class DirectLineGetTokenDto +{ + public string ConversationId { get; set; } + public string Token { get; set; } + [JsonProperty("expires_in")] + public int ExpiresIn { get; set; } +} From 993f2102f51479bd127fc6d4c09524383b17cb44 Mon Sep 17 00:00:00 2001 From: MrDave1999 Date: Mon, 23 Jan 2023 13:12:47 -0500 Subject: [PATCH 06/16] Added providers that implement Direct Line API * InDirectLine * Azure Bot --- .../Providers/DirectLineAzureService.cs | 60 +++++++++++++++++++ .../DirectLine/Providers/DirectLineService.cs | 30 ++++++++++ .../Providers/InDirectLineService.cs | 42 +++++++++++++ 3 files changed, 132 insertions(+) create mode 100644 src/Features/Chatbot/DirectLine/Providers/DirectLineAzureService.cs create mode 100644 src/Features/Chatbot/DirectLine/Providers/DirectLineService.cs create mode 100644 src/Features/Chatbot/DirectLine/Providers/InDirectLineService.cs diff --git a/src/Features/Chatbot/DirectLine/Providers/DirectLineAzureService.cs b/src/Features/Chatbot/DirectLine/Providers/DirectLineAzureService.cs new file mode 100644 index 00000000..9096396f --- /dev/null +++ b/src/Features/Chatbot/DirectLine/Providers/DirectLineAzureService.cs @@ -0,0 +1,60 @@ +namespace DentallApp.Features.Chatbot.DirectLine.Providers; + +/// +/// Represents the Direct Line channel of Azure Bot. +/// +public class DirectLineAzureService : DirectLineService +{ + private readonly DirectLineSettings _directLineSettings; + + public DirectLineAzureService(IHttpClientFactory httpFactory, DirectLineSettings directLineSettings) + : base(httpFactory, namedClient: nameof(DirectLineAzureService)) + { + _directLineSettings = directLineSettings; + } + + /// + /// Adds the prefix dl_ to the user ID, as required by the Direct Line API. + /// + /// The user ID to add the prefix. + /// The user ID with the prefix. + private static string AddPrefixToUserId(int userId) + => $"dl_{userId}"; + + /// + public async override Task> GetTokenAsync(int userId) + { + var requestBody = new + { + user = new + { + id = AddPrefixToUserId(userId) + } + }; + var tokenRequest = new HttpRequestMessage(HttpMethod.Post, RequestUri) + { + Headers = + { + { "Authorization", $"Bearer {_directLineSettings.DirectLineSecret}" }, + }, + Content = new StringContent(content: JsonConvert.SerializeObject(requestBody), + Encoding.UTF8, + MediaTypeNames.Application.Json), + }; + + var tokenResponseMessage = await Client.SendAsync(tokenRequest, default); + + if (!tokenResponseMessage.IsSuccessStatusCode) + return new Response { Message = DirectLineTokenFailedMessage }; + + var responseContentString = await tokenResponseMessage.Content.ReadAsStringAsync(); + var tokenResponse = JsonConvert.DeserializeObject(responseContentString); + + return new Response + { + Success = true, + Data = tokenResponse, + Message = GetResourceMessage + }; + } +} diff --git a/src/Features/Chatbot/DirectLine/Providers/DirectLineService.cs b/src/Features/Chatbot/DirectLine/Providers/DirectLineService.cs new file mode 100644 index 00000000..721b7ede --- /dev/null +++ b/src/Features/Chatbot/DirectLine/Providers/DirectLineService.cs @@ -0,0 +1,30 @@ +namespace DentallApp.Features.Chatbot.DirectLine.Providers; + +public abstract class DirectLineService +{ + public const string RequestUri = "v3/directline/tokens/generate"; + private readonly HttpClient _client; + protected HttpClient Client => _client; + + /// + /// Initializes a new instance of the class with a specified . + /// + /// An instance of . + public DirectLineService(HttpClient client) + => _client = client; + + /// + /// Initializes a new instance of the class with a specified factory and a reference with named client. + /// + /// A factory that can create instances. + /// The logical name of the client to create. + public DirectLineService(IHttpClientFactory httpFactory, string namedClient) + => _client = httpFactory.CreateClient(namedClient); + + /// + /// Gets the Direct Line token to access a single conversation associated with the bot. + /// + /// The ID of the authenticated user. + /// + public abstract Task> GetTokenAsync(int userId); +} diff --git a/src/Features/Chatbot/DirectLine/Providers/InDirectLineService.cs b/src/Features/Chatbot/DirectLine/Providers/InDirectLineService.cs new file mode 100644 index 00000000..11142723 --- /dev/null +++ b/src/Features/Chatbot/DirectLine/Providers/InDirectLineService.cs @@ -0,0 +1,42 @@ +namespace DentallApp.Features.Chatbot.DirectLine.Providers; + +/// +/// Represents an own implementation of Direct Line API that runs without Azure. +/// See . +/// +public class InDirectLineService : DirectLineService +{ + public InDirectLineService(IHttpClientFactory httpFactory) + : base(httpFactory, namedClient: nameof(InDirectLineService)) { } + + /// + public async override Task> GetTokenAsync(int userId) + { + var requestBody = new + { + userId = userId.ToString(), + password = "" + }; + var tokenRequest = new HttpRequestMessage(HttpMethod.Post, RequestUri) + { + Content = new StringContent(content: JsonConvert.SerializeObject(requestBody), + Encoding.UTF8, + MediaTypeNames.Application.Json), + }; + + var tokenResponseMessage = await Client.SendAsync(tokenRequest, default); + + if (!tokenResponseMessage.IsSuccessStatusCode) + return new Response { Message = DirectLineTokenFailedMessage }; + + var responseContentString = await tokenResponseMessage.Content.ReadAsStringAsync(); + var tokenResponse = JsonConvert.DeserializeObject(responseContentString); + + return new Response + { + Success = true, + Data = tokenResponse, + Message = GetResourceMessage + }; + } +} From 9774041f7812d6242182a5757116fa88802d3200 Mon Sep 17 00:00:00 2001 From: MrDave1999 Date: Mon, 23 Jan 2023 13:16:36 -0500 Subject: [PATCH 07/16] Added a route for generate a Direct Line token --- .../DirectLine/DirectLineController.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/Features/Chatbot/DirectLine/DirectLineController.cs diff --git a/src/Features/Chatbot/DirectLine/DirectLineController.cs b/src/Features/Chatbot/DirectLine/DirectLineController.cs new file mode 100644 index 00000000..4e3240c2 --- /dev/null +++ b/src/Features/Chatbot/DirectLine/DirectLineController.cs @@ -0,0 +1,21 @@ +namespace DentallApp.Features.Chatbot.DirectLine; + +[Route("directline/token")] +[ApiController] +public class DirectLineController : ControllerBase +{ + private readonly DirectLineService _directLineService; + + public DirectLineController(DirectLineService directLineService) + { + _directLineService = directLineService; + } + + [AuthorizeByRole(RolesName.BasicUser)] + [HttpGet] + public async Task>> Get() + { + var response = await _directLineService.GetTokenAsync(User.GetUserId()); + return response.Success ? Ok(response) : BadRequest(response); + } +} From e8fa5d8b7158ced0d8590d9a460d60865c5a981c Mon Sep 17 00:00:00 2001 From: MrDave1999 Date: Mon, 23 Jan 2023 13:18:34 -0500 Subject: [PATCH 08/16] Added extension method to register Direct Line service --- .../DirectLine/DirectLineExtensions.cs | 27 +++++++++++++++++++ .../Extensions/ServiceCollectionExtensions.cs | 3 ++- 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 src/Features/Chatbot/DirectLine/DirectLineExtensions.cs diff --git a/src/Features/Chatbot/DirectLine/DirectLineExtensions.cs b/src/Features/Chatbot/DirectLine/DirectLineExtensions.cs new file mode 100644 index 00000000..7905d7cb --- /dev/null +++ b/src/Features/Chatbot/DirectLine/DirectLineExtensions.cs @@ -0,0 +1,27 @@ +namespace DentallApp.Features.Chatbot.DirectLine; + +public static class DirectLineExtensions +{ + public static IServiceCollection AddDirectLineService(this IServiceCollection services) + { + var settings = new EnvBinder() + .AllowBindNonPublicProperties() + .Bind(); + + services.AddSingleton(settings); + var providerName = settings.GetProviderName(); + services.AddHttpClient( + name: providerName, + httpClient => httpClient.BaseAddress = new Uri(settings.GetDirectLineBaseUrl()) + ); + + services.AddTransient( + serviceType: typeof(DirectLineService), + implementationType: Type.GetType(GetProviderTypeName(providerName)) + ); + return services; + } + + private static string GetProviderTypeName(string providerName) + => $"{typeof(DirectLineService).Namespace}.{providerName}"; +} diff --git a/src/Features/Chatbot/Extensions/ServiceCollectionExtensions.cs b/src/Features/Chatbot/Extensions/ServiceCollectionExtensions.cs index 12396ba2..66d680c2 100644 --- a/src/Features/Chatbot/Extensions/ServiceCollectionExtensions.cs +++ b/src/Features/Chatbot/Extensions/ServiceCollectionExtensions.cs @@ -14,6 +14,8 @@ public static IServiceCollection AddBotServices(this IServiceCollection services services.AddSingleton(); services.AddScoped(); + services.AddDirectLineService(); + // Create the Bot Framework Authentication to be used with the Bot Adapter. services.AddSingleton(); @@ -22,7 +24,6 @@ public static IServiceCollection AddBotServices(this IServiceCollection services // Create the bot as a transient. In this case the ASP Controller is expecting an IBot. services.AddTransient>(); - return services; } } From 2f0bc8e923b845041b88cbe526482e442df9dfe5 Mon Sep 17 00:00:00 2001 From: MrDave1999 Date: Mon, 23 Jan 2023 13:19:14 -0500 Subject: [PATCH 09/16] Updated global usings --- src/GlobalUsings.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/GlobalUsings.cs b/src/GlobalUsings.cs index 08dc3f57..7af4a421 100644 --- a/src/GlobalUsings.cs +++ b/src/GlobalUsings.cs @@ -1,4 +1,7 @@ global using DentallApp.Features.Chatbot; +global using DentallApp.Features.Chatbot.DirectLine; +global using DentallApp.Features.Chatbot.DirectLine.Providers; +global using DentallApp.Features.Chatbot.DirectLine.DTOs; global using DentallApp.Features.Chatbot.Dialogs; global using DentallApp.Features.Chatbot.Factories; global using DentallApp.Features.Chatbot.Helpers; @@ -90,6 +93,7 @@ global using System.Data; global using System.Text; +global using System.Net.Mime; global using System.Text.Encodings.Web; global using System.Linq.Expressions; global using System.Reflection; @@ -112,7 +116,6 @@ global using Microsoft.AspNetCore.Authentication.JwtBearer; global using Microsoft.AspNetCore.Authorization; - global using Microsoft.EntityFrameworkCore; global using Microsoft.EntityFrameworkCore.Diagnostics; global using Microsoft.EntityFrameworkCore.Metadata.Builders; From d9e70dfb2bc075141b0b48bbd786fa8066ad49f0 Mon Sep 17 00:00:00 2001 From: MrDave1999 Date: Mon, 23 Jan 2023 13:22:12 -0500 Subject: [PATCH 10/16] Added unit tests for the 'DirectLineSettings' class --- .../Configuration/DirectLineSettingsTests.cs | 78 +++++++++++++++++++ tests/GlobalUsings.cs | 9 ++- 2 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 tests/Configuration/DirectLineSettingsTests.cs diff --git a/tests/Configuration/DirectLineSettingsTests.cs b/tests/Configuration/DirectLineSettingsTests.cs new file mode 100644 index 00000000..4b4d3eb0 --- /dev/null +++ b/tests/Configuration/DirectLineSettingsTests.cs @@ -0,0 +1,78 @@ +namespace DentallApp.Tests.Configuration; + +[TestClass] +public class DirectLineSettingsTests +{ + private const string LocalHostBaseUrl = "http://localhost:5000"; + private IEnvBinder _envBinder; + + [TestInitialize] + public void TestInitialize() + { + _envBinder = new EnvBinder().AllowBindNonPublicProperties(); + Environment.SetEnvironmentVariable(DirectLineSettings.DirectLineSecretSetting, "SECRET"); + } + + [TestMethod] + public void GetDirectLineBaseUrl_WhenDirectLineBaseUrlIsEmptyOrWhiteSpace_ShouldReturnsDefaultBaseUrl() + { + Environment.SetEnvironmentVariable(DirectLineSettings.DirectLineBaseUrlSetting, " "); + var settings = _envBinder.Bind(); + + var baseUrl = settings.GetDirectLineBaseUrl(); + + Assert.AreEqual(expected: DirectLineSettings.DefaultBaseUrl, actual: baseUrl); + } + + [DataTestMethod] + [DataRow(LocalHostBaseUrl + "/")] + [DataRow(LocalHostBaseUrl)] + [DataRow(LocalHostBaseUrl + "///")] + public void GetDirectLineBaseUrl_WhenDirectLineBaseUrlIsNotEmpty_ShouldReturnsBaseUrlWithSlashAtEnd(string value) + { + Environment.SetEnvironmentVariable(DirectLineSettings.DirectLineBaseUrlSetting, value); + var settings = _envBinder.Bind(); + + var baseUrl = settings.GetDirectLineBaseUrl(); + + Assert.AreEqual(expected: "http://localhost:5000/", actual: baseUrl); + } + + [TestMethod] + public void GetProviderName_WhenBaseUrlIsEmptyOrWhiteSpace_ShouldReturnsDefaultProviderName() + { + Environment.SetEnvironmentVariable(DirectLineSettings.DirectLineBaseUrlSetting, " "); + var settings = _envBinder.Bind(); + + var providerName = settings.GetProviderName(); + + Assert.AreEqual(expected: DirectLineSettings.DefaultProviderName, actual: providerName); + } + + [DataTestMethod] + [DataRow(DirectLineSettings.DefaultBaseUrl)] + [DataRow(DirectLineSettings.DefaultBaseUrl + "///")] + public void GetProviderName_WhenBaseUrlStartsWithDefaultBaseUrl_ShouldReturnsDefaultProviderName(string baseUrl) + { + Environment.SetEnvironmentVariable(DirectLineSettings.DirectLineBaseUrlSetting, baseUrl); + var settings = _envBinder.Bind(); + + var providerName = settings.GetProviderName(); + + Assert.AreEqual(expected: DirectLineSettings.DefaultProviderName, actual: providerName); + } + + [DataTestMethod] + [DataRow(LocalHostBaseUrl + "/")] + [DataRow(LocalHostBaseUrl)] + [DataRow(LocalHostBaseUrl + "///")] + public void GetProviderName_WhenBaseUrlNotStartsWithDefaultBaseUrl_ShouldReturnsInDirectLineService(string baseUrl) + { + Environment.SetEnvironmentVariable(DirectLineSettings.DirectLineBaseUrlSetting, baseUrl); + var settings = _envBinder.Bind(); + + var providerName = settings.GetProviderName(); + + Assert.AreEqual(expected: nameof(InDirectLineService), actual: providerName); + } +} diff --git a/tests/GlobalUsings.cs b/tests/GlobalUsings.cs index a2164de3..5c5e8603 100644 --- a/tests/GlobalUsings.cs +++ b/tests/GlobalUsings.cs @@ -6,6 +6,7 @@ global using Telerik.JustMock; global using System.Security.Claims; global using Newtonsoft.Json.Linq; +global using DotEnv.Core; global using DentallApp.Features.AvailabilityHours; global using DentallApp.Features.AvailabilityHours.DTOs; @@ -13,8 +14,6 @@ global using DentallApp.Features.WeekDays; global using DentallApp.Features.AppointmentCancellation; global using DentallApp.Features.AppointmentCancellation.DTOs; -global using DentallApp.Helpers.InstantMessaging; -global using DentallApp.Helpers.DateTimeHelpers; global using DentallApp.Features.SecurityToken; global using DentallApp.Features.Roles; global using DentallApp.Features.Appointments; @@ -23,15 +22,19 @@ global using DentallApp.Features.EmployeeSchedules.DTOs; global using DentallApp.Features.GeneralTreatments; global using DentallApp.Features.GeneralTreatments.DTOs; +global using DentallApp.Features.SpecificTreatments.DTOs; global using DentallApp.Features.PublicHolidays.Offices; global using DentallApp.Features.Chatbot; global using DentallApp.Features.Chatbot.Dialogs; global using DentallApp.Features.Chatbot.Models; +global using DentallApp.Features.Chatbot.DirectLine.Providers; + global using DentallApp.Entities; +global using DentallApp.Helpers.InstantMessaging; +global using DentallApp.Helpers.DateTimeHelpers; global using DentallApp.Configuration; global using DentallApp.DataAccess.Repositories; global using DentallApp.Responses; -global using DentallApp.Features.SpecificTreatments.DTOs; global using static DentallApp.Constants.ResponseMessages; global using static DentallApp.Tests.Features.Chatbot.BotServiceMockFactory; From 6c2e44d0743df0bdd928596f0071d691307fdc47 Mon Sep 17 00:00:00 2001 From: MrDave1999 Date: Mon, 23 Jan 2023 18:05:29 -0500 Subject: [PATCH 11/16] Changed the response that returns the action --- src/Features/Chatbot/DirectLine/DirectLineController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Features/Chatbot/DirectLine/DirectLineController.cs b/src/Features/Chatbot/DirectLine/DirectLineController.cs index 4e3240c2..56ae38be 100644 --- a/src/Features/Chatbot/DirectLine/DirectLineController.cs +++ b/src/Features/Chatbot/DirectLine/DirectLineController.cs @@ -13,9 +13,9 @@ public DirectLineController(DirectLineService directLineService) [AuthorizeByRole(RolesName.BasicUser)] [HttpGet] - public async Task>> Get() + public async Task Get() { var response = await _directLineService.GetTokenAsync(User.GetUserId()); - return response.Success ? Ok(response) : BadRequest(response); + return response.Success ? Ok(response.Data.Token) : BadRequest(response.Message); } } From 1bf029612d6b56e84694c4e691b448548ea26eb8 Mon Sep 17 00:00:00 2001 From: MrDave1999 Date: Mon, 23 Jan 2023 21:28:11 -0500 Subject: [PATCH 12/16] Added missing property to the response --- src/Features/Chatbot/DirectLine/DirectLineController.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Features/Chatbot/DirectLine/DirectLineController.cs b/src/Features/Chatbot/DirectLine/DirectLineController.cs index 56ae38be..aeb15160 100644 --- a/src/Features/Chatbot/DirectLine/DirectLineController.cs +++ b/src/Features/Chatbot/DirectLine/DirectLineController.cs @@ -16,6 +16,8 @@ public DirectLineController(DirectLineService directLineService) public async Task Get() { var response = await _directLineService.GetTokenAsync(User.GetUserId()); - return response.Success ? Ok(response.Data.Token) : BadRequest(response.Message); + return response.Success ? + Ok(new { response.Data.Token }) : + BadRequest(new { response.Message }); } } From b2455b12b2927e14a79dc7796bf59fa0c8054342 Mon Sep 17 00:00:00 2001 From: MrDave1999 Date: Tue, 24 Jan 2023 11:11:42 -0500 Subject: [PATCH 13/16] Removed unnecessary files --- .../Providers/DirectLineAzureService.cs | 60 ------------------- .../DirectLine/Providers/DirectLineService.cs | 30 ---------- .../Providers/InDirectLineService.cs | 42 ------------- 3 files changed, 132 deletions(-) delete mode 100644 src/Features/Chatbot/DirectLine/Providers/DirectLineAzureService.cs delete mode 100644 src/Features/Chatbot/DirectLine/Providers/DirectLineService.cs delete mode 100644 src/Features/Chatbot/DirectLine/Providers/InDirectLineService.cs diff --git a/src/Features/Chatbot/DirectLine/Providers/DirectLineAzureService.cs b/src/Features/Chatbot/DirectLine/Providers/DirectLineAzureService.cs deleted file mode 100644 index 9096396f..00000000 --- a/src/Features/Chatbot/DirectLine/Providers/DirectLineAzureService.cs +++ /dev/null @@ -1,60 +0,0 @@ -namespace DentallApp.Features.Chatbot.DirectLine.Providers; - -/// -/// Represents the Direct Line channel of Azure Bot. -/// -public class DirectLineAzureService : DirectLineService -{ - private readonly DirectLineSettings _directLineSettings; - - public DirectLineAzureService(IHttpClientFactory httpFactory, DirectLineSettings directLineSettings) - : base(httpFactory, namedClient: nameof(DirectLineAzureService)) - { - _directLineSettings = directLineSettings; - } - - /// - /// Adds the prefix dl_ to the user ID, as required by the Direct Line API. - /// - /// The user ID to add the prefix. - /// The user ID with the prefix. - private static string AddPrefixToUserId(int userId) - => $"dl_{userId}"; - - /// - public async override Task> GetTokenAsync(int userId) - { - var requestBody = new - { - user = new - { - id = AddPrefixToUserId(userId) - } - }; - var tokenRequest = new HttpRequestMessage(HttpMethod.Post, RequestUri) - { - Headers = - { - { "Authorization", $"Bearer {_directLineSettings.DirectLineSecret}" }, - }, - Content = new StringContent(content: JsonConvert.SerializeObject(requestBody), - Encoding.UTF8, - MediaTypeNames.Application.Json), - }; - - var tokenResponseMessage = await Client.SendAsync(tokenRequest, default); - - if (!tokenResponseMessage.IsSuccessStatusCode) - return new Response { Message = DirectLineTokenFailedMessage }; - - var responseContentString = await tokenResponseMessage.Content.ReadAsStringAsync(); - var tokenResponse = JsonConvert.DeserializeObject(responseContentString); - - return new Response - { - Success = true, - Data = tokenResponse, - Message = GetResourceMessage - }; - } -} diff --git a/src/Features/Chatbot/DirectLine/Providers/DirectLineService.cs b/src/Features/Chatbot/DirectLine/Providers/DirectLineService.cs deleted file mode 100644 index 721b7ede..00000000 --- a/src/Features/Chatbot/DirectLine/Providers/DirectLineService.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace DentallApp.Features.Chatbot.DirectLine.Providers; - -public abstract class DirectLineService -{ - public const string RequestUri = "v3/directline/tokens/generate"; - private readonly HttpClient _client; - protected HttpClient Client => _client; - - /// - /// Initializes a new instance of the class with a specified . - /// - /// An instance of . - public DirectLineService(HttpClient client) - => _client = client; - - /// - /// Initializes a new instance of the class with a specified factory and a reference with named client. - /// - /// A factory that can create instances. - /// The logical name of the client to create. - public DirectLineService(IHttpClientFactory httpFactory, string namedClient) - => _client = httpFactory.CreateClient(namedClient); - - /// - /// Gets the Direct Line token to access a single conversation associated with the bot. - /// - /// The ID of the authenticated user. - /// - public abstract Task> GetTokenAsync(int userId); -} diff --git a/src/Features/Chatbot/DirectLine/Providers/InDirectLineService.cs b/src/Features/Chatbot/DirectLine/Providers/InDirectLineService.cs deleted file mode 100644 index 11142723..00000000 --- a/src/Features/Chatbot/DirectLine/Providers/InDirectLineService.cs +++ /dev/null @@ -1,42 +0,0 @@ -namespace DentallApp.Features.Chatbot.DirectLine.Providers; - -/// -/// Represents an own implementation of Direct Line API that runs without Azure. -/// See . -/// -public class InDirectLineService : DirectLineService -{ - public InDirectLineService(IHttpClientFactory httpFactory) - : base(httpFactory, namedClient: nameof(InDirectLineService)) { } - - /// - public async override Task> GetTokenAsync(int userId) - { - var requestBody = new - { - userId = userId.ToString(), - password = "" - }; - var tokenRequest = new HttpRequestMessage(HttpMethod.Post, RequestUri) - { - Content = new StringContent(content: JsonConvert.SerializeObject(requestBody), - Encoding.UTF8, - MediaTypeNames.Application.Json), - }; - - var tokenResponseMessage = await Client.SendAsync(tokenRequest, default); - - if (!tokenResponseMessage.IsSuccessStatusCode) - return new Response { Message = DirectLineTokenFailedMessage }; - - var responseContentString = await tokenResponseMessage.Content.ReadAsStringAsync(); - var tokenResponse = JsonConvert.DeserializeObject(responseContentString); - - return new Response - { - Success = true, - Data = tokenResponse, - Message = GetResourceMessage - }; - } -} From 2ced25ab820ad5caaacbc23bd859430c1f761aad Mon Sep 17 00:00:00 2001 From: MrDave1999 Date: Tue, 24 Jan 2023 11:12:53 -0500 Subject: [PATCH 14/16] Renamed class names --- .../Services/DirectLineAzureService.cs | 60 +++++++++++++++++++ .../DirectLine/Services/DirectLineService.cs | 33 ++++++++++ .../Services/InDirectLineService.cs | 42 +++++++++++++ src/GlobalUsings.cs | 2 +- tests/GlobalUsings.cs | 2 +- 5 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 src/Features/Chatbot/DirectLine/Services/DirectLineAzureService.cs create mode 100644 src/Features/Chatbot/DirectLine/Services/DirectLineService.cs create mode 100644 src/Features/Chatbot/DirectLine/Services/InDirectLineService.cs diff --git a/src/Features/Chatbot/DirectLine/Services/DirectLineAzureService.cs b/src/Features/Chatbot/DirectLine/Services/DirectLineAzureService.cs new file mode 100644 index 00000000..e4235be7 --- /dev/null +++ b/src/Features/Chatbot/DirectLine/Services/DirectLineAzureService.cs @@ -0,0 +1,60 @@ +namespace DentallApp.Features.Chatbot.DirectLine.Services; + +/// +/// Represents the Direct Line channel of Azure Bot. +/// +public class DirectLineAzureService : DirectLineService +{ + private readonly DirectLineSettings _directLineSettings; + + public DirectLineAzureService(IHttpClientFactory httpFactory, DirectLineSettings directLineSettings) + : base(httpFactory, namedClient: nameof(DirectLineAzureService)) + { + _directLineSettings = directLineSettings; + } + + /// + /// Adds the prefix dl_ to the user ID, as required by the Direct Line API. + /// + /// The user ID to add the prefix. + /// The user ID with the prefix. + private static string AddPrefixToUserId(int userId) + => $"dl_{userId}"; + + /// + public async override Task> GetTokenAsync(int userId) + { + var requestBody = new + { + user = new + { + id = AddPrefixToUserId(userId) + } + }; + var tokenRequest = new HttpRequestMessage(HttpMethod.Post, RequestUri) + { + Headers = + { + { "Authorization", $"Bearer {_directLineSettings.DirectLineSecret}" }, + }, + Content = new StringContent(content: JsonConvert.SerializeObject(requestBody), + Encoding.UTF8, + MediaTypeNames.Application.Json), + }; + + var tokenResponseMessage = await Client.SendAsync(tokenRequest, default); + + if (!tokenResponseMessage.IsSuccessStatusCode) + return new Response { Message = DirectLineTokenFailedMessage }; + + var responseContentString = await tokenResponseMessage.Content.ReadAsStringAsync(); + var tokenResponse = JsonConvert.DeserializeObject(responseContentString); + + return new Response + { + Success = true, + Data = tokenResponse, + Message = GetResourceMessage + }; + } +} diff --git a/src/Features/Chatbot/DirectLine/Services/DirectLineService.cs b/src/Features/Chatbot/DirectLine/Services/DirectLineService.cs new file mode 100644 index 00000000..1ac56f26 --- /dev/null +++ b/src/Features/Chatbot/DirectLine/Services/DirectLineService.cs @@ -0,0 +1,33 @@ +namespace DentallApp.Features.Chatbot.DirectLine.Services; + +/// +/// Represents the client that obtains the Direct Line token. +/// +public abstract class DirectLineService +{ + public const string RequestUri = "v3/directline/tokens/generate"; + private readonly HttpClient _client; + protected HttpClient Client => _client; + + /// + /// Initializes a new instance of the class with a specified . + /// + /// An instance of . + public DirectLineService(HttpClient client) + => _client = client; + + /// + /// Initializes a new instance of the class with a specified factory and a reference with named client. + /// + /// A factory that can create instances. + /// The logical name of the client to create. + public DirectLineService(IHttpClientFactory httpFactory, string namedClient) + => _client = httpFactory.CreateClient(namedClient); + + /// + /// Gets the Direct Line token to access a single conversation associated with the bot. + /// + /// The ID of the authenticated user. + /// + public abstract Task> GetTokenAsync(int userId); +} diff --git a/src/Features/Chatbot/DirectLine/Services/InDirectLineService.cs b/src/Features/Chatbot/DirectLine/Services/InDirectLineService.cs new file mode 100644 index 00000000..7356a469 --- /dev/null +++ b/src/Features/Chatbot/DirectLine/Services/InDirectLineService.cs @@ -0,0 +1,42 @@ +namespace DentallApp.Features.Chatbot.DirectLine.Services; + +/// +/// Represents an own implementation of Direct Line API that runs without Azure. +/// See . +/// +public class InDirectLineService : DirectLineService +{ + public InDirectLineService(IHttpClientFactory httpFactory) + : base(httpFactory, namedClient: nameof(InDirectLineService)) { } + + /// + public async override Task> GetTokenAsync(int userId) + { + var requestBody = new + { + userId = userId.ToString(), + password = "" + }; + var tokenRequest = new HttpRequestMessage(HttpMethod.Post, RequestUri) + { + Content = new StringContent(content: JsonConvert.SerializeObject(requestBody), + Encoding.UTF8, + MediaTypeNames.Application.Json), + }; + + var tokenResponseMessage = await Client.SendAsync(tokenRequest, default); + + if (!tokenResponseMessage.IsSuccessStatusCode) + return new Response { Message = DirectLineTokenFailedMessage }; + + var responseContentString = await tokenResponseMessage.Content.ReadAsStringAsync(); + var tokenResponse = JsonConvert.DeserializeObject(responseContentString); + + return new Response + { + Success = true, + Data = tokenResponse, + Message = GetResourceMessage + }; + } +} diff --git a/src/GlobalUsings.cs b/src/GlobalUsings.cs index 7af4a421..112a7911 100644 --- a/src/GlobalUsings.cs +++ b/src/GlobalUsings.cs @@ -1,6 +1,6 @@ global using DentallApp.Features.Chatbot; global using DentallApp.Features.Chatbot.DirectLine; -global using DentallApp.Features.Chatbot.DirectLine.Providers; +global using DentallApp.Features.Chatbot.DirectLine.Services; global using DentallApp.Features.Chatbot.DirectLine.DTOs; global using DentallApp.Features.Chatbot.Dialogs; global using DentallApp.Features.Chatbot.Factories; diff --git a/tests/GlobalUsings.cs b/tests/GlobalUsings.cs index 5c5e8603..de5394fd 100644 --- a/tests/GlobalUsings.cs +++ b/tests/GlobalUsings.cs @@ -27,7 +27,7 @@ global using DentallApp.Features.Chatbot; global using DentallApp.Features.Chatbot.Dialogs; global using DentallApp.Features.Chatbot.Models; -global using DentallApp.Features.Chatbot.DirectLine.Providers; +global using DentallApp.Features.Chatbot.DirectLine.Services; global using DentallApp.Entities; global using DentallApp.Helpers.InstantMessaging; From 144168ce4e4c0a7102b4b39cad523505174d49c4 Mon Sep 17 00:00:00 2001 From: MrDave1999 Date: Tue, 24 Jan 2023 11:35:13 -0500 Subject: [PATCH 15/16] Renamed methods and local variables to improve readability * Added missing XML comments --- src/Configuration/DirectLineSettings.cs | 21 ++++++++++++++----- .../DirectLine/DirectLineExtensions.cs | 10 ++++----- .../Configuration/DirectLineSettingsTests.cs | 18 ++++++++-------- 3 files changed, 30 insertions(+), 19 deletions(-) diff --git a/src/Configuration/DirectLineSettings.cs b/src/Configuration/DirectLineSettings.cs index a44467bf..62b7cc45 100644 --- a/src/Configuration/DirectLineSettings.cs +++ b/src/Configuration/DirectLineSettings.cs @@ -5,7 +5,7 @@ public class DirectLineSettings public const string DirectLineSecretSetting = "DIRECT_LINE_SECRET"; public const string DirectLineBaseUrlSetting = "DIRECT_LINE_BASE_URL"; public const string DefaultBaseUrl = "https://directline.botframework.com/"; - public const string DefaultProviderName = nameof(DirectLineAzureService); + public const string DefaultServiceName = nameof(DirectLineAzureService); public string DirectLineSecret { get; set; } private string DirectLineBaseUrl { get; set; } @@ -15,16 +15,27 @@ public string GetDirectLineBaseUrl() DefaultBaseUrl : DirectLineBaseUrl.TrimEnd('/') + "/"; - public string GetProviderName() + /// + /// Gets the name of the Direct Line service based on the URL loaded from the .env file. + /// + /// + /// Available services: + /// + /// + /// + /// + /// + /// The name of the Direct Line service. + public string GetServiceName() { var baseUrl = GetDirectLineBaseUrl(); return string.IsNullOrWhiteSpace(baseUrl) ? - DefaultProviderName : + DefaultServiceName : GetServiceName(baseUrl); } - private static string GetServiceName(string baseUrl) + private string GetServiceName(string baseUrl) => baseUrl.StartsWith(DefaultBaseUrl) ? - DefaultProviderName : + DefaultServiceName : nameof(InDirectLineService); } diff --git a/src/Features/Chatbot/DirectLine/DirectLineExtensions.cs b/src/Features/Chatbot/DirectLine/DirectLineExtensions.cs index 7905d7cb..1c3eefb8 100644 --- a/src/Features/Chatbot/DirectLine/DirectLineExtensions.cs +++ b/src/Features/Chatbot/DirectLine/DirectLineExtensions.cs @@ -9,19 +9,19 @@ public static IServiceCollection AddDirectLineService(this IServiceCollection se .Bind(); services.AddSingleton(settings); - var providerName = settings.GetProviderName(); + var serviceName = settings.GetServiceName(); services.AddHttpClient( - name: providerName, + name: serviceName, httpClient => httpClient.BaseAddress = new Uri(settings.GetDirectLineBaseUrl()) ); services.AddTransient( serviceType: typeof(DirectLineService), - implementationType: Type.GetType(GetProviderTypeName(providerName)) + implementationType: Type.GetType(typeName: GetServiceNameWithNamespace(serviceName)) ); return services; } - private static string GetProviderTypeName(string providerName) - => $"{typeof(DirectLineService).Namespace}.{providerName}"; + private static string GetServiceNameWithNamespace(string serviceName) + => $"{typeof(DirectLineService).Namespace}.{serviceName}"; } diff --git a/tests/Configuration/DirectLineSettingsTests.cs b/tests/Configuration/DirectLineSettingsTests.cs index 4b4d3eb0..81b617c6 100644 --- a/tests/Configuration/DirectLineSettingsTests.cs +++ b/tests/Configuration/DirectLineSettingsTests.cs @@ -39,40 +39,40 @@ public void GetDirectLineBaseUrl_WhenDirectLineBaseUrlIsNotEmpty_ShouldReturnsBa } [TestMethod] - public void GetProviderName_WhenBaseUrlIsEmptyOrWhiteSpace_ShouldReturnsDefaultProviderName() + public void GetServiceName_WhenBaseUrlIsEmptyOrWhiteSpace_ShouldReturnsDefaultServiceName() { Environment.SetEnvironmentVariable(DirectLineSettings.DirectLineBaseUrlSetting, " "); var settings = _envBinder.Bind(); - var providerName = settings.GetProviderName(); + var serviceName = settings.GetServiceName(); - Assert.AreEqual(expected: DirectLineSettings.DefaultProviderName, actual: providerName); + Assert.AreEqual(expected: DirectLineSettings.DefaultServiceName, actual: serviceName); } [DataTestMethod] [DataRow(DirectLineSettings.DefaultBaseUrl)] [DataRow(DirectLineSettings.DefaultBaseUrl + "///")] - public void GetProviderName_WhenBaseUrlStartsWithDefaultBaseUrl_ShouldReturnsDefaultProviderName(string baseUrl) + public void GetServiceName_WhenBaseUrlStartsWithDefaultBaseUrl_ShouldReturnsDefaultServiceName(string baseUrl) { Environment.SetEnvironmentVariable(DirectLineSettings.DirectLineBaseUrlSetting, baseUrl); var settings = _envBinder.Bind(); - var providerName = settings.GetProviderName(); + var serviceName = settings.GetServiceName(); - Assert.AreEqual(expected: DirectLineSettings.DefaultProviderName, actual: providerName); + Assert.AreEqual(expected: DirectLineSettings.DefaultServiceName, actual: serviceName); } [DataTestMethod] [DataRow(LocalHostBaseUrl + "/")] [DataRow(LocalHostBaseUrl)] [DataRow(LocalHostBaseUrl + "///")] - public void GetProviderName_WhenBaseUrlNotStartsWithDefaultBaseUrl_ShouldReturnsInDirectLineService(string baseUrl) + public void GetServiceName_WhenBaseUrlNotStartsWithDefaultBaseUrl_ShouldReturnsInDirectLineService(string baseUrl) { Environment.SetEnvironmentVariable(DirectLineSettings.DirectLineBaseUrlSetting, baseUrl); var settings = _envBinder.Bind(); - var providerName = settings.GetProviderName(); + var serviceName = settings.GetServiceName(); - Assert.AreEqual(expected: nameof(InDirectLineService), actual: providerName); + Assert.AreEqual(expected: nameof(InDirectLineService), actual: serviceName); } } From 04125c1872976b5e065ed84a32e7ebcfec6e041e Mon Sep 17 00:00:00 2001 From: MrDave1999 Date: Tue, 24 Jan 2023 11:47:08 -0500 Subject: [PATCH 16/16] Moved 'DirectLineSettings' class to the 'DirectLine' layer --- .../Chatbot/DirectLine}/DirectLineSettings.cs | 8 ++++---- .../Chatbot/DirectLine}/DirectLineSettingsTests.cs | 2 +- tests/GlobalUsings.cs | 1 + 3 files changed, 6 insertions(+), 5 deletions(-) rename src/{Configuration => Features/Chatbot/DirectLine}/DirectLineSettings.cs (88%) rename tests/{Configuration => Features/Chatbot/DirectLine}/DirectLineSettingsTests.cs (98%) diff --git a/src/Configuration/DirectLineSettings.cs b/src/Features/Chatbot/DirectLine/DirectLineSettings.cs similarity index 88% rename from src/Configuration/DirectLineSettings.cs rename to src/Features/Chatbot/DirectLine/DirectLineSettings.cs index 62b7cc45..feec8a50 100644 --- a/src/Configuration/DirectLineSettings.cs +++ b/src/Features/Chatbot/DirectLine/DirectLineSettings.cs @@ -1,18 +1,18 @@ -namespace DentallApp.Configuration; +namespace DentallApp.Features.Chatbot.DirectLine; public class DirectLineSettings { public const string DirectLineSecretSetting = "DIRECT_LINE_SECRET"; public const string DirectLineBaseUrlSetting = "DIRECT_LINE_BASE_URL"; - public const string DefaultBaseUrl = "https://directline.botframework.com/"; + public const string DefaultBaseUrl = "https://directline.botframework.com/"; public const string DefaultServiceName = nameof(DirectLineAzureService); public string DirectLineSecret { get; set; } private string DirectLineBaseUrl { get; set; } public string GetDirectLineBaseUrl() - => string.IsNullOrWhiteSpace(DirectLineBaseUrl) ? - DefaultBaseUrl : + => string.IsNullOrWhiteSpace(DirectLineBaseUrl) ? + DefaultBaseUrl : DirectLineBaseUrl.TrimEnd('/') + "/"; /// diff --git a/tests/Configuration/DirectLineSettingsTests.cs b/tests/Features/Chatbot/DirectLine/DirectLineSettingsTests.cs similarity index 98% rename from tests/Configuration/DirectLineSettingsTests.cs rename to tests/Features/Chatbot/DirectLine/DirectLineSettingsTests.cs index 81b617c6..72f25558 100644 --- a/tests/Configuration/DirectLineSettingsTests.cs +++ b/tests/Features/Chatbot/DirectLine/DirectLineSettingsTests.cs @@ -1,4 +1,4 @@ -namespace DentallApp.Tests.Configuration; +namespace DentallApp.Tests.Features.Chatbot.DirectLine; [TestClass] public class DirectLineSettingsTests diff --git a/tests/GlobalUsings.cs b/tests/GlobalUsings.cs index de5394fd..8b80a356 100644 --- a/tests/GlobalUsings.cs +++ b/tests/GlobalUsings.cs @@ -27,6 +27,7 @@ global using DentallApp.Features.Chatbot; global using DentallApp.Features.Chatbot.Dialogs; global using DentallApp.Features.Chatbot.Models; +global using DentallApp.Features.Chatbot.DirectLine; global using DentallApp.Features.Chatbot.DirectLine.Services; global using DentallApp.Entities;