Bot Framework V4: Value cannot be null. (Parameter 'uriString') - c#

I have been following the tutorial (Bot Framework Integrate Composer Into Skill Project) to integrate composer dialogs with a skill project. This includes composer created dialogs which call a skill. I am running into the exception "Value cannot be null. (Parameter 'uriString')" when calling the skill.
I ran into various other issues when setting this up, as outlined here Previous Issues, which have been resolved though I had to do a work around to get the settings to work. I now need to determine why the configuration settings are not resolving.
To recreate the issue use the "skill-setting-issue" branch of the repo Git Repo
Prerequisites
Install Bot Emulator:Bot Emulator Install Instructions
Open the solution file in visual studio Integrate-Composer-Dialog-Using-Skill.sln
Put a breakpoint on line 79 of DefaultAdapter.cs - this is where the error can be seen
Start debugging the project
Open Bot Emulator
Connect to the bot: http://localhost:3978/api/messages
Type "Greeting"
Bot should respond with "Hello, I have recognized that you said greeting" - This means that the composer dialog integration is working as expected.
Type "Skill"
The breakpoint on line 79 of DefaultAdapter.cs should trigger giving details of the error.
The error seems to be occurring because the settings values within Composer-With-Skill.dialog between line 52 & 57 cannot be resolved.
"botId": "=settings.MicrosoftAppId",
"skillHostEndpoint": "=settings.skillHostEndpoint",
"connectionName": "=settings.connectionName",
"allowInterruptions": true,
"skillEndpoint": "=settings.skill['integrateComposerDialogUsingSkill'].endpointUrl",
"skillAppId": "=settings.skill['integrateComposerDialogUsingSkill'].msAppId",
If I replace the portions which use =settings.... with actual values then the application works (see master branch for code which has that setup).
e.g.
"botId": "=settings.MicrosoftAppId",
"skillHostEndpoint": "http://localhost:3978/api/skills",
"connectionName": "=settings.connectionName",
"allowInterruptions": true,
"skillEndpoint": "http://localhost:3978/api/echo/messages",
"skillAppId": "00000000-0000-0000-0000-000000000000",
Note that botId & connectionName are not used so do not cause runtime errors.
These values should be retrieved from ComposerDialogs\settings\appsettings.json line 67 through 79.
"skill": {
"integrateComposerDialogUsingSkill": {
"endpointUrl": "http://localhost:3978/api/echo/messages",
"msAppId": "00000000-0000-0000-0000-000000000000"
}
},
"defaultLanguage": "en-us",
"languages": [
"en-us"
],
"customFunctions": [],
"skillHostEndpoint": "http://localhost:3978/api/skills"
}
ComposerDialogs\settings\appsettings.json has been configured to be a setting file in the application in Startup.cs line 51 as per the tutorial I am following.
public class Startup
{
public Startup(IWebHostEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddJsonFile("cognitivemodels.json", optional: true)
.AddJsonFile($"cognitivemodels.{env.EnvironmentName}.json", optional: true)
//from instructions: https://microsoft.github.io/botframework-solutions/skills/handbook/experimental-add-composer/
.AddJsonFile($"ComposerDialogs\\settings\\appsettings.json", optional: true)
.AddEnvironmentVariables();
I'm not sure why the variables in the Composer-With-Skill.dialog file are not being resolved by the settings values in ComposerDialogs\settings\appsettings.json
The error occurs on line 156 of file Microsoft.Bot.Builder.Dialogs.Adaptive.BeginSkill in method BeginDialogAsync
Bot Builder Git - BeginSkill.cs when the code tries to populate the DialogOptions.SkillHostEndpoint value.
public override async Task<DialogTurnResult> BeginDialogAsync(DialogContext dc, object options = null, CancellationToken cancellationToken = default)
{
if (Disabled != null && Disabled.GetValue(dc.State))
{
return await dc.EndDialogAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
}
// Update the dialog options with the runtime settings.
DialogOptions.BotId = BotId.GetValue(dc.State);
DialogOptions.SkillHostEndpoint = new Uri(SkillHostEndpoint.GetValue(dc.State));
DialogOptions.ConversationIdFactory = dc.Context.TurnState.Get<SkillConversationIdFactoryBase>() ?? throw new NullReferenceException("Unable to locate SkillConversationIdFactoryBase in HostContext");
DialogOptions.SkillClient = dc.Context.TurnState.Get<BotFrameworkClient>() ?? throw new NullReferenceException("Unable to locate BotFrameworkClient in HostContext");
DialogOptions.ConversationState = dc.Context.TurnState.Get<ConversationState>() ?? throw new NullReferenceException($"Unable to get an instance of {nameof(ConversationState)} from TurnState.");
DialogOptions.ConnectionName = ConnectionName.GetValue(dc.State);
Edit: Putting the value, "skillHostEndpoint": "http://localhost:3978/api/skills", in the main appsettings.json file causes the code to work.
Edit: moving ComposerDialogs/settings/appsettings.json up to the main folder & renaming to appsettings-composer.json & adjusting the startup.cs does not help
Edit: removing .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) does not prevent the app settings from being available in BeginSkill.cs. So, this seems to indicate that configuring the builder in Startup.cs has no impact on the available settings.

Determined that the IConfiguration instance which was configured & created in the constructor of Startup.cs was not being used when dependency injection was happening on IConfiguration. This was causing the new configuration files added not to be available.
Added
services.AddSingleton<IConfiguration>(Configuration);
To ConfigureServices of Startup.cs so that the correct object would be retrieved & the values would be available.
skill-setting-issue branch of the Git Repo has been updated to reflect this.
This Post helped to determine the issue.

Related

Can we use a single appsettings.json file instead of maintaining multiple versions of appsettings.{environmentname}.json for different environments?

I'm using .NET 6 and I want to achieve 2 things:
Using a single appsettings.json file instead of maintaining multiple versions of appsettings.{environmentname}.json for different environments
Remove hard-coding from the appsettings.{environmentname}.json file to allow (1)
(Currently I'm hardcoding secrets like DB connection string, key vault configration details etc. for different environments in respective appsettings files which I want to remove and instead read the same secret values from Azure Key Vault where I'm already storing the same)
How can I achieve these?
Below code snippet is how I'm maintaining 2 appsettings.{environmentname}.json files(Development & Release):
var builder = WebApplication.CreateBuilder(args);
var hostingEnvironment = builder.Environment;
if (hostingEnvironment.IsDevelopment())
{
builder.Configuration.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", false, true)
.AddJsonFile($"appsettings.Development.json", false, true)
.AddEnvironmentVariables();
}
else
{
builder.Configuration.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", false, true)
.AddJsonFile($"appsettings.Release.json", false, true)
.AddEnvironmentVariables();
}
Note: I did some research and understood that for (1) we can probably do something like:
builder.Configuration.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", false, true)
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable(
"ASPNETCORE_ENVIRONMENT")}.json", false, true)
.AddEnvironmentVariables();
But then, assuming the fact that the app would be automatically deployed every time leveraging CI/CD, can I set the value of ASPNETCORE_ENVIRONMENT in the pipeline?
As for, (2), I tried removing hardcoded secrets from appsettings.{environment}.json files and configured key vault like below but it doesn't work:
builder.Services.Configure<KeyVaultConfig>(options => builder.Configuration.GetSection("KeyVaultConfig").Bind(options));
if (!string.IsNullOrEmpty(builder.Configuration["KeyVaultConfig:KeyVaultUrl"]))
{
var credentials = new ClientSecretCredential(builtConfig["KeyVaultConfig:TenantID"], builtConfig["KeyVaultConfig:ClientID"], builtConfig["KeyVaultConfig:ClientKey"]);
var secretClient = new SecretClient(new Uri(builtConfig["KeyVaultConfig:KeyVaultUrl"]), credentials);
//builder.Configuration.AddAzureKeyVault...
}
If in the end you are going to use CI/CD you can only need 2 files (appsettings.json and appsettings.Development.json) and the default C# handling of these.
In the development one you hardcode all the configuration you need for dev.
In the standard one, you put keys that would be replaced during the CI/CD pipelines.
for example
in appsettings.development.json you have
"ConnectionStrings": {
"myDb": "Server=myDbServer;Database=myDb;User Id=userDev;Password=myPassword;"
}
in appsettings.json you have
"ConnectionStrings": {
"myDb": "#{connectionString}#"
}
And then replace "#{connectionString}#" with the environment value in the CI/CD pipelines configuration
If you really want to have only one file, you could try Json variable substitution like documented here documented here during the CI/CD pipelines, but the first approach allows you to handle secrets with vault more easily

Unable to get Linux container running as Azure function in isolated process

The function app runs locally but is deployed as a Linux container. The deployed function isn't reporting any issues in the portal and I can see the three functions listed in the Functions blade. However, none are working (one is a simple HTTP ping which is returning a 502 Bad Gateway response).
So no obvious issues in the portal, but inspection of logs reveals this recurring exception:
System.Threading.Tasks.TaskCanceledException at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess
The formatted message is:
Failed to start a new language worker for runtime: dotnet-isolated
My suspicion is that this is something to do with the value of the site setting linuxFxVersion. Mind you, I've tried using DOTNET|6.0 and DOTNET-ISOLATED|6.0. In both cases it doesn't seem to make a difference and anyway when I export the function app's ARM template it has linuxFxVersion set to the URI of the container image prefixed with DOCKER|.
That seems to relate to this specific advice from Microsoft about pinning the host version on Linux. But it still isn't clear to me which value I should us, and anyway, in another place the advice from another Microsoft document states:
To support zip deployment and running from the deployment package on Linux, you also need to update the linuxFxVersion site config setting to DOTNET-ISOLATED|6.0.
Anyway, Here are the details of my config. I've followed every bit of guidance from Microsoft so I hope someone can spot what I've missed...
First two parts of project file:
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<AzureFunctionsVersion>V4</AzureFunctionsVersion>
<OutputType>Exe</OutputType>
<ImplicitUsings>true</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.CosmosDB" Version="3.0.9" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.0.13" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="1.3.0" OutputItemType="Analyzer" />
<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="1.5.2" />
</ItemGroup>
host.json:
{
"version": "2.0",
"logging": {
"applicationInsights": {
"samplingSettings": {
"isEnabled": true,
"excludedTypes": "Request"
}
}
}
}
Main method in Program.cs:
public static async Task Main()
{
var host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults()
.ConfigureAppConfiguration((context, configurationBuilder) =>
{
configurationBuilder.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
if (context.HostingEnvironment.IsDevelopment())
{
configurationBuilder.AddJsonFile("local.settings.json", optional: true, reloadOnChange: true);
}
else
{
configurationBuilder.AddJsonFile($"appsettings.{context.HostingEnvironment.EnvironmentName}.json", optional: true, reloadOnChange: true);
}
configurationBuilder.AddEnvironmentVariables();
})
.ConfigureServices((context, collection) => ConfigureServices(collection, context.Configuration, context.HostingEnvironment))
.Build()
.RunAsync();
}
The function provisioning pipeline sets the following app settings:
FUNCTIONS_EXTENSION_VERSION: '~4'
FUNCTIONS_WORKER_RUNTIME: 'dotnet-isolated'
As stated above, the latest version of my site config defined during provisioning includes this:
linuxFxVersion: 'DOTNET-ISOLATED|6.0'
The Docker image uses mcr.microsoft.com/azure-functions/dotnet-isolated:4 as a base image for the published app, and mcr.microsoft.com/dotnet/sdk:6.0 to build it.
Please tell me there's something obvious I'm missing. We currently have two function apps, and neither can be deployed as dotnet-isolated. It's driving me mad!
The exception what you saw is not directly from your application, instead it is a general exception without detaills from an Azure Function runtime-process - so it's useless.
The reasons can be anything, for example: forget to await on a task.
public static async Task Main()
{
var host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults()
.ConfigureAppConfiguration(builder =>
{
builder
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}.json", optional: true)
.Build();
})
.ConfigureServices((context, collection) => ConfigureServices(collection, context.Configuration, context.HostingEnvironment))
.Build();
host.RunAsync(); // await host.RunAsync();
}
Or a corrupted appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Warning",
"Microsoft": "Warning",
"Host": "Warning",
"Function": "Information",
"Host.Aggregator": "Information"
}
}
Or a missing configure call during startup.
public static async Task Main()
{
var host = new HostBuilder()
// .ConfigureFunctionsWorkerDefaults()
.ConfigureAppConfiguration(builder =>
{
builder
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}.json", optional: true)
.Build();
}))
.ConfigureServices((context, collection) => ConfigureServices(collection, context.Configuration, context.HostingEnvironment))
.Build();
await host.RunAsync();
}
That runtime-process from Microsoft which launches your application captures all your console logs and trashes them in case of exceptions during its own startup.
Btw, you should also enable Code Analysis which is per default not enabled in .net 6.
After battling with this for 2 days I decide to strip right down to the bare bones. Followed the Microsoft guide from scratch. This led me to identify the following line as the problem:
configurationBuilder.AddEnvironmentVariables();
As soon as I add this line to my Program and push a new image to the function app, everything breaks upon restart. However, there's no indication as to why. The Docker logs exposed by Kudo report this as the final entry:
2022-05-18T14:06:46.295Z INFO - Container [image-name] for site [function-name] initialized successfully and is ready to serve requests.
App Insights traces indicate that an exception has occurred at line 52 of Program.cs, which is my host.Run(); line.
The exception itself is:
System.InvalidOperationException at Microsoft.Azure.WebJobs.Script.Workers.Rpc.RpcFunctionInvocationDispatcherLoadBalancer.GetLanguageWorkerChannel
And there's a message hidden in there saying "Did not find any initialized language workers" which I understand is Azure Function language for "something bad happened during startup but I'm not going to tell you what or why".
So this at least explains why my function wasn't running, and hopefully my experience will save someone else time in the future, but since my app depends on configuration added to the function by a pipeline I still don't have a working app. For that I will ask a new question and link it here...
Update
Here is the follow up question, which I've already answered!

