Comparing the performance between the Minimal API and classic Controllers

Today, we are going to benchmark the performance of the Minimal API in ASP.NET 9 (and for reference against ASP.NET 8 as well) against the classic Controllers. We will test a few scenarios and check how the performance of the Minimal API compares to the classic Controllers. Let's get right into it!

What are we going to do?

We are going to create the same API (from the clients point of view) using both the Minimal API and the classic Controllers. We will then test the performance of both APIs using the BenchmarkDotNet library. I try to explain all my steps and reasons and hope that I didn't miss anything obvious. Therefore a small disclaimer:


Those tests are not perfect and I might have missed something very crucial which skews the picture. Check for your concrete scenario what you need and just because something might be faster than something else, doesn't mean it is the right tool in your environment! Always cross-check results and don't just blindly trust benchmarks ( especially not mine 😄 )!

The test were run on a MacBook M2 Pro. I did run the tests against net8.0 and net9.0-rc.2. Here the reference output for my machine:

BenchmarkDotNet v0.14.0, macOS Sequoia 15.0.1 (24A348) [Darwin 24.0.0]
Apple M2 Pro, 1 CPU, 12 logical and 12 physical cores
.NET SDK 9.0.100-rc.2.24474.11
  [Host]   : .NET 8.0.8 (8.0.824.36612), Arm64 RyuJIT AdvSIMD
  .NET 8.0 : .NET 8.0.8 (8.0.824.36612), Arm64 RyuJIT AdvSIMD
  .NET 9.0 : .NET 9.0.0 (9.0.24.47305), Arm64 RyuJIT AdvSIMD

Mileage may vary! Keep in mind that this is an arm64 machine and you might have different results on your Windows or Linux x64 machine.

The test setup

The basic idea is to have two app builder creating the same API. One using the Minimal API and the other using the classic Controllers. I split them into two, so that they don't interfere with each other.

We use a HttpClient to make requests to the API and measure the performance of the it. So it the basic setup looks like this:

[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net80)]
[SimpleJob(RuntimeMoniker.Net90)]
public class Benchmark
{
    private WebApplication _minimalApiApp;
    private WebApplication _controllerApiApp;
		private HttpClient _minimalApiClient;
    private HttpClient _controllerApiClient;

    [Benchmark]
    public async Task<ReturnValue?> GetSimpleMinimalApi()
    {
        return await _minimalApiClient.GetFromJsonAsync<ReturnValue>("/simple");
    }
    
    [Benchmark]
    public async Task<ReturnValue?> GetSimpleController()
    {
        return await _controllerApiClient.GetFromJsonAsync<ReturnValue>("/simple-controller");
    }
    
    [GlobalSetup]
    public void Setup()
    {
        _minimalApiApp = CreateMinimalApiApp();
        _controllerApiApp = CreateControllerApp();

        _minimalApiClient = new HttpClient
        {
            BaseAddress = new Uri("http://localhost:1234")
        };
        _controllerApiClient = new HttpClient
        {
            BaseAddress = new Uri("http://localhost:1235")
        };
    }

    [GlobalCleanup]
    public void Cleanup()
    {
        _minimalApiApp.StopAsync().GetAwaiter().GetResult();
        _controllerApiApp.StopAsync().GetAwaiter().GetResult();
        _minimalApiClient.Dispose();
        _controllerApiClient.Dispose();
    }
    ...

We can see, that we have two methods GetSimpleMinimalApi and GetSimpleController which make a request to the Minimal API and the Controller API respectively. The GlobalSetup method creates the two applications and the GlobalCleanup method stops the applications and disposes the HttpClient. The interesting bids are inside the Create methods:

private static WebApplication CreateMinimalApiApp()
{
    var builder = WebApplication.CreateBuilder();
    var minimalApiApp = builder.Build();
    minimalApiApp.RunAsync("http://localhost:1234");
    return minimalApiApp;
}

private static WebApplication CreateControllerApp()
{
    var builder = WebApplication.CreateBuilder();
    builder.Services.AddControllers();
    var controllerApp = builder.Build();
    controllerApp.MapControllers();
    controllerApp.RunAsync("http://localhost:1235");
    return controllerApp;
}

That isn't all: In the appsettings.json I disabled any logging as I might skew the picture:

{
  "Logging": {
    "LogLevel": {
      "Default": "None",
      "Microsoft": "None",
      "Microsoft.Hosting.Lifetime": "None"
    }
  }
}

Also in the launchSettings.json I set the environment variable ASPNETCORE_ENVIRONMENT to Production to disable any development features that might interfere with the performance. Furthermore I disabled launchBrowser to avoid any browser opening. But that's it. Well kind of a lot code, but the rest will be easier. So let's start simple:

Test 1: Simple Get Request

The first example is a just making a get request that returns a pre-populated object in JSON. What I wanted to test here is the basic pipeline.

Code:

// Minimal API:
app.MapGet("/simple", () => ReturnValue.Instance);

// Controller
[ApiController]
public class SimpleController : ControllerBase
{
    [HttpGet("/simple-controller")]
    public ReturnValue Get() => ReturnValue.Instance;
}

public sealed record ReturnValue(string PropertyA, string PropertyB, IReadOnlyCollection<int> Numbers)
{
    public static readonly ReturnValue Instance = new("Hello", "World", [1, 2, 3, 4, 5, 6, 7]);
}

Results:

| Method              | Job      | Runtime  | Mean     | Error    | StdDev   | Allocated |
|-------------------- |--------- |--------- |---------:|---------:|---------:|----------:|
| GetSimpleMinimalApi | .NET 8.0 | .NET 8.0 | 60.89 us | 1.044 us | 1.497 us |   4.72 KB |
| GetSimpleController | .NET 8.0 | .NET 8.0 | 62.79 us | 0.661 us | 0.619 us |   7.11 KB |
| GetSimpleMinimalApi | .NET 9.0 | .NET 9.0 | 55.02 us | 0.364 us | 0.340 us |   4.67 KB |
| GetSimpleController | .NET 9.0 | .NET 9.0 | 59.11 us | 0.178 us | 0.167 us |   7.02 KB |

Noticeable is that the Minimal API is a bit faster than the Controller API with less allocations. The difference is not huge, but it is there. The Minimal API is faster in both .NET 8.0 and .NET 9.0. While .NET 9.0 is faster in overall runtime, it might be because the HttpClient is optimized. The better performance doesn't necessarily have to stem from "better" controllers!

I felt it was kind of obvious that the Minimal API would be faster, as it has less overhead. Here the comment from David Fowler:

Minimal APIs was the final phase in breaking up the monolith MVC framework that was a carry-over from ASP.NET on .NET Framework into "pay for play" pieces that could be used to build applications that scale from a single endpoint to many endpoints in your web application. Over time, we refactored many of the features of MVC like action descriptors and routing, different types of filters, model binding, results etc into the core platform. This is one of the reasons why minimal APIs is faster, it's pay for play and less extensible than MVC (by design!).

Source: Reddit

Test 2: Get Request with injected service

Controller and Minimal API do differ in the way you inject services into them. So let's have a look at the code:

// Minimal API
private static WebApplication CreateMinimalApiApp()
{
    var builder = WebApplication.CreateBuilder();
    builder.Services.AddScoped<SimpleService>();
    var minimalApiApp = builder.Build();
    minimalApiApp.MapGet("/simple", (SimpleService service) => new ReturnValue(service.GetHello(), "World", [1, 2, 3, 4, 5, 6, 7]));
    minimalApiApp.RunAsync("http://localhost:1234");
    return minimalApiApp;
}

// Controller
[ApiController]
public class SimpleController : ControllerBase
{
    private readonly SimpleService _simpleService;

    public SimpleController(SimpleService simpleService)
    {
        _simpleService = simpleService;
    }

    [HttpGet("/simple-controller")]
    public ReturnValue Get() => new(_simpleService.GetHello(), "World", [1, 2, 3, 4, 5, 6, 7]);
}

public class SimpleService
{
    public string GetHello() => "Hello";
}

Results:

| Method              | Job      | Runtime  | Mean     | Error    | StdDev   | Allocated |
|-------------------- |--------- |--------- |---------:|---------:|---------:|----------:|
| GetSimpleMinimalApi | .NET 8.0 | .NET 8.0 | 59.21 us | 0.340 us | 0.284 us |   5.22 KB |
| GetSimpleController | .NET 8.0 | .NET 8.0 | 62.23 us | 0.221 us | 0.184 us |   7.26 KB |
| GetSimpleMinimalApi | .NET 9.0 | .NET 9.0 | 55.65 us | 0.506 us | 0.448 us |   5.17 KB |
| GetSimpleController | .NET 9.0 | .NET 9.0 | 58.98 us | 0.239 us | 0.223 us |   7.17 KB |

Roughly the same picture as before - which isn't that suprising to be honest!

Test 3: Post Request with a body and query parameter

In this test, we will send a post request with a body and a query parameter. The code looks like this:

// Minimal API
minimalApiApp.MapPost("/simple", (
    Request request, 
    [FromQuery]int aNumber,
    SimpleService service) => new ReturnValue(service.GetHello(), request.Name, [aNumber, 2, 3, 4, 5, 6, 7]));

// Controller
[ApiController]
public class SimpleController : ControllerBase
{
    private readonly SimpleService _simpleService;

    public SimpleController(SimpleService simpleService)
    {
        _simpleService = simpleService;
    }
    
    [HttpPost("/simple-controller")]
    public ReturnValue Post([FromBody]Request request, [FromQuery] int aNumber)
    {
        return new ReturnValue(_simpleService.GetHello(), request.Name, [aNumber, 2, 3, 4, 5, 6, 7]);
    }
}

public record Request(string Name)
{
    public static readonly Request Instance = new("World");
}

I used a static readonly field to avoid any allocations that might interfere with the results.

The adopted benchmark code:

[Benchmark]
public async Task<HttpResponseMessage> PostWithQueryParameterMinimalApi()
{
    return await _minimalApiClient.PostAsJsonAsync("/simple?aNumber=2", Request.Instance);
}

[Benchmark]
public async Task<HttpResponseMessage> PostWithQueryParameterController()
{
    return await _controllerApiClient.PostAsJsonAsync("/simple-controller?aNumber=2", Request.Instance);
}
| Method                              | Job      | Runtime  | Mean     | Error    | StdDev   | Allocated |
|--------------------------------- |--------- |--------- |---------:|---------:|---------:|----------:|
| PostWithQueryParameterMinimalApi | .NET 8.0 | .NET 8.0 | 65.83 us | 1.172 us | 0.979 us |   7.03 KB |
| PostWithQueryParameterController | .NET 8.0 | .NET 8.0 | 69.27 us | 1.191 us | 1.055 us |  12.28 KB |
| PostWithQueryParameterMinimalApi | .NET 9.0 | .NET 9.0 | 61.69 us | 0.670 us | 0.560 us |   7.07 KB |
| PostWithQueryParameterController | .NET 9.0 | .NET 9.0 | 67.39 us | 1.234 us | 1.030 us |  12.27 KB |

Well - it doesn't really change much here. Again not all that surprising! Besides allocations, which are surprisingly high for the Controller API! But we are far from done!

Test 4: Middleware

Almost all .NET Web API's do have some kind of middleware. That starts by your authorization middleware and goes to your logging middleware. So let's create a few middlewares and see how they perform. Basically we want to add a correlation id header if not present and have something like a global exception handler:

// Minimal API
// Add a correlation id header if not present!
minimalApiApp.Use((context, next) =>
{
    if (!context.Request.Headers.ContainsKey("Correlation-Id"))
    {
        context.Request.Headers.Append("Correlation-Id", Guid.NewGuid().ToString());
    }
    
    return next(context);
});

// Check for an exception and return a 500 response if one is thrown
minimalApiApp.Use((context, next) =>
{
    try
    {
        return next(context);
    }
    catch (Exception e)
    {
        context.Response.StatusCode = 500;
        return context.Response.WriteAsync(e.Message);
    }
});

// Controller
controllerApp.UseMiddleware<CorrelationIdMiddleware>();
controllerApp.UseMiddleware<ExceptionHandlerMiddleware>();

public class CorrelationIdMiddleware
{
    private readonly RequestDelegate _next;

    public CorrelationIdMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        if (!context.Request.Headers.ContainsKey("Correlation-Id"))
        {
            context.Request.Headers.Append("Correlation-Id", Guid.NewGuid().ToString());
        }
        
        await _next(context);
    }
}

public class ExceptionHandlerMiddleware
{
    private readonly RequestDelegate _next;

    public ExceptionHandlerMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public Task InvokeAsync(HttpContext context)
    {
        try
        {
            return _next(context);
        }
        catch (Exception e)
        {
            context.Response.StatusCode = 500;
            return context.Response.WriteAsync(e.Message);
        }
    }
}

