I’ve been posting and making videos about ideas I’ve had for discovering Minimal APIs instead of mapping them all in Program.cs
for a while. I’ve finally codified it into an experimental nuget package. Let’s talk about how it works.
I also made a Coding Short video that covers this same topic, if you’d rather watch than read:
How It Works
The package can be installed via the dotnet tool:
dotnet add package WilderMinds.MinimalApiDiscovery
Once it is installed, you can use an interface called IApi
to implement classes that can register Minimal APIs. The IApi
interface looks like this:
/// <summary>
/// An interface for Identifying and registering APIs
/// </summary>
public interface IApi
{
/// <summary>
/// This is automatically called by the library to add your APIs
/// </summary>
/// <param name="app">The WebApplication object to register the API </param>
void Register(WebApplication app);
}
Essentially, you can implement classes that get passed the WebApplication
object to map your API calls:
public class StateApi : IApi
{
public void Register(WebApplication app)
{
app.MapGet("/api/states", (StateCollection states) =>
{
return states;
});
}
}
This would allow you to register a number of related API calls. I think one class per API is too restrictive. When used in .NET 7 and later, you could make a class per group
:
public void Register(WebApplication app)
{
var group = app.MapGroup("/api/films");
group.MapGet("", async (BechdelRepository repo) =>
{
return Results.Ok(await repo.GetAll());
})
.Produces(200);
group.MapGet("{id:regex(tt[0-9]*)}",
async (BechdelRepository repo, string id) =>
{
Console.WriteLine(id);
var film = await repo.GetOne(id);
if (film is null) return Results.NotFound("Couldn't find Film");
return Results.Ok(film);
})
.Produces(200);
group.MapGet("{year:int}", (BechdelRepository repo,
int year,
bool? passed = false) =>
{
var results = await repo.GetByYear(year, passed);
if (results.Count() == 0)
{
return Results.NoContent();
}
return Results.Ok(results);
})
.Produces(200);
group.MapPost("", (Film model) =>
{
return Results.Created($"/api/films/{model.IMDBId}", model);
})
.Produces(201);
}
Because of lambdas missing some features (e.g. default values), you can always move the lambdas to just static methods:
public void Register(WebApplication app)
{
var grp = app.MapGroup("/api/customers");
grp.MapGet("", GetCustomers);
grp.MapGet("", GetCustomer);
grp.MapPost("{id:int}", SaveCustomer);
grp.MapPut("{id:int}", UpdateCustomer);
grp.MapDelete("{id:int}", DeleteCustomer);
}
static async Task<IResult> GetCustomers(CustomerRepository repo)
{
return Results.Ok(await repo.GetCustomers());
}
//...
The reason for the suggestion of using static methods (instance methods would work too) is that you do not want these methods to rely on state. You might think that constructor service injection would be a good idea:
public class CustomerApi : IApi
{
private CustomerRepository _repo;
// MinimalApiDiscovery will log a warning because
// the repo will become a singleton and lifetime
// will be tied to the implementation methods.
// Better to use method injection in this case.
public CustomerApi(CustomerRepository repo)
{
_repo = repo;
}
// ...
This doesn’t work well as the call to Register
happens once at startup and since this class is sharing that state, the injected service becomes a singleton for the lifetime of the server. The library will log a warning if you do this to help you avoid it. Because of that I suggest that you use static methods instead to prevent this from accidently happening.
NOTE: I considered using static interfaces, but that requires that the instance is still a non-static class. It would also limit this library to use in .NET 7/C# 11 - which I didn’t want to do. It works in .NET 6 and above.
When you’ve created these classes, you can simple make two calls in startup to register all IApi
classes:
using UsingMinimalApiDiscovery.Data;
using WilderMinds.MinimalApiDiscovery;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddTransient<CustomerRepository>();
builder.Services.AddTransient<StateCollection>();
// Add all IApi classes to the Service Collection
builder.Services.AddApis();
var app = builder.Build();
// Call Register on all IApi classes
app.MapApis();
app.Run();
The idea here is to use reflection to find all IApi
classes and add them to the service collection. Then the call to MapApis()
will get all IApi
from the service collection and call Register.
How it works
The call to AddApis
simply uses reflection to find all classes that implement IApi
and add them to the service collection:
var apis = assembly.GetTypes()
.Where(t => t.IsAssignableTo(typeof(IApi)) &&
t.IsClass &&
!t.IsAbstract)
.ToArray();
// Add them all to the Service Collection
foreach (var api in apis)
{
// ...
coll.Add(new ServiceDescriptor(typeof(IApi), api, lifetime));
}
Once they’re all registered, the call to MapApis
is pretty simple:
var apis = app.Services.GetServices<IApi>();
foreach (var api in apis)
{
if (api is null) throw new InvalidProgramException("Apis not found");
api.Register(app);
}
Futures
While I’m happy with this use of Reflection since it is only a ‘startup’ time cost, I have it on my list to look at using a Source Generator instead.
If you have experience with Source Generators and want to give it a shot, feel free to do a pull request at https://github.com/wilder-minds/minimalapidiscovery.
I’m also considering removing the AddApis and just have the MapApis
call just reflect to find all the IApis and call register since we don’t actually need them in the Service Collection.
You can see the complete source and example here:
This work by Shawn Wildermuth is licensed under a Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported License.
Based on a work at wildermuth.com.
If you liked this article, see Shawn's courses on Pluralsight.
Top comments (0)