Skip to content

Instantly share code, notes, and snippets.

@davidfowl
Last active November 25, 2024 10:06
Show Gist options
  • Save davidfowl/0e0372c3c1d895c3ce195ba983b1e03d to your computer and use it in GitHub Desktop.
Save davidfowl/0e0372c3c1d895c3ce195ba983b1e03d to your computer and use it in GitHub Desktop.
.NET 6 ASP.NET Core Migration

Migration to ASP.NET Core in .NET 6

WebApplication and WebApplicationBuilder

.NET 6 introduces a new hosting model for ASP.NET Core applications. This model is streamlined and reduces the amount of boilerplate code required to get a basic ASP.NET Core application up and running.

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World");

app.Run();

This model unifies Startup.cs and Program.cs into a single file experience that takes advantage of top level statements to remove any boilerplate. There should be a mostly mechanical translation from .NET 5 projects using a Startup class to the new hosting model:

Program.cs (.NET 5)

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

Startup.cs (.NET 5)

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddRazorPages();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Error");
            // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
            app.UseHsts();
        }

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

        app.UseRouting();

        app.UseAuthorization();

        app.MapRazorPages();
    }
}

Program.cs (.NET 6)

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

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

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

The above shows that ConfigureServices(IServiceCollection) can be configured using WebApplicationBuilder.Services and Configure(IApplicationBuilder...) can be configured by using WebApplication.

Differences in the hosting model

  • The developer exception page middleware is enabled when the environment is Development.

  • The application name always defaults to the entry point assembly's name Assembly.GetEntryAssembly().GetName().FullName. When using the WebApplicationBuilder in a library, you will need to explicitly change the application name to the library's assembly to allow MVC's application part discovery to work (finding controllers, views etc) (see the Cheatsheet for instructions on how to do this).

  • The endpoint routing middleware wraps the entire middleware pipeline. This means there's no need to have explicit calls to UseEndpoints to register routes. UseRouting can still be used to move where route matching happens.

  • The final pipeline is created before any IStartupFilter runs. This means that exceptions caused while building the main pipeline won't be visible to the IStartupFilter call chain.

  • Some tools (like EF migrations) use Program.CreateHostBuilder to access the application's IServiceProvider to execute custom logic in the context of the application, these tools have been updated to use a new technique to achieve the same thing. We will work with the ecosystem to make sure tools are all updated to use the new model.

  • It is not possible to change any host settings (application name, environment or the content root) after the creation of the WebApplicationBuilder (see the Cheatsheet for instructions on how to do this). The following APIs will throw an exception:

    WebHost

    builder.WebHost.UseContentRoot(Directory.GetCurrentDirectory());
    builder.WebHost.UseEnvironment(Environments.Staging);
    
    builder.WebHost.UseSetting(WebHostDefaults.ApplicationKey, "ApplicationName2");
    builder.WebHost.UseSetting(WebHostDefaults.ContentRootKey, Directory.GetCurrentDirectory());
    builder.WebHost.UseSetting(WebHostDefaults.EnvironmentKey, Environments.Staging);

    Host

    builder.Host.UseEnvironment(Environments.Staging);
    builder.Host.UseContentRoot(Directory.GetCurrentDirectory());
  • It is not possible to use the Startup class via the WebApplicationBuilder.Host or WebApplicationBuilder.WebHost. The following will throw an exception:

    var builder = WebApplication.CreateBuilder(args);
    builder.Host.ConfigureWebHostDefaults(webHostBuilder =>
    {
        webHostBuilder.UseStartup<Startup>();
    });

    OR

    var builder = WebApplication.CreateBuilder(args);
    builder.WebHost.UseStartup<Startup>();
  • The IHostBuilder implementation on WebApplicationBuilder (WebApplicationBuilder.Host), does not defer execution of ConfigureServices, ConfigureAppConfiguration or ConfigureHostConfiguration methods. This allows code using WebApplicationBuilder to observe changes made to the IServiceCollection and IConfiguration. The below example will only add Service1 as an IService.

    using Microsoft.Extensions.DependencyInjection.Extensions;
    
    var builder = WebApplication.CreateBuilder(args);
    
    builder.Host.ConfigureServices(services =>
    {
        services.TryAddSingleton<IService, Service1>();
    });
    
    builder.Services.TryAddSingleton<IService, Service2>();
    
    var app = builder.Build();
    
    // This will print Service1
    Console.WriteLine(app.Services.GetRequiredService<IService>());
    
    app.Run();
    
    class Service1 : IService
    {
    
    }
    
    class Service2 : IService
    {
    
    }
    
    interface IService
    {
    
    }

Building libraries for ASP.NET Core

The existing .NET ecosystem has built extensibility around IServiceCollection, IHostBuilder and IWebHostBuilder. These properties are available on the WebApplicationBuilder as Services, Host and WebHost.

The WebApplication implements both Microsoft.AspNetCore.Builder.IApplicationBuilder and Microsoft.AspNetCore.Routing.IEndpointRouteBuilder.

We expect library authors to continue targeting IHostBuilder, IWebHostBuilder, IApplicationBuilder and IEndpointRouteBuilder when building ASP.NET Core specific components. This will ensure that your middleware, route handler, or other extensibility points continue to work across different hosting models.

FAQ

Is the new hosting model less capable

No, it should be functionally equivalent for 98% to what you can do with the IHostBuilder and the IWebHostBuilder. There are more advanced scenarios (the 2%) that will require specific knobs on IHostBuilder but we expect those to be extremely rare.

Is the generic hosting model dead/deprecated?

No, it's not. It's an alternative model that will keep working forever. The generic host still underpins the new hosting model and is still the primary way to host worker-based applications.

Do I have to migrate to the new hosting model

No, you don't have to. It's the preferred way to host ASP.NET Core applications from .NET 6 and onwards but you aren't forced to change your project layout. This means you can upgrade from .NET 5 to .NET 6.0 by changing the target framework in your project file from net5.0 to net6.0.

Do I have to use top-level statements?

The new project templates all use top-level statements, but these new hosting APIs can be used in any .NET 6 application to host a webserver/web application.

Where do I put state that was stored as fields in my Program/Startup class?

There are 2 solutions to this problem:

  1. You can store the state on another class. Assuming this was static state that you were accessing from anywhere in the application.
  2. There's a Program class generated by top level statements that you can put this state on if you wish to keep that semantic.

This is an example of #2:

.NET 5

public class Startup
{
    public static string ConfigurationValue { get; private set; }

    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;

        ConfigurationValue = Configuration["SomeValue"];
    }

    public IConfiguration Configuration { get; }

    // More configuration here
}

.NET 6

var builder = WebApplication.CreateBuilder(args);

ConfigurationValue = builder.Configuration["SomeValue"];

var app = builder.Build();

app.Run();

partial class Program
{
    public static string ConfigurationValue { get; private set; }
}

This would make it possible to use Program.ConfigurationValue in your .NET 6 application.

NOTE: We recommend using dependency injection to flow state in your ASP.NET Core applications.

