Minimal API uygulamalarında filtreler

Fiyaz Bin Hasan, Martin Costello ve Rick Anderson tarafından

En düşük API filtreleri, geliştiricilerin şunları destekleyen iş mantığını uygulamasına olanak sağlar:

  • Uç nokta işleyiciden önce ve sonra kod çalıştırma.
  • Uç nokta işleyicisi çağırma sırasında sağlanan parametreleri inceleme ve değiştirme.
  • Bir uç nokta işleyicisinin yanıt davranışını kesme.

Filtreler aşağıdaki senaryolarda yararlı olabilir:

  • Bir uç noktaya gönderilen istek parametrelerini ve gövdesini doğrulama.
  • İstek ve yanıt hakkındaki günlük bilgileri.
  • İsteğin desteklenen bir API sürümünü hedeflediğini doğrulama.

Filtreler, bir alan EndpointFilterInvocationContext ve döndüren EndpointFilterDelegatebir Temsilci sağlanarak kaydedilebilir. , EndpointFilterInvocationContext isteğin HttpContext ve işleyicinin bildiriminde göründükleri sırada işleyiciye geçirilen bağımsız değişkenleri gösteren bir Arguments listeye erişim sağlar.

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();

Yukarıdaki kod:

  • AddEndpointFilter Uç noktaya filtre /colorSelector/{color} eklemek için uzantı yöntemini çağırır.
  • değeri "Red"dışında belirtilen rengi döndürür.
  • İstendiğinde /colorSelector/Red Results.Problem döndürür.
  • İşlem hattında sonraki filtreyi EndpointFilterDelegate veya son filtre çağrıldıysa istek temsilcisini çağırmak için ve invocationContext olarak EndpointFilterInvocationContext kullanırnext.

Filtre, uç nokta işleyiciden önce çalıştırılır. İşleyici üzerinde birden çok AddEndpointFilter çağrı yapıldığında:

  • (next) çağrılmadan önce EndpointFilterDelegate çağrılan filtre kodu İlk Gelen, İlk Çıkış (FIFO) sırasına göre yürütülür.
  • (next) çağrıldıktan sonra EndpointFilterDelegate çağrılan filtre kodu İlk Gelen, Son Çıkış (FILO) sırasına göre yürütülür.
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();

Yukarıdaki kodda filtreler ve uç nokta aşağıdaki çıkışı günlüğe kaydeder:

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

Aşağıdaki kod arabirimini uygulayan IEndpointFilter filtreleri kullanır:

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();

Yukarıdaki kodda filtreler ve işleyiciler günlükleri çalıştırılacakları sırayı gösterir:

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

Arabirimi uygulayan IEndpointFilter filtreler aşağıdaki örnekte gösterilmiştir:


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) { }
}

Filtreyle nesneyi doğrulama

Bir nesneyi doğrulayan bir Todo filtre düşünün:

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);
});

Önceki kodda:

  • nesnesi, EndpointFilterInvocationContext yöntemi aracılığıyla GetArguments uç noktaya verilen belirli bir istekle ilişkili parametrelere erişim sağlar.
  • Filtre, bir alan EndpointFilterInvocationContext ve döndüren bir EndpointFilterDelegatekullanılarak delegate kaydedilir.

Temsilci olarak geçirilmeye ek olarak, arabirim uygulanarak IEndpointFilter filtreler kaydedilebilir. Aşağıdaki kod, uygulayan IEndpointFilterbir sınıfta kapsüllenen önceki filtreyi gösterir:

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);
    }
}

Arabirimi uygulayan IEndpointFilter filtreler, bağımlılıkları önceki kodda gösterildiği gibi Bağımlılık Ekleme(DI) ile çözebilir. Filtreler DI'den bağımlılıkları çözümleyese de, filtrelerin kendisi DI'den çözümlenemez.

ToDoIsValidFilter aşağıdaki uç noktalara uygulanır:

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>();

Aşağıdaki filtre nesneyi doğrular Todo ve özelliğini değiştirir Name :

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);
    }
}

Uç nokta filtre fabrikası kullanarak filtre kaydetme

Bazı senaryolarda, filtrede MethodInfo sağlanan bazı bilgilerin önbelleğe alınması gerekebilir. Örneğin, uç nokta filtresinin eklendiği işleyicinin bir türe göre değerlendirilen ilk parametresi olduğunu doğrulamak istediğimizi Todo varsayalım.

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); 
});

Önceki kodda:

  • nesnesi, EndpointFilterFactoryContext uç noktanın MethodInfo işleyicisiyle ilişkili öğesine erişim sağlar.
  • İşleyicinin imzası, beklenen tür imzası denetlenerek MethodInfo incelenir. Beklenen imza bulunursa doğrulama filtresi uç noktaya kaydedilir. Bu fabrika düzeni, hedef uç nokta işleyicisinin imzasına bağlı bir filtre kaydetmek için kullanışlıdır.
  • Eşleşen bir imza bulunamazsa, geçiş filtresi kaydedilir.

Denetleyici eylemlerine filtre kaydetme

Bazı senaryolarda, hem yol işleyicisi tabanlı uç noktalar hem de denetleyici eylemleri için aynı filtre mantığını uygulamak gerekebilir. Bu senaryoda, eylemlerde ve uç noktalarda ControllerActionEndpointConventionBuilder aynı filtre mantığını yürütmeyi desteklemek için çağrısı AddEndpointFilter yapmak mümkündür.

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();

Ek Kaynaklar