返回文章列表文章索引阅读全文
STARBLOG / 阅读手记
Asp-Net-Core开发笔记

ASP.NET Core Development Notes: Implementing Non-intrusive Parameter Validation Using ActionFilterAttribute

Using ActionFilterAttribute for non-intrusive validation to avoid redundant code. Supports Query, Body, Route, Header, with data shared via HttpContext. OAuth2.0 is being learned on the fly, the project is messy, but functionality is functional enough.

曦远 (DealiAxy)约 22 分钟
目录14 个章节

Introduction

In modern application development, ensuring the security and reliability of APIs is crucial.

Aspect-Oriented Programming (AOP) greatly enhances code modularity and maintainability by separating cross-cutting concerns (such as validation, logging, and exception handling) from the core business logic.

In ASP.NET Core, utilizing ActionFilterAttribute provides a convenient way to implement the principles of AOP, enabling custom validation in a concise and efficient manner.

This article will share how to validate client IDs by creating a ValidateClientAttribute and explore how this approach embodies the many advantages of AOP.

Use Case

The scenario for this article is within a single sign-on (SSO) authentication project I previously developed. The project was named IdentityServerLite, designed as a lightweight SSO solution referencing IdentityServer4. However, it was not yet fully refined, as I built it while simultaneously learning OAuth 2.0 and OpenID Connect, resulting in somewhat disorganized code. I may write a separate article later to introduce this SSO project in detail, and there are currently plans to refactor and open-source it.

In the SSO project, operations such as login, acquiring an AccessToken, and requesting a token all require validating whether the Client ID parameter provided by the user is valid. This logic involved some repetition, which led me to seek a more efficient way to implement this functionality.

Having previously applied Aspect-Oriented Programming (AOP) principles to implement non-intrusive audit logging, I utilized the same approach here to implement this validation feature.

ActionFilterAttribute

I noticed that my previous two articles on implementing audit logs didn't cover this topic in much detail.

Let's recap:

Now, let's elaborate a bit more~

ActionFilterAttribute is a convenient tool provided by ASP.NET Core for adding custom logic before or after the execution of a controller's action method. This mechanism allows us to insert additional processing logic, such as validation, logging, exception handling, etc., without modifying the action method itself. This feature embodies the concept of Aspect-Oriented Programming (AOP), which effectively separates concerns and improves the modularity and maintainability of the code.

By inheriting from ActionFilterAttribute, you can override the OnActionExecuting and OnActionExecuted methods to execute custom logic before and after the action method runs. For example, validating the validity of input parameters, logging the execution time of requests, handling exceptions, and more.

Clarifying Thoughts (理清思路)

Features to Implement

  • Validate whether the incoming Client ID parameter is valid based on configuration (the parameter name and its location are not fixed and require configuration).
  • If validation fails, return error information.
  • If validation passes, the corresponding Client object must be accessible within the interface.

How is it implemented?

First, it was determined that this feature would be added to the interface externally using an Attribute. The specific validation logic is implemented by overriding the OnActionExecutionAsync method of the ActionFilterAttribute.

Afterward, the Client object retrieved from the database needs to be saved into HttpContext so that it can be easily accessed within the interface.

HttpContext is an object in ASP.NET Core that encapsulates the HTTP request and response. It provides a unified way to access HTTP-specific information, including request details, response content, user information, session data, request headers, response headers, etc. Each HTTP request corresponds to an HttpContext instance, which persists throughout the entire lifecycle of the request processing.

Here, we use the Items key-value pair collection provided by HttpContext (used to share data between different middleware and components during the request) to share the Client object.

var client = HttpContext.Items["client"] as Client;

Start Writing Code

ClientIdSource Enum

The location of the Client ID is not fixed and needs to be configured when it is used.

Define an enum as follows:

public enum ClientIdSource {
  Query,
  Body,
  Route,
  Header
}

ValidateClientAttribute Implementation

Create the file ValidateClientAttribute.cs within the Filters directory.

Based on configuration, read the Client ID from a specified location using a designated parameter name, then query the database.