Does WebApplicationFactory/TestServer still work?

WebApplicationFactory<TEntryPoint> is the way to test the new hosting model. See the Cheatsheet for an example.

What if I was using a custom dependency injection container?

This is still supported, see the Cheatsheet for an example.

I like the Startup class; can I keep it?

Yes, you can. Here's a shim you can use to keep it working as is with the new hosting model:

Program.cs

var builder = WebApplication.CreateBuilder(args);

var startup = new Startup(builder.Configuration);

startup.ConfigureServices(builder.Services);

// Uncomment if using a custom DI container
// builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
// builder.Host.ConfigureContainer<ContainerBuilder>(startup.ConfigureContainer);

var app = builder.Build();

startup.Configure(app, app.Environment);

app.Run();

Startup.cs

class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {

    }

//  Uncomment if using a custom DI container
//  public void ConfigureContainer(ContainerBuilder builder)
//  {
//  }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment environment)
    {

    }
}

There are a few differences here:

  • You control the instantiation and lifetime of the Startup class.
  • Any additional services injected into the Configure method need to be manually resolved by your Program class.

Cheatsheet

Adding middleware

.NET 5

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.UseStaticFiles();
    }
}

.NET 6

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.UseStaticFiles();

app.Run();

Adding routes

.NET 5

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.UseRouting();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapGet("/", () => "Hello World");
        });
    }
}

.NET 6

In .NET 6, routes can be added directly to the WebApplication without an explicit call to UseEndpoints.

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () => "Hello World");

app.Run();

NOTE: Routes added directly to the WebApplication will execute at the end of the pipeline.

Changing the content root, application name and environment

.NET 5

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .UseContentRoot(Directory.GetCurrentDirectory())
        .UseEnvironment(Environments.Staging)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>()
                      .UseSetting(WebHostDefaults.ApplicationKey, typeof(Program).Assembly.FullName);
        });

.NET 6

var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    ApplicationName = typeof(Program).Assembly.FullName,
    ContentRootPath = Directory.GetCurrentDirectory(),
    EnvironmentName = Environments.Staging
});

Console.WriteLine($"Application Name: {builder.Environment.ApplicationName}");
Console.WriteLine($"Environment Name: {builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot Path: {builder.Environment.ContentRootPath}");

var app = builder.Build();

Adding configuration providers

.NET 5

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureAppConfiguration(config =>
        {
            config.AddIniFile("appsettings.ini");
        })
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });

.NET 6

var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddIniFile("appsettings.ini");

var app = builder.Build();

Adding logging providers

.NET 5

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureLogging(logging =>
        {
            logging.AddJsonConsole();
        })
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });

.NET 6

var builder = WebApplication.CreateBuilder(args);

// Configure JSON logging to the console
builder.Logging.AddJsonConsole();

var app = builder.Build();

Adding services

.NET 5

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Add the memory cache services
        services.AddMemoryCache();

        // Add a custom scoped service
        services.AddScoped<ITodoRepository, TodoRepository>();
    }
}

.NET 6

var builder = WebApplication.CreateBuilder(args);

// Add the memory cache services
builder.Services.AddMemoryCache();

// Add a custom scoped service
builder.Services.AddScoped<ITodoRepository, TodoRepository>();

var app = builder.Build();

Customizing the IHostBuilder

.NET 5

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureHostOptions(o => o.ShutdownTimeout = TimeSpan.FromSeconds(30));
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });

Existing extension methods on IHostBuilder can be accessed using the Host property.

.NET 6

var builder = WebApplication.CreateBuilder(args);

// Wait 30 seconds for graceful shutdown
builder.Host.ConfigureHostOptions(o => o.ShutdownTimeout = TimeSpan.FromSeconds(30));

var app = builder.Build();

Customizing the IWebHostBuilder

.NET 5

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            // Change the HTTP server implemenation to be HTTP.sys based
            webBuilder.UseHttpSys()
                      .UseStartup<Startup>();
        });

.NET 6

Existing extension methods on IWebHostBuilder can be accessed using the WebHost property.

var builder = WebApplication.CreateBuilder(args);

// Change the HTTP server implemenation to be HTTP.sys based
builder.WebHost.UseHttpSys();

var app = builder.Build();

Changing the web root

By default, the web root is relative to the content root in the wwwroot folder. This is where the static files middleware expects to find static files. You can change this by using the UseWebRoot method on the WebHost property:

.NET 5

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            // Look for static files in webroot
            webBuilder.UseWebRoot("webroot")
                      .UseStartup<Startup>();
        });

.NET 6

var builder = WebApplication.CreateBuilder(args);

// Look for static files in webroot
builder.WebHost.UseWebRoot("webroot");

var app = builder.Build();

Custom dependency injection container

This example uses Autofac

.NET 5

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .UseServiceProviderFactory(new AutofacServiceProviderFactory())
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });
public class Startup
{
    public void ConfigureContainer(ContainerBuilder containerBuilder)
    {
    }
}

.NET 6

var builder = WebApplication.CreateBuilder(args);

builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());

// Register your own things directly with Autofac here. Don't
// call builder.Populate(), that happens in AutofacServiceProviderFactory
// for you.
builder.Host.ConfigureContainer<ContainerBuilder>(builder => builder.RegisterModule(new MyApplicationModule()));

var app = builder.Build();

Accessing additional services

.NET 5

In Startup.Configure you can inject any service added via the IServiceCollection.

public class Startup
{
    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<IService, Service>();
    }

    // Anything added to the service collection can be injected into Configure
    public void Configure(IApplicationBuilder app, 
                          IWebHostEnvironment env,
                          IHostApplicationLifetime lifetime,
                          IService service,
                          ILogger<Startup> logger)
    {
        lifetime.ApplicationStarted.Register(() => 
            logger.LogInformation($"The application {env.ApplicationName} started in we injected {service}"));
    }
}

.NET 6

In .NET 6, there are a few common services available as top level properties on WebApplication and additional services need to be manually resolved from the IServiceProvider via WebApplication.Services.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton<IService, Service>();

var app = builder.Build();

IService service = app.Services.GetRequiredService<IService>();
ILogger logger = app.Logger;
IHostApplicationLifetime lifetime = app.Lifetime;
IWebHostEnvironment env = app.Environment;

lifetime.ApplicationStarted.Register(() =>
    logger.LogInformation($"The application {env.ApplicationName} started in we injected {service}"));

app.Run();

Testing with WebApplicationFactory/TestServer

In the below samples, the test project uses TestServer and WebApplicationFactory. These ship as separate packages that need to explicit referenced:

WebApplicationFactory

<ItemGroup>
  <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="{Version}" />
</ItemGroup>

TestServer

<ItemGroup>
  <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="{Version}" />
</ItemGroup>

This sample is using xUnit and IHelloService will be shared between both examples:

public interface IHelloService
{
    string HelloMessage { get; }
}

public class HelloService : IHelloService
{
    public string HelloMessage => "Hello World";
}

