在 Asp.net 核心中间件中访问 ModelState

Posted

技术标签:

【中文标题】在 Asp.net 核心中间件中访问 ModelState【英文标题】:Access ModelState in Asp.net core Middleware 【发布时间】:2019-02-21 13:12:28 【问题描述】:

我需要在 Asp.net Core 2.1 中间件中访问 ModelState,但这只能从 Controller 访问。

例如,我有 ResponseFormatterMiddleware,在这个中间件中我需要忽略 ModelState 错误并在“响应消息”中显示它的错误:

public class ResponseFormatterMiddleware

    private readonly RequestDelegate _next;
    private readonly ILogger<ResponseFormatterMiddleware> _logger;
    public ResponseFormatterMiddleware(RequestDelegate next, ILoggerFactory loggerFactory)
    
        _next = next ?? throw new ArgumentNullException(nameof(next));
        _logger = loggerFactory?.CreateLogger<ResponseFormatterMiddleware>() ?? throw new ArgumentNullException(nameof(loggerFactory));
    

    public async Task Invoke(HttpContext context)
    
        var originBody = context.Response.Body;

        using (var responseBody = new MemoryStream())
        
            context.Response.Body = responseBody;
            // Process inner middlewares and return result.
            await _next(context);

            responseBody.Seek(0, SeekOrigin.Begin);
            using (var streamReader = new StreamReader(responseBody))
            
                // Get action result come from mvc pipeline
                var strActionResult = streamReader.ReadToEnd();
                var objActionResult = JsonConvert.DeserializeObject(strActionResult);
                context.Response.Body = originBody;

                // if (!ModelState.IsValid) => Get error message

                // Create uniuqe shape for all responses.
                var responseModel = new GenericResponseModel(objActionResult, (HttpStatusCode)context.Response.StatusCode, context.Items?["Message"]?.ToString());

                // Set all response code to 200 and keep actual status code inside wrapped object.
                context.Response.StatusCode = (int)HttpStatusCode.OK;
                context.Response.ContentType = "application/json";
                await context.Response.WriteAsync(JsonConvert.SerializeObject(responseModel));
            
        
    


// Extension method used to add the middleware to the HTTP request pipeline.
public static class ResponseFormatterMiddlewareExtensions

    public static IApplicationBuilder UseResponseFormatter(this IApplicationBuilder builder)
    
        return builder.UseMiddleware<ResponseFormatterMiddleware>();
    


[Serializable]
[DataContract]
public class GenericResponseModel

    public GenericResponseModel(object result, HttpStatusCode statusCode, string message)
    
        StatusCode = (int)statusCode;
        Result = result;
        Message = message;
    
    [DataMember(Name = "result")]
    public object Result  get; set; 

    [DataMember(Name = "statusCode")]
    public int StatusCode  get; set; 

    [DataMember(Name = "message")]
    public string Message  get; set; 

    [DataMember(Name = "version")]
    public string Version  get; set;  = "V1.0"

这是我的预期结果:


    "result": null,
    "statusCode": 400,
    "message": "Name is required",
    "version": "V1"

但现在观察到的结果是:


    "result": 
        "Name": [
            "Name is required"
        ]
    ,
    "statusCode": 400,
    "message": null,
    "version": "V1"

【问题讨论】:

ModelState 在一般的中间件中根本不存在。这是一个 MVC 概念。 【参考方案1】:

ModelState 仅在模型绑定后可用。只需使用动作过滤器自动存储ModelState,因此您可以在中间件中使用它。

首先,添加一个动作过滤器以将 ModelState 设置为特征:

public class ModelStateFeatureFilter : IAsyncActionFilter


    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    
        var state = context.ModelState;
        context.HttpContext.Features.Set<ModelStateFeature>(new ModelStateFeature(state));
        await next();
    

这里的 ModelStateFeature 是一个包含 ModelState 的虚拟类:

public class ModelStateFeature

    public ModelStateDictionary ModelState  get; set; 

    public ModelStateFeature(ModelStateDictionary state)
    
        this.ModelState= state;
    

要使动作过滤器自动发生,我们需要配置 MVC

services.AddMvc(opts=> 
    opts.Filters.Add(typeof(ModelStateFeatureFilter));
)

现在我们可以在您的中间件中使用ModelState,如下所示:

public class ResponseFormatterMiddleware

    // ...

    public async Task Invoke(HttpContext context)
    
        var originBody = context.Response.Body;

        using (var responseBody = new MemoryStream())
        
            context.Response.Body = responseBody;
            // Process inner middlewares and return result.
            await _next(context);

            var ModelState = context.Features.Get<ModelStateFeature>()?.ModelState;
            if (ModelState==null) 
                return ;      //  if you need pass by , just set another flag in feature .
            

            responseBody.Seek(0, SeekOrigin.Begin);
            using (var streamReader = new StreamReader(responseBody))
            
                // Get action result come from mvc pipeline
                var strActionResult = streamReader.ReadToEnd();
                var objActionResult = JsonConvert.DeserializeObject(strActionResult);
                context.Response.Body = originBody;

               // Create uniuqe shape for all responses.
                var responseModel = new GenericResponseModel(objActionResult, (HttpStatusCode)context.Response.StatusCode, context.Items?["Message"]?.ToString());

                // => Get error message
                if (!ModelState.IsValid)
                
                    var errors= ModelState.Values.Where(v => v.Errors.Count > 0)
                        .SelectMany(v=>v.Errors)
                        .Select(v=>v.ErrorMessage)
                        .ToList();
                    responseModel.Result = null;
                    responseModel.Message = String.Join(" ; ",errors) ;
                 

                // Set all response code to 200 and keep actual status code inside wrapped object.
                context.Response.StatusCode = (int)HttpStatusCode.OK;
                context.Response.ContentType = "application/json";
                await context.Response.WriteAsync(JsonConvert.SerializeObject(responseModel));
            
        
    

让我们用一个简单的模型来测试

public class MyModel 
    [MinLength(6)]
    [MaxLength(12)]
    public string Name  get; set; 
    public int Age  get; set; 

还有一个简单的控制器:

public class HomeController : Controller


    public IActionResult Index(string name)
    
        return new JsonResult(new 
            Name=name
        );
    

    [HttpPost]
    public IActionResult Person([Bind("Age,Name")]MyModel model)
    
        return new JsonResult(model);
    

如果我们发送带有有效载荷的请求:

POST https://localhost:44386/Home/Person HTTP/1.1
content-type: application/x-www-form-urlencoded

name=helloo&age=20

响应将是:

HTTP/1.1 200 OK
Transfer-Encoding: chunked
Content-Type: application/json
Server: Kestrel
X-SourceFiles: =?UTF-8?B?RDpccmVwb3J0XDIwMThcOVw5LTE4XEFwcFxBcHBcQXBwXEhvbWVcUGVyc29u?=
X-Powered-By: ASP.NET


  "result": 
    "name": "helloo",
    "age": 20
  ,
  "statusCode": 200,
  "message": null,
  "version": "V1.0"

如果我们发送一个带有无效模型的请求:

POST https://localhost:44386/Home/Person HTTP/1.1
content-type: application/x-www-form-urlencoded

name=hello&age=i20

响应将是

HTTP/1.1 200 OK
Transfer-Encoding: chunked
Content-Type: application/json
Server: Kestrel
X-SourceFiles: =?UTF-8?B?RDpccmVwb3J0XDIwMThcOVw5LTE4XEFwcFxBcHBcQXBwXEhvbWVcUGVyc29u?=
X-Powered-By: ASP.NET


  "result": null,
  "statusCode": 200,
  "message": "The value 'i20' is not valid for Age. ; The field Name must be a string or array type with a minimum length of '6'.",
  "version": "V1.0"

【讨论】:

使用这种方法,当 ModelState.IsValid == false 时,我很难让 OnActionExecutionAsync 触发。显然,当你做出这个答案时它起作用了,所以我想知道 asp.net core 2.2 中是否发生了一些变化? @raterus 我只是用 ASP.NET Core 2.2 尝试上面的代码,它对我来说很好。您在UseMvc() 之后注册了中间件吗?如果是这样,它不会生效。并确保 ModelStateFeatureFilter 已添加到 MVC 服务中。【参考方案2】:

我在 .net core 2.2 中也遇到了问题,似乎 IAsyncActionFilter 不适用于我的情况,但与 IActionResult 合作。以下是我修改后的代码,但不确定这是否是预期的。

public class ModelStateFeatureFilter : IActionResult

    public Task ExecuteResultAsync(ActionContext context)
    
        var state = context.ModelState;
        context.HttpContext.Features.Set(new ModelStateFeature(state));
        return Task.CompletedTask;
    
 

和下面的启动类

services.Configure<ApiBehaviorOptions>(options =>

    options.InvalidModelStateResponseFactory = ctx => new ModelStateFeatureFilter();
);

【讨论】:

【参考方案3】:

如果你正在实现类似action filter的东西,你可以通过'ActionFilterAttribute'基类的覆盖方法OnActionExecutingcontext参数访问它

public class ModelStateValidationFilter : ActionFilterAttribute

     public override void OnActionExecuting(ActionExecutingContext context)
     
         // You can access it via context.ModelState
         ModelState.AddModelError("YourFieldName", "Error details...");
         base.OnActionExecuting(context);
     

【讨论】:

是的,我知道,但我在中间件中需要它 Middleware 是什么意思?您能否描述一下访问ModelState 的目的? github.com/aspnet/Mvc/issues/3454 没错,我需要一些模型绑定忽略 我没明白。为什么?你想忽略什么?在什么用例中? 假设一个属性可以是单数或复数,具体取决于 AllowMultiple 属性,也许在这种情况下可以使用

以上是关于在 Asp.net 核心中间件中访问 ModelState的主要内容,如果未能解决你的问题,请参考以下文章

处理asp.net核心中的异常?

asp.net核心中间件怎么做DI?

在 ASP.NET 5 的中间件中访问 DbContext

在 asp.net 5.0 web api 项目中访问中间件中的 TempData

csharp RequestCounter自定义中间件asp.net核心

ASP.NET Core Web API - 如何在中间件管道中隐藏 DbContext 事务?