blog post image
Andrew Lock avatar

Andrew Lock

~6 min read

Upgrading a .NET 5 "Startup-based" app to .NET 6

Exploring .NET 6 - Part 12

In this short post I tackle a question I have received several times—"how can I update an ASP.NET Core 5 app that uses Startup to .NET 6's minimal hosting APIs"?

Background: Minimal Hosting in .NET 6

I've had quite a few emails recently from people about some of the changes introduced in .NET 6 which I've described in this series. Most of the questions are around the "minimal hosting" changes, and "minimal APIs", and what that means for their existing .NET 5 apps.

I've covered the new WebApplication and WebApplicationBuilder types a lot in this post, so if you're not familiar with them at all, I suggest looking at the second post in this series. In summary, your typical "empty" .NET 5 app goes from two files, Program.cs and Startup.cs, to just one, which contains something like the following:

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

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

app.Run();

There are many C# features that have gone into .NET 6 to really make this file as simple as possible, such as global using statements and top level programs, but the new WebApplication types are what really seem to trip people up.

The "problem", is that the templates (and, to be fair, many of the demos) put all the logic that used to be spread across at least two files and multiple methods into a single file.

The argument for doing that is sound enough—this is all "procedural" setup code, so why not just put it in a single "script-like" file, and avoid unneccessary layers of abstractions? The trouble is that for bigger apps, this file could definitely get chunky. So what do I recommend?

Option 1: Do nothing

If you have a .NET 5 app that you're upgrading to .NET 6, and you're worried about what to do about Program.cs and Startup.cs, then the simple answer is: don't do anything.

The "old" style startup using the generic host and Startup is completely supported. After all, under the hood, the new WebApplication hotness is just using the generic Host.. Literally, the only changes you will likely need to make are to update the target framework in your .csproj file and update some NuGet packages:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <!--               👇                 -->
    <TargetFramework>net6.0</TargetFramework>
  </PropertyGroup>

</Project>

Your Program.cs file will work as normal. Your Startup class will work as normal. Just enjoy those sweet performance improvements and move on 😄

Option 2: Re-use your Startup class

But what if you really want to use the new WebApplication-style, but don't want to put everything in Program.cs. I get it, it's shiny and new, but that file could get loooong, depending on how much work you're doing in your Startup class.

One approach you could take is to keep your Startup class, but call it manually, instead of using the magical UseStartup<T>() method. For example, let's assume your Startup class is relatively typical:

  • It takes an IConfigurationRoot in the constructor
  • It has a ConfigureServices() method for configuring DI
  • It has a Configure() method for configuring your middleware and endpoints, which includes other services injected from the built DI container (IHostApplicationLifetime in this case).

Overall, it probably looks something like this:

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

    public IConfigurationRoot Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        // ...
    }

    public void Configure(IApplicationBuilder app, IHostApplicationLifetime lifetime)
    {
        // ...
    }
}

With the pre-.NET 6 UseStartup<T> method used with generic hosting, the Startup class was magically created, and its methods were called automatically at the appropriate points. However, there's nothing to stop you doing the same steps "manually" with the new WebApplicationBuilder hosting model.

For example, if we start with the hello-world of minimal hosting:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();

We can update it to use our existing Startup class:

var builder = WebApplication.CreateBuilder(args);

// Manually create an instance of the Startup class
var startup = new Startup(builder.Configuration);

// Manually call ConfigureServices()
startup.ConfigureServices(builder.Services);

var app = builder.Build();

// Fetch all the dependencies from the DI container 
// var hostLifetime = app.Services.GetRequiredService<IHostApplicationLifetime>();
// As pointed out by DavidFowler, IHostApplicationLifetime is exposed directly on ApplicationBuilder

// Call Configure(), passing in the dependencies
startup.Configure(app, app.Lifetime);

app.Run();

This is probably the simplest approach to re-use your Startup class if you want to shift to the new WebApplication approach.

You need to be aware there are some small differences when you use WebApplication that may not be initially apparent. For example, you can't change settings like the app name or environment after you've created a WebApplicationBuilder. See the docs for more of these subtle differences.

Option 3: Local methods in Program.cs

If I was starting a new .NET 6 application, I probably wouldn't choose to create a Startup class, but I probably would add similar methods into my Program.cs file to give it some structure.

For example, a typical structure I would choose might look something like the following:

var builder = WebApplication.CreateBuilder(args);

ConfigureConfiguration(builder.configuration);
ConfigureServices(builder.Services);

var app = builder.Build();

ConfigureMiddleware(app, app.Services);
ConfigureEndpoints(app, app.Services);

app.Run();

void ConfigureConfiguration(ConfigurationManager configuration) => { }
void ConfigureServices(IServiceCollection services) => { }
void ConfigureMiddleware(IApplicationBuilder app, IServiceProvider services) => { }
void ConfigureEndpoints(IEndpointRouteBuilder app, IServiceProvider services) => { }

Overall, this looks very similar to the Startup-based version in Option 2, but I've removed some of the boilerplate of having a separate class. There's a few other things to note:

  • I have separate methods for setting up middleware and endpoint: the former are sensitive to order, and the latter are not, so I like to keep these separate.
  • I used the IApplicationBuilder and IEndpointRouteBuilder types in the method signatures to enforce it.
  • It's easy to update the method signatures or break these out if we need more flexibility .

Overall, I think this is as good a pattern as any in many cases, but it really doesn't matter - you can impose as little or as much structure here as you're comfortable with.

Bonus: Extract modules, use Carter

If you're really keen to use WebApplication, then it should be very easy to "inline" your Startup class into Program.cs, by copy-pasting the ConfigureServices() and Configure() methods into the right place, similar to the above.

Of course, at some point, you might realise that you were drunk with power, and your Program.cs is now a huge mess of configuration code and endpoints. What then?

One of the things some people like about the new hosting model is that it doesn't enforce any particular patterns or requirements on your code. That gives you a lot of scope to apply whatever patterns work for you.

Personally, I would prefer my frameworks to have fewer choices, to elect a "winner" pattern, and for projects to all look quite similar. The trouble is, historically, the "blessed" pattern in ASP.NET/ASP.NET Core has not been a great one. I'm looking at you Controllers folder…

One approach to tackle extract common features into "modules". This may be especially useful if you're using the minimal APIs, and don't want to have them all listed in Program.cs!

If by this point you're thinking "someone must already have a library for managing this", then you're in luck: Carter is what you're looking for.

Carter contains a variety of helper methods for working with minimal APIs, one of which is a handy "module" grouping (taken from their docs):

public class HomeModule : ICarterModule
{
    public void AddRoutes(IEndpointRouteBuilder app)
    {
        app.MapGet("/", () => "Hello from Carter!");
        app.MapGet("/conneg", (HttpResponse res) => res.Negotiate(new { Name = "Dave" }));
        app.MapPost("/validation", HandlePost);
    }

    private IResult HandlePost(HttpContext ctx, Person person, IDatabase database)
    {
        // ...
    }
}

You can use these modules to add some structure back to your application if you go too far destructuring your application from Web APIs to minimal APIs! If you're interested in Carter, I strongly suggest watching the introduction on the .NET community standup.

Summary

In this short post I described how to update a .NET 5 ASP.NET Core app to .NET 6. The simple approach is to ignore the minimal hosting WebApplicationBuilder APIs, and just update your target framework! If you want to use the minimal hosting APIs, but have a large Startup class you don't want to replace, I showed how you could call your Startup class directly. Finally, if you want to go the other way and add some structure to your minimal APIs, check out Carter!

Andrew Lock | .Net Escapades
Want an email when
there's new posts?