.NET 5

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<IHelloService, HelloService>();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHelloService helloService)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapGet("/", async context =>
            {
                await context.Response.WriteAsync(helloService.HelloMessage);
            });
        });
    }
}

With TestServer

[Fact]
public async Task HelloWorld()
{
    using var host = Host.CreateDefaultBuilder()
        .ConfigureWebHostDefaults(builder =>
        {
            // Use the test server and point to the application's startup
            builder.UseTestServer()
                    .UseStartup<WebApplication1.Startup>();
        })
        .ConfigureServices(services =>
        {
            // Replace the service
            services.AddSingleton<IHelloService, MockHelloService>();
        })
        .Build();

    await host.StartAsync();

    var client = host.GetTestClient();

    var response = await client.GetStringAsync("/");

    Assert.Equal("Test Hello", response);
}

class MockHelloService : IHelloService
{
    public string HelloMessage => "Test Hello";
}

With WebApplicationFactory

[Fact]
public async Task HelloWorld()
{
    var application = new WebApplicationFactory<Program>()
        .WithWebHostBuilder(builder =>
        {
            builder.ConfigureServices(services =>
            {
                services.AddSingleton<IHelloService, MockHelloService>();
            });
        });

    var client = application.CreateClient();

    var response = await client.GetStringAsync("/");

    Assert.Equal("Test Hello", response);
}

class MockHelloService : IHelloService
{
    public string HelloMessage => "Test Hello";
}

.NET 6

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton<IHelloService, HelloService>();

var app = builder.Build();

var helloService = app.Services.GetRequiredService<IHelloService>();

app.MapGet("/", async context =>
{
    await context.Response.WriteAsync(helloService.HelloMessage);
});

app.Run();

In .NET 6, WebApplicationFactory<TEntryPoint> is used to test application using new hosting model. The compiler produces an internal Program class applications that use top level statements. We need to make this available to the test project by using InternalsVisibleTo. This can be done using the project file or in any other .cs file:

MyProject.csproj

<ItemGroup>
    <InternalsVisibleTo Include="MyTestProject" />
</ItemGroup>

OR

[assembly: InternalsVisibleTo("MyTestProject")]

The other solution is to make the Program class public. You can do this with top level statements by defining a public partial Program class anywhere in the project (or in Program.cs):

Program.cs

var builder = WebApplication.CreateBuilder(args);

// ... Wire up services and routes etc

app.Run();

public partial class Program { }
[Fact]
public async Task HelloWorld()
{
    var application = new WebApplicationFactory<Program>()
        .WithWebHostBuilder(builder =>
        {
            builder.ConfigureServices(services =>
            {
                services.AddSingleton<IHelloService, MockHelloService>();
            });
        });

    var client = application.CreateClient();

    var response = await client.GetStringAsync("/");

    Assert.Equal("Test Hello", response);
}

class MockHelloService : IHelloService
{
    public string HelloMessage => "Test Hello";
}

The .NET 5 version and .NET 6 version with the WebApplicationFactory are identical. This is by design.

@coleman-rik
Copy link

@davidfowl
To the new .Net 6 Program.cs:

IConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
IConfigurationRoot settings = configurationBuilder.AddJsonFile("appsettings.json").Build();
    // trying to get into the key vault
string? keyVaultEndpoint = settings["AzureKeyVault:dom-dev-kv:Endpoint"];
string? keyVaultTenantId = settings["AzureKeyVault:dom-dev-kv:TenantId"];
string? keyVaultClientId = settings["AzureKeyVault:dom-dev-kv:ClientId"];
string? keyVaultClientSecret = settings["AzureKeyVault:dom-dev-kv:ClientSecret"];

if (string.IsNullOrEmpty(keyVaultEndpoint) || string.IsNullOrEmpty(keyVaultClientId) ||
    string.IsNullOrEmpty(keyVaultClientSecret) || string.IsNullOrEmpty(keyVaultTenantId))
{
    configurationBuilder.AddAzureAppConfiguration(options => {
        options.Connect(settings.GetConnectionString("AzureAppConfig"))
            .Select(KeyFilter.Any, null)
            .Select(KeyFilter.Any, "Test")
            .Select(KeyFilter.Any, "PConnect")
            .Select(KeyFilter.Any, "Are")
            .Select(KeyFilter.Any, "app");
    });
}
else {
    ClientSecretCredential csc = new(keyVaultTenantId, keyVaultClientId, keyVaultClientSecret);

    configurationBuilder.AddAzureKeyVault(
        new Uri(keyVaultEndpoint), csc);

    configurationBuilder.AddAzureAppConfiguration(options => {
        options.Connect(settings.GetConnectionString("AzureAppConfig"))
            .ConfigureKeyVault(kv => kv.SetCredential(csc))
            .Select(KeyFilter.Any, "app")
            .Select(KeyFilter.Any, "PConnect")
            .Select(KeyFilter.Any, "Are");
    });
}


var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddAzureAppConfiguration();

// Internal
builder.Services.AddSingleton<IDatabaseConnectionServiceMSSql, DatabaseConnectionServiceMSSql>();
builder.Services.AddSingleton<IDatabaseConnectionServicePostgreSql, DatabaseConnectionServicePostgreSql>();
builder.Services.AddScoped<IPeakConnectService, PeakConnectService>();
// end Internal

