Chapter 06

MVC Filters

IAuthorizationFilter, TypeFilter & ServiceFilter trong ASP.NET Core

Giới thiệu IAuthorizationFilter

ASP.NET Core có hai cơ chế authorization khác nhau:

Middleware-Based (Chapter 03)

Chạy ở middleware pipeline, trước khi MVC bắt đầu xử lý. Dùng [Authorize(Policy = "X")], policies, requirements và handlers.

MVC Filter-Based (Chapter 06)

Chạy bên trong MVC filter pipeline, sau routing nhưng trước action method. Dùng IAuthorizationFilter với access đến ActionDescriptor, RouteData.

IAuthorizationFilter nằm trong namespace Microsoft.AspNetCore.Mvc.Filters. Nó chạy ở đầu MVC filter pipeline — sau routing nhưng TRƯỚC action method, model binding và các filter khác.

Khi nào dùng IAuthorizationFilter thay vì policy-based? Khi bạn cần access đến thông tin MVC-specific: ActionDescriptor (biết action nào đang chạy), RouteData (biết route values), hoặc cần logic phân quyền phụ thuộc vào metadata của action/controller. Trong hầu hết trường hợp, policy-based authorization (Chapter 03) là đủ và được khuyến khích.

Interface

Có 2 interface: synchronous và asynchronous.

IAuthorizationFilter ← Sync interface
+ void OnAuthorization(AuthorizationFilterContext context)
IAsyncAuthorizationFilter ← Async interface
+ Task OnAuthorizationAsync(AuthorizationFilterContext context)
implements
CustomAuthorizationFilter ← Your filter
+OnAuthorization(context)
→ Kiểm tra quyền
→ context.Result = ForbidResult nếu từ chối

AuthorizationFilterContext cung cấp:

Implementation

Sync — IAuthorizationFilter

C#
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

public class AdminOnlyFilter : IAuthorizationFilter
{
    private readonly ILogger<AdminOnlyFilter> _logger;

    public AdminOnlyFilter(ILogger<AdminOnlyFilter> logger)
    {
        _logger = logger;
    }

    public void OnAuthorization(AuthorizationFilterContext context)
    {
        var user = context.HttpContext.User;

        if (!user.Identity?.IsAuthenticated ?? true)
        {
            _logger.LogWarning("Unauthorized access attempt");
            context.Result = new UnauthorizedResult(); // 401
            return;
        }

        if (!user.IsInRole("Admin"))
        {
            _logger.LogWarning("Forbidden: user {User} is not Admin",
                user.Identity.Name);
            context.Result = new ForbidResult(); // 403
            return;
        }

        // Không set Result → pipeline tiếp tục
    }
}

Async — IAsyncAuthorizationFilter

C#
public class SubscriptionFilter : IAsyncAuthorizationFilter
{
    private readonly ISubscriptionService _subscriptions;

    public SubscriptionFilter(ISubscriptionService subscriptions)
    {
        _subscriptions = subscriptions;
    }

    public async Task OnAuthorizationAsync(
        AuthorizationFilterContext context)
    {
        var userId = context.HttpContext.User
            .FindFirstValue(ClaimTypes.NameIdentifier);

        if (userId == null)
        {
            context.Result = new UnauthorizedResult();
            return;
        }

        // Gọi service async để kiểm tra subscription
        var isActive = await _subscriptions
            .IsActiveAsync(userId);

        if (!isActive)
        {
            context.Result = new JsonResult(new
            {
                error = "Subscription expired"
            })
            {
                StatusCode = 403
            };
        }
    }
}
Khi bạn set context.Result, pipeline dừng ngay lập tức. Action method sẽ KHÔNG được gọi. Các filter phía sau (Resource Filters, Action Filters) cũng không chạy.

Vị trí trong Filter Pipeline

Authorization Filters chạy đầu tiên trong MVC filter pipeline:

Authorization
Filters
Đầu tiên
Resource
Filters
cache, etc.
Model
Binding
bind params
Action
Filters
before/after
Action
Method
execute

Full Request Lifecycle

Vị trí của IAuthorizationFilter so với middleware authorization:

Request đến server
Middleware Pipeline
Routing → Authentication Middleware → Authorization Middleware (policy check)
MVC Pipeline bắt đầu
► IAuthorizationFilter chạy ở đây
Có access đến ActionDescriptor, RouteData, HttpContext. Có thể short-circuit bằng context.Result.
Resource Filters → Model Binding → Action Filters
Action Method thực thi

TypeFilterAttribute & ServiceFilterAttribute

Filter class với constructor dependencies không thể dùng trực tiếp như một [Attribute]. ASP.NET Core cung cấp 2 cách để apply filter với DI support:

TypeFilterAttribute

Tự động resolve dependencies qua DI, không cần đăng ký filter trong container.

C# — Sử dụng TypeFilter
// Apply filter lên controller hoặc action
[TypeFilter(typeof(AdminOnlyFilter))]
public class AdminController : Controller
{
    public IActionResult Dashboard() => View();
}

// Hoặc trên từng action
public class PostsController : Controller
{
    [TypeFilter(typeof(AdminOnlyFilter))]
    public IActionResult Delete(int id) => View();
}

TypeFilter còn hỗ trợ truyền Arguments bổ sung:

C# — TypeFilter với Arguments
// Filter nhận cả DI services và arguments
public class RoleFilter : IAuthorizationFilter
{
    private readonly string _requiredRole;
    private readonly ILogger<RoleFilter> _logger;

    public RoleFilter(string requiredRole, ILogger<RoleFilter> logger)
    {
        _requiredRole = requiredRole;   // từ Arguments
        _logger = logger;               // từ DI container
    }

