Minimal API Advanced

Back

Loading concept...

๐Ÿš€ 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:

  1. Checks if they have an invitation โœ‰๏ธ
  2. Makes sure they brought a gift ๐ŸŽ
  3. 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?

  1. Consistent: Same format every time
  2. Clear: Other developers understand it
  3. HTTP Standards: Follows web rules
  4. 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! ๐Ÿš€

Loading story...

Story - Premium Content

Please sign in to view this story and start learning.

Upgrade to Premium to unlock full access to all stories.

Stay Tuned!

Story is coming soon.

Story Preview

Story - Premium Content

Please sign in to view this concept and start learning.

Upgrade to Premium to unlock full access to all content.