question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

"Minimal hosting" for ASP.NET Core applications

See original GitHub issue

Summary

We want to introduce a new “direct hosting” model for ASP.NET Core applications. This is a more focused, low ceremony way of creating a web application.

Motivation and goals

Introducing a lower ceremony replacement for the WebHost to remove some of the ceremony in hosting ASP.NET Core applications. We’ve received lots of feedback over the years about how much ceremony it is to get a simple API up and running and we have a chance to improve that with the deprecation of the WebHost.

In scope

  • Build on the same primitives as ASP.NET Core
  • Take advantage of existing ASP.NET Core middleware and frameworks built on top
  • Ability to use existing extension methods on the IServiceCollection, IHostBuilder and IWebHostBuilder

Out of scope

  • Changing the DI registration model
  • Testability - While this is possible makes it very hard to reduce some of the ceremony

Risks / unknowns

  • Having multiple ways to build a web application.
  • Tools are broken
    • EF Core Tools (for example, migration) try to invoke Program.CreateHostBuilder() which no longer exists
    • Unit testing with Test Server

Strawman proposal

The idea is to reduce the number of concepts while keeping compatibility with the ecosystem we have today. Some core ideas in this new model is to:

  • Reduce the number of callbacks used to configure top level things
  • Expose the number of top level properties for things people commonly resolve in Startup.Configure. This allows them to avoid using the service locator pattern for IConfiguration, ILogger, IHostApplicationLifetime.
  • Merge the IApplicationBuilder, the IEndpointRouteBuilder and the IHost into a single object. This makes it easy to register middleware and routes without needed an additional level of lambda nesting (see the first point).
  • Merge the IConfigurationBuilder, IConfiguration, and IConfigurationRoot into a single Configuration type so that we can access configuration while it’s being built. This is important since you often need configuration data as part of configuring services.
  • UseRouting and UseEndpoints are called automatically (if they haven’t already been called) at the beginning and end of the pipeline.
public class WebApplicationBuilder
{
    public IWebHostEnvironment Environment { get; }
    public IServiceCollection Services { get; }
    public Configuration Configuration { get; }
    public ILoggingBuilder Logging { get; }

    // Ability to configure existing web host and host
    public ConfigureWebHostBuilder WebHost { get; }
    public ConfigureHostBuilder Host { get; }

    public WebApplication Build();
}

public class Configuration : IConfigurationRoot, IConfiguration, IConfigurationBuilder { }

// The .Build() methods are explicitly implemented interface method that throw NotSupportedExceptions
public class ConfigureHostBuilder : IHostBuilder { }
public class ConfigureWebHostBuilder : IWebHostBuilder { }

public class WebApplication : IHost, IApplicationBuilder, IEndpointRouteBuilder, IAsyncDisposable
{
    // Top level properties to access common services
    public ILogger Logger { get; }
    public IEnumerable<string> Addresses { get; }
    public IHostApplicationLifetime Lifetime { get; }
    public IServiceProvider Services { get; }
    public IConfiguration Configuration { get; }
    public IWebHostEnvironment Environment { get; }

    // Factory methods
    public static WebApplication Create(string[] args);
    public static WebApplication Create();
    public static WebApplicationBuilder CreateBuilder();
    public static WebApplicationBuilder CreateBuilder(string[] args);

    // Methods used to start the host
    public void Run(params string[] urls);
    public void Run();
    public Task RunAsync(params string[] urls);
    public Task RunAsync(CancellationToken cancellationToken = default);
    public Task StartAsync(CancellationToken cancellationToken = default);
    public Task StopAsync(CancellationToken cancellationToken = default);

    public void Dispose();
    public ValueTask DisposeAsync();
}

Examples

Hello World

using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;

var app = WebApplication.Create(args);

app.MapGet("/", async http =>
{
    await http.Response.WriteAsync("Hello World");
});

await app.RunAsync();

Hello MVC

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

var app = builder.Build();

app.MapControllers();

await app.RunAsync();

public class HomeController
{
    [HttpGet("/")]
    public string HelloWorld() => "Hello World";
}

Integrated with 3rd party ASP.NET Core based frameworks (Carter)

using Carter;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCarter();

var app = builder.Build();

app.Listen("http://localhost:3000");

app.MapCarter();

await app.RunAsync();

public class HomeModule : CarterModule
{
    public HomeModule()
    {
        Get("/", async (req, res) => await res.WriteAsync("Hello from Carter!"));
    }
}