public class ValidateClientAttribute(
  ClientIdSource source = ClientIdSource.Query
) : ActionFilterAttribute {
  /// <summary>
  /// The parameter name for the Client ID. Note that this is the property name within the DTO, not the field name in the request body JSON.
  /// </summary>
  public string ParameterName { get; set; } = "client_id";

  /// <summary>
  /// Sets the key for the `Client` object stored in the `HttpContext.Items` collection after successful validation.
  /// </summary>
  public string ClientItemKey { get; set; } = "client";

  public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) {
    var clientId = "";

    switch (source) {
      case ClientIdSource.Query:
        clientId = context.HttpContext.Request.Query[ParameterName];
        break;
      case ClientIdSource.Body:
        // Use reflection to read client_id from the request body.
        // The body read here is the first parameter of the Controller's Action method, typically a JSON data model from the request body bound to the corresponding DTO instance.
        var body = context.ActionArguments.Values.FirstOrDefault();
        if (body != null) {
          var clientProp = body.GetType().GetProperty(ParameterName);
          if (clientProp != null) {
            clientId = clientProp.GetValue(body) as string;
          }
        }
        break;
      case ClientIdSource.Route:
        clientId = context.RouteData.Values[ParameterName] as string;
        break;
      case ClientIdSource.Header:
        clientId = context.HttpContext.Request.Headers[ParameterName];
        break;
    }

    if (string.IsNullOrWhiteSpace(clientId)) {
      throw new ArgumentNullException(ParameterName);
    }

    var clientRepo = context.HttpContext.RequestServices.GetRequiredService<IBaseRepository<Client>>();
    var client = await clientRepo.Select.Where(a => a.ClientId == clientId).FirstAsync();

    if (client != null) {
      context.HttpContext.Items["client"] = client;
      await next();
    }
    else {
      context.Result = new NotFoundObjectResult(
        new ApiResponse { Message = $"client with id {clientId} not found" });
    }
  }
}

There are a few things to note, which are explained below.

Using Reflection to Obtain Request Body Parameters

The other parameter locations are relatively easy to obtain.

For POST or PUT methods, data is typically sent as JSON in the Request Body.

At this point, we could read the value of this Body, but after reading, we would have to parse the JSON ourselves and write the Stream back, which is quite cumbersome. Moreover, if the Body is in XML format, a different parsing method would be required.

Here, I used reflection to let the AspNetCore framework handle the Request Body. Then, I directly used reflection to read the Client ID based on the parameter name.

Usage

Here are some usage examples

Parameter is in the Body

The parameter name in the DTO is ClientId.

public class PwdLoginDto : LoginDto {
  [Required]
  [JsonPropertyName("username")]
  public string Username { get; set; }

  [Required]
  [JsonPropertyName("password")]
  public string Password { get; set; }
}

Usage in Interface

[HttpPost("login/password")]
[ValidateClient(ClientIdSource.Body, ParameterName = "ClientId")]
public async Task<IActionResult> LoginByPassword(PwdLoginDto dto) {

}

Parameters are in Query Params

The parameter name is client_id

[HttpGet("authorize/url")]
[ValidateClient(ClientIdSource.Query, ParameterName = "client_id")]
public ApiResponse<string> GetAuthorizeUrl([FromQuery] AuthorizeInput input) {
  return new ApiResponse<string>(GenerateAuthorizeUrl(input));
}

References

_EOF_

ASP.NET Core Development Notes: Implementing Non-intrusive Parameter Validation Using ActionFilterAttribute

本文作者:
程序设计实验室
版权声明:
本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

相关文章

Asp-Net-Core开发笔记:使用NPM和gulp管理前端静态文件

前言 本文介绍的是AspNetCore的MVC项目,WebApi+独立前端这种前后端分离的项目就不需要多此一举了~默认前端小伙伴是懂得使用前端工具链的。 为啥要用MVC这种服务端渲染技术呢? 简单项目不需要强行分离增加复杂度(如:我正在开发的博客项目) 后端渲染利于SEO,对博客网站友好 OK,虽然MVC的技术老了点,但依然可以结合现代前端工具链来提高效率 本文的食用需要先安装好Node.js环境

2022-02-17 · Asp-Net-Core开发笔记 · 约 1 分钟

讨论

共 0 条

良言一句三冬暖,恶语伤人六月寒。

加载评论中…