0

I have a web app running in Azure which is currently running on .NET 5, which I'm trying to upgrade to 6. I have refactored the code to remove the Startup.cs file and refactored Program.cs to suit the new structure of .NET 6 apps. Before anybody else tells me that the old way with two files still works, yes I know, but I want to move to the new standard so that this can serve as a template for future apps, which by default when first created in VS only use the one file.

In the existing Startup.cs file, I have something like this:

using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Identity.Web;

namespace MyApp.Web.Server
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
                .AddMicrosoftIdentityWebApp(Configuration.GetSection(Constants.AzureAdB2C))
                .EnableTokenAcquisitionToCallDownstreamApi(new string[] { "https://example.com/api/query" })
                .AddInMemoryTokenCaches();

            // Other services added here
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");

                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(
                endpoints =>
                    {
                        endpoints.MapControllers();
                        endpoints.MapBlazorHub();
                        endpoints.MapFallbackToPage("/_Host");
                    });
        }
    }
}

When the Startup constructor is called, an IConfiguration instance is injected in, which I understand is done dynamically when running on DevOps to include all app settings set in the App Service -> Settings -> Configuration -> Application settings page. This all works fine at the moment.

However, the new Program.cs file with the .NET 6 syntax looks like this:

using System.Reflection;

using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.Identity.Web;

IConfiguration configuration;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

string appSettingsPath = Path.Combine(
    Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),
    "appsettings.json");

configuration = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile(appSettingsPath, optional: false, reloadOnChange: true)
    .Build();

builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(configuration.GetSection(Constants.AzureAdB2C))
    .EnableTokenAcquisitionToCallDownstreamApi(new string[] { "https://example.com/api/query" })
    .AddInMemoryTokenCaches();

// Other services added here

WebApplication app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/Error");

    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.UseEndpoints(
    endpoints =>
    {
        endpoints.MapControllers();
        endpoints.MapBlazorHub();
        endpoints.MapFallbackToPage("/_Host");
    });

app.Run();

As you can see above, there is no constructor and therefore I have no way of injecting the IConfiguration instance, so at the moment I'm presuming there is an appsettings.json file and creating it from that. This won't work when running in the Azure environment as the settings specified in the dashboard don't get added to the appsettings.json file, they are set as environment variables.

I can retrieve environment variables but I can't retrieve variable groups, as is the requirement in this particular case of adding Microsoft.Identity authentication. Is there no way of dynamically resolving/injecting the IConfiguration instance as before?

ataraxia
  • 995
  • 13
  • 31

1 Answers1

0

Turns out I was over-thinking this. I figured that there is still an IServiceCollection being created and/or dynamically added to by the Azure app service runtime so tried removing all code that tries to create an IConfiguration instance and just presume that there is already one there after creating the .NET 6 WebApplicationBuilder instance.

Sure enough, there is. All I needed to do was reference the builder.Configuration instance, which already has the IConfiguration injected from the Azure container:

using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.Identity.Web;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Services.AddApplicationInsightsTelemetry();

builder.Services.AddRazorPages();

builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection(Constants.AzureAdB2C))
    .EnableTokenAcquisitionToCallDownstreamApi(new string[] { "https://example.com/api/query" })
    .AddInMemoryTokenCaches();

builder.Services.Configure<OpenIdConnectOptions>(builder.Configuration.GetSection("AzureAdB2C"));

// Other services added here

WebApplication app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/Error");

    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.UseEndpoints(
    endpoints =>
    {
        endpoints.MapControllers();
        endpoints.MapBlazorHub();
        endpoints.MapFallbackToPage("/_Host");
    });

app.Run();

Just for info, if like me you have local appsettings.json files for running in debug or testing, I have the following methods in a helper class:

public static IConfiguration GetConfigurationFromAssembly(string appSettingsFileName)
{
    Assembly dataAssembly = Assembly.LoadFrom(
        Path.Combine(
        
    Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),
                    "MyApp.Web.Data.dll")); // Put your own project DLL containing the embedded resource here

    Stream jsonStream =
        dataAssembly.GetManifestResourceStream($"MyApp.Web.Data.{appSettingsFileName}");

    return new ConfigurationBuilder()
        .AddJsonStream(jsonStream)
        .SetBasePath(Directory.GetCurrentDirectory())
        .Build();
}

public static IConfiguration GetConfigurationFromFile(string appSettingsFileName)
{
    string appSettingsPath = Path.Combine(
        Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),
        appSettingsFileName);

    return new ConfigurationBuilder()
        .SetBasePath(Directory.GetCurrentDirectory())
        .AddJsonFile(appSettingsPath, optional: false, reloadOnChange: true)
        .Build();
}

You can then add the returned IConfiguration object to the WebApplicationBuilder like this:

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

#if DEBUG
configuration = Configurations.GetConfigurationFromAssembly("appsettings.Development.json");

builder.Configuration.AddConfiguration(configuration);
#endif

builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection(Constants.AzureAdB2C))
    .EnableTokenAcquisitionToCallDownstreamApi(new string[] { "https://example.com/api/query" })
    .AddInMemoryTokenCaches();
ataraxia
  • 995
  • 13
  • 31