Access ModelState in Asp.net core Middleware
Asked Answered
B

4

11

I need to access ModelState in Asp.net Core 2.1 Middleware, but this is just accessible from Controller.

For example I have ResponseFormatterMiddleware and in this Middleware I need to ignore ModelState error and show it's errors in 'Response Message':

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"
}

and this is my excpected result:

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

but now the observed result is:

{
    "result": {
        "Name": [
            "Name is required"
        ]
    },
    "statusCode": 400,
    "message": null,
    "version": "V1"
}
Brock answered 17/9, 2018 at 15:2 Comment(1)
ModelState simply doesn’t exist in general middleware. It’s an MVC concept.Mnemonics
B
22

ModelState is only available after model binding . Just store the ModelState automatically with an action filter , thus you can use it within middleware .

Firstly , add a action filter to set the ModelState as an feature :

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();
    }
}

Here the ModelStateFeature is a dummy class that holds the ModelState:

public class ModelStateFeature
{
    public ModelStateDictionary ModelState { get; set; }

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

to make the action filter take place automatically , we need configure the MVC

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

And now we can use the ModelState within your Middleware as below:

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));
            }
        }
    }
}

Let's test with a simple Model

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

and a simple controller:

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);
    }
}

enter image description here

If we send a request with a valid payload :

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

name=helloo&age=20

the response will be :

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"
}

And if we send a request with an invalid model :

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

name=hello&age=i20

the response will be

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"
}
Bodywork answered 18/9, 2018 at 5:0 Comment(2)
Using this approach, I'm having difficulty getting OnActionExecutionAsync to fire when ModelState.IsValid == false. Clearly it worked when you made this answer though, so I'm wondering if something has changed in asp.net core 2.2?Vries
@Vries I just try the above code with ASP.NET Core 2.2, it works fine for me. Did your register the middleware after the UseMvc(). If that's the case, it won't take effect. And make sure the ModelStateFeatureFilter has been added to the MVC service.Bodywork
C
4

I also faced issues in .net core 2.2 and seems IAsyncActionFilter was not working in my case, but worked with IActionResult. Below is my modified code, but not sure if this is what intended.

public class ModelStateFeatureFilter : IActionResult
{
    public Task ExecuteResultAsync(ActionContext context)
    {
        var state = context.ModelState;
        context.HttpContext.Features.Set(new ModelStateFeature(state));
        return Task.CompletedTask;
    }
} 

and startup class like below

services.Configure<ApiBehaviorOptions>(options =>
{
    options.InvalidModelStateResponseFactory = ctx => new ModelStateFeatureFilter();
});
Compose answered 13/1, 2020 at 1:31 Comment(0)
S
1

If you are implementing something like action filter, you can access it via context parameter of overriden method OnActionExecuting of 'ActionFilterAttribute' base class

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);
     }
}
Scenic answered 17/9, 2018 at 15:18 Comment(5)
Yes I know, but I need it in MiddlewareBrock
What kind of Middleware do you mean? And could you please describe the purpose of accessing ModelState there? github.com/aspnet/Mvc/issues/3454Scenic
Exactly, I need some Model binding ignorationBrock
I did not get it. Why? What you'd like to ignore? In what use case?Scenic
Suppose a property can be singular or multiple depending on an AllowMultiple property, maybe in that case this can be usedGretel
Q
0

During migration of project from .NET Core 2.2 to 7 I wanted to switch from Filters to Middlewares, and I found it easier to extend my existing validation exception to wrap model state and to create a middleware to catch it.

using Microsoft.AspNetCore.Mvc.ModelBinding;

public class CustomValidationException : Exception
{
    public ModelStateDictionary ModelState { get; }

    public CustomValidationException(ModelStateDictionary modelState)
    {
        ModelState = modelState;
    }
}

Middleware

public class ValidationExceptionMiddleware : IMiddleware
{
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        try
        {
            await next(context);
        }
        catch (CustomValidationException exception)
        {
            // exception.ModelState
        }
    }
}
Quadragesimal answered 13/7, 2023 at 19:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.