And yes I am aware that there are different ways of adding a middleware to a controller. But I wanted to show the most common way. The results are:

| Method                           | Job      | Runtime  | Mean     | Error    | StdDev   | Median   | Allocated |
|--------------------------------- |--------- |--------- |---------:|---------:|---------:|---------:|----------:|
| PostWithQueryParameterMinimalApi | .NET 8.0 | .NET 8.0 | 64.45 us | 0.303 us | 0.685 us | 64.26 us |   7.12 KB |
| PostWithQueryParameterController | .NET 8.0 | .NET 8.0 | 72.81 us | 2.040 us | 6.014 us | 68.80 us |  12.38 KB |
| PostWithQueryParameterMinimalApi | .NET 9.0 | .NET 9.0 | 61.53 us | 0.221 us | 0.206 us | 61.62 us |   7.17 KB |
| PostWithQueryParameterController | .NET 9.0 | .NET 9.0 | 66.00 us | 0.567 us | 0.530 us | 66.23 us |  12.37 KB |

Test 5: Parallel requests

In this test case I want to bombard the API with 100 requests in parallel and once they are done, we do it from the beginning. For starters I will remove all middlewares and just have a simple get request we had in the beginning:

// Minimal API
minimalApiApp.MapGet("/simple", () => ReturnValue.Instance);

// Controller
[ApiController]
public class SimpleController : ControllerBase
{
    [HttpGet("/simple-controller")]
    public ReturnValue Get() => ReturnValue.Instance;
}

// Setup
private static WebApplication CreateMinimalApiApp()
{
    var builder = WebApplication.CreateBuilder();
    var minimalApiApp = builder.Build();
    minimalApiApp.MapGet("/simple", () => ReturnValue.Instance);
    minimalApiApp.RunAsync("http://localhost:1234");
    return minimalApiApp;
}

private static WebApplication CreateControllerApp()
{
    var builder = WebApplication.CreateBuilder();
    builder.Services.AddControllers();
    var controllerApp = builder.Build();
    controllerApp.MapControllers();

    controllerApp.RunAsync("http://localhost:1235");
    return controllerApp;
}

The benchmark like this:

[Benchmark]
public async Task<List<ReturnValue>?> GetTwoTimes100InParallelMinimalApi()
{
    var tasks = new List<Task<ReturnValue?>>();
    for (var i = 0; i < 100; i++)
    {
        tasks.Add(_minimalApiClient.GetFromJsonAsync<ReturnValue?>("/simple"));
    }
    
    await Task.WhenAll(tasks);
    
    for (var i = 0; i < 100; i++)
    {
        tasks.Add(_minimalApiClient.GetFromJsonAsync<ReturnValue?>("/simple"));
    }
    
    await Task.WhenAll(tasks);
    return tasks.Select(t => t.Result).Select(s => s).ToList();
}

[Benchmark]
public async Task<List<ReturnValue>?> GetTwoTimes100InParallelController()
{
    var tasks = new List<Task<ReturnValue?>>();
    for (var i = 0; i < 100; i++)
    {
        tasks.Add(_controllerApiClient.GetFromJsonAsync<ReturnValue>("/simple-controller"));
    }

    await Task.WhenAll(tasks);
    
    for (var i = 0; i < 100; i++)
    {
        tasks.Add(_controllerApiClient.GetFromJsonAsync<ReturnValue>("/simple-controller"));
    }
    
    await Task.WhenAll(tasks);
    return tasks.Select(t => t.Result).Select(s => s).ToList();
}

My 12 CPU cores are going to be busy! The results are:

| Method                             | Job      | Runtime  | Mean     | Error     | StdDev    | Gen0   | Allocated  |
|----------------------------------- |--------- |--------- |---------:|----------:|----------:|-------:|-----------:|
| GetTwoTimes100InParallelMinimalApi | .NET 8.0 | .NET 8.0 | 3.027 ms | 0.0575 ms | 0.0510 ms |      - |   922.9 KB |
| GetTwoTimes100InParallelController | .NET 8.0 | .NET 8.0 | 3.131 ms | 0.0449 ms | 0.0398 ms | 7.8125 | 1403.14 KB |
| GetTwoTimes100InParallelMinimalApi | .NET 9.0 | .NET 9.0 | 2.962 ms | 0.0585 ms | 0.0928 ms | 3.9063 |  913.31 KB |
| GetTwoTimes100InParallelController | .NET 9.0 | .NET 9.0 | 2.960 ms | 0.0495 ms | 0.0413 ms | 7.8125 | 1382.78 KB |

