Filter di aplikasi Minimal API

Oleh Fiyaz Bin Hasan, Martin Costello, dan Rick Anderson

Filter API minimal memungkinkan pengembang untuk menerapkan logika bisnis yang mendukung:

  • Menjalankan kode sebelum dan sesudah handler titik akhir.
  • Memeriksa dan memodifikasi parameter yang disediakan selama pemanggilan handler titik akhir.
  • Mencegat perilaku respons handler titik akhir.

Filter dapat membantu dalam skenario berikut:

  • Memvalidasi parameter dan isi permintaan yang dikirim ke titik akhir.
  • Mencatat informasi tentang permintaan dan respons.
  • Memvalidasi bahwa permintaan menargetkan versi API yang didukung.

Filter dapat didaftarkan dengan menyediakan Delegasi yang mengambil EndpointFilterInvocationContext dan mengembalikan EndpointFilterDelegate. EndpointFilterInvocationContext menyediakan akses ke HttpContext permintaan dan daftar yang Arguments menunjukkan argumen yang diteruskan ke handler dalam urutan muncul dalam deklarasi handler.

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

string ColorName(string color) => $"Color specified: {color}!";

app.MapGet("/colorSelector/{color}", ColorName)
    .AddEndpointFilter(async (invocationContext, next) =>
    {
        var color = invocationContext.GetArgument<string>(0);

        if (color == "Red")
        {
            return Results.Problem("Red not allowed!");
        }
        return await next(invocationContext);
    });

app.Run();

Kode sebelumnya:

  • AddEndpointFilter Memanggil metode ekstensi untuk menambahkan filter ke /colorSelector/{color} titik akhir.
  • Mengembalikan warna yang ditentukan kecuali untuk nilai "Red".
  • Mengembalikan Hasil.Masalah saat /colorSelector/Red diminta.
  • next Menggunakan sebagai dan invocationContext sebagai EndpointFilterDelegateEndpointFilterInvocationContext untuk memanggil filter berikutnya dalam alur atau delegasi permintaan jika filter terakhir telah dipanggil.

Filter dijalankan sebelum handler titik akhir. Ketika beberapa AddEndpointFilter pemanggilan dilakukan pada handler:

  • Kode filter yang EndpointFilterDelegate disebut sebelum (next) dipanggil dijalankan dalam urutan Urutan Pertama Masuk, Keluar Pertama (FIFO).
  • Kode filter yang EndpointFilterDelegate dipanggil setelah (next) dipanggil dijalankan dalam urutan Urutan Masuk Pertama, Terakhir Keluar (FILO).
var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () =>
    {
        app.Logger.LogInformation("             Endpoint");
        return "Test of multiple filters";
    })
    .AddEndpointFilter(async (efiContext, next) =>
    {
        app.Logger.LogInformation("Before first filter");
        var result = await next(efiContext);
        app.Logger.LogInformation("After first filter");
        return result;
    })
    .AddEndpointFilter(async (efiContext, next) =>
    {
        app.Logger.LogInformation(" Before 2nd filter");
        var result = await next(efiContext);
        app.Logger.LogInformation(" After 2nd filter");
        return result;
    })
    .AddEndpointFilter(async (efiContext, next) =>
    {
        app.Logger.LogInformation("     Before 3rd filter");
        var result = await next(efiContext);
        app.Logger.LogInformation("     After 3rd filter");
        return result;
    });

app.Run();

Dalam kode sebelumnya, filter dan titik akhir mencatat output berikut:

Before first filter
    Before 2nd filter
        Before 3rd filter
            Endpoint
        After 3rd filter
    After 2nd filter
After first filter

Kode berikut menggunakan filter yang mengimplementasikan IEndpointFilter antarmuka:

using Filters.EndpointFilters;

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () =>
    {
        app.Logger.LogInformation("Endpoint");
        return "Test of multiple filters";
    })
    .AddEndpointFilter<AEndpointFilter>()
    .AddEndpointFilter<BEndpointFilter>()
    .AddEndpointFilter<CEndpointFilter>();

app.Run();

Dalam kode sebelumnya, filter dan log handler menunjukkan urutan dijalankannya:

AEndpointFilter Before next
BEndpointFilter Before next
CEndpointFilter Before next
      Endpoint
CEndpointFilter After next
BEndpointFilter After next
AEndpointFilter After next

Filter yang mengimplementasikan IEndpointFilter antarmuka ditampilkan dalam contoh berikut:


namespace Filters.EndpointFilters;

public abstract class ABCEndpointFilters : IEndpointFilter
{
    protected readonly ILogger Logger;
    private readonly string _methodName;

    protected ABCEndpointFilters(ILoggerFactory loggerFactory)
    {
        Logger = loggerFactory.CreateLogger<ABCEndpointFilters>();
        _methodName = GetType().Name;
    }

    public virtual async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context,
        EndpointFilterDelegate next)
    {
        Logger.LogInformation("{MethodName} Before next", _methodName);
        var result = await next(context);
        Logger.LogInformation("{MethodName} After next", _methodName);
        return result;
    }
}

class AEndpointFilter : ABCEndpointFilters
{
    public AEndpointFilter(ILoggerFactory loggerFactory) : base(loggerFactory) { }
}

class BEndpointFilter : ABCEndpointFilters
{
    public BEndpointFilter(ILoggerFactory loggerFactory) : base(loggerFactory) { }
}

class CEndpointFilter : ABCEndpointFilters
{
    public CEndpointFilter(ILoggerFactory loggerFactory) : base(loggerFactory) { }
}

Memvalidasi objek dengan filter

Pertimbangkan filter yang memvalidasi Todo objek:

