Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>netcoreapp3.0</TargetFramework>
<AzureFunctionsVersion>v3</AzureFunctionsVersion>
<UserSecretsId>2eb1c70c-5cd2-4c08-8aab-df989c347067</UserSecretsId>
</PropertyGroup>
Expand All @@ -12,7 +12,6 @@
<CodeAnalysisRuleSet>..\core\Microsoft.BotFramework.Composer.Core.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.2" />
<PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="1.0.0" />
<PackageReference Include="Microsoft.Bot.Builder" Version="4.10.5" />
<PackageReference Include="Microsoft.Bot.Builder.AI.Luis" Version="4.10.5" />
Expand Down
137 changes: 98 additions & 39 deletions runtime/dotnet/azurefunctions/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,61 +21,42 @@
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))]

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<Startup>();
}

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<IConfiguration>(rootConfiguration);
services.AddSingleton<IConfiguration>(config);

services.AddLogging();

Expand Down Expand Up @@ -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<IBotFrameworkHttpAdapter, BotFrameworkHttpAdapter>(s =>
{
// Retrieve required dependencies
//IConfiguration configuration = s.GetService<IConfiguration>();
IStorage storage = s.GetService<IStorage>();
UserState userState = s.GetService<UserState>();
ConversationState conversationState = s.GetService<ConversationState>();
TelemetryInitializerMiddleware telemetryInitializerMiddleware = s.GetService<TelemetryInitializerMiddleware>();

var adapter = new BotFrameworkHttpAdapter(new ConfigurationCredentialProvider(rootConfiguration));
var adapter = new BotFrameworkHttpAdapter(new ConfigurationCredentialProvider(config));

adapter
.UseStorage(storage)
.UseBotState(userState, conversationState)
.Use(new RegisterClassMiddleware<IConfiguration>(rootConfiguration))
.Use(new RegisterClassMiddleware<IConfiguration>(config))
.Use(telemetryInitializerMiddleware);

// Configure Middlewares
Expand All @@ -174,7 +154,7 @@ public override void Configure(IFunctionsHostBuilder builder)
return adapter;
});

var defaultLocale = rootConfiguration.GetValue<string>("defaultLanguage") ?? "en-us";
var defaultLocale = config.GetValue<string>(DefaultLanguageSetting) ?? EnglishLocale;

var removeRecipientMention = settings?.Feature?.RemoveRecipientMention ?? false;

Expand All @@ -187,7 +167,7 @@ public override void Configure(IFunctionsHostBuilder builder)
s.GetService<BotFrameworkClient>(),
s.GetService<SkillConversationIdFactoryBase>(),
s.GetService<IBotTelemetryClient>(),
GetRootDialog(Path.Combine(rootDirectory, settings.Bot)),
GetRootDialog(assetsDirectory.FullName),
defaultLocale,
removeRecipientMention));
}
Expand Down Expand Up @@ -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;
}
Expand All @@ -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<Startup>();
}

config.AddEnvironmentVariables();

return config.Build();
}
}
}