diff --git a/runtime/dotnet/azurefunctions/Microsoft.BotFramework.Composer.Functions.csproj b/runtime/dotnet/azurefunctions/Microsoft.BotFramework.Composer.Functions.csproj index 23b45287f8..9e7914cf92 100644 --- a/runtime/dotnet/azurefunctions/Microsoft.BotFramework.Composer.Functions.csproj +++ b/runtime/dotnet/azurefunctions/Microsoft.BotFramework.Composer.Functions.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1 + netcoreapp3.0 v3 2eb1c70c-5cd2-4c08-8aab-df989c347067 @@ -12,7 +12,6 @@ ..\core\Microsoft.BotFramework.Composer.Core.ruleset - diff --git a/runtime/dotnet/azurefunctions/Startup.cs b/runtime/dotnet/azurefunctions/Startup.cs index 7b9d6c9264..e390ec2ff7 100644 --- a/runtime/dotnet/azurefunctions/Startup.cs +++ b/runtime/dotnet/azurefunctions/Startup.cs @@ -21,16 +21,14 @@ using Microsoft.Bot.Connector.Authentication; using Microsoft.BotFramework.Composer.Core; using Microsoft.BotFramework.Composer.Core.Settings; - -//using Microsoft.BotFramework.Composer.CustomAction; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Linq; using System.Reflection; -using System.Text; [assembly: FunctionsStartup(typeof(Microsoft.BotFramework.Composer.Functions.Startup))] @@ -38,44 +36,27 @@ namespace Microsoft.BotFramework.Composer.Functions { public class Startup : FunctionsStartup { - private IConfigurationRoot BuildConfiguration(string rootDirectory) - { - var config = new ConfigurationBuilder(); - - // Config precedence 1: root app.settings - config.SetBasePath(rootDirectory); - - // Config precedence 2: ComposerDialogs/settings settings which are injected by the composer publish - // Hard code the settings path to 'ComposerDialogs' for deployment - - var configFile = Path.GetFullPath(Path.Combine(rootDirectory, @"ComposerDialogs/settings/appsettings.json")); - config.AddJsonFile(configFile, optional: true, reloadOnChange: true); - - config.UseComposerSettings(); - - if (!Debugger.IsAttached) - { - config.AddUserSecrets(); - } - - config.AddEnvironmentVariables(); - - return config.Build(); - } + private const string AssetsDirectoryName = "ComposerDialogs"; + private const string SettingsRelativePath = "settings/appsettings.json"; + private const string WwwRoot = "wwwroot"; + private const string DialogFileExtension = ".dialog"; + private const string DefaultLanguageSetting = "DefaultLanguage"; + private const string EnglishLocale = "en-us"; public override void Configure(IFunctionsHostBuilder builder) { - var binDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); - var rootDirectory = Directory.GetParent(binDirectory).FullName; + // Get assets directory. + var assetsDirectory = GetAssetsDirectory(); - var rootConfiguration = BuildConfiguration(rootDirectory); + // Build configuration with assets. + var config = BuildConfiguration(assetsDirectory); var settings = new BotSettings(); - rootConfiguration.Bind(settings); + config.Bind(settings); var services = builder.Services; - services.AddSingleton(rootConfiguration); + services.AddSingleton(config); services.AddLogging(); @@ -138,25 +119,24 @@ public override void Configure(IFunctionsHostBuilder builder) services.AddSingleton(conversationState); // Resource explorer to track declarative assets - var resourceExplorer = new ResourceExplorer().AddFolder(Path.Combine(rootDirectory, settings?.Bot ?? ".")); + var resourceExplorer = new ResourceExplorer().AddFolder(assetsDirectory.FullName); services.AddSingleton(resourceExplorer); // Adapter services.AddSingleton(s => { // Retrieve required dependencies - //IConfiguration configuration = s.GetService(); IStorage storage = s.GetService(); UserState userState = s.GetService(); ConversationState conversationState = s.GetService(); TelemetryInitializerMiddleware telemetryInitializerMiddleware = s.GetService(); - var adapter = new BotFrameworkHttpAdapter(new ConfigurationCredentialProvider(rootConfiguration)); + var adapter = new BotFrameworkHttpAdapter(new ConfigurationCredentialProvider(config)); adapter .UseStorage(storage) .UseBotState(userState, conversationState) - .Use(new RegisterClassMiddleware(rootConfiguration)) + .Use(new RegisterClassMiddleware(config)) .Use(telemetryInitializerMiddleware); // Configure Middlewares @@ -174,7 +154,7 @@ public override void Configure(IFunctionsHostBuilder builder) return adapter; }); - var defaultLocale = rootConfiguration.GetValue("defaultLanguage") ?? "en-us"; + var defaultLocale = config.GetValue(DefaultLanguageSetting) ?? EnglishLocale; var removeRecipientMention = settings?.Feature?.RemoveRecipientMention ?? false; @@ -187,7 +167,7 @@ public override void Configure(IFunctionsHostBuilder builder) s.GetService(), s.GetService(), s.GetService(), - GetRootDialog(Path.Combine(rootDirectory, settings.Bot)), + GetRootDialog(assetsDirectory.FullName), defaultLocale, removeRecipientMention)); } @@ -221,7 +201,7 @@ private string GetRootDialog(string folderPath) var dir = new DirectoryInfo(folderPath); foreach (var f in dir.GetFiles()) { - if (f.Extension == ".dialog") + if (f.Extension == DialogFileExtension) { return f.Name; } @@ -234,5 +214,84 @@ private bool ConfigSectionValid(string val) { return !string.IsNullOrEmpty(val) && !val.StartsWith('<'); } + + private static DirectoryInfo GetAssetsDirectory() + { + // The directory structure in functions is as follows + // wwwroot + // | bin + // | ComposerDialogs + // | messages + // | ... + // + // However depending on the exact runtime environment and architecture (i.e. x64) + // there can be variations in the folder structure, for example having the binaries in + // an x64 subfolder within bin. + // To make this more flexible, we obtain the executing assembly location and navigate up + // the directory tree until wwwroot, and pick up the composer dialogs path from there, + // making this more robust. + + // Obtain executing binary directory + var binDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + + var parent = Directory.GetParent(binDirectory); + + // Navigate folder structure upwards until we find the folder, reach wwwroot or there are no + // more parents to navigate + while (parent != null) + { + // For the current directory, check if there is a child directory named + // 'ComposerDialogs' + var children = parent.EnumerateDirectories(AssetsDirectoryName); + + if (children.Any()) + { + // We found our assets directory! + return children.First(); + } + else + { + // In functions, if we reached wwwroot, we cannot go further up. Fail the operation + // so that an error is displayed in the functions telemetry and start page. + if (parent.Name.Contains(WwwRoot)) + { + throw new InvalidDataException($"Failed to start functions bot, could not find asset folder {AssetsDirectoryName} in path {parent.FullName}."); + } + else + { + // If we didn't reach wwwroot, keep going up. + parent = parent.Parent; + } + } + } + + // We should never be here unless we failed to find the folder. Throw clear exception. + throw new InvalidDataException($"Failed to start functions bot, could not find asset folder {AssetsDirectoryName}."); + } + + + private static IConfigurationRoot BuildConfiguration(DirectoryInfo assetsDirectory) + { + var config = new ConfigurationBuilder(); + + // Config precedence 1: root app.settings in case users add it. + // Note that the function root (wwwroot) is one directory above the assets dir. + config.SetBasePath(assetsDirectory.Parent.FullName); + + // Config precedence 2: ComposerDialogs/settings settings which are injected by the composer publish. + var configFile = Path.GetFullPath(Path.Combine(assetsDirectory.FullName, SettingsRelativePath)); + config.AddJsonFile(configFile, optional: true, reloadOnChange: true); + + config.UseComposerSettings(); + + if (!Debugger.IsAttached) + { + config.AddUserSecrets(); + } + + config.AddEnvironmentVariables(); + + return config.Build(); + } } }