Configuration.GetSection("SectionName") is always null when using manually added settings.json file to dotnet core console app

I am trying to add settings.json file manually to a .net core 2.1 console application. So I add these NuGet packages to the project:
Microsoft.Extensions.Configuration
Microsoft.Extensions.Configuration.FileExtensions
Microsoft.Extensions.Configuration.Json
and create the appsettings.json file like this:
{
"Section1": {
"Prop1": "value",
"Prop2": 300
}
}
Finally, I try to get value from the settings file like this:
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
_configuration = builder.Build();
var section = _configuration.GetSection("Section1");//section.Value is null
var test = _configuration.GetSection("Section1:Prop1");//it returns the proper value
var model = section as Section1Model;
But, section.Value is null and in consequence, the model is null. If I try to get values like _configuration.GetSection("Section1:Prop1") it returns the correct value. Also, If I call _configuration.GetSection("Section1).GetChildren() it returns a collection of settings. What I did wrong?
P.S: I promise the settings file is copied to the bin folder
ConfigurationBuilder only returns generic IConfiguration instances. If you want a strongly typed model out of that, it has to be bound first. There are several ways of doing that -- Microsoft's own implementation lives in Microsoft.Extensions.Configuration.Binder. This then gives you access to the static class ConfigurationBinder in several ways with extension methods on IConfiguration:
var model = _configuration.GetSection("Section1").Get<Section1Model>();
Oddly enough there's no extension method for directly getting a section into an object, but it would be easy enough to write one.
If you're using dependency injection (Microsoft.Extensions.DependencyInjection, Microsoft.Extensions.Options.ConfigurationExtensions) there'll be a .Configure<Section1Model>() extension method to register this binding.

Environment variables configuration in .NET Core

I'm using the .NET Core 1.1 in my API and am struggling with a problem:
I need to have two levels of configurations: appsettings.json and environment variables.
I want to use the DI for my configurations via IOptions.
I need environment variables to override appsettings.json values.
So I do it like this so far:
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddEnvironmentVariables();
}
public IServiceProvider ConfigureServices(IServiceCollection services)
{
// Something here
services.Configure<ConnectionConfiguration>(options =>
Configuration.GetSection("ConnectionStrings").Bind(options));
// Something there
}
With my appsettings.json composed like this
{
"ConnectionStrings": {
"ElasticSearchUrl": "http://localhost:4200",
"ElasticSearchIndexName": "myindex",
"PgSqlConnectionString": "blablabla"
}
}
I get all the configurations mapped to my class ConnectionConfiguration.cs. But I cannot get the environment variables to be mapped as well. I tried the names like: ConnectionStrings:ElasticSearchUrl, ElasticSearchUrl, even tried specifying the prefix to .AddEnvironmentVariables("ConnectionStrings") without any result.
How should I name the environment variables so it can be mapped with services.Configure<TConfiguration>()?
The : separator doesn't work with environment variable hierarchical keys on all platforms. __, the double underscore, is supported by all platforms and it is automatically replaced by a :
Try to name the environment variable like so ConnectionStrings__ElasticSearchUrl
Source
Have a look at ASP.NET Core Configuration with Environment Variables in IIS
To do the same with environment variables, you just separate the levels with a colon (:), such as HitchhikersGuideToTheGalaxy:MeaningOfLife.
In your case you'll use ConnectionStrings:ElasticSearchUrl as mentioned in your question.
Also notice the following:
When you deploy to IIS, however, things get a little trickier, and unfortunately the documentation on using configuration with IIS is lacking. You go into your server's system settings and configure all your environment variables. Then, you deploy your app to IIS, and it... explodes, because it's missing those necessary settings.
Turns out, in order to use environment variables with IIS, you need to edit the advanced settings for your App Pool. There, you'll find a setting called "Load User Profile". Set that to True. Then, recycle the App Pool to load in the environment variables. Note: you must do this even if your environment variables are added as "System variables", rather than "User variables".
I believe you're looking for this:
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
this.Configuration = builder.Build();
I've 3 configurations
dev-final
dev-local
dev-test
And 4 *.json files
appsettings.json
appsettnigs.dev-final.json
appsettings.dev-local.json
appsettings.dev-test.json
appsettings.json holds global configuration values, and the other files specific ones.