More complex, taking advantage of the existing ecosystem of extension methods

using System.Threading.Tasks;
using Autofac;
using Autofac.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Serilog;

var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddYamlFile("appsettings.yml", optional: true);

builder.Host.UseSerilog((context, configuration)
    => configuration
        .Enrich
        .FromLogContext()
        .WriteTo
        .Console()
    );

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

builder.Host.ConfigureContainer<ContainerBuilder>(b =>
{
    // Register services using Autofac specific methods here
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

app.UseRouting();

app.MapGet("/", async http =>
{
    await http.Response.WriteAsync("Hello World");
});

await app.RunAsync("http://localhost:3000");

cc @LadyNaggaga @halter73 @shirhatti

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Reactions:24
  • Comments:47 (40 by maintainers)

github_iconTop GitHub Comments

4reactions
kspeakmancommented, Apr 1, 2021

Some ideas as a user of the current builder APIs.

For a point of reference, here is what I need to do to turn the existing APIs into modular host options in F#. This is the (under-parameterized) code to focus on.

        createBuilder ()
        |> setConfig basePath
        |> setLogger
        |> setKestrel
        |> setJwtAuth
        |> setCorsPolicy
        |> setAppRoute (App.routes settings logger)
        |> build

The C# equivalent would be a fluent builder interface. The current scheme uses builders, but with some unnecessary pain points:

  • Figuring out which namespace to install/open to get the correct UseX extension
    • Examples frequently use a builder extension without showing the open, or the open differs from the package name.
    • I guess more of a packaging concern
  • Finding the right method overload for my needs is trial and error and/or online research
    • Alternatively, knowing which I-thing to pull out of DI is similar pain
  • AddX vs UseX
    • in some cases both are required and UseX is easy to miss
    • Inconsistent experience. Most CORS config is in UseX, but in AddX for JWT auth
  • Sub-builders frequently require invoking methods
    • Worse, the order of these matter and can be incorrect for purpose
    • Where possible, the configuration should just be data, plus the occasional lambda clause 🎅
    • Required method calls could be just data to the user (the method’s parameters), and internally use the command pattern
    • If correct ordering is needed, it can be determined at command execution (host build) time
  • Group related options into a record rather than lambda option-setting or a builder method per property
    • The provided record would get merged into the feature default (like lambda options do)
    • Configuration examples should avoid positional record syntax. Labels make things clearer.

The builders that we have now are mostly data disguised as method calls and are sometimes abused as the latter. Probably the simplest way to improve them is remove the disguise. And allow different features to be well isolated from each other. I always dreaded the Startup class because, it was such a mix of concerns that it was hard to maintain over time. (Also I didn’t like it because of reflection-based startup… It’s well worth the line of code to know where startup is called and have the ability to run code after it ends.)

The best case scenario would be the ability to copy and paste feature config data, tweak some values, and have that feature running. No special method sequencing. No lambda procedures. Just data. And being able to mix and match config for different features without rework. After that, some marketing-focused shortcut overloads for common scenarios can be made for people to ooh and aah over.

Hopefully these are helpful comments and appropriate for the discussion. I can delete if not.

3reactions
davidfowlcommented, Mar 27, 2021

@charlesroddie this issue has nothing to do with the request handling part of the code, it’s about describing a new host API with the existing request handling APIs.

That said, the RequestDelegate is a well established primitive in the stack. We can always add new method overloads but I’d be wary about introducing a new core primitive type like a different response/request. The existing response doesn’t fit this model but higher level abstractions can be built on top.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Code samples migrated to the new minimal hosting model ...
This article provides samples of code migrated to ASP.NET Core 6.0. ASP.NET Core 6.0 uses a new minimal hosting model.
Read more >
How to use the minimal hosting model in ASP.NET Core 6
The minimal hosting model in ASP.NET Core 6 means having to write less boilerplate code to get your application up and running.
Read more >
The Previous Hosting Model versus the New Minimal ...
NET 6, a new Minimal Hosting Model was introduced to create, configure, and run a host for our ASP.NET Core web apps. A...
Read more >
Why migrate to the ASP.NET Core 6 minimal hosting model?
I'm upgrading a system from ASP.NET Core 5 to 6. I've read the migration guide for the new "minimal hosting model". The docs...
Read more >
Adding Clarity To .NET Minimal Hosting APIs
Minimal hosting generally takes advantage of C# 9 and C# 10 features to reduce the boilerplate necessary to write, maintain, and extend a...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found