app.MapPut("/todoitems/{id}", async (Todo inputTodo, int id, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;
    
    await db.SaveChangesAsync();

    return Results.NoContent();
}).AddEndpointFilter(async (efiContext, next) =>
{
    var tdparam = efiContext.GetArgument<Todo>(0);

    var validationError = Utilities.IsValid(tdparam);

    if (!string.IsNullOrEmpty(validationError))
    {
        return Results.Problem(validationError);
    }
    return await next(efiContext);
});

Dalam kode sebelumnya:

  • Objek EndpointFilterInvocationContext menyediakan akses ke parameter yang terkait dengan permintaan tertentu yang dikeluarkan ke titik akhir melalui GetArguments metode .
  • Filter terdaftar menggunakan delegate yang mengambil EndpointFilterInvocationContext dan mengembalikan EndpointFilterDelegate.

Selain diteruskan sebagai delegasi, filter dapat didaftarkan dengan menerapkan IEndpointFilter antarmuka. Kode berikut menunjukkan filter sebelumnya yang dienkapsulasi dalam kelas yang mengimplementasikan IEndpointFilter:

public class TodoIsValidFilter : IEndpointFilter
{
    private ILogger _logger;

    public TodoIsValidFilter(ILoggerFactory loggerFactory)
    {
        _logger = loggerFactory.CreateLogger<TodoIsValidFilter>();
    }

    public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext efiContext, 
        EndpointFilterDelegate next)
    {
        var todo = efiContext.GetArgument<Todo>(0);

        var validationError = Utilities.IsValid(todo!);

        if (!string.IsNullOrEmpty(validationError))
        {
            _logger.LogWarning(validationError);
            return Results.Problem(validationError);
        }
        return await next(efiContext);
    }
}

Filter yang mengimplementasikan IEndpointFilter antarmuka dapat mengatasi dependensi dari Dependency Injection(DI), seperti yang ditunjukkan pada kode sebelumnya. Meskipun filter dapat mengatasi dependensi dari DI, filter itu sendiri tidak dapat diselesaikan dari DI.

ToDoIsValidFilter diterapkan ke titik akhir berikut:

app.MapPut("/todoitems2/{id}", async (Todo inputTodo, int id, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;

    await db.SaveChangesAsync();

    return Results.NoContent();
}).AddEndpointFilter<TodoIsValidFilter>();

app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
}).AddEndpointFilter<TodoIsValidFilter>();

Filter berikut memvalidasi Todo objek dan memodifikasi Name properti:

public class TodoIsValidUcFilter : IEndpointFilter
{
    public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext efiContext, 
        EndpointFilterDelegate next)
    {
        var todo = efiContext.GetArgument<Todo>(0);
        todo.Name = todo.Name!.ToUpper();

        var validationError = Utilities.IsValid(todo!);

        if (!string.IsNullOrEmpty(validationError))
        {
            return Results.Problem(validationError);
        }
        return await next(efiContext);
    }
}

Mendaftarkan filter menggunakan pabrik filter titik akhir

Dalam beberapa skenario, mungkin perlu untuk menyimpan beberapa informasi yang disediakan dalam MethodInfo filter. Misalnya, mari kita asumsikan bahwa kita ingin memverifikasi bahwa handler filter titik akhir dilampirkan untuk memiliki parameter pertama yang mengevaluasi ke jenis Todo .

app.MapPut("/todoitems/{id}", async (Todo inputTodo, int id, TodoDb db) =>
{
    var todo = await db.Todos.FindAsync(id);

    if (todo is null) return Results.NotFound();

    todo.Name = inputTodo.Name;
    todo.IsComplete = inputTodo.IsComplete;
    
    await db.SaveChangesAsync();

    return Results.NoContent();
}).AddEndpointFilterFactory((filterFactoryContext, next) =>
{
    var parameters = filterFactoryContext.MethodInfo.GetParameters();
    if (parameters.Length >= 1 && parameters[0].ParameterType == typeof(Todo))
    {
        return async invocationContext =>
        {
            var todoParam = invocationContext.GetArgument<Todo>(0);

            var validationError = Utilities.IsValid(todoParam);

            if (!string.IsNullOrEmpty(validationError))
            {
                return Results.Problem(validationError);
            }
            return await next(invocationContext);
        };
    }
    return invocationContext => next(invocationContext); 
});

Dalam kode sebelumnya:

  • Objek EndpointFilterFactoryContext menyediakan akses ke yang MethodInfo terkait dengan handler titik akhir.
  • Tanda tangan handler diperiksa dengan memeriksa MethodInfo tanda tangan jenis yang diharapkan. Jika tanda tangan yang diharapkan ditemukan, filter validasi terdaftar ke titik akhir. Pola pabrik ini berguna untuk mendaftarkan filter yang bergantung pada tanda tangan handler titik akhir target.
  • Jika tanda tangan yang cocok tidak ditemukan, filter pass-through akan didaftarkan.

Mendaftarkan filter pada tindakan pengontrol

Dalam beberapa skenario, mungkin perlu untuk menerapkan logika filter yang sama untuk titik akhir berbasis penangan rute dan tindakan pengontrol. Untuk skenario ini, dimungkinkan untuk memanggil AddEndpointFilterControllerActionEndpointConventionBuilder untuk mendukung eksekusi logika filter yang sama pada tindakan dan titik akhir.

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapController()
    .AddEndpointFilter(async (efiContext, next) =>
    {
        efiContext.HttpContext.Items["endpointFilterCalled"] = true;
        var result = await next(efiContext);
        return result;
    });

app.Run();

Sumber Tambahan