How can I use config.json to set environment-specific variables for a Visual Studio 2015/DNX project?

I have a Visual Studio 2015 solution made up of projects targeting DNX framework. I have been working locally but I plan to deploy to Azure environments (dev/test/prod). Naturally, the solution uses different database connection strings and other variables dependent on the environment. In the past I made use of cloud configuration files to set these variables, reading them with the CloudConfigurationManager.
I am given to understand that I need to use a config.json file. I have the following code in my Startup.cs file:
public Startup(IHostingEnvironment env, IApplicationEnvironment app)
{
Configuration = new ConfigurationBuilder(app.ApplicationBasePath)
.AddJsonFile("config.json")
.AddJsonFile($"config.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables()
.Build();
Configuration.Set("ASPNET_ENV", "Development");
}
My config.json file is currently an empty object { }. How do I add variables to this file (syntax?), and how do I access them from code?
Note this line in your startup code:
.AddJsonFile($"config.{env.EnvironmentName}.json", optional: true)
This adds an additional JSON config file which is named depending on your environment name. So add a new file to your project called:
config.<environment-name>.json
And set up the details in there, such as:
{
"AppSettings": {
"SomeVariable": "Blah"
},
"Data": {
"YourConnectionString": {
"ConnectionString": "<connection string here>"
}
}
}
For reading the configuration, there's a good answer here: Using IConfiguration globally in mvc6
You can use plain old JSON for that. For example, here's how to define a connection string:
{
"Foo": "Bar"
}
To access the configuration value, you do:
config.Get("username")
or:
config["username"];
You can find more information here: http://docs.asp.net/en/latest/fundamentals/configuration.html

Categories