Dependency Injection in Minimal APIs in .Net 6

Minimal Apis in .Net 6 are really an absolutely amazing feature – you can create an API in about 5 or 6 lines of code. If you have a look online, you’ll see a plethora of examples… but unfortunately, they all show you how to write a “Hello World” API.

I’ve recently been playing with these, and initially found the least obvious part to be the DI part. In a controller, you’d simply inject the dependencies into the constructor of the controller; however, the approach is more nuanced with the minimal APIs. Essentially, you use the parameters of the method itself.

Let’s investigate this by looking at a very simple post method:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.MapPost("/test", () =>
{
    return Results.Ok();
});

app.Run();

This is a cut down version of the code you’ll get from the weather forecast example in the default template. Now, let’s assume that we want to use the IHttpClientFactory from the controller; we can simply do this:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddHttpClient(); // Add HttpClient here

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.MapPost("/test", (IHttpClientFactory httpClientFactory) =>
{
    var client = httpClientFactory.CreateClient();
    return Results.Ok();
});

app.Run();

As you can see, we’ve injected IHttpClientFactory into our method. This works well, but what if we now want to accept a serialised body in our request; consider the following example:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddHttpClient(); // Add HttpClient here

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.MapPost("/test", (MyClass myClass, IHttpClientFactory httpClientFactory) =>
{
    var client = httpClientFactory.CreateClient();
    return Results.Ok();
});

app.Run();

We’re passing in MyClass; and, in fact, this will now work – when you run it, you’ll see from swagger that it will expect you to pass the contents of MyClass into the endpoint. “That’s great,” I hear you ask, “But what if I want to register MyClass in the DI and pass that in?” To which I’ll respond: “Well – this actually works out of the box”:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddHttpClient(); // Add HttpClient here
builder.Services.AddSingleton<MyClass>(new MyClass() { SomeProperty = "aardvark"});

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.MapPost("/test", (MyClass myClass, IHttpClientFactory httpClientFactory) =>
{
    var client = httpClientFactory.CreateClient();
    return Results.Ok();
});

app.Run();

The DI is clever enough to determine that you’ve registered a singleton, and so you don’t need that information passing into the endpoint. Again, I hear the reader query this: “All very well, but what if I haven’t registered the class in the DI, but I wanted to and forgot?” Again, I’d respond: “Ah – an excellent question!” You can, indeed force the issue: just as you can use the [FromBody] attribute to insist that the endpoint takes the value from the endpoint body, so you can use the [FromServices] attribute to tell it that you want the class to be resolved from the DI:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddHttpClient(); // Add HttpClient here
//builder.Services.AddSingleton<MyClass>(new MyClass() { SomeProperty = "aardvark"});

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.MapPost("/test", ([FromServices]MyClass myClass, IHttpClientFactory httpClientFactory) =>
{
    var client = httpClientFactory.CreateClient();
    return Results.Ok();
});

app.Run();

The above code will run, and when you call the endpoint, it will crash, telling you that you haven’t told it what you want MyClass to be.

Summary

All pretty nifty if you ask me – it works by default for the 90% case, and for the last 10% there are some simple attributes you can set to force the issue.

I should probably just add that, generally speaking, adding functionality directly to the controller / endpoint method is a bad idea. Most of the time, what you’ll really want to do in the endpoint is simply resolve a service and call a method there.

References

https://benfoster.io/blog/mvc-to-minimal-apis-aspnet-6/

https://docs.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis

https://www.hanselman.com/blog/exploring-a-minimal-web-api-with-aspnet-core-6

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.