Also here allocations are down by 1/3 for the Minimal API. The performance difference is not huge, but it is there. I am a bit puzzled how their is 0 Gen0 collections for the Minimal API on .NET 8.0. Also given, that on .NET 9.0 we do have some! If you see a bug in my code, please let me know!

Test 6: XXL everything

Now I want to know it - let's have:

  • A bigger response object
  • Add a query parameter as well!
  • Add back the middleware from earlier!
  • Add back the service, but let it do some more work (well not really, but it's bigger!)

I try to pre-allocate as much as possible to have the "cleanest" picutre in terms of what does the API do (HttpClient as well as web server). So my objects might look odd! Here is the whole code:

[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net80)]
[SimpleJob(RuntimeMoniker.Net90)]
public class Benchmark
{
    private HttpClient _minimalApiClient;
    private WebApplication _minimalApiApp;
    private WebApplication _controllerApiApp;
    private HttpClient _controllerApiClient;

[Benchmark]
public async Task<List<ReturnValue>?> GetTwoTimes100InParallelMinimalApi()
{
    var tasks = new List<Task<ReturnValue?>>();
    for (var i = 0; i < 100; i++)
    {
        tasks.Add(_minimalApiClient.GetFromJsonAsync<ReturnValue?>("/simple?a=1&b=2"));
    }
    
    await Task.WhenAll(tasks);
    
    for (var i = 0; i < 100; i++)
    {
        tasks.Add(_minimalApiClient.GetFromJsonAsync<ReturnValue?>("/simple?a=1&b=2"));
    }
    
    await Task.WhenAll(tasks);
    return tasks.Select(t => t.Result).Select(s => s).ToList();
}

[Benchmark]
public async Task<List<ReturnValue>?> GetTwoTimes100InParallelController()
{
    var tasks = new List<Task<ReturnValue?>>();
    for (var i = 0; i < 100; i++)
    {
        tasks.Add(_controllerApiClient.GetFromJsonAsync<ReturnValue>("/simple-controller?a=1&b=2"));
    }
    
    await Task.WhenAll(tasks);
    
    for (var i = 0; i < 100; i++)
    {
        tasks.Add(_controllerApiClient.GetFromJsonAsync<ReturnValue>("/simple-controller?a=1&b=2"));
    }
    
    await Task.WhenAll(tasks);
    return tasks.Select(t => t.Result).Select(s => s).ToList();
}

    [GlobalSetup]
    public void Setup()
    {
        _minimalApiApp = CreateMinimalApiApp();
        _controllerApiApp = CreateControllerApp();

        _minimalApiClient = new HttpClient
        {
            BaseAddress = new Uri("http://localhost:1234")
        };
        _controllerApiClient = new HttpClient
        {
            BaseAddress = new Uri("http://localhost:1235")
        };
    }

    [GlobalCleanup]
    public void Cleanup()
    {
        _minimalApiApp.StopAsync().GetAwaiter().GetResult();
        _controllerApiApp.StopAsync().GetAwaiter().GetResult();
        _minimalApiClient.Dispose();
        _controllerApiClient.Dispose();
    }

private static WebApplication CreateMinimalApiApp()
{
    var builder = WebApplication.CreateBuilder();
    builder.Services.AddScoped<Service>();
    var minimalApiApp = builder.Build();
    
    // Add a correlation id header if not present!
    minimalApiApp.Use((context, next) =>
    {
        if (!context.Request.Headers.ContainsKey("Correlation-Id"))
        {
            context.Request.Headers.Append("Correlation-Id", Guid.NewGuid().ToString());
        }
    
        return next(context);
    });

    // Check for an exception and return a 500 response if one is thrown
    minimalApiApp.Use((context, next) =>
    {
        try
        {
            return next(context);
        }
        catch (Exception e)
        {
            context.Response.StatusCode = 500;
            return context.Response.WriteAsync(e.Message);
        }
    });
    minimalApiApp.MapGet("/simple", ([FromQuery]string a, [FromQuery]string b, Service service) => 
        service.Get(a, b));
    minimalApiApp.RunAsync("http://localhost:1234");
    return minimalApiApp;
}

private static WebApplication CreateControllerApp()
{
    var builder = WebApplication.CreateBuilder();
    builder.Services.AddScoped<Service>();
    builder.Services.AddControllers();
    var controllerApp = builder.Build();
    controllerApp.UseMiddleware<CorrelationIdMiddleware>();
    controllerApp.UseMiddleware<ExceptionHandlerMiddleware>();
    controllerApp.MapControllers();
    controllerApp.RunAsync("http://localhost:1235");
    return controllerApp;
}
}

[ApiController]
public class SimpleController : ControllerBase
{
    private readonly Service _service;

    public SimpleController(Service service) => _service = service;

    [HttpGet("/simple-controller")]
    public ReturnValue Get([FromQuery] string a, [FromQuery] string b) => _service.Get(a, b);
}

public sealed record ReturnValue
{
    private static readonly List<int> PreNumbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    
    public required string Property { get; init; }
    public string Property2 { get; init; } = "Some Value";
    public IReadOnlyCollection<int> Numbers { get; init; } = PreNumbers;
    public ReturnValue? Child { get; init; }
}

public class CorrelationIdMiddleware
{
    private readonly RequestDelegate _next;

    public CorrelationIdMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        if (!context.Request.Headers.ContainsKey("Correlation-Id"))
        {
            context.Request.Headers.Append("Correlation-Id", Guid.NewGuid().ToString());
        }
        
        await _next(context);
    }
}

public class ExceptionHandlerMiddleware
{
    private readonly RequestDelegate _next;

    public ExceptionHandlerMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public Task InvokeAsync(HttpContext context)
    {
        try
        {
            return _next(context);
        }
        catch (Exception e)
        {
            context.Response.StatusCode = 500;
            return context.Response.WriteAsync(e.Message);
        }
    }
}

public class Service
{
    public ReturnValue Get(string a, string b) =>
        new()
        {
            Property = a,
            Child = new ReturnValue
            {
                Property = b
            }
        };
}

The results are:

| Method                             | Job      | Runtime  | Mean     | Error     | StdDev    | Gen0    | Gen1   | Allocated |
|----------------------------------- |--------- |--------- |---------:|----------:|----------:|--------:|-------:|----------:|
| GetTwoTimes100InParallelMinimalApi | .NET 8.0 | .NET 8.0 | 3.040 ms | 0.0603 ms | 0.1310 ms |  7.8125 |      - |   1.19 MB |
| GetTwoTimes100InParallelController | .NET 8.0 | .NET 8.0 | 3.185 ms | 0.0635 ms | 0.0594 ms | 15.6250 | 7.8125 |   2.14 MB |
| GetTwoTimes100InParallelMinimalApi | .NET 9.0 | .NET 9.0 | 2.973 ms | 0.0593 ms | 0.1198 ms |  7.8125 |      - |   1.18 MB |
| GetTwoTimes100InParallelController | .NET 9.0 | .NET 9.0 | 3.077 ms | 0.0609 ms | 0.1187 ms | 15.6250 |      - |   2.13 MB |

Yeah - you get the picture.

Conclusion

Minimal API delivers! Most noticable is the lower allocation rate. The performance difference is not huge, but it is there. But performance should be only one dimension of your decision. Microsoft has a nice article about the differences and what to choose: "Choose between controller-based APIs and minimal APIs".

Marking API's as obsolete or as experimental

Often times your API in your program or library evolves. So you will need a mechanism of telling that a specific API (an interface or just a simple method call) is obsolete and might be not there anymore in the next major version.

Also it can happen that you have a preview version of a API, which might not be rock-stable and the API-surface might change. How do we indicate that to the user?

Building a Minimal ASP.NET Core clone

In this article, we will build a minimal version of what ASP.NET Core does - yes, you read right. We will create a very simplistic clone of ASP.NET Core to discuss how the whole thing works. Beginning with a simple console application, we will add the necessary components to make it work as a web server. Bonus points for our own middleware pipeline and dependency injection.

Organizing Parameters in Minimal API with the AsParametersAttribute

Even though it was introduced in .NET 7, I came across recently the AsParametersAttribute. Let's have a look what it is good for.

An error has occurred. This application may no longer respond until reloaded. Reload x