// Auth0
builder.Services.AddAuthentication(options => {
    options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
    .AddCookie()
    .AddOpenIdConnect("Auth0", options => {
        options.Authority = $"https://{settings["Auth0:Domain"]}";

        options.ClientId = settings["Auth0:ClientId"];
        options.ClientSecret = settings["Auth0:ClientSecret"];

        options.ResponseType = OpenIdConnectResponseType.Code;

        options.Scope.Clear();
        options.Scope.Add("openid");  // mandatory
                    options.Scope.Add("profile"); // <- optional extra
                    options.Scope.Add("email");   // <- optional extra
                                                  // add more scopes add needed.

                    // resetting the Name claim to name, Role claim to role
                    options.TokenValidationParameters = new()
        {
            NameClaimType = "name",
        };
        options.TokenValidationParameters = new()
        {
            RoleClaimType = "role",
        };


        options.CallbackPath = new PathString("/callback");
        options.ClaimsIssuer = "Auth0";
        options.SaveTokens = true;

                    // Add handling of logout
                    options.Events = new OpenIdConnectEvents
        {
            OnRedirectToIdentityProviderForSignOut = (context) =>
            {
                var logoutUri = $"https://{settings["Auth0:Domain"]}/v2/logout?client_id={settings["Auth0:ClientId"]}";

                var postLogoutUri = context.Properties.RedirectUri;
                if (!string.IsNullOrEmpty(postLogoutUri))
                {
                    if (postLogoutUri.StartsWith("/"))
                    {
                        var request = context.Request;
                        postLogoutUri = request.Scheme + "://" + request.Host + request.PathBase + postLogoutUri;
                    }
                    logoutUri += $"&returnTo={ Uri.EscapeDataString(postLogoutUri)}";
                }

                context.Response.Redirect(logoutUri);
                context.HandleResponse();

                return Task.CompletedTask;
            }
        };
    });

var app = builder.Build();


// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

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

app.UseRouting();

// Auth0
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages(); 

app.MapBlazorHub();
app.MapFallbackToPage("/_Host");

app.Run();

@davidfowl
Copy link
Author

davidfowl commented Nov 16, 2021

You should be able to wire up configuration directly like so:

var builder = WebApplication.CreateBuilder(args);
var config = builder.Configuration;

string? keyVaultEndpoint = config["AzureKeyVault:dom-dev-kv:Endpoint"];
string? keyVaultTenantId = config["AzureKeyVault:dom-dev-kv:TenantId"];
string? keyVaultClientId = config["AzureKeyVault:dom-dev-kv:ClientId"];
string? keyVaultClientSecret = config["AzureKeyVault:dom-dev-kv:ClientSecret"];

if (string.IsNullOrEmpty(keyVaultEndpoint) || string.IsNullOrEmpty(keyVaultClientId) ||
    string.IsNullOrEmpty(keyVaultClientSecret) || string.IsNullOrEmpty(keyVaultTenantId))
{
    config.AddAzureAppConfiguration(options => {
        options.Connect(config.GetConnectionString("AzureAppConfig"))
            .Select(KeyFilter.Any, null)
            .Select(KeyFilter.Any, "Test")
            .Select(KeyFilter.Any, "PConnect")
            .Select(KeyFilter.Any, "Are")
            .Select(KeyFilter.Any, "app");
    });
}
else {
    ClientSecretCredential csc = new(keyVaultTenantId, keyVaultClientId, keyVaultClientSecret);

    config.AddAzureKeyVault(
        new Uri(keyVaultEndpoint), csc);

    config.AddAzureAppConfiguration(options => {
        options.Connect(config.GetConnectionString("AzureAppConfig"))
            .ConfigureKeyVault(kv => kv.SetCredential(csc))
            .Select(KeyFilter.Any, "app")
            .Select(KeyFilter.Any, "PConnect")
            .Select(KeyFilter.Any, "Are");
    });
}

@coleman-rik
Copy link

@davidfowl Thanks for the suggestion. That allows it to now build.

Unfortunately, now the Auth0 integration is broken and it throws the errors below as soon as it renders the home page. Switching back to the .Net 5 style of Program.cs / Startup.cs (even when the target framework is set to net6.0) restores complete functionality and eliminates all of the errors.

I think I'll stick with the tried an true c# application style until more of the bugs are worked out of this new style.

Thanks again for your help,

blazor.server.js:1 [2021-11-16T16:27:11.855Z] Error: There was an error applying batch 2.
log @ blazor.server.js:1
blazor.server.js:1 Uncaught (in promise) TypeError: Cannot read properties of null (reading 'removeChild')
    at j (blazor.server.js:1)
    at blazor.server.js:1
    at ae.updateComponent (blazor.server.js:1)
    at blazor.server.js:1
    at Dn.processBatch (blazor.server.js:1)
    at Tt.<anonymous> (blazor.server.js:1)
    at blazor.server.js:1
    at Array.forEach (<anonymous>)
    at Tt._invokeClientMethod (blazor.server.js:1)
    at Tt._processIncomingData (blazor.server.js:1)
blazor.server.js:1 [2021-11-16T16:27:12.000Z] Error: System.AggregateException: One or more errors occurred. (TypeError: Cannot read properties of null (reading 'removeChild'))
 ---> System.InvalidOperationException: TypeError: Cannot read properties of null (reading 'removeChild')
   at Microsoft.AspNetCore.Components.RenderTree.Renderer.InvokeRenderCompletedCallsAfterUpdateDisplayTask(Task updateDisplayTask, Int32[] updatedComponents)
   --- End of inner exception stack trace ---

@davidfowl
Copy link
Author

Unfortunately, now the Auth0 integration is broken and it throws the errors below as soon as it renders the home page. Switching back to the .Net 5 style of Program.cs / Startup.cs (even when the target framework is set to net6.0) restores complete functionality and eliminates all of the errors.

I'm not sure how those errors are related.

I think I'll stick with the tried an true c# application style until more of the bugs are worked out of this new style. Thanks again for your help,

No problem.

@peacemill
Copy link

@davidfowl
I came across this thread trying to solve a problem with Configuration on a new .NET 6 app. I recently installed VS 2022, but have several applications I work with in earlier versions that use our organizations configuration setup. In appsettings.Development.json "Application" contains "ApplicationGuid"...

Initially the new app was reading the Guid, but I ran into a problem when VS tried to open the browser, which lead to adding IIS Development support in the VS Installer. Since then Configuration is empty.

I've simplified the app, removing the services that were previously loading, confirmed that the EnvironmentName is correctly reading "Development", etc. When running Program.cs, Configuration in the following has only "Non-Public members".

var builder = WebApplication.CreateBuilder(args);
var config = builder.Configuration;
...

Like I said, before I installed "Development time IIS support" from Sourabh Shirhatti's blog, I had config data and used it to load services.
Do you have any suggestions? Thanks.

@davidfowl
Copy link
Author

@peacemill if you can make a minimal GitHub repo, file a bug in dotnet/AspNetCore with it and instructions on how to reproduce the issue

@peacemill
Copy link

@davidfowl Thanks for the suggestion, it might've led to the problem.

I created a new ASP.NET Core Web App (Model-View-Controller) from the template, added the ApplicationGuid key/value to appsettings.Development.json and ran it. builder.Configuration still showed only 'Non-Public members', but assigning config["Application:ApplicationGuid"] to a variable yeilded the value. So there was no problem in the clean app. The original app still had null in this scenario.

After picking through the appsettings and launchSettings json I found that removing an entry "workingDirectory" from launchSettings solved the problem, but only after a Clean Solution.

The "workingDirectory" value originally contained the file path to the application. I tried changing the value to "test" and running, but got an error that it wasn't a valid path. I used another path on the system, but still had a null value in the config["Application:ApplicationGuid"] result. I removed the key/value "workingDirectory" and ran and still had null. But, after I cleaned the solution I had the expected Guid in var result = config["Application:ApplicationGuid"];

I'm not sure where "workingDirectory" came from. It wasn't in the original configuration I added to the project.

@davidfowl
Copy link
Author

After picking through the appsettings and launchSettings json I found that removing an entry "workingDirectory" from launchSettings solved the problem, but only after a Clean Solution.

Likely because it couldn't find the configuration file in the working directory.

@odesuk
Copy link

odesuk commented Nov 20, 2021

Need a little help here. Tried all possible combinations and whatever I could find in documents. Nothing works.
I'm trying to migrate dotNet 3.1 web API to NET 6.0. There are no compilation errors but non of my endpoints is visible and the swagger doesn't recognize anything.
I replaced Program.cs with this call invoking the existing working (with 3.1) code:

`
using Microsoft.AspNetCore.Builder;
using Byoxon.BoB.Api;

var builder = WebApplication.CreateBuilder(args);

var startup = new Startup(builder.Configuration, builder.Environment);

startup.ConfigureServices(builder.Services);

var app = builder.Build();

startup.Configure(app, app.Environment);

app.Run();
`

Here is the Startup.cs file:

`//// Startup.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Byoxon.BoB.Api.Authorization;
using Byoxon.BoB.Api.Helpers;
using Byoxon.BoB.Domains;
using Byoxon.BoB.Domains.Mapping;
using Byoxon.BoB.Entities.Context;
using Byoxon.BoB.Entities.Entity;
using Byoxon.BoB.Entities.Repository;
using Byoxon.BoB.Entities.UnitOfWork;
using Byoxon.BoB.Services;
using Byoxon.BoB.Services.Helpers;
using Byoxon.BoB.Services.Interfaces;
using Byoxon.BoB.Services.Settings;

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Versioning;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;

using NETCore.MailKit.Core;

using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;

using Serilog;

using Swashbuckle.AspNetCore.SwaggerGen;

namespace Byoxon.BoB.Api
{
public class Startup
{

    public static IConfiguration Configuration { get; set; }
    public IWebHostEnvironment HostingEnvironment { get; private set; }

    public Startup(IConfiguration configuration, IWebHostEnvironment env)
    {
        Configuration = configuration;
        HostingEnvironment = env;
    }

    public void ConfigureServices(IServiceCollection services)
    {

        Log.Information("Startup::ConfigureServices");

        try
        {
            services.AddControllers(
                opt =>
                {
                    opt.EnableEndpointRouting = false;
                }
            );

            #region "API versioning"

            //API versioning service
            ConfigureApIversion(services);

            #endregion

            if (Environment.GetEnvironmentVariable("EnvironmentName", EnvironmentVariableTarget.Process) ==
                "Production")
            {
                services.AddDbContext<ByoxonBoBContext>(options =>
                        options.UseSqlServer(
                            Environment.GetEnvironmentVariable("ConnectionString",
                                EnvironmentVariableTarget.Process),
                            x => x.UseQuerySplittingBehavior(QuerySplittingBehavior.SingleQuery)),
                    ServiceLifetime.Transient);
            }
            else
            {
                services.AddDbContext<ByoxonBoBContext>(options =>
                        options.UseSqlServer(Configuration["ConnectionStrings:ByoxonBoBDB"],
                            x => x.UseQuerySplittingBehavior(QuerySplittingBehavior.SingleQuery)),
                    ServiceLifetime.Transient);
            }
            
            services.AddMvc(option => option.EnableEndpointRouting = false);

            #region "Authentication"

            ConfigureAuthentication(services);

            #endregion

            #region "CORS"

            ConfigureCors(services);

            #region "DI code"

            ConfigureServicesDi(services);

            #region "Swagger API"

            ConfigureSwaggerApi(services);

            #endregion

            #region "Cloudinary"

            services.Configure<CloudinarySettings>(Configuration.GetSection("CloudinarySettings"));

            #endregion

            #region Blob Storage

            services.Configure<BlobStorageSettings>(Configuration.GetSection("BlobStorageAccount"));

            #endregion

            #region Front app settings

            services.Configure<FrontAppSettings>(Configuration.GetSection("FrontAppSettings"));

            #endregion
        }
        catch (Exception ex)
        {
            Log.Error(ex.Message);
        }
    }

    private void ConfigureSwaggerApi(IServiceCollection services)
    {

        services.AddSwaggerGen(c =>
        {
            c.SwaggerDoc("v1", new OpenApiInfo { Title = "Tickets Advantage API", Version = "v1" });
            c.ResolveConflictingActions(apiDescriptions => apiDescriptions.First());
            c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
            {
                Description =
                    "JWT Authorization header using the Bearer scheme. \r\n\r\n Enter 'Bearer' [space] and then your token in the text input below.\r\n\r\nExample: \"Bearer 12345abcdef\"",
                Name = "Authorization",
                In = ParameterLocation.Header,
                Type = SecuritySchemeType.ApiKey,
                Scheme = "Bearer"
            });

            c.AddSecurityRequirement(new OpenApiSecurityRequirement()
            {
                {
                    new OpenApiSecurityScheme
                    {
                        Reference = new OpenApiReference
                        {
                            Type = ReferenceType.SecurityScheme,
                            Id = "Bearer"
                        },
                        Scheme = "oauth2",
                        Name = "Bearer",
                        In = ParameterLocation.Header,

                    },
                    new List<string>()
                }
            });

        });
    }

    private void ConfigureApIversion(IServiceCollection services)
    {
        services.AddApiVersioning(
            o =>
            {
                o.AssumeDefaultVersionWhenUnspecified = true;
                o.ReportApiVersions = true;
                o.ApiVersionReader = new UrlSegmentApiVersionReader();
            }
        );

        // format code as "'v'major[.minor][-status]"
        services.AddVersionedApiExplorer(
            options =>
            {
                options.GroupNameFormat = "'v'VVV";
                //versioning by url segment
                options.SubstituteApiVersionInUrl = true;
            });
    }

    private void ConfigureAuthentication(IServiceCollection services)
    {
        if (Configuration["Authentication:UseIndentityServer4"] == "False")
        {
            //JWT API authentication service
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options =>
                {
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateIssuer = true,
                        ValidateAudience = true,
                        ValidateLifetime = true,
                        ValidateIssuerSigningKey = true,
                        ValidIssuer = Configuration["Jwt:Issuer"],
                        ValidAudience = Configuration["Jwt:Issuer"],
                        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
                    };
                }
                );
            services.AddScoped<IAuthorizationHandler, PermissionAuthorizationHandler>();
            services.AddAuthorization(options =>
            {
                foreach (var permission in Permissions.GetAll())
                {
                    options.AddPolicy(permission, builder =>
                    {
                        builder.AddRequirements(new PermissionRequirement(permission));
                    });
                }
            });
        }
        else
        {
            //Indentity Server 4 API authentication service
            services.AddAuthorization();

            //.AddJsonFormatters();
            services.AddAuthentication("Bearer")
                .AddIdentityServerAuthentication(option =>
                {
                    option.Authority = Configuration["Authentication:IndentityServer4IP"];
                    option.RequireHttpsMetadata = false;
                    //option.ApiSecret = "secret";
                    option.ApiName =
                        "ByoxonBoB"; //This is the resourceAPI that we defined in the Config.cs in the AuthServ project (apiresouces.json and clients.json). They have to be named equal.
                });
        }
    }

    private void ConfigureCors(IServiceCollection services)
    {
        // include support for CORS
        // More often than not, we will want to specify that our API accepts requests coming from other origins (other domains). When issuing AJAX requests, browsers make preflights to check if a server accepts requests from the domain hosting the web app. If the response for these preflights don't contain at least the Access-Control-Allow-Origin header specifying that accepts requests from the original domain, browsers won't proceed with the real requests (to improve security).
        services.AddCors(options =>
        {
            options.AddPolicy("CorsPolicy-public",
                builder => builder
                    .AllowAnyOrigin() //WithOrigins and define a specific origin to be allowed (e.g. https://mydomain.com)
                    .AllowAnyMethod()
                    .AllowAnyHeader()
                    .Build());
        });

        #endregion

        //mvc service
        services.AddMvc(option => option.EnableEndpointRouting = false)
            .AddNewtonsoftJson(options =>
            {
                options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
                options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
            });
    }

    private void ConfigureServicesDi(IServiceCollection services)
    {
        //general unitofwork injections
        services.AddTransient<IUnitOfWork, UnitOfWork>();
        services.AddTransient(typeof(IRepository<>), typeof(Repository<>));

        //services injections

        services.AddTransient(typeof(IBaseService<>), typeof(BaseService<>));
        services.AddTransient(typeof(IService<,>), typeof(GenericService<,>));
        services.AddTransient(typeof(I18NLanguageService<,>), typeof(I18NLanguageService<,>));
        services.AddTransient(typeof(EmailService<>), typeof(EmailService<>));
        services.AddTransient(typeof(OrganizationPhotoService<,>), typeof(OrganizationPhotoService<,>));
        services.AddTransient(typeof(PositionService<,>), typeof(PositionService<,>));
        services.AddTransient(typeof(TimeCategoryService<,>), typeof(TimeCategoryService<,>));
        services.AddTransient(typeof(UserHistoryRecordService<,>), typeof(UserHistoryRecordService<,>));
        services.AddTransient(typeof(UserService<,>), typeof(UserService<,>));
        services.AddTransient(typeof(CustomerService<,>), typeof(CustomerService<,>));
        services.AddTransient(typeof(CustomerContactService<,>), typeof(CustomerContactService<,>));
        services.AddTransient(typeof(EducationService<,>), typeof(EducationService<,>));
        services.AddTransient(typeof(UserPhotoService<,>), typeof(UserPhotoService<,>));
        services.AddTransient(typeof(UserProjectService<,>), typeof(UserProjectService<,>));
        services.AddTransient(typeof(UserRateService<,>), typeof(UserRateService<,>));
        services.AddTransient(typeof(UserRateHistoryService<,>), typeof(UserRateHistoryService<,>));
        services.AddTransient(typeof(UserBankInfoService<,>), typeof(UserBankInfoService<,>));
        services.AddTransient(typeof(OrganizationService<,>), typeof(OrganizationService<,>));
        services.AddTransient(typeof(OrganizationBankInfoService<,>), typeof(OrganizationBankInfoService<,>));
        services.AddTransient(typeof(AddressService<,>), typeof(AddressService<,>));
        services.AddTransient(typeof(UserSkillService<,>), typeof(UserSkillService<,>));
        services.AddTransient(typeof(FamilyService<,>), typeof(FamilyService<,>));
        services.AddTransient(typeof(SocialMediaService<,>), typeof(SocialMediaService<,>));
        services.AddTransient(typeof(DepartmentService<,>), typeof(DepartmentService<,>));
        services.AddTransient(typeof(UserProfileService<,>), typeof(UserProfileService<,>));
        services.AddTransient(typeof(UserSettingsService<,>), typeof(UserSettingsService<,>));
        services.AddTransient(typeof(I18NMessageService<,>), typeof(I18NMessageService<,>));
        services.AddTransient(typeof(TimeEntryService<,>), typeof(TimeEntryService<,>));
        services.AddTransient(typeof(ProjectService<,>), typeof(ProjectService<,>));
        services.AddTransient(typeof(TagService<,>), typeof(TagService<,>));
        services.AddTransient(typeof(ProjectTagService<,>), typeof(ProjectTagService<,>));
        services.AddTransient(typeof(ProjectResourceService<,>), typeof(ProjectResourceService<,>));
        services.AddTransient(typeof(TeamResourceService<,>), typeof(TeamResourceService<,>));
        services.AddTransient(typeof(ReportTemplateService<,>), typeof(ReportTemplateService<,>));
        services.AddTransient(typeof(BankInfoService<,>), typeof(BankInfoService<,>));
        services.AddTransient(typeof(WeeklyReportService<,>), typeof(WeeklyReportService<,>));
        services.AddTransient(typeof(JiraInfoService<,>), typeof(JiraInfoService<,>));
        services.AddTransient(typeof(TeamService<,>), typeof(TeamService<,>));
        services.AddTransient(typeof(TeamMemberService<,>), typeof(TeamMemberService<,>));
        services.AddTransient(typeof(VacationRequestService<,>), typeof(VacationRequestService<,>));
        services.AddTransient(typeof(VacationRequestTypeService<,>), typeof(VacationRequestTypeService<,>));
        services.AddTransient(typeof(VacationApprovalService<,>), typeof(VacationApprovalService<,>));
        services.AddTransient(typeof(RequestActionService<,>), typeof(RequestActionService<,>));

        services.AddScoped(typeof(PermissionService<,>), typeof(PermissionService<,>));
        services.AddScoped(typeof(UserRoleService<,>), typeof(UserRoleService<,>));
        services.AddScoped(typeof(RoleService<,>), typeof(RoleService<,>));
        services.AddScoped(typeof(RolePermissionService<,>), typeof(RolePermissionService<,>));
        services.AddScoped(typeof(MenuService<,>), typeof(MenuService<,>));
        services.AddScoped(typeof(IBlobStorageService), typeof(AzureBlobService));
        services.AddScoped(typeof(SiteConfigService<,>), typeof(SiteConfigService<,>));
        services.AddScoped(typeof(IProjectService<,>), typeof(ProjectService<,>));
        services.AddScoped(typeof(SkillsService<,>), typeof(SkillsService<,>));
        services.AddScoped<IViewRenderService, ViewRenderService>();

        services.AddSingleton<IWebHostEnvironment>(this.HostingEnvironment);

        #endregion

        var emailSettings = new EmailSettings();
        Configuration.Bind("EmailSettings", emailSettings);
        services.Configure<EmailSettings>(Configuration.GetSection("EmailSettings"));

        //data mapper services configuration
        services.AddAutoMapper(typeof(MappingProfile));
    }


    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        try
        {
            app.UseMiddleware<ExceptionHandler>();

            app.UseForwardedHeaders(new ForwardedHeadersOptions
            {
                ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
            });

            if (env.EnvironmentName == "Development")
            {
                app.UseCors(x => x.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod()); // TODO: enhance (do not use on production)
            }
            else
            {
                app.UseCors("CorsPolicy-public");  //apply to every request
            }

            app.UseAuthentication(); //needs to be up in the pipeline, before MVC
            app.UseAuthorization();
            app.UseResponseCaching();
            app.UseMvc();

            //Swagger API documentation
            app.UseSwagger();//.UseAuthentication().UseAuthorization();

            app.UseSwaggerUI(c =>
            {
                if (Environment.GetEnvironmentVariable("EnvironmentName", EnvironmentVariableTarget.Process) == "Production")
                {
                    c.SwaggerEndpoint("v1/swagger.json", "ByoxonBoB API V1");
                }
                else
                {
                    c.SwaggerEndpoint("/swagger/v1/swagger.json", "ByoxonBoB API V1");
                }

                c.DisplayOperationId();
                c.DisplayRequestDuration();
            });

            //migrations and seeds from json files
            using var serviceScope = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>().CreateScope();
            if (Configuration["ConnectionStrings:UseInMemoryDatabase"] == "False" && !serviceScope.ServiceProvider.GetService<ByoxonBoBContext>().AllMigrationsApplied())
            {
                if (Configuration["ConnectionStrings:UseMigrationService"] == "True")
                    serviceScope.ServiceProvider.GetService<ByoxonBoBContext>().Database.Migrate();
            }
            //it will seed tables on aservice run from json files if tables empty
            if (Configuration["ConnectionStrings:UseSeedService"] == "True")
                serviceScope.ServiceProvider.GetService<ByoxonBoBContext>().EnsureSeeded();
        }
        catch (Exception ex)
        {
            Log.Error(ex.Message);
        }
    }
}


public class SwaggerSecurityRequirementsDocumentFilter : IDocumentFilter
{
    public void Apply(OpenApiDocument document, DocumentFilterContext context)
    {
        document.SecurityRequirements = new List<OpenApiSecurityRequirement>
        {
            new OpenApiSecurityRequirement{
                {
                    new OpenApiSecurityScheme{
                        Reference = new OpenApiReference{
                            Id = "Bearer", //The name of the previously defined security scheme.
                            Type = ReferenceType.SecurityScheme
                        }
                    },new List<string>()
                }
            },
            new OpenApiSecurityRequirement{
                {
                    new OpenApiSecurityScheme{
                        Reference = new OpenApiReference{
                            Id = "Basic", //The name of the previously defined security scheme.
                            Type = ReferenceType.SecurityScheme
                        }
                    },new List<string>()
                }
            }
        };

    }
}

}
`

@Jens-H-Eriksen
Copy link

@developer9969 has moved the issue here: dotnet/aspnetcore#37993

@grumpykiwi
Copy link

Question regarding moving startup code to .Net 6. Implementing key vault as follows:

var builder = Microsoft.AspNetCore.Builder.WebApplication.CreateBuilder(args);
var config = builder.Configuration;

	var thumbprint = builder.Configuration.GetValue<string>("AzureAd:CertThumbprint");
	var store = new X509Store(StoreLocation.LocalMachine);
	store.Open(OpenFlags.ReadOnly);
	var certs = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false);

	var tenantId = builder.Configuration.GetValue<string>("AzureAd:TenantId");
	var clientId = builder.Configuration.GetValue<string>("AzureAd:ClientId");
	var keyVaultName = builder.Configuration.GetValue<string>("AzureAd:KeyVaultName");
	builder.Configuration.AddAzureKeyVault(new Uri($"https://{keyVaultName}.vault.azure.net/"),
		new ClientCertificateCredential(tenantId, clientId, certs.OfType<X509Certificate2>().Single()),
		new Azure.Extensions.AspNetCore.Configuration.Secrets.KeyVaultSecretManager());
	store.Close();

The call to AddAzureKeyVault fails with the error below:

Azure.Identity.AuthenticationFailedException
HResult=0x80131500
Message=ClientCertificateCredential authentication failed: Method not found: 'Microsoft.Identity.Client.ConfidentialClientApplicationBuilder Microsoft.Identity.Client.ConfidentialClientApplicationBuilder.WithCertificate(System.Security.Cryptography.X509Certificates.X509Certificate2)'.
Source=Azure.Identity
StackTrace:
at Azure.Identity.CredentialDiagnosticScope.FailWrapAndThrow(Exception ex)
at Azure.Identity.ClientCertificateCredential.d__22.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd(Task task)
at System.Threading.Tasks.ValueTask1.get_Result() at System.Runtime.CompilerServices.ConfiguredValueTaskAwaitable1.ConfiguredValueTaskAwaiter.GetResult()
at Azure.Security.KeyVault.ChallengeBasedAuthenticationPolicy.d__9.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.ConfiguredTaskAwaitable.ConfiguredTaskAwaiter.GetResult()
at Azure.Security.KeyVault.ChallengeBasedAuthenticationPolicy.d__8.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at Azure.Core.Pipeline.RedirectPolicy.d__5.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at Azure.Core.Pipeline.RetryPolicy.d__11.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at Azure.Core.Pipeline.RetryPolicy.d__11.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.ConfiguredValueTaskAwaitable.ConfiguredValueTaskAwaiter.GetResult()
at Azure.Core.Pipeline.HttpPipeline.d__11.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Threading.Tasks.ValueTask1.get_Result() at System.Runtime.CompilerServices.ConfiguredValueTaskAwaitable1.ConfiguredValueTaskAwaiter.GetResult()
at Azure.Security.KeyVault.KeyVaultPipeline.d__27.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.ConfiguredTaskAwaitable1.ConfiguredTaskAwaiter.GetResult() at Azure.Security.KeyVault.KeyVaultPipeline.<GetPageAsync>d__171.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at Azure.Core.PageResponseEnumerator.FuncAsyncPageable1.<AsPages>d__2.MoveNext() at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Threading.Tasks.Sources.ManualResetValueTaskSourceCore1.ThrowForFailedGetResult(Int16 token)
at System.Threading.Tasks.Sources.ManualResetValueTaskSourceCore1.GetResult(Int16 token) at Azure.Core.PageResponseEnumerator.FuncAsyncPageable1.d__2.System.Threading.Tasks.Sources.IValueTaskSource<System.Boolean>.GetResult(Int16 token)
at Azure.AsyncPageable1.<GetAsyncEnumerator>d__6.MoveNext() at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at Azure.AsyncPageable1.d__6.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Threading.Tasks.Sources.ManualResetValueTaskSourceCore1.ThrowForFailedGetResult(Int16 token) at System.Threading.Tasks.Sources.ManualResetValueTaskSourceCore1.GetResult(Int16 token)
at Azure.AsyncPageable`1.d__6.System.Threading.Tasks.Sources.IValueTaskSource<System.Boolean>.GetResult(Int16 token)
at Azure.Extensions.AspNetCore.Configuration.Secrets.AzureKeyVaultConfigurationProvider.d__10.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at Azure.Extensions.AspNetCore.Configuration.Secrets.AzureKeyVaultConfigurationProvider.d__10.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
at Azure.Extensions.AspNetCore.Configuration.Secrets.AzureKeyVaultConfigurationProvider.Load()
at Microsoft.Extensions.Configuration.ConfigurationManager.AddSource(IConfigurationSource source)
at Microsoft.Extensions.Configuration.ConfigurationManager.Microsoft.Extensions.Configuration.IConfigurationBuilder.Add(IConfigurationSource source)
at Microsoft.Extensions.Configuration.AzureKeyVaultConfigurationExtensions.AddAzureKeyVault(IConfigurationBuilder configurationBuilder, SecretClient client, AzureKeyVaultConfigurationOptions options)
at Microsoft.Extensions.Configuration.AzureKeyVaultConfigurationExtensions.AddAzureKeyVault(IConfigurationBuilder configurationBuilder, Uri vaultUri, TokenCredential credential, KeyVaultSecretManager manager)
at Program.

$(String[] args) in D:\projects\Teams.Blazor.Net6\Teams.Blazor.Net6\Program.cs:line 40

Inner Exception 1:
MissingMethodException: Method not found: 'Microsoft.Identity.Client.ConfidentialClientApplicationBuilder Microsoft.Identity.Client.ConfidentialClientApplicationBuilder.WithCertificate(System.Security.Cryptography.X509Certificates.X509Certificate2)'.
I have spent hours on Google and SO but have not found anything that points to the issue.

Thanks

@davidfowl
Copy link
Author

Method not found: 'Microsoft.Identity.Client.ConfidentialClientApplicationBuilder Microsoft.Identity.Client.ConfidentialClientApplicationBuilder.WithCertificate(System.Security.Cryptography.X509Certificates.X509Certificate2)'.
Source=Azure.Identity

Method not found exceptions usually mean you have a package version mismatch. Make sure all of your dependencies are updated. That error is coming from the identity* packages.

@davidfowl
Copy link
Author

@odesuk where are your controllers defined?

@grumpykiwi
Copy link

Thanks David. That did the trick. I had some old RC level references in package manager. Updated the following to 1.21.1 to resolve:

Azure.Extensions.AspNetCore.Configuration.Secrets
Microsoft.Identity.Web
Microsoft.Identity.Web.MicrosoftGraph
Microsoft.Identity.Web.UI

@xavierjohn
Copy link

During the build phase, I need to add a certificate from Azure Key Vault and also log the thumbprint.
Would this be the correct way to do it?

            builder.Services.Configure<GenevaClientSettings>(c =>
            {
                var privateKeyBytes = Convert.FromBase64String(builder.Configuration["GenevaCertificate"]);
                 c.GenevaCertificate = new X509Certificate2(privateKeyBytes, "");
                 // How can I log here? app does not exist so I can't access it's ILogger.
            });

Thanks.

@grumpykiwi
Copy link

grumpykiwi commented Dec 11, 2021 via email

@xavierjohn
Copy link

Hi Mark,
I also have the requirement that I need to do different things based on the environment. The builder object has access to the environment so this is my code to configure KV.

void AddKeyVault(WebApplicationBuilder builder)
{
    var keyVaultUri = builder.Configuration["VaultUri"];
    if (string.IsNullOrWhiteSpace(keyVaultUri))
        throw new ApplicationException("keyVaultUri is not set");

    TokenCredential tokenCredential;
    if (builder.Environment.IsDevelopment())
        tokenCredential = new AzureCliCredential();
    else
    {
        var clientId = builder.Configuration["AZURE_CLIENT_ID"];
        tokenCredential = new ManagedIdentityCredential(clientId);
    }
    var secretClient = new SecretClient(new Uri(keyVaultUri), tokenCredential);
    AzureKeyVaultConfigurationOptions azureKeyVaultConfigurationOptions = new()
    {
        ReloadInterval = TimeSpan.FromHours(1),
    };
    builder.Configuration.AddAzureKeyVault(secretClient, azureKeyVaultConfigurationOptions);
}

Since certificate can change while the process is running, I need the logger object in the config callback function.

@grumpykiwi
Copy link

The best I have been able to come up with is the following. Some lines removed for clarity

var builder = Microsoft.AspNetCore.Builder.WebApplication.CreateBuilder(args);
builder.Logging.AddConsole();
builder.Logging.AddDebug();
builder.Logging.AddSimpleConsole();

var app = builder.Build();

ILogger logger = app.Logger;

switch (app.Environment.EnvironmentName.ToLower())
{
	case "development":
		builder.Configuration.AddUserSecrets<Program>();
		logger.LogInformation("Starting in development mode");
		break;
}

KeyVaultHandler.KeyVaultCallBack(ref logger, "Inside the callbackmethod");

logger.LogInformation("Starting application....");
app.Run();

public static class KeyVaultHandler
{
	public static void KeyVaultCallBack(ref ILogger log, string message)
	{
		log.LogInformation(message);
	}
}

The issue I ran into is I am not sure how to define a variable that is visible to the whole file/namespace etc. As a result, as you can see I am still having to pass the logger object to the KeyVaultHandler method. Not sure if your callback method can do that. Did not find a whole lot of documentation on the structure of the callback. I am likely missing something, but without seeing all of your relevant code, I am doing a lot of guessing.

Using the .Net 5.0 structure for program.cs would at least allow you to define a method inside the class with the necessary scope.

I am sure David can shine more light on the subject. Hope I was able to contribute the spark of an idea if nothing else.

Have a great weekend. Good luck.

@kaylechen
Copy link

kaylechen commented Jan 5, 2022

I need help too.
I'm trying to migrate .Net 5 to .Net 6. but I can't find any way to chane JSON Serializer Setting

.Net 5 Code :
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers().AddNewtonsoftJson(options =>
{
options.SerializerSettings.ContractResolver = new DefaultContractResolver();
});
}

@odesuk
Copy link

odesuk commented Jan 5, 2022

@kaylechen Maybe something like this will help...

builder.Services.AddControllers().AddJsonOptions(x =>
{
x.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
x.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
});

@kaylechen
Copy link

@odesuk Thanks your suggestion. I'm use this way.

builder.Services.AddControllers().AddJsonOptions(x =>
{
x.JsonSerializerOptions.PropertyNamingPolicy = null;
});

@davidfowl
Copy link
Author

Those control MVC specifically, if you want to control minimal APIs then you need to configure the JSON options again:

builder.Services.Configure<Microsoft.AspNetCore.Http.Json.JsonOptions>(o =>
{
    o.SerializerOptions.PropertyNamingPolicy = null;
});

@smigalni
Copy link

smigalni commented Jan 17, 2022

Hi David

I am trying to get Environment from appsettings. We are using Azure DevOps config replacement and have 3 different environments: Development, Staging and Production. It looks like the only place where I can do it is here

var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
EnvironmentName = "Staging"
});

I would like to have something like this

var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
EnvironmentName = GetEnvironmentFromAppsettings()
});

.NET 5 we solved it like that

public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseJbfSerilog()
.ConfigureWebHostDefaults(webBuilder => webBuilder
.UseStartup())
.ConfigureHostConfiguration(configure => configure
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("hostsettings.json", optional: true))
;

and hostsettigns.json looks like this

{
"Environment": "Staging"
}

When we release using Azure DevOps pipelines we just change hostsettigns.json Environment based on Environment to Deploy to. How can we do the same using this new .NET model.

@grumpykiwi
Copy link

grumpykiwi commented Jan 20, 2022 via email

@bdaniel7
Copy link

Is there a way to use TestServer in .net 6? Because I need to actually start a server to respond to the requests made by the client. When I try to use await application.Server.Host.StartAsync(); I get an error The TestServer constructor was not called with a IWebHostBuilder so IWebHost is not available.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment