Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to remove prefixes from ModelState keys?

Tags:

For example, there is a Web Api action method:

public HttpMessageResponse Post(UserDto userDto) {     if (!this.ModelState.IsValid)     {         return this.Request.CreateErrorResponse(             HttpStatusCode.BadRequest, this.ModelState);     }      // ... } 

A client sends the following request:

HTTP POST: /api/user { "username": "me", "password": "Pa$sw0rd" } 

And gets the response:

HTTP 201/Created: { "message": "Your request is invalid.",   "modelState": { "userDto.Password": "Your password is too strong." } } 

By default the action method exposes implementation details by prefixing model errors with an argument name used inside the action method. What if client apps will hardcode this prefix name when cleaning up model errors, then server-side code changes (for example you replaced Post(UserDto userDto) signature with Post(UserDto dto)) and all client apps stop working.

That's why you need to make sure this prefix is removed on the server-side. The question is, how to do it properly, without complicating things. For example you can create a custom serializer and remove these prefixes during serialization. But in order to do so, you need to know the name of the model argument and the calling code may look something like this:

public HttpMessageResponse Post(UserDto userDto) {     if (!this.ModelState.IsValid)     {         return this.Request.CreateCustomErrorResponse(             HttpStatusCode.BadRequest, this.ModelState, modelName: "userDto");     }      // ... } 
like image 534
Grief Coder Avatar asked Jun 14 '13 10:06

Grief Coder


People also ask

What does ModelState remove do?

Remove(KeyValuePair<String,ModelState>)Removes the first occurrence of the specified object from the model-state dictionary.

What is the point of checking ModelState?

The ModelState has two purposes: to store the value submitted to the server, and to store the validation errors associated with those values.

Why is ModelState not valid MVC?

You get Model state not valid error when the server side validation of the model property has failed. So go to your action where you will find the following if condition that checks model state is valid: if (ModelState. IsValid)


1 Answers

For the first part:

Also error messages returned to a client shouldn't contain those prefixes

I agree having the parameter name as a prefix on all model state errors isn't great behavior. Fortunately, the service that has this behavior is replaceable. You just need to have a custom IBodyModelValidator. Here's what it would look like (using the Decorator pattern to let the default service do most of the work):

public class PrefixlessBodyModelValidator : IBodyModelValidator {     private readonly IBodyModelValidator _innerValidator;      public PrefixlessBodyModelValidator(IBodyModelValidator innerValidator)     {         if (innerValidator == null)         {             throw new ArgumentNullException("innerValidator");         }          _innerValidator = innerValidator;     }      public bool Validate(object model, Type type, ModelMetadataProvider metadataProvider, HttpActionContext actionContext, string keyPrefix)     {         // Remove the keyPrefix but otherwise let innerValidator do what it normally does.         return _innerValidator.Validate(model, type, metadataProvider, actionContext, String.Empty);     } } 

Then, wrap the default service with yours:

config.Services.Replace(typeof(IBodyModelValidator), new PrefixlessBodyModelValidator(config.Services.GetBodyModelValidator())); 

For the second part:

elso replace "modelState" with "errors"

The reason it currently says "modelState" is your current code:

return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState); 

Is effectively doing the following:

HttpError error = new HttpError(ModelState, false); return Request.CreateResponse(HttpStatusCode.BadRequest, error); 

Since HttpError is being serialized, and it has a property named "ModelState", that's what you see in the response.

If you want a different property name, you can use a custom error class:

public class PrettyHttpError {     public PrettyHttpError(ModelStateDictionary modelState)     {         Message = "Your request is invalid.";         Errors = new Dictionary<string, IEnumerable<string>>();          foreach (var item in modelState)         {             var itemErrors = new List<string>();             foreach (var childItem in item.Value.Errors)             {                 itemErrors.Add(childItem.ErrorMessage);             }             Errors.Add(item.Key, itemErrors);         }     }      public string Message { get; set; }      public IDictionary<string, IEnumerable<string>> Errors { get; set; } } 

And then create your response with this error type instead of HttpError:

PrettyHttpError error = new PrettyHttpError(ModelState); return Request.CreateResponse(HttpStatusCode.BadRequest, error); 

The combination of PrettyHttpError and PrefixlessBodyModelValidator gives the output you requested.

like image 121
dmatson Avatar answered Oct 23 '22 22:10

dmatson