When you create a new "Empty" web application, you get just 4 code lines in Program.cs:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
There is a new cut-down lambda syntax for endpoints, suitable for simple microservices. There is no routing and no controllers.
It's the new .net 6 no-class single-file philosophy. As I discussed before, it's nice for demos and teaching, but I'm sceptical for real-world professional development. You don't have to religiously adhere to SRP etc to recognise that 100s of lines of code in a single file is bad news.
There is also the risk of confusion- we already have asp MVC, web api, Razor pages and Blazor. You can mix these types, even in a single website, but that leaves an even heavier learning curve. You can start with one approach and over time the requirements change, forcing a rewrite.
The theory is that this is useful for microservices, doing simple and focussed tasks which are not going to evolve and grow. Of course, in practice everything becomes more complex over time.
If it looks familiar, it's an evolution of the .net 3 endpoints, which you often ignore because you just do endpoints.MapControllers.
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Hello World!");
});
});
Under the covers, it is still calling UseRouting() and UseEndpoints(), they are just hidden.
app.UseRouting();
Let's take a semi-realistic microservice- a monitoring service. Other services ping it to say they are alive; the monitoring services tracks the pings and can show a list of services that are out-of-date, possibly hung or stopped.
I'm using Swashbuckle.AspNetCore and EntityFrameworkCore.Sqlite here.
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddDbContext<MonitorDb>(opt => opt.UseSqlite("Data Source=Monitors.db;"));
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
//this endpoint just to create the empty db without doing a migration
app.MapGet("/", async (MonitorDb db) =>
await db.Database.EnsureCreatedAsync());
//all monitors
app.MapGet("/list", async (MonitorDb db) =>
await db.Monitors
.OrderBy(m => m.Name)
.ToListAsync());
//all monitors more than 5 minutes old
app.MapGet("/listOld", async (MonitorDb db) =>
await db.Monitors
.OrderBy(m => m.Recorded)
.Where(m => m.Recorded <= DateTime.Now.AddMinutes(-5))
.ToListAsync());
//ping from a named service
app.MapPost("/ping", async (string name, MonitorDb db) =>
{
var monitor = await db.Monitors.FirstOrDefaultAsync(x => x.Name == name);
if (monitor == null)
{
monitor = new Monitor { Name = name };
db.Monitors.Add(monitor);
}
monitor.Recorded = DateTime.Now;
await db.SaveChangesAsync();
return Results.Accepted($"/ping/{monitor.Name}", monitor);
});
app.Run();
//The model/table
class Monitor
{
public int Id { get; set; }
public DateTime? Recorded { get; set; }
public string? Name { get; set; }
}
//The EF DbContext
class MonitorDb : DbContext
{
public MonitorDb(DbContextOptions<MonitorDb> options)
: base(options) { }
public DbSet<Monitor> Monitors => Set<Monitor>();
}
That's 64 lines, not bad. You could reduce it by moving the DbContext and model class to their own files, and not doing the sketchy database creation, but you would add configuration of connection string, perhaps a database migrateAsync initialization, validation (which is lacking, see below), authorization etc
- Routing is via http action handlers- MapGet/MapPost/MapPut/MapDelete. The methods are lambdas, but you can create a method (returning IResult). The same MVC/API binding is being used for arguments, plus DI properties (see below)
- There is an implicit app.UseRouting() called before everything else (so you can app.UseAuthorization() anywhere- usually the order is critical)
- You can add .WithName("routeName") to help with routing redirects.
- Whereas controllers have IActionResult, minimal API lambdas return IResult (eg from static Results.Ok(), Results.BadRequest(), etc).
- DI cannot be via constructor args, so it has parameter DI (and standard HttpContext, HttpRequest, ClaimsPrincipal).
- There is no built-in model validation. You don't have MVC's controller.ModelState.IsValid() or the [ApiController] attribute. You have to do it manually.
- You can add authorization:
app.MapGet("/auth", () => Results.Ok).RequireAuthorization();
If you are confident your microservice will be small, simple and won't change, it looks ok, but it won't take much additional requirements to make good old controllers look easier.