在 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'基类的覆盖方法OnActionExecuting
的context
参数访问它
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 5.0 web api 项目中访问中间件中的 TempData