Minimal API アプリのフィルター
Note
これは、この記事の最新バージョンではありません。 現在のリリースについては、この記事の .NET 8 バージョンを参照してください。
警告
このバージョンの ASP.NET Core はサポート対象から除外されました。 詳細については、「.NET および .NET Core サポート ポリシー」を参照してください。 現在のリリースについては、この記事の .NET 8 バージョンを参照してください。
重要
この情報はリリース前の製品に関する事項であり、正式版がリリースされるまでに大幅に変更される可能性があります。 Microsoft はここに示されている情報について、明示か黙示かを問わず、一切保証しません。
現在のリリースについては、この記事の .NET 8 バージョンを参照してください。
作成者: Fiyaz Bin Hasan、Martin Costello、Rick Anderson
Minimal API のフィルターを使うと、開発者は以下をサポートするビジネス ロジックを実装できます。
- エンドポイント ハンドラーの前後でコードを実行する。
- エンドポイント ハンドラーの呼び出し中に指定されたパラメーターの検査と変更。
- エンドポイント ハンドラーの応答動作のインターセプト。
フィルターは、次のシナリオで役立つことがあります。
- エンドポイントに送信される要求パラメーターと本文を検証する。
- 要求と応答に関する情報をログに記録する。
- サポートされている API バージョンを要求が対象としていることを検証する。
フィルターを登録するには、EndpointFilterInvocationContext
を受け取って EndpointFilterDelegate
を返すデリゲートを指定します。 EndpointFilterInvocationContext
により、要求の HttpContext
と、ハンドラーに渡される引数をハンドラーの宣言に出現する順序で示す Arguments
のリストへのアクセスが提供されます。
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();
上記のコードでは次の操作が行われます。
AddEndpointFilter
拡張メソッドを呼び出して、/colorSelector/{color}
エンドポイントにフィルターを追加します。- 値
"Red"
を除く指定された色を返します。 /colorSelector/Red
が要求されたときは Results.Problem を返します。next
をEndpointFilterDelegate
として、invocationContext
をEndpointFilterInvocationContext
として使用し、パイプライン内の次のフィルターか、最後のフィルターが呼び出された場合は要求デリゲートを呼び出します。
フィルターは、エンドポイント ハンドラーの前に実行されます。 ハンドラーで複数の AddEndpointFilter
の呼び出しが行われる場合:
EndpointFilterDelegate
(next
) が呼び出される前に呼び出されるフィルター コードは、先入れ先出し (FIFO) の順序で実行されます。EndpointFilterDelegate
(next
) が呼び出された後に呼び出されるフィルター コードは、先入れ後出し (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();
上記のコードでは、フィルターとエンドポイントによって次の出力がログに記録されます。
Before first filter
Before 2nd filter
Before 3rd filter
Endpoint
After 3rd filter
After 2nd filter
After first filter
次のコードでは、IEndpointFilter
インターフェイスを実装するフィルターを使用します。
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();
上記のコードでは、フィルターとハンドラーのログに、実行される順序が表示されます。
AEndpointFilter Before next
BEndpointFilter Before next
CEndpointFilter Before next
Endpoint
CEndpointFilter After next
BEndpointFilter After next
AEndpointFilter After next
IEndpointFilter
インターフェイスを実装するフィルターを次の例に示します。
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) { }
}
フィルターを使ってオブジェクトを検証する
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();
}).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);
});
上のコードでは以下の操作が行われます。
EndpointFilterInvocationContext
オブジェクトによって、GetArguments
メソッドを使用してエンドポイントに発行された特定の要求に関連付けられているパラメーターへのアクセスが提供されます。- フィルターは、
EndpointFilterInvocationContext
を受け取ってEndpointFilterDelegate
を返すdelegate
を使用して登録されます。
フィルターは、デリゲートとして渡すだけでなく、IEndpointFilter
インターフェイスを実装することでも登録できます。 次のコードは、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);
}
}
IEndpointFilter
インターフェイスを実装するフィルターは、上記のコードに示すように、依存関係の挿入 (DI) から依存関係を解決できます。 フィルターは DI から依存関係を解決できますが、フィルター自体を DI から解決することは "できません"。
ToDoIsValidFilter
は次のエンドポイントに適用されます。
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>();
次のフィルターでは、Todo
オブジェクトを検証し、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);
}
}
エンドポイント フィルター ファクトリを使用してフィルターを登録する
一部のシナリオでは、MethodInfo
で提供される情報の一部をフィルターでキャッシュすることが必要になる場合があります。 たとえば、エンドポイント フィルターのアタッチ先のハンドラーに、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);
});
上のコードでは以下の操作が行われます。
EndpointFilterFactoryContext
オブジェクトによって、エンドポイントのハンドラーに関連付けられているMethodInfo
へのアクセスが提供されます。- 想定されている型のシグネチャについて
MethodInfo
を検査して、ハンドラーのシグネチャが調べられます。 想定されているシグネチャが見つかった場合は、検証フィルターがエンドポイントに登録されます。 このファクトリ パターンは、ターゲット エンドポイント ハンドラーのシグネチャに依存するフィルターを登録する場合に便利です。 - 一致するシグネチャが見つからない場合は、パススルー フィルターが登録されます。
コントローラー アクションにフィルターを登録する
一部のシナリオでは、ルートハンドラー ベースのエンドポイントとコントローラー アクションの両方に同じフィルター ロジックを適用する必要がある場合があります。 このシナリオでは、ControllerActionEndpointConventionBuilder
で AddEndpointFilter
を呼び出して、アクションとエンドポイントで同じフィルター ロジックを実行することをサポートできます。
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();
その他のリソース
ASP.NET Core