    public void OnAuthorization(AuthorizationFilterContext context)
    {
        if (!context.HttpContext.User.IsInRole(_requiredRole))
        {
            context.Result = new ForbidResult();
        }
    }
}

// Sử dụng — truyền "Admin" qua Arguments
[TypeFilter(typeof(RoleFilter), Arguments = new object[] { "Admin" })]
public IActionResult ManageUsers() => View();

// Reuse với role khác
[TypeFilter(typeof(RoleFilter), Arguments = new object[] { "Editor" })]
public IActionResult EditPost() => View();

ServiceFilterAttribute

Yêu cầu filter phải được đăng ký trong DI container. Lifecycle (Transient/Scoped/Singleton) do container kiểm soát.

C# — Đăng ký và sử dụng ServiceFilter
// 1. Đăng ký filter trong DI container (Program.cs)
builder.Services.AddScoped<SubscriptionFilter>();

// 2. Apply bằng ServiceFilter
[ServiceFilter(typeof(SubscriptionFilter))]
public class PremiumController : Controller
{
    public IActionResult PremiumContent() => View();
}

So sánh

TypeFilterAttribute

  • Không cần đăng ký DI
  • Hỗ trợ Arguments bổ sung
  • Tạo instance mới mỗi lần
  • Resolve DI qua ObjectFactory
  • Phù hợp khi cần truyền tham số

ServiceFilterAttribute

  • Bắt buộc đăng ký DI
  • Không hỗ trợ Arguments
  • Lifecycle do DI container quản lý
  • Resolve qua IServiceProvider
  • Phù hợp khi cần kiểm soát lifetime
Nên dùng cái nào? Nếu filter cần tham số động (như role name, IP range) → TypeFilter. Nếu filter cần scoped services hoặc bạn muốn kiểm soát lifecycle → ServiceFilter.

Khi nào dùng cách nào?

Ba cách tiếp cận authorization, mỗi cách phù hợp với ngữ cảnh khác nhau:

Policy-Based (Ch.03)
Best for: Declarative, reusable authorization rules
Access: HttpContext, Claims
Đăng ký: options.AddPolicy(...)
Dùng khi: Kiểm tra role/claim/policy chuẩn
TypeFilter
Best for: Custom logic cần MVC context + tham số
Access: ActionDescriptor, RouteData, HttpContext
Đăng ký: [TypeFilter(typeof(...))]
Dùng khi: Cần route data, action metadata, arguments
ServiceFilter
Best for: Logic phức tạp với DI-managed services
Access: ActionDescriptor, RouteData + scoped services
Đăng ký: DI + [ServiceFilter(typeof(...))]
Dùng khi: Cần lifecycle control, scoped/singleton

Ví dụ thực tế

Các pattern phổ biến khi dùng IAuthorizationFilter trong production:

01
IP Whitelist Authorization
Chỉ cho phép request từ các IP được phép — dùng TypeFilter với Arguments

Sử dụng TypeFilter để truyền danh sách IP allowed:

Request IP So sánh whitelist Match → Allow / No match → 403
C#
public class IpWhitelistFilter : IAuthorizationFilter
{
    private readonly string[] _allowedIps;
    private readonly ILogger<IpWhitelistFilter> _logger;

    public IpWhitelistFilter(
        string[] allowedIps,          // từ Arguments
        ILogger<IpWhitelistFilter> logger) // từ DI
    {
        _allowedIps = allowedIps;
        _logger = logger;
    }

    public void OnAuthorization(
        AuthorizationFilterContext context)
    {
        var remoteIp = context.HttpContext.Connection
            .RemoteIpAddress?.ToString();

        if (remoteIp == null
            || !_allowedIps.Contains(remoteIp))
        {
            _logger.LogWarning(
                "Blocked IP: {Ip}", remoteIp);
            context.Result = new JsonResult(new
            {
                error = "IP not allowed"
            })
            { StatusCode = 403 };
        }
    }
}

// Sử dụng
[TypeFilter(typeof(IpWhitelistFilter),
    Arguments = new object[] {
        new[] { "10.0.0.1", "10.0.0.2", "192.168.1.100" }
    })]
public class InternalApiController : Controller
{ }
  • TypeFilter tự động inject ILogger từ DI
  • Mảng IP được truyền qua Arguments
  • Có thể reuse với danh sách IP khác nhau cho từng controller
02
Feature Flag Authorization
Chỉ cho phép truy cập khi feature được bật — dùng ServiceFilter với scoped service

Sử dụng ServiceFilter vì IFeatureFlagService là scoped service:

C#
public class FeatureFlagFilter : IAuthorizationFilter
{
    private readonly IFeatureFlagService _features;

    public FeatureFlagFilter(
        IFeatureFlagService features)
    {
        _features = features;
    }

    public void OnAuthorization(
        AuthorizationFilterContext context)
    {
        // Lấy feature name từ route data
        var feature = context.RouteData.Values["feature"]
            ?.ToString();

        if (feature != null
            && !_features.IsEnabled(feature))
        {
            context.Result = new NotFoundResult();
        }
    }
}

// Đăng ký (Program.cs)
builder.Services.AddScoped<FeatureFlagFilter>();

// Sử dụng
[ServiceFilter(typeof(FeatureFlagFilter))]
public class BetaController : Controller
{
    // Route: /beta/{feature}/...
    public IActionResult Index(string feature)
        => View();
}
  • ServiceFilter đảm bảo filter dùng cùng scope với request
  • IFeatureFlagService có thể cache per-request
  • Filter đọc route data để biết feature nào cần kiểm tra