Skip to content

Instantly share code, notes, and snippets.

@davidfowl
Last active December 2, 2024 11:08
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.

@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