ASP.NET Core - 19. 필터(Filter)
Filter는 요청처리되는 절차 안으로 추가적인 logic을 주입하는 것입니다. Filter는 action 또는 page handler method가 될 수 있는 단일 endpoint에 적용되는 middleware 같은 것으로 일련의 요청을 관리하기 위한 유용한 방법을 제공합니다. 이번 글에서는 filter가 작동하는 방식과 ASP.NET Core에서 지원하는 filter유형을 알아보고 사용자정의 filter도 직접 만들어 볼 것입니다.
1. Project 준비하기
Project는 이전 글에서 설명하던 Project를 그대로 사용할 것이지만 아래와 같이 더이상 필요하지 않은 file은 삭제해야 합니다. PowerShell을 열고 Project folder를 찾아가 아래 명령을 실행하면 해당 file들을 삭제할 것입니다.
Remove-Item -Path Controllers,Views,Pages -Recurse -Exclude _*,Shared -Force |
위 명령은 Shared layout과 data model그리고 설정 file을 제외한 Controller와 View, Page 등을 모두 삭제합니다. 명령의 실행이 완료되었으면 다시 Project에서 Controllers folder를 만들고 HomeController.cs file을 아래와 같이 추가합니다.
using Microsoft.AspNetCore.Mvc;
namespace MyWebApp.Controllers
{
public class HomeController : Controller
{
public IActionResult Index()
{
return View("Message", "This is the Index action on the Home controller");
}
}
}
해당 action method는 Message라는 View를 Render하고 view data로 문자열값을 전달하고 있습니다. 따라서 Views/Shared folder에 Message.cshtml file을 아래와 같이 추가하고
@{
Layout = "_SimpleLayout";
}
@if (Model is string)
{
@Model
}
else if (Model is IDictionary<string, string>)
{
var dict = Model as IDictionary<string, string>;
<table class="table table-sm table-striped table-bordered">
<thead><tr><th>Name</th><th>Value</th></tr></thead>
<tbody>
@foreach (var kvp in dict ?? new Dictionary<string, string>())
{
<tr><td>@kvp.Key</td><td>@kvp.Value</td></tr>
}
</tbody>
</table>
}
Pages folder에서도 같은 이름의 file을 같이 추가해 줍니다.
@page "/pages/message"
@model MessageModel
@using Microsoft.AspNetCore.Mvc.RazorPages
@if (Model.Message is string)
{
@Model.Message
}
else if (Model.Message is IDictionary<string, string>)
{
var dict = Model.Message as IDictionary<string, string>;
<table class="table table-sm table-striped table-bordered">
<thead><tr><th>Name</th><th>Value</th></tr></thead>
<tbody>
@foreach (var kvp in dict ?? new Dictionary<string, string>())
{
<tr><td>@kvp.Key</td><td>@kvp.Value</td></tr>
}
</tbody>
</table>
}
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace MyWebApp.Pages
{
public class MessageModel : PageModel
{
public object Message { get; set; } = "This is the Message Razor Page";
}
}
마지막으로 Program.cs를 변경해 기본 controller route를 사용하고 이전에 사용된 몇몇 service와 component 역시 삭제합니다.
using Microsoft.EntityFrameworkCore;
using MyWebApp.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<NorthwindContext>(opts => {opts.UseSqlServer(builder.Configuration["ConnectionStrings:DefaultConnection"]);
opts.EnableSensitiveDataLogging(true);
});
builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
var app = builder.Build();
app.UseStaticFiles();
app.MapDefaultControllerRoute();
app.MapRazorPages();
app.Run();
이제 project를 실행하여 다음과 같은 응답이 생성되는지 확인합니다.
또한 /pages/message URL을 호출하여 역시 다음과 같은 응답이 생성되는지 확인합니다.
2. Filter 사용하기
filter를 사용하면 middleware component나 action method에서 적용되는 logic을 class에서 정의할 수 있습니다. 이로 인해 필요한 곳에 쉽게 재사용할 수 있습니다.
예를 들어 HTTS로의 접속을 몇몇 action method에서 강제하고자 하는 경우가 있을때 middleware에서는 HttpRequest개체의 IsHttps속성을 읽음으로써 가능할 수 있었으나 이것의 문제점은 middleware에서 특정한 action method의 요청을 어떻게 가로챌 수 있을지를 알아내기 위해 routing system의 설정을 이해하고 있어야 한다는 것입니다. 이보다 좀 더 유용한 접근방식은 아래와 같이 Controllers folder의 HomeController.cs file에서 처럼 action method안에서 HttpRequest.IsHttps속성을 읽는 것입니다.
public IActionResult Index()
{
if (Request.IsHttps)
{
return View("Message", "This is the Index action on the Home controller");
}
else
{
return new StatusCodeResult(StatusCodes.Status403Forbidden);
}
}
Project를 실행하고 비 https연결을 시도하면 다음과 같은 응답이 생성되지만
https연결로 변경하게 되면 다음과 같은 응답을 볼 수 있습니다.
참고로 비https연결 port는 project의 Properties folder에서 launchSettings.json file에서 확인할 수 있습니다.
만약 예제와 같이 예상한 결과를 얻을 수 없다면 Browser의 history를 삭제해봐야 합니다. Browser는 어떤 경우 server로의 요청을 보내기 위해 이전에 생성한 HTTPS error를 재사용할 수 있습니다. 보안상 좋은 방식이기는 하지만 개발과정 중에는 맞지 않을 수 있습니다
이러한 접근법은 잘 작동하기는 하지만 몇가지 문제점을 안고 있습니다. 우선 첫 번째는 action method가 보안정책을 구현하기 위해 요청을 처리하는 것보다 더 많은 code를 포함하게 될 수 있습니다. 두 번째 더 심각한 문제는 HTTP를 감지하는 code를 action method에 포함시킴으로써 action method의 기능 확장성이 어려워지고 controller의 모든 action method에 필요한 code를 중복해서 구현해야 한다는 것입니다. 즉, 모든 controller의 모든 action method마다 HTTPS연결이 필요하다면 동일한 구현을 추가하게 되는 셈입니다.
보안정책을 구현하기 위한 code는 controller에서는 중요한 부분이긴 하지만 controller자체를 이해하기 어렵게 만들 수 있고 새로운 action method에 code를 추가해야 함을 잊어버리게 된 경우 보안정책에 구멍이 생기는 것은 시간문제가 됩니다.
다행스럽게도 이러한 문제는 filter를 통해 해결될 수 있습니다. 아래 예제는 Controllers folder의 HomeController.cs file에 filter를 추가한 것으로 이전에 HTTP확인처리 logic을 바꾸고 filter로 대신 구현한 것입니다.
[RequireHttps]
public IActionResult Index()
{
return View("Message", "This is the Index action on the Home controller");
}
RequireHttps attribute는 ASP.NET Core에 의해 제공되는 내장 filter중 하나로서 action method의 접근을 제한하여 HTTPS요쳥만이 수용되도록 합니다. 더불어 각 method에서 임의로 작성한 보안 code를 모두 제거할 수 있게 되었고 수용된 요청만을 처리하는데 집중할 수 있게 되었습니다.
RequireHttps는 이전에 추가한 code와 같이 동일한 방법으로는 작동하지 않습니다. GET요청에서 RequireHttps attribute는 사용자를 본래 필요한 URL로 redirect시키게 되는데 이때 https scheme를 사용함으로써 http를 https로 전환하게 되는 것입니다. 이러한 방식은 대부분의 경우 큰 문제는 없지만 개발단계에서는 HTTP와 HTTPS는 다른 지역 port를 사용하기 때문에 적합하지 않을 수 있습니다. RequireHttpsAttribute class는 HandleNonHttpsRequest라 불리는 protected method를 정의하고 있으며 override를 통해 동작을 재정의할 수 있습니다.
하지만 위와 같은 상황에서도 RequireHttps attribute를 각 action method에 적용해야 한다는 문제가 생길 수 있지만 filter는 controller에도 적용할 수 있고 이를 통해 controller내부의 모든 action method에 적용한 것과 동일한 효과를 가져올 수 있습니다.
[RequireHttps]
public class HomeController : Controller
filter는 세분화를 통해 다양한 수준에서 적용될 수 있습니다. 일부 action method에만 접근을 제한하고자 한다면 필요한 method에만 RequireHttps attribute를 적용할 수 있고 controller안에서 향후에 추가될것 까지 포함해 모든 action method를 보호하고자 한다면 class수준에서 적용하면 됩니다. 또한 filter를 application전체에 적용하고자 한다면 global filter를 사용할 수도 있습니다.
(1) Razor page에서 Filter사용하기
filter역시도 Raozr Page에서 사용할 수 있습니다. 예를 들어 Message Razor Page에서 HTTPS연결 보안정책을 구현하려면 아래 Pages folder의 Message.cshtml에서와 같이 연결형태를 확인할 수 있는 handler method를 추가해야 합니다.
public class MessageModel : PageModel
{
public object Message { get; set; } = "This is the Message Razor Page";
public IActionResult OnGet()
{
if (!Request.IsHttps)
{
return new StatusCodeResult(StatusCodes.Status403Forbidden);
}
else
{
return Page();
}
}
}
handler method는 잘 작동하기는 하지만 다소 견고하지 못한 면이 있고 action method에서 마주친 것과 같은 문제를 드러낼 수 있습니다. Razor Page에서 filter를 사용하고자 할 때 attribute는 handler method에 적용하거나 아래 예제와 같이 class전체에 적용할 수 있습니다.
[RequireHttps]
public class MessageModel : PageModel
{
public object Message { get; set; } = "This is the Message Razor Page";
}
https로 /pages/message URL을 요청하게 된다면 통상적인 응답을 얻을 수 있지만 http를 통한 /pages/message URL을 요청하게 된다면 filter는 요청을 redirect하게 될 것이며 이전에 봤던 것과 동일한 eeror message를 보게 될 것입니다.
3. filter의 이해
ASP.NET Core은 다른 목적을 위한 다양한 유형의 filter를 지원하고 있으며 아래 표는 filter의 종류를 나타낸 것입니다.
Authorization filters | 해당 filter는 application의 인증정책을 적용하기 위해 사용됩니다. |
Resource filters | 해당 filter는 요청을 가로채기 위해 사용되며 일반적으로 caching과 같은 기능을 구현합니다. |
Action filters | 해당 filter는 요청이 action method에 도달하기 전에 요청을 변경하거나 생성된 action의 결과를 변경하는데 사용되며 오로지 controller와 action에만 적용될 수 있습니다. |
Page filters | 해당 filter는 요청이 Razor page handler method에 도달하기 전에 요청을 변경하거나 생성된 action의 결과를 변경하는데 사용되며 오로지 Raozr page에서만 적용될 수 있습니다. |
Result filters | 해당 filter는 action이 실행되기 전에 action의 결과를 변경하거나 실행된 후 결과를 변경하는데 사용됩니다. |
Exception filters | 해당 filter는 action method나 page handler가 실행중에 발생한 예외를 처리하기 위해 사용됩니다. |
filter는 자신만의 고유한 pipeline이 있으며 아래 그림과 같이 특정한 순서로 실행됩니다.
Filter는 요청이 다음 filter로 전달되는 것을 막기위해 pipeline을 종단시킬 수도 있습니다. 예를 들어 authorization filter는 사용자가 인증되지 않은 상태라면 pipeline을 종단시키고 error응답을 반환하게 됩니다. resource, action, page filter 등은 endpoint에서 처리되기 전/후에서 요청을 확인할 수 있고 이러한 유형의 filter는 pipeline종단할 수 있고 요청이 처리되기 전에 요청을 변경하거나 응답을 변경할 수 있습니다. (또한 Page filters는 model binding이 처리되기 전/후 모두에서 동작할 수 있습니다.)
각 filter의 유형은 ASP.NET Core에서 정의된 interface를 사용해 구현되었으며 filter의 몇몇 유형에서는 attribute로서 손쉽게 적용될 수 있도록 필요한 기반 class를 다음표와 같이 제공하고 있습니다.
Authorization filters | IAuthorizationFilterIAsyncAuthorizationFilter | 해당 attribute class는 제공되지 않았습니다. |
Resource filters | IResourceFilter IAsyncResourceFilter | 해당 attribute class는 제공되지 않았습니다. |
Action filters | IActionFilterIAsyncActionFilter | ActionFilterAttribute |
Page filters | IPageFilterIAsyncPageFilter | 해당 attribute class는 제공되지 않았습니다. |
Result filters | IResultFilterIAsyncResultFilterIAlwaysRunResult FilterIAsyncAlwaysRunResultFilter |
ResultFilterAttribute |
Exception Filters | IExceptionFilterIAsyncExceptionFilter | ExceptionFilterAttribute |
4. 사용자 정의 filter 생성하기
Filter는 Microsoft.AspNetCore.Mvc.Filters에 있는 IFilterMetadata interface를 구현하고 있습니다.
namespace Microsoft.AspNetCore.Mvc.Filters {
public interface IFilterMetadata { }
}
그런데 interface를 보면 내부에 어떠한 것도 존재하지 않고 어떠한 동작의 구현도 요구하지 않고 있습니다. 이것은 위에서 설명된 filter는 각 category에서와 같이 다양한 방식으로 작동할 수 있기 때문입니다. filter는 FilterContext 개체로 부터 context data와 함께 제공되며 아래 표에서 FilterContext가 제공하는 속성들을 확인해 볼 수 있습니다.
ActionDescriptor | 해당 속성은 action method를 서술하는 ActionDescriptor개체를 반환합니다. |
HttpContext | 해당 속성은 HTTP요청에 대한 상세와 결과로 전송될 HTTP응답에 대한 상세를 제공하는 HttpContext개체를 반환합니다. |
ModelState | 해당 속성은 client에서 전송된 data의 유효성을 검증하는데 사용되는 ModelStateDictionary개체를 반환합니다. |
RouteData | 해당 속성은 routing system이 요청을 처리하는 방식에 대해 서술하는 RouteData개체를 반환합니다. |
Filters | 해당 속성은 action method에 적용된 filter의 목록을 반환하며 IList<IFilterMetadata>로 표현될 수 있습니다. |
(1) Authorization Filter
Authorization filter는 application의 보안정책을 구현하는데 사용되며 다른 유형의 filter보다 앞서 그리고 endpoint가 요청을 처리하기 전에 실행됩니다. 아래는 IAuthorizationFilter interface의 구현을 나타내고 있습니다.
namespace Microsoft.AspNetCore.Mvc.Filters {
public interface IAuthorizationFilter : IFilterMetadata {
void OnAuthorization(AuthorizationFilterContext context);
}
}
OnAuthorization method는 요청에 대한 인증기회를 filter에 제공하기 위해 호출됩니다. 아래는 비동기 인증 filter를 위한 IAsyncAuthorizationFilter interfaqce를 나타내고 있습니다.
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Mvc.Filters {
public interface IAsyncAuthorizationFilter : IFilterMetadata {
Task OnAuthorizationAsync(AuthorizationFilterContext context);
}
}
여기서 OnAuthorizationAsync method는 filter가 요청을 인증하기 위해 호출됩니다. 어떤 interface를 사용하더라도 filter는 AuthorizationFilterContext 개체를 통해 요청을 서술하는 context data를 전달받습니다. 특히 AuthorizationFilterContext 개체는 FilterContext class로부터 파생되며 아래 표의 속성을 추가합니다.
Result | 해당 IActionResult 속성은 요청이 application의 인증정책에 맞지 않은 경우 authorization filter에 의해 설정됩니다. 만약 속성에 값이 설정된다면 ASP.NET Core는 IActionResult를 endpoint를 호출하는 대신 실행하게 됩니다. |
● Authorization Filter 생성하기
authorization filter가 작동하는 방식을 알아보기 위해 예제에서 Filters folder를 생성하고 HttpsOnlyAttribute.cs이름의 file을 아래와 같이 추가합니다.
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc;
namespace MyWebApp.Filters
{
public class HttpsOnlyAttribute : Attribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
if (!context.HttpContext.Request.IsHttps)
{
context.Result = new StatusCodeResult(StatusCodes.Status403Forbidden);
}
}
}
}
authorization filter는 요청이 인증정책을 통과한 경우라면 아무것도 하지 않으며 ASP.NET Core는 다음 filter로 이동하게 되면서 최종적으로 endpoint를 실행하게 됩니다. 만약 이 과정에서 문제가 발생한다면 filter는 OnAuthorization method로 전달되는 AuthorizationFilterContext개체의 Result속성을 설정하게 되며 더 이상의 실행을 방지하고 client에 result를 반환할 수 있습니다. 예제에서 HttpsOnlyAttribute class는 HttpRequest context 개체의 IsHttps 속성을 확인하고 요청이 HTTPS로 만들어진 경우가 아니면 실행을 중단하기 위해 Result 속성을 설정하게 됩니다. Authorization filter는 controller와 action method 그리고 Razor page에 적용할 수 있으며 아래 예제에서는 Controllers folder의 HomeController.cs에 위의 filter를 적용하였습니다.
using Microsoft.AspNetCore.Mvc;
using MyWebApp.Filters;
namespace MyWebApp.Controllers
{
[HttpsOnly]
public class HomeController : Controller
{
public IActionResult Index()
{
return View("Message", "This is the Index action on the Home controller");
}
public IActionResult Secure()
{
return View("Message", "This is the Secure action on the Home controller");
}
}
}
위 예제는 이전 예제에서 포함된 기능을 재작성한 것으로 내장 RequireHttps filter처럼 redirection을 수행하는 것 보다는 실제 project에서 그다지 유용하지는 않습니다. 왜냐하면 대부분의 사용자는 오류로 나온 화면에서 403 상태 code의 값이 무엇을 의미하는지 알지 못하기 때문입니다. 하지만 예제를 통해 Authorization filter가 작동하는 방식에 대한 예를 확인해 볼 수 있습니다. Project를 실행하여 http와 https로 접속을 각각 시도하여 다음과 같은 응답이 생성되는지를 확인합니다.
(2) Resource Filter
Resource filter는 하나의 요청에 대해 2번씩 실행되는데 ASP.NET Core model binding이 처리되기 전과 action result가 처리되거 결과를 생성하기 전이 그것입니다. 다음은 IResourceFilter interface가 구현된 것을 나타내고 있습니다.
namespace Microsoft.AspNetCore.Mvc.Filters
{
public interface IResourceFilter : IFilterMetadata
{
void OnResourceExecuting(ResourceExecutingContext context);
void OnResourceExecuted(ResourceExecutedContext context);
}
}
OnResourceExecuting method는 요청이 처리될때 호출되며 OnResourceExecuted method는 endpoint가 요청을 처리한 후 그리고 action result가 실행되기 전에 호출됩니다. 비동기 resource filter의 경우에는 아래와 같이 IAsyncResourceFilter interface를 구현하면 됩니다.
namespace Microsoft.AspNetCore.Mvc.Filters
{
public interface IAsyncResourceFilter : IFilterMetadata
{
Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next);
}
}
해당 interface는 context 개체와 호출할 delegate를 전달받는 단일 method를 정의하고 있습니다. resource filter는 delegate가 호출되기 전에 요청을 확인할 수 있고 이것이 실행되기 전에 응답을 확인할 수 있습니다. OnResourceExecuting method는 ResourceExecutingContext class를 사용하는 context와 함께 제공되는데 해당 class는 FilterContext class에서 정의된 것 이외에 다음과 같은 표의 속성들을 정의하고 있습니다.
Result | 해당 IActionResult속성은 pipeline의 단락에 대한 결과를 제공하기 위해 사용됩니다. |
ValueProviderFactories | 해당 속성은 IList<IValueProviderFactory>를 반환하며 model binding process에 값을 제공하는 개체로의 접근을 제공합니다. |
OnResourceExecuted method는 ResourceExecutedContext class를 사용하는 context와 함께 제공되며 FilterContext class에서 정의한것 외에 아래 표의 속성을 정의하고 있습니다.
Result | 해당 IActionResult속성은 응답을 생성하기 위해 사용되는 action result를 제공합니다. |
Canceled | 해당 bool형식의 속성은 다른 filter에서 ActionExecutingContext개체의 Result속성에 action result를 할당함으로서 pipeline을 단락시킨 경우 true로 설정됩니다. |
Exception | 해당 속성은 실행하는 동안 발생된 예외를 저장하기 위해 사용됩니다. |
ExceptionDispatchInfo | 해당 속성은 실행중에 던져진 모든 예외에 대한 static trace 상세를 포함하는 ExceptionDispatchInfo개체를 반환합니다. |
ExceptionHandled | 해당 속성을 true로 설정하는 것은 filter가 더이상 전파되지 않는 예외를 처리하였음 나타내는 것입니다. |
● Resource Filter 생성하기
Resource filter는 대게 pipeline의 단락이 가능한 경우에 사용되며 data caching을 구현할때와 같이 초기 응답을 제공합니다. 간단한 caching filter를 만들어 보기 위해 filters folder에 SimpleCacheAttribute.cs file을 아래와 같이 추가합니다.
* filter와 의존성 주입
attribute로 적용되는 filter는 자체적으로 IFilterFactory interface를 구현하고 직접적으로 instance를 생성하는 것 외에 생성자에서 의존을 선언할 수 없습니다.
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace MyWebApp.Filters
{
public class SimpleCacheAttribute : Attribute, IResourceFilter
{
private Dictionary<PathString, IActionResult> CachedResponses = new Dictionary<PathString, IActionResult>();
public void OnResourceExecuting(ResourceExecutingContext context)
{
PathString path = context.HttpContext.Request.Path;
if (CachedResponses.ContainsKey(path))
{
context.Result = CachedResponses[path];
CachedResponses.Remove(path);
}
}
public void OnResourceExecuted(ResourceExecutedContext context)
{
if (context.Result != null)
CachedResponses.Add(context.HttpContext.Request.Path, context.Result);
}
}
}
예제에서의 cache가 그다지 유용하지 않지만 대신 resource filter가 어떻게 작동하는지는 확인해 볼 수 있습니다. OnResourceExecuting method는 context개체의 Result속성에서 이전에 cache 된 action result를 설정함으로써 pipeline을 단락 시킬 수 있는 filter를 제공합니다. 값이 Result속성에 할당되었다면 filter pipeline은 단락 되며 action result는 client로의 응답을 생성하기 위해 실행됩니다. Cache 된 action result는 단 한 번만 사용된 후 cache로부터 제거되지만 반면 Result속성에 할당된 값이 없다면 요청은 다른 filter나 endpoint가 될 수 있는 다음 pipeline으로 전달됩니다.
OnResourceExecuted method는 pipeline이 단락되지 않았을 때 생성되는 action result를 filter에 제공합니다. 이 경우 filter는 action result를 cache 함으로써 다음요청에 사용될 수 있도록 합니다. Resource filter는 controller, action method, Razor Page 등에 적용될 수 있습니다. 아래 예제는 Pages folder의 Message.cshtml.cs file에서 사용자정의 resource filter를 Message Razor Page에 적용하고 anction result가 cache 되는 시기를 확인하는데 도움이 될 timestamp를 추가하였습니다.
[RequireHttps]
[SimpleCache]
public class MessageModel : PageModel
{
public object Message { get; set; } = $"{DateTime.Now.ToLongTimeString()} - This is the Message Razor Page";
}
적용한 resource filter의 효과를 확인해 보기 위해 project를 실행하고 /pages/message로 URL을 요청합니다. 이것은 경로에 대한 첫번째 요청이기 때문에 cache 된 결과는 없을 것이며 요청은 pipeline을 따라 전달될 것입니다. 응답이 처리됨으로써 resource filter는 다음번 사용을 위해 action result를 cache 하게 되며 요청을 반복하기 위해 browser를 새로고침하게 되면 cache 된 action result가 사용되었음을 나타내는 같은 timestamp를 보게 됩니다. 하지만 다시 사용하게 되면 cache 된 item은 제거되며 새롭게 생성된 timestamp의 응답이 생성됩니다.
● 비동기 Resource filter 생성하기
비동기 resource filter의 interface는 요청을 filter pipeline을 따라 전달하는 delegate를 받는 단일 method를 사용합니다. 아래 예제는 filters folder의 SimpleCacheAttribute.cs file에서 이전 예제의 cache filer를 변경해 IAsyncResourceFilter interface를 구현한 것입니다.
namespace MyWebApp.Filters
{
public class SimpleCacheAttribute : Attribute, IAsyncResourceFilter
{
private Dictionary<PathString, IActionResult> CachedResponses = new Dictionary<PathString, IActionResult>();
public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next)
{
PathString path = context.HttpContext.Request.Path;
if (CachedResponses.ContainsKey(path))
{
context.Result = CachedResponses[path];
CachedResponses.Remove(path);
}
else
{
ResourceExecutedContext execContext = await next();
if (execContext.Result != null)
{
CachedResponses.Add(context.HttpContext.Request.Path,
execContext.Result);
}
}
}
}
}
OnResourceExecutionAsync method는 pipeline이 단락되었는지를 확인하기 위해 사용되는 ResourceExecutingContext 개체를 전달받고 있습니다. 만약 그럴 수 없다면 delegate는 인수 없이 호출되고 요청이 처리되고 pipeline을 따라 다시 되돌아갈 때 ResourceExecutedContext개체를 비동기적으로 생성합니다. project를 실행하고 이전과 동일한 URL을 다시 요청하면 동일한 cache동작을 확인할 수 있습니다.
주의
두 context개체를 혼동하지 않는 것이 중요합니다. endpoint에 의해 생성된 action result는 오로지 delegate에서 반환된 context 개체에서만 사용할 수 있습니다.
(3) Action filter
resource filter와 마찬가지로 action filter역시 이중으로 실행됩니다. 차이점이라면 model bindin 처리 전에 실행되는 resource filter와 달리 action filter는 model binding 처리 후에 실행됩니다. 이는 resource filter가 pipeline을 단락 시킬 수 있고 ASP.NET Core가 요청에 대해 처리할 작업을 최소화할 수 있다는 것을 의미합니다. action filter는 model binding이 요구될 때 사용되는데 차이점은 model을 변경하거나 유효성검증을 강제하는 작업에 사용된다는 것입니다. action filter는 또한 razor page에서도 가능한 resource filter와 달리 오로지 controller와 action method에서만 적용될 수 있습니다.(Razor page에서 action filter와 동일한 것으로 page filter라는 것이 따로 존재합니다.) 아래는 IActionFilter interface를 나타내고 있습니다.
namespace Microsoft.AspNetCore.Mvc.Filters
{
public interface IActionFilter : IFilterMetadata
{
void OnActionExecuting(ActionExecutingContext context);
void OnActionExecuted(ActionExecutedContext context);
}
}
action filter가 action method에 적용될때는 action method가 호출되기 전에 OnActionExecuting method가 호출되며 OnActionExecuted method는 그 이후에 호출됩니다. 또한 action filter는 두 개의 다른 context class를 통해 context data와 함께 제공되는데 OnActionExecuting method에서의 ActionExecutingContext와 OnActionExecuted method에서의 ActionExecutedContext가 그것입니다.
호출될 action을 서술하는데 사용되는 ActionExecutingContext class는 FilterContext 속성 이외에 아래 표의 속성을 정의하고 있습니다.
Controller | 해당 속성은 action method를 호출하려고 하는 controller를 반환합니다.(action method의 세부사항은 base class로 부터 상속된 ActionDescriptor속성을 통해서만 확인할 수 있습니다.) |
ActionArguments | 해당 속성은 action method로 전달될 인수로서 name으로 index된 dictionary를 반환합니다. 여기서 filter는 dictionary를 삽입, 삭제, 변경할 수 있습니다. |
Result | filer가 이 속성에 IActionResult를 할당하였다면 pipeline은 단절되며 action result는 action method를 호출하지 않고 client로의 응답을 생성하는데 사용됩니다. |
ActionExecutedContext class는 실행된 action을 표현하는데 사용되며 FilterContext속성 외에 아래 표의 속성을 정의하고 있습니다.
Controller | 이 속성은 action method를 호출할 controller 개체를 반환합니다. |
Canceled | 해당 bool형식의 속성은 ActionExecutingContext 개체의 Result속성에 action method를 할당함으로서 다른 action filter에 의해 pipeline이 단락되었다면 true로 설정됩니다. |
Exception | 이 속성은 action method에서 발생한 모든 예외를 포함합니다. |
ExceptionDispatchInfo | 이 속성은 action method에서 발생한 모든 예외의 stack trace 세부사항을 포함하고 있는 ExceptionDispatchInfo개체를 반환합니다. |
ExceptionHandled | 이 속성을 true로 설정하면 filter가 더이상 전파되지 않는 예외를 처리하였음을 나타냅니다. |
Result | 이 속성은 action method에 의해 생성된 IActionResult를 반환합니다. 필요하다면 filter는 action result를 변경하거나 교체할 수 있습니다. |
비동기 action filter는 IAsyncActionFilter interface를 사용하여 구현됩니다.
namespace Microsoft.AspNetCore.Mvc.Filters
{
public interface IAsyncActionFilter : IFilterMetadata
{
Task OnActionExecutionAsync(ActionExecutingContext context,
ActionExecutionDelegate next);
}
}
해당 interface는 IAsyncResourceFilter interface와 동일한 pattern을 따르고 있습니다. OnActionExecutionAsync method는 ActionExecutingContext 개체 및 delegate 함께 제공됩니다. 이때 ActionExecutingContext 개체는 action method에 의해 수신되기 이전의 요청을 서술하며 filter는 또한 ActionExecutingContext. Result 속성의 값을 할당하여 pipeline을 단락 시키거나 delegate를 호출함에 따라 전달할 수 있습니다. 여기서 delegate는 비동기적으로 action method로부터 결과를 서술하는 ActionExecutedContext개체를 생성합니다.
● Action filter 생성하기
filters folder에 ChangeArgAttribute.cs이름의 file을 아래와 같이 추가합니다.
using Microsoft.AspNetCore.Mvc.Filters;
namespace MyWebApp.Filters
{
public class ChangeArgAttribute : Attribute, IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
if (context.ActionArguments.ContainsKey("message1"))
context.ActionArguments["message1"] = "ccc";
await next();
}
}
}
filter에서는 'message1'이라는 action의 인수를 찾고 action method를 호출하는데 사용될 값을 변경합니다. action method인수로 사용될 값은 model binding process에 의해 결정됩니다. 아래 예제에서는 Controllers folder에서 HomeController.cs file을 변경하여 Home controller에 action method를 추가하고 새로운 filter를 적용하였습니다.
[ChangeArg]
public IActionResult Messages(string message1, string message2 = "None")
{
return View("Message", $"{message1}, {message2}");
}
project를 실행하여 /home/messages?message1=aaa&message2=bbb로 URL을 요청합니다. model binding process는 query string으로부터 action method에 정의된 매개변수의 값을 찾고 action filter에 의해 값이 변경되면 아래와 같은 응답을 생성할 것입니다.
● Attribute Base Class를 사용해 action filter 구현하기
action attribute는 ActionFilterAttribute class를 파생하여 구현함으로서 Attribute를 확장하고 IActionFilter와 IAsyncActionFilter interface상속함으로써 구현 class에서 필요한 method만을 재정의합니다. 아래 예제는 filters folder의 ChangeArgsAttribute.cs file에서 ChangeArg filter를 재작성하여 ActionFilterAttribute으로부터 파생되도록 하였습니다.
using Microsoft.AspNetCore.Mvc.Filters;
namespace MyWebApp.Filters
{
public class ChangeArgAttribute : ActionFilterAttribute
{
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
if (context.ActionArguments.ContainsKey("message1"))
context.ActionArguments["message1"] = "ccc";
await next();
}
}
}
위 예제는 이전에 구현된것과 같은 방법으로 동작하며 base class를 사용할 것인가에 대한 선택은 단지 선호도의 문제일 뿐입니다. project를 실행하고 /home/messages?message1=aaa&message2=bbb URL을 호출하면 이전과 같은 응답을 보게 될 것입니다.
● Controller Filter Method 사용
Razor view를 render하는 controller의 기반 class인 Controller class는 IActionFilter와 IAsyncActionFilter interface를 구현하기 때문에 이에 대한 기능을 정의하고 controller 및 파생된 모든 controller에서 정의된 action에 적용할 수 있습니다. 아래 예제는 Controllers folder의 HomeController.cs file에서 HomeController clsss에 직접적으로 ChangeArg filter기능을 구현한 것입니다.
//[ChangeArg]
public IActionResult Messages(string message1, string message2 = "None")
{
return View("Message", $"{message1}, {message2}");
}
public override void OnActionExecuting(ActionExecutingContext context)
{
if (context.ActionArguments.ContainsKey("message1"))
{
context.ActionArguments["message1"] = "ccc";
}
}
예제의 Home controller에서는 OnActionExecuting method의 구현 controller를 재정의하고 있으며 실행 method로 전달될 인수를 변경하기 위해 사용되고 있습니다.
project를 실행하고 /home/messages?message1=hello&message2=bbb로 URL을 호출하면 이전과 동일한 결과를 확인할 수 있습니다.
(4) Page filter
Page filter는 Razor page에 해당하는 action filter라고 볼 수 있습니다. 아래는 동기 page filter에서 구현된 IPageFilter interface를 나타내고 있습니다.
namespace Microsoft.AspNetCore.Mvc.Filters
{
public interface IPageFilter : IFilterMetadata
{
void OnPageHandlerSelected(PageHandlerSelectedContext context);
void OnPageHandlerExecuting(PageHandlerExecutingContext context);
void OnPageHandlerExecuted(PageHandlerExecutedContext context);
}
}
OnPageHandlerSelected method는 ASP.NET Core가 page handler method를 선택한 후, model binding이 수행되기 전에 호출됩니다. 이는 handler method의 인수가 결정되지 않았음을 의미합니다. 이 method는 FilterContext class에서 정의한 것 외에 아래 표의 속성을 정의하고 있는 PageHandlerSelectedContext class를 통해 context를 수신합니다. 비록 pipeline 단락에는 사용할 수 없지만 요청을 수신하는 handler method를 변경할 수 있습니다.
ActionDescriptor | 이 속성은 Razor Page에 대한 Description을 반환합니다. |
HandlerMethod | 이 속성은 선택된 handler method를 서술하는 HandlerMethodDescriptor 개체를 반환합니다. |
HandlerInstance | 이 속성은 요청을 처리할 Razor Page의 instance를 반환합니다. |
OnPageHandlerExecuting method는 model binding process가 완료된 후, page handler method가 호출되기 전에 실행됩니다. 또한 PageHandlerSelectedContext class에서 정의된것 외에 아래 표의 속성을 정의하고 있는 PageHandlerExecutingContext class를 통해 context를 수신합니다.
HandlerArguments | 이 속성은 page handler 인수를 포함하고 있는 name으로 indexing된 dictionary를 반환합니다. |
Result | filter는 해당 속성에 IActionResult 개체를 할당함으로서 pipeline을 단락할 수 있습니다. |
OnPageHandlerExecuted method는 page handler method가 호출된 후 action result가 처리되어 응답이 생성되기 전에 호출됩니다. 또한 PageHandlerExecutingContext 속성외에 아래 표의 속성을 정의하고 있는 PageHandlerExecutedContext class를 통해 context를 수신합니다.
Canceled | 이 속성은 다른 filter가 filter pipeline을 단락시킨 경우 true값을 반환합니다. |
Exception | 이 속성은 page handler method에서 예외가 발생한 경우 예욀르 반환합니다. |
ExceptionHandled | 이 속성은 page handler에서 발생한 예외가 filter에서 처리되었다면 이를 나타내기 위해 true를 반환합니다. |
Result | 이 속성은 client로의 응답을 생성하기 위해 사용되는 action result를 반환합니다. |
비동기 page filter는 아래와 같이 정의된 IAsyncPageFilter interface를 구현함으로서 생성됩니다.
namespace Microsoft.AspNetCore.Mvc.Filters
{
public interface IAsyncPageFilter : IFilterMetadata
{
Task OnPageHandlerSelectionAsync(PageHandlerSelectedContext context);
Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context,
PageHandlerExecutionDelegate next);
}
}
OnPageHandlerSelectionAsync는 handler method가 선택된 이 후 호출되며 동기화 OnPageHandlerSelected method와 동일합니다. OnPageHandlerExecutionAsync는 pipeline을 단락 시킬 수 있는 PageHandlerExecutingContext개체와 요청을 전달하기 위해 호출되는 delegate와 함께 제공됩니다. 이때 delegate는 handler method에서 생성되는 action result를 확인하거나 변경하기 위해 사용되 PageHandlerExecutedContext 개체를 생성합니다.
● Page Filter 생성
page filter를 만들기 위해 ChangePageArgs.cs이름의 file을 filters folder에 아래와 같이 추가합니다.
using Microsoft.AspNetCore.Mvc.Filters;
namespace MyWebApp.Filters
{
public class ChangePageArgs : Attribute, IPageFilter
{
public void OnPageHandlerSelected(PageHandlerSelectedContext context)
{
}
public void OnPageHandlerExecuting(PageHandlerExecutingContext context)
{
if (context.HandlerArguments.ContainsKey("message1"))
{
context.HandlerArguments["message1"] = "ccc";
}
}
public void OnPageHandlerExecuted(PageHandlerExecutedContext context)
{
}
}
}
예제의 page filter는 이전에 생성한 action filter와 같은 작업을 수행합니다. 아래 예제는 Pages folder의 Message.cshtml.cs file을 변경한 것으로 Message Razor Page에서 handler method를 정의하고 여기에 page filer를 적용하였습니다. Page filter는 각각의 handler method에 적용하거나 예제에서와 같이 page model class에도 적용하여 모든 handler method에서 filter를 사용할 수 있습니다. (참고로 이전에 적용했던 SimpleCache filter는 주석처리하였습니다. Resource filter는 page filter와 함께 작동할 수 있으나 caching response는 일부 예제를 더 이해하기 어렵게 만들 수 있기 때문입니다.)
[RequireHttps]
//[SimpleCache]
[ChangePageArgs]
public class MessageModel : PageModel
{
public object Message { get; set; } = $"{DateTime.Now.ToLongTimeString()} - This is the Message Razor Page";
public void OnGet(string message1, string message2)
{
Message = $"{message1}, {message2}";
}
}
project를 실행하고 /pages/message?message1=aaa&message2=bbb로 URL을 요청합니다. page filer는 OnGet handler method에서 message1 인수의 값을 변경하여 다음과 같은 응답을 생성하게 됩니다.
● Page Model Filter Method 사용
PageModel class는 page model class의 기반으로서 사용되며 IPageFilter와 IAsyncPageFilter interface를 구현합니다. 따라서 필요한 경우 page model에 직접적으로 filter의 기능을 추가할 수 있는데, 아래 예제는 Pages folder의 Message.cshtml.cs에서 PageModel Filter Method를 적용한 것입니다.
public void OnGet(string message1, string message2)
{
Message = $"{message1}, {message2}";
}
public override void OnPageHandlerExecuting(PageHandlerExecutingContext context)
{
if (context.HandlerArguments.ContainsKey("message1"))
context.HandlerArguments["message1"] = "ccc";
}
예제와 같이 변경한 후 이전과 동일한 URL을 요청합니다. page model class에서 구현된 위 예제의 method는 이전과 같은 결과를 반환할 것입니다.
(5) Result Filter
Result filter는 action result가 응답을 생성하기 위해 사용되기 전, 후 모두에서 실행되므로 endpoint에서 처리한 후에 응답을 수정할 수 있습니다. 아래는 IResultFilter interface를 정의한 것을 나타내고 있습니다.
namespace Microsoft.AspNetCore.Mvc.Filters
{
public interface IResultFilter : IFilterMetadata
{
void OnResultExecuting(ResultExecutingContext context);
void OnResultExecuted(ResultExecutedContext context);
}
}
OnResultExecuting method는 endpoint가 action result를 생성한 후에 호출됩니다. 또한 FilterContext class에서 정의된것 외에 아래표의 속성들을 정의하고 있는 ResultExecutingContext class를 통해 context를 수신합니다.
Controller | 이 속성은 endpoint를 포함한 개체를 반환합니다. |
Cancel | 이 속성을 true로 설정하면 result filer pipeline이 단락될 것입니다. |
Result | 이 속성은 endpoint에서 생성한 action result를 반환합니다. |
OnResultExecuted method는 action result가 client로의 응답을 생성하기 위해 실행된 후 호출됩니다. 또한 FilterContext class로 부터 상속받은 것에 더해 아래 표의 속성을 정의하고 있는 ResultExecutedContext class를 통해 context를 수신합니다.
Canceled | 이 속성은 다른 filer가 filter pipeline을 단락했다면 true를 반환합니다. |
Controller | 이 속성은 endpoint를 포함한 개체를 반환합니다. |
Exception | 이 속성은 page handler method에서 예외가 발생한 경우 예외를 반환합니다. |
ExceptionHandled | 이 속성은 page handler에서 발생한 예외가 filter에 의해 처리되었다면 이를 나타내기 위해 true로 설정됩니다. |
Result | 이 속성은 client로의 응답을 생성하기 위해 사용된 action result를 반환하며 읽기 전용입니다. |
비동기 result filer는 IAsyncResultFilter interface를 아래와 같이 구현합니다.
namespace Microsoft.AspNetCore.Mvc.Filters
{
public interface IAsyncResultFilter : IFilterMetadata
{
Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next);
}
}
해당 interface는 다른 filter type에 의해 설정된 pattern을 따릅니다. OnResultExecutionAsync method는 Result 속성을 사용하여 응답을 변경할 수 있는 context 개체와 pipeline을 따라 응답을 전달할 delegate 함께 호출됩니다.
● Always-Run Result Filter
IResultFilter와 IAsyncResultFilter interface를 구현하는 Filter는 요청이 정상적으로 endpoint에서 처리되는 경우에만 사용됩니다. 다른 filter가 pipeline을 단락 하거나 예외가 발생한 경우에는 사용되지 않습니다. pipeline이 단락 된 경우에도 응답을 생성하거나 변경해야 하는 Filter는 IAlwaysRunResultFilter 또는 IAsyncAlwaysRunResultFilter interface를 구현할 수 있습니다. 이들 interface는 IResultFilter와 IAsyncResultFilter로부터 파생되지만 새로운 기능은 없습니다. 대신 ASP.NET Core는 always-run interface를 발견하고 filter를 항상 적용합니다.
● Result Filter 생성
Filters folder에 ResultDiagnosticsAttribute.cs이름의 file을 아래와 같이 추가합니다.
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
namespace MyWebApp.Filters
{
public class ResultDiagnosticsAttribute : Attribute, IAsyncResultFilter
{
public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
{
if (context.HttpContext.Request.Query.ContainsKey("diag"))
{
Dictionary<string, string?> diagData = new Dictionary<string, string?> { {"Result type", context.Result.GetType().Name } };
if (context.Result is ViewResult vr)
{
diagData["View Name"] = vr.ViewName;
diagData["Model Type"] = vr.ViewData?.Model?.GetType().Name;
diagData["Model Data"] = vr.ViewData?.Model?.ToString();
}
else if (context.Result is PageResult pr)
{
diagData["Model Type"] = pr.Model.GetType().Name;
diagData["Model Data"] = pr.ViewData?.Model?.ToString();
}
context.Result = new ViewResult()
{
ViewName = "/Views/Shared/Message.cshtml", ViewData = new ViewDataDictionary(
new EmptyModelMetadataProvider(),
new ModelStateDictionary())
{
Model = diagData
}
};
}
await next();
}
}
}
예제의 filter는 요청을 분석하여 요청에 diag이름의 query string 매개변수를 포함하고 있는지의 여부를 확인합니다. 만약 그런 경우라면 filter는 endpoint에서 생성된 출력대신 분석정보를 표시하는 결과를 생성하게 됩니다. 위 예제의 filter는 Home controller에서 정의된 action 혹은 Message Razor Page를 통해 작동하는데 아래예제는 Controllers folder의 HomeController.cs를 변경하여 result filter를 Home controller에 적용한 것을 나타내고 있습니다.
위 예제에서 action result를 생성할때 view의 이름을 완전한 경로가 주어진 이름을 사용하였습니다. 이 것은 ASP.NET Core가 새로운 결과를 Razor Page로 실행을 시도하고 model type에 대한 예외를 발생시키는 Razor Page에 filter를 적용되는 문제를 피하기 위해서입니다.
[HttpsOnly]
[ResultDiagnostics]
public class HomeController : Controller
project를 실행하여 /?diag로 URL을 요청합니다. filter에 의해 query string 매개변수가 확인되어 다음과 같은 분석정보를 생성하게 될 것입니다.
● Attribute Base Class를 사용한 Result Filter 구현
ResultFilterAttribute class는 Attribute로부터 파생된 것이며 IResultFilter와 IAsyncResultFilter interface를 구현하고 있고 Filter folder의 ResultDiagnosticsAttribute.cs file에서 Attribute Base Class를 사용한 아래 예제와 같이 result filter에서 Base class로서 사용될 수 있습니다. 항상 실행되는 interface를 위한 attribute base class는 존재하지 않습니다.
public class ResultDiagnosticsAttribute : ResultFilterAttribute
{
public override async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
{
if (context.HttpContext.Request.Query.ContainsKey("diag"))
{
Dictionary<string, string?> diagData = new Dictionary<string, string?> { {"Result type", context.Result.GetType().Name } };
if (context.Result is ViewResult vr)
{
diagData["View Name"] = vr.ViewName;
diagData["Model Type"] = vr.ViewData?.Model?.GetType().Name;
diagData["Model Data"] = vr.ViewData?.Model?.ToString();
}
else if (context.Result is PageResult pr)
{
diagData["Model Type"] = pr.Model.GetType().Name;
diagData["Model Data"] = pr.ViewData?.Model?.ToString();
}
context.Result = new ViewResult()
{
ViewName = "/Views/Shared/Message.cshtml", ViewData = new ViewDataDictionary(
new EmptyModelMetadataProvider(),
new ModelStateDictionary())
{
Model = diagData
}
};
}
await next();
}
}
project를 실행하고 /?diag로 URL을 요청하면 filter는 이전과 동일한 응답을 생성할 것입니다.
(6) Exception Filter
Exception filter를 사용하면 필요한 action method에서 일일이 try ~ catch구문을 작성하지 않고도 예외를 응답할 수 있습니다. Exception filter는 controller class, action method, page model class 또는 handler method에 적용할 수 있으며 endpoint나 action page 그리고 endpoint에 적용된 result filter에서 예외가 처리되지 않은 경우 호출됩니다.(Action, page 그리고 result filter는 자신들의 context개체의 ExceptionHandled속성에 true를 설정함으로써 미처리된 예외를 처리할 수 있습니다. ) Exception filter는 아래와 같은 IExceptionFilter interface를 구현하고 있습니다.
namespace Microsoft.AspNetCore.Mvc.Filters
{
public interface IExceptionFilter : IFilterMetadata
{
void OnException(ExceptionContext context);
}
}
OnException method는 미처리된 예외를 마주하게 되는 경우 호출되며 IAsyncExceptionFilter interface는 비동기 exception filter를 생성하는 데 사용됩니다. 아래는 비동기 interface를 정의한 것입니다.
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Mvc.Filters
{
public interface IAsyncExceptionFilter : IFilterMetadata
{
Task OnExceptionAsync(ExceptionContext context);
}
}
OnExceptionAsync method는 비동기적으로 IExceptionFilter interface로부터 OnException method와 대응되며 미처리 예외가 존재할 때 호출됩니다. 두 interface에 FilterContext에서 파생된 ExceptionContext class를 통해 context data가 제공되며 아래 표의 속성을 추가적으로 정의하고 있습니다.
Exception | 해당 속성은 발생한 모둔 예외를 포함합니다. |
ExceptionHandled | 해당 bool속성은 예외가 처리되었을 경우 이를 표현하기 위해 사용됩니다. |
Result | 이 속성은 응답을 생성하기 위해 사용될 IActionResult를 설정합니다. |
(7) Exception Filter 생성
Exception filter는 filter interface 중 하나를 구현하거나 Attribute로부터 파생되고 IExceptionFilter와 IAsyncException filter둘다 구현하는 ExceptionFilterAttribute class로부터 파생하는 형태로 생성될 수 있습니다. exception filter의 가장 흔한 사용방식은 특정 예외에서 사용자정의 error page를 표시함으로써 client에게 기본적으로 제공되는 error표현보다 좀 더 유용한 정보를 제공해 주는 것입니다.
exception filter를 생성하기 위해 Filters folder에 RangeExceptionAttribute.cs file을 아래와 같이 추가합니다.
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Mvc;
namespace MyWebApp.Filters
{
public class RangeExceptionAttribute : ExceptionFilterAttribute
{
public override void OnException(ExceptionContext context)
{
if (context.Exception is ArgumentOutOfRangeException)
{
context.Result = new ViewResult()
{
ViewName = "/Views/Shared/Message.cshtml",
ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary())
{
Model = @"The data received by the application cannot be processed"
}
};
}
}
}
}
해당 filter는 ExceptionContext개체를 사용하여 미처리 예외 type을 구하고 있으며 type이 ArgumentOutOfRangeException인 경우 사용자에게 message를 표시하는 action result를 생성합니다. 아래 예제는 Controllers folder에서 HomeController.cs file을 변경하여 Exception Filter를 적용한 것으로 HomeController에 exception filter를 적용한 action method를 추가한 것입니다.
[RangeException]
public ViewResult GenerateException(int? id)
{
if (id == null)
throw new ArgumentNullException(nameof(id));
else if (id > 10)
throw new ArgumentOutOfRangeException(nameof(id));
else
return View("Message", $"The value is {id}");
}
GenerateException method는 기본적인 routing pattern에 의존하여 요청 URL로부터 null가능한 int형식의 값을 전달받고 있습니다. 이에 따라 action method는 URL segment와 일치하지 않는 경우 ArgumentNullException예외를 발생시키고 값이 50보다 더 큰 경우 ArgumentOutOfRangeException예외를 발생시키고 있습니다. 이와 반대로 값이 존재하고 해당 값이 범위 안에 있는 경우라면 action method는 ViewResult를 반환할 것입니다.
project를 실행하여 /home/generateexception/100으로 URL을 요청합니다. 마지막 segment는 action method에서 받아야 할 값의 범위를 초과하는 것이며 따라서 filter에 의해 처리된 예외가 발생하게 되어 다음과 같은 결과가 표시될 것입니다.
또한 /home/generateexception으로만 URL을 요청하게 된다면 action method에서 발생한 예외는 filter에서 처리되지 않을 것이며 이에 따라 기본 error handling이 사용될 것입니다.
5. Filter Lifecycle 관리
ASP.NET Core는 생성하는 filter개체를 관리하고 다음요청에서 재사용합니다. 이러한 동작방식을 원하지 않는 경우는 생성된 filter를 제어할 수 있는 다른 방법이 사용됩니다. 우선 filter의 생명주기를 확인하기 위해 Filters folder에서 GuidResponseAttribute.cs이름의 file을 아래와 같이 추가합니다.
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
namespace MyWebApp.Filters
{
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true)]
public class GuidResponseAttribute : Attribute, IAsyncAlwaysRunResultFilter
{
private int counter = 0;
private string guid = Guid.NewGuid().ToString();
public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
{
Dictionary<string, string> resultData;
if (context.Result is ViewResult vr && vr.ViewData.Model is Dictionary<string, string> data)
resultData = data;
else
{
resultData = new Dictionary<string, string>();
context.Result = new ViewResult() {
ViewName = "/Views/Shared/Message.cshtml",
ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary())
{
Model = resultData
}
};
}
while (resultData.ContainsKey($"Counter_{counter}"))
counter++;
resultData[$"Counter_{counter}"] = guid;
await next();
}
}
}
해당 예제의 result filter는 endpoint에서 생성된 action result를 Message View를 render 하고 고유한 GUID값을 표시하는 action result로 변경합니다. filter는 같은 대상에 한번 이상 적용될 수 있도록 구성되었으며 pipeline의 이전 filter가 적절한 응답을 생성한 경우 새로운 message를 추가할 것입니다. 아래 예제는 Controllers folder의 HomeControllers.cs에서 Home controller에 filter를 이중으로 적용한 것을 나타내고 있습니다. (또한 action method의 간결함을 위해 하나를 제외하고 모두 삭제하였습니다.)
using Microsoft.AspNetCore.Mvc;
using MyWebApp.Filters;
namespace MyWebApp.Controllers
{
[HttpsOnly]
[ResultDiagnostics]
[GuidResponse]
[GuidResponse]
public class HomeController : Controller
{
public IActionResult Index()
{
return View("Message", "This is the Index action on the Home controller");
}
}
}
filter가 재사용되는 것을 확인하기 위해 project를 실행하고 /? diag URL을 요청합니다. 이에 대한 응답은 두 번의 GuidResponse filter attribute에서 생성된 GUID값을 포함하게 될 것입니다. filter에 대한 2개의 instance가 요청을 처리하기 위해 생성되는데 Browser를 새로고침 하면 같은 GUID값이 표시됨을 알 수 있습니다. 이것은 처음 요청을 처리하기 위해 생성된 filter개체가 재사용되었다는 것을 의미합니다.
(1) Filter Factory 생성
Filter는 filter는 IFilterFactory interface를 구현하여 filter의 instance를 생성할 수 있으며 이들 instance를 재사용할 수 있는지의 여부를 지정할 수 있습니다. IFilterFactory interface는 아래 표의 member를 정의하고 있습니다.
IsReusable | 해당 bool속성은 filter의 instance가 재사용될 수 있는지의 여부를 나타냅니다. |
CreateInstance(serviceProvider) | 해당 method는 filter의 새로운 instance를 생성하기 위해 호출되며 IServiceProvider 개체와 함께 제공됩니다. |
아래 예제에서는 Filters folder의 GuidResponseAttribute.cs file에서 IFilterFactory interface를 구현하고 IsReusable속성에서 false를 반환하여 filter가 재사용되는 것을 방지하도록 합니다.
public class GuidResponseAttribute : Attribute, IAsyncAlwaysRunResultFilter, IFilterFactory
{
private int counter = 0;
private string guid = Guid.NewGuid().ToString();
public bool IsReusable => false;
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
return ActivatorUtilities.GetServiceOrCreateInstance<GuidResponseAttribute>(serviceProvider);
}
예제는 Microsoft.Extensions.DependencyInjection namespace의 ActivatorUtilities class에서 정의된 GetServiceOrCreateInstance method를 사용해 새로운 filter개체를 생성하고 있습니다. 물론 filter를 생성하기 위해 new keyword를 사용할 수도 있지만 이러한 접근법은 filter의 생성자에서 선언된 모든 service에 대한 의존성을 해결할 수 있습니다.
IFilterFactory interface의 구현 효과를 확인하기 위해 project를 실행하고 /? diag로 URL을 요청합니다. 그리고 응답이 생성되면 browser를 새 로고침해 봅니다. 그러면 요청이 처리될 때마다 새로운 filter가 생성될 것이며 따라서 새로운 GUID가 다음과 같이 표시될 것입니다.
(2) filter의 생명주기 관리를 위한 의존성 Injection Scope 사용하기
Filter는 service로서도 등록될 수 있으며 이로 인해 이들 생명주는 의존성주입을 통하여 제어될 수 있습니다. 아래 예제는 project의 Program.cs file에서 filter service를 생성하고 있는 것으로서 예제에서의 GuidResponse filter를 scoped service로서 등록하고 있습니다.
builder.Services.AddScoped<GuidResponseAttribute>();
var app = builder.Build();
기본적으로 ASP.NET Core는 각 요청에 대한 scope를 생성하게 되는데 이것은 filter의 단일 instance가 각각의 요청에 대해 만들어질 수 있다는 것을 의미합니다. project를 실행하고 /?diag로 URL을 요청합니다. Home controller에 적용된 두 attribute모두 같은 filter의 instance를 사용해 처리되며 따라서 응답되는 두 GUID는 동일할 것입니다. browser를 새로고침 하면 새로운 scope가 생성되고 새로운 filter 개체가 사용됩니다.
IFilterFactory interface 없이 service로서 filter사용하기
위 예제에서 생명주기에 대한 변화는 즉각적인 효과를 불러옵니다. 이것은 예제에서 IFilterFactory interface를 구현할 때 filter개체를 생성하기 위해 ActivatorUtilities.GetServiceOrCreateInstance method를 사용했기 때문입니다. 이 method는 생성자를 호출하기 전 요청된 type에 대한 가능한 service가 있는지를 확인하게 됩니다. IFilterFactory interface를 구현하지 않고 ActivatorUtilities를 통해 service로서 filter를 사용하고자 한다면 아래와 같이 ServiceFilter attribute를 filter에 적용할 수 있습니다.
[ServiceFilter(typeof(GuidResponseAttribute))]
이렇게 하면 ASP.NET Core는 service로부터 filter개체를 생성하고 요청에 적용하게 됩니다. 이러한 방법으로 적용된 filter는 Attribute class로부터 파생될 수 없습니다.
6. Global Filter
Global filter는 ASP.NET Core에서 처리하는 모든 요청에 적용되는 것이며 controller나 Razor page에 개별적으로 적용하여 사용할 수 없습니다. 모든 filter가 global filter로 사용될 수 있지만 action filter의 경우 endpoint가 action method인 곳의 요청에만 적용될 수 있으며 page filter는 endpoint가 Razor page인 곳의 요청에만 적용될 수 있습니다.
Global filter는 아래와 같이 Program.cs에서 option pattern을 사용해 설정됩니다.
builder.Services.AddScoped<GuidResponseAttribute>();
builder.Services.Configure<MvcOptions>(opts => opts.Filters.Add<HttpsOnlyAttribute>());
var app = builder.Build();
MvcOptions.Filters속성은 Add<T> method를 사용하거나 service이기도 한 fitler에 대해 AddService<T> method를 사용하여 전역적으로 적용하기 위해 추가된 filter의 collection을 반환합니다. 또한 generic type 인수가 없는 Add method를 통해 global filter로서 특정 개체를 등록할 수 있습니다.
위 예제에서는 이전에 예제로 생성했던 HttpOnly filter를 전역적으로 등록한 것으로서 이로 인해 각각의 controller나 Razor page에서는 더 이상 개별적으로 해당 filter를 적용하지 않아도 됩니다. 따라서 아래 Controllers folder의 HomeController.cs file에서 처럼 HttpOnly filter의 등록을 제거할 수 있습니다.
또한 GuidResponse도 함께 제거하였습니다. 이 filter는 always-run result filter이며 global filter에서 생성한 결과를 바꾸게 됩니다.
//[HttpsOnly]
[ResultDiagnostics]
//[GuidResponse]
//[GuidResponse]
public class HomeController : Controller
{
결과적으로 Controller에 HttpOnly가 적용되지 않았지만 Global filter로 등록되었기 때문에 http로의 연결은 어디에서도 사용할 수 없게 됩니다.
7. filter 작동 순서
Filter는 다음과 같은 특정한 순서에 의해 작동합니다.
- authorization
- resource
- action 또는 page
- result
하지만 주어진 type에서 여러 filter가 존재한다면 이들이 적용되는 순서는 filter가 적용된 범위에 따라 결정됩니다. 이러한 동작방식을 알아보기 위해 Filters folder에 MessageAttribute.cs file을 아래와 같이 추가합니다.
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
namespace MyWebApp.Filters
{
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true)]
public class MessageAttribute : Attribute, IAsyncAlwaysRunResultFilter
{
private int counter = 0;
private string msg = string.Empty;
public MessageAttribute(string message) => msg = message;
public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
{
Dictionary<string, string> resultData;
if (context.Result is ViewResult vr && vr.ViewData.Model is Dictionary<string, string> data)
resultData = data;
else {
resultData = new Dictionary<string, string>();
context.Result = new ViewResult()
{
ViewName = "/Views/Shared/Message.cshtml",
ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary())
{
Model = resultData
}
};
}
while (resultData.ContainsKey($"Message_{counter}"))
counter++;
resultData[$"Message_{counter}"] = msg;
await next();
}
}
}
예제의 result filter는 이전 예제에서 봤었던 기법을 사용하여 endpoint로부터의 result를 변경하고 다중 filter가 사용자에게 표시할 일련의 message를 작성하도록 합니다. 아래 예제는 Controllers folder의 HomeController.cs file을 변경하여 Home controller로 Message filer의 몇몇 instance를 적용하고 있습니다.
[Message("This is the controller-scoped filter")]
public class HomeController : Controller
{
[Message("This is the first action-scoped filter")]
[Message("This is the second action-scoped filter")]
public IActionResult Index()
{
return View("Message", "This is the Index action on the Home controller");
}
}
아래 예제에서는 Program.cs에서 다른 filter와 함께 Message filter를 전역적으로 등록하고 있습니다.
builder.Services.Configure<MvcOptions>(opts => {
opts.Filters.Add<HttpsOnlyAttribute>();
opts.Filters.Add(new MessageAttribute("This is the globally-scoped filter"));
});
이로서 같은 filter에 대한 4개의 instance를 생성하였습니다. 이들이 적용되는 순서를 확인하기 위해 project를 실행하면 아래와 같은 응답을 확인할 수 있습니다.
보시는 바와 같이 기본적으로 ASP.NET Core는 global filter를 시작으로 controller와 page model class의 filter가 적용되며 마지막으로 action 혹은 handler method에 적용된 filter가 적용됩니다.
(1) Filter의 순서 변경
Filter의 기본적인 실행순서는 ASP.NET Core가 filter의 순서를 확인하는 IOrderedFilter interface를 구현함으로써 바꿀 수 있습니다. 아래는 해당 interface의 정의를 나타내고 있습니다.
namespace Microsoft.AspNetCore.Mvc.Filters
{
public interface IOrderedFilter : IFilterMetadata
{
int Order { get; }
}
}
Order속성은 int값을 반환하며 낮은 값을 가진 filter가 높은 값을 가진 것보다 먼저 적용됩니다. 아래 예제는 Filters folder의 MessageAttribute.cs file을 변경하여 Message filter에 interface를 구현하여 filter를 적용할 때 Order속성에 값을 지정할 수 있도록 하였습니다.
public MessageAttribute(string message) => msg = message;
public int Order { get; set; }
이를 통해 아래 예제와 같이 filter를 적용할때 생성자 인수를 통하여 Order의 값을 바꿀 수 있게 되었습니다.
[Message("This is the controller-scoped filter", Order = 2)]
public class HomeController : Controller
{
[Message("This is the first action-scoped filter", Order = 1)]
[Message("This is the second action-scoped filter", Order = -1)]
public IActionResult Index()
{
return View("Message", "This is the Index action on the Home controller");
}
}
Order값은 음수로도 지정할 수 있는데 이를 통해 기본 Order값을 가진 전역 filter보다도 먼저 filter가 적용될 수 있도록 하는 것도 가능합니다.(물론 전역 filter를 생성할 때 Order값을 -로 지정하여 전역 filter가 다른 filter보다도 먼저 적용되도록 할 수 있습니다.) project를 실행하면 다음과 같은 응답을 통해 filter의 순서가 어떻게 적용되는지를 확인할 수 있습니다.