Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Access ModelState in Asp.net core Middleware

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"
}
like image 262
Imran Sh Avatar asked Sep 17 '18 15:09

Imran Sh


3 Answers

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"
}
like image 129
itminus Avatar answered Nov 20 '22 10:11

itminus


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();
});
like image 33
RX100 Avatar answered Nov 20 '22 10:11

RX100


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);
     }
}
like image 1
Dmitry Pavlov Avatar answered Nov 20 '22 11:11

Dmitry Pavlov