๐ Minimal API Advanced: The Secret Superpowers
Imagine youโre building a LEGO castle. You already know how to snap blocks together. Now you want to add secret doors, magic paint, a treasure map, and a guard who checks everyone at the gate. Thatโs what weโre learning today!
๐ฏ What Weโll Discover
| Superpower | What It Does |
|---|---|
| Endpoint Filters | Guards checking visitors |
| Results Class | Standard message boxes |
| TypedResults | Smart message boxes |
| OpenAPI | Treasure maps for your API |
| Validation | Making sure data is correct |
๐ก๏ธ Endpoint Filters: The Guards at the Gate
What Are They?
Think of a birthday party. Before guests come in, someone at the door:
- Checks if they have an invitation โ๏ธ
- Makes sure they brought a gift ๐
- After the party, gives them a goodie bag ๐๏ธ
Endpoint Filters work the same way! They run code before and after your API endpoint.
Why Use Them?
- โ Check if someone is allowed in (authentication)
- โ Write down who visited (logging)
- โ Measure how long things take (performance)
- โ Change the response before sending it back
Simple Example
app.MapGet("/hello", () => "Hi there!")
.AddEndpointFilter(async (context, next) =>
{
// BEFORE: Guest arriving
Console.WriteLine("Someone is coming!");
// Let them in
var result = await next(context);
// AFTER: Guest leaving
Console.WriteLine("Goodbye!");
return result;
});
Real-World Filter Class
public class LoggingFilter : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
var start = DateTime.Now;
var result = await next(context);
var time = DateTime.Now - start;
Console.WriteLine(quot;Took {time.TotalMilliseconds}ms");
return result;
}
}
Using it:
app.MapGet("/products", GetProducts)
.AddEndpointFilter<LoggingFilter>();
Chaining Multiple Filters
Like having multiple guards, each checking different things:
app.MapPost("/order", CreateOrder)
.AddEndpointFilter<AuthFilter>() // Check ID
.AddEndpointFilter<LoggingFilter>() // Write in logbook
.AddEndpointFilter<TimingFilter>(); // Track time
graph TD A["๐จ Request Arrives"] --> B["๐ก๏ธ Filter 1: Auth"] B --> C["๐ Filter 2: Logging"] C --> D["โฑ๏ธ Filter 3: Timing"] D --> E["๐ฏ Your Endpoint Code"] E --> F["โฑ๏ธ Filter 3: Done"] F --> G["๐ Filter 2: Done"] G --> H["๐ก๏ธ Filter 1: Done"] H --> I["๐ค Response Sent"]
๐ฆ Results Class: Standard Message Boxes
What Is It?
When you send a letter, you put it in a standard envelope. Everyone knows how to open it!
The Results class gives you standard ways to send responses.
Common Results
| Method | What It Means |
|---|---|
Results.Ok(data) |
โ Hereโs your stuff! (200) |
Results.NotFound() |
โ Canโt find it! (404) |
Results.BadRequest() |
๐ซ You asked wrong! (400) |
Results.Created() |
๐ Made something new! (201) |
Simple Example
app.MapGet("/toy/{id}", (int id) =>
{
var toy = FindToy(id);
if (toy == null)
return Results.NotFound("Toy not found!");
return Results.Ok(toy);
});
More Results You Can Use
// Redirect to another page
Results.Redirect("/new-page");
// Send a file
Results.File(bytes, "image/png");
// Send JSON data
Results.Json(myObject);
// No content (empty success)
Results.NoContent();
// Server error
Results.Problem("Something broke!");
Why Use Results?
- Consistent: Same format every time
- Clear: Other developers understand it
- HTTP Standards: Follows web rules
- Easy Testing: Simple to check in tests
๐ฏ TypedResults: Smart Message Boxes
The Problem with Results
Look at this code:
app.MapGet("/item/{id}", (int id) =>
{
if (id < 0)
return Results.BadRequest();
return Results.Ok(new Item { Id = id });
});
Question: What does this return? ๐ค
The compiler doesnโt know! It just sees IResult.
TypedResults to the Rescue!
app.MapGet("/item/{id}", Results<Ok<Item>, BadRequest> (int id) =>
{
if (id < 0)
return TypedResults.BadRequest();
return TypedResults.Ok(new Item { Id = id });
});
Now the compiler knows exactly what responses are possible!
The Magic Difference
| Feature | Results | TypedResults |
|---|---|---|
| Return Type | IResult |
Ok<T>, NotFound, etc. |
| Compile-time Check | โ No | โ Yes |
| OpenAPI Docs | Manual | Automatic! |
| IntelliSense | Limited | Full support |
Combining Multiple Response Types
app.MapGet("/user/{id}",
Results<Ok<User>, NotFound, BadRequest<string>>
(int id) =>
{
if (id < 0)
return TypedResults.BadRequest("ID must be positive");
var user = FindUser(id);
if (user == null)
return TypedResults.NotFound();
return TypedResults.Ok(user);
});
graph TD A["Request: GET /user/5"] --> B{id < 0?} B -->|Yes| C["BadRequest ๐ซ"] B -->|No| D{User exists?} D -->|No| E["NotFound โ"] D -->|Yes| F["Ok with User โ "]
๐บ๏ธ OpenAPI in Minimal APIs: Your Treasure Map
What Is OpenAPI?
Imagine you built an amazing playground. But how do friends know:
- What slides exist? ๐
- How to use the swings?
- Whatโs the climbing wall like?
OpenAPI creates a map of your API so everyone knows whatโs available!
Setting It Up
Step 1: Add the package
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
Step 2: Enable the UI
app.UseSwagger();
app.UseSwaggerUI();
Step 3: Visit /swagger - See your map! ๐บ๏ธ
Adding Descriptions
app.MapGet("/toys", () => GetAllToys())
.WithName("GetToys")
.WithDescription("Gets all available toys")
.WithTags("Toys");
Documenting Parameters
app.MapGet("/toy/{id}", (int id) => FindToy(id))
.WithName("GetToyById")
.WithOpenApi(op =>
{
op.Summary = "Find a toy";
op.Description = "Finds a toy by its ID number";
op.Parameters[0].Description = "The toy's ID";
return op;
});
Documenting Responses
app.MapGet("/toy/{id}", GetToy)
.Produces<Toy>(200) // Success with Toy
.Produces(404) // Not found
.ProducesProblem(500); // Server error
With TypedResults (Automatic!)
app.MapGet("/toy/{id}",
Results<Ok<Toy>, NotFound> (int id) =>
{
var toy = FindToy(id);
return toy is not null
? TypedResults.Ok(toy)
: TypedResults.NotFound();
});
// OpenAPI docs are generated automatically! โจ
โ Minimal API Validation: Checking the Homework
Why Validate?
If someone orders a pizza with -5 toppings or an email like โnot.anโ email", thatโs a problem!
Validation = Making sure data is correct before using it.
Method 1: Manual Validation
app.MapPost("/user", (User user) =>
{
if (string.IsNullOrEmpty(user.Name))
return Results.BadRequest("Name is required");
if (user.Age < 0 || user.Age > 150)
return Results.BadRequest("Invalid age");
// Save user...
return Results.Ok(user);
});
Method 2: Data Annotations
public class User
{
[Required]
[StringLength(50)]
public string Name { get; set; }
[Range(0, 150)]
public int Age { get; set; }
[EmailAddress]
public string Email { get; set; }
}
Method 3: Using a Validation Filter
public class ValidationFilter<T> : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext ctx,
EndpointFilterDelegate next)
{
var obj = ctx.Arguments
.OfType<T>()
.FirstOrDefault();
if (obj == null)
return Results.BadRequest("Missing data");
var results = new List<ValidationResult>();
var context = new ValidationContext(obj);
if (!Validator.TryValidateObject(
obj, context, results, true))
{
return Results.BadRequest(results);
}
return await next(ctx);
}
}
Using the filter:
app.MapPost("/user", CreateUser)
.AddEndpointFilter<ValidationFilter<User>>();
Method 4: FluentValidation (Popular Library)
public class UserValidator : AbstractValidator<User>
{
public UserValidator()
{
RuleFor(x => x.Name)
.NotEmpty()
.MaximumLength(50);
RuleFor(x => x.Age)
.InclusiveBetween(0, 150);
RuleFor(x => x.Email)
.EmailAddress();
}
}
graph TD A["๐จ Data Arrives"] --> B{Valid?} B -->|โ No| C["Return Error Message"] B -->|โ Yes| D["Process the Data"] D --> E["Send Success Response"]
๐ช Putting It All Together
Hereโs a complete example using everything we learned:
var builder = WebApplication.CreateBuilder(args);
// OpenAPI setup
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Enable Swagger
app.UseSwagger();
app.UseSwaggerUI();
// Our endpoint with ALL superpowers!
app.MapPost("/toys",
Results<Created<Toy>, BadRequest<string>>
(Toy toy) =>
{
if (string.IsNullOrEmpty(toy.Name))
return TypedResults.BadRequest("Name required");
// Save toy...
toy.Id = GenerateId();
return TypedResults.Created(quot;/toys/{toy.Id}", toy);
})
.AddEndpointFilter<LoggingFilter>()
.WithName("CreateToy")
.WithDescription("Creates a new toy")
.WithTags("Toys");
app.Run();
๐ Quick Summary
| Concept | One-Liner |
|---|---|
| Endpoint Filters | Run code before/after endpoints |
| Results | Standard HTTP response helpers |
| TypedResults | Type-safe response helpers |
| OpenAPI | Auto-generated API documentation |
| Validation | Ensure data is correct |
๐ฏ Remember This!
Filters = Security guards ๐ก๏ธ Results = Standard envelopes ๐ง TypedResults = Smart envelopes ๐ฌ OpenAPI = Your APIโs treasure map ๐บ๏ธ Validation = Homework checker โ
Youโve just unlocked the advanced superpowers of Minimal APIs! Now go build something amazing! ๐
