Reputation: 3253
I am migrating an application from legacy asp.net webapi to asp.net core mvc. I have noticed an issue. For some requests, we send partial or even invalid values in the POST body. And asp.net core is refusing to deserialize it.
E.g. post model
public class PostModel
{
public int Id { get; set; }
public Category? Category { get; set; }
}
public enum Category
{
Public,
Personal
}
action
[HttpPost]
public async Task<Response> Post([FromBody]PostModel model)
=> this.Service.Execute(model);
for the following sample request
POST /endpoint
{
id: 3,
category: "all"
}
The ModelState
collection records an error - indicating that all
is an invalid category, and the PostModel
argument model
is null. Is it possible to disable this behaviour and just attempt to bind all properties that are possible from the post body, and ignoring the ones it can't bind? This is how it was done for us in our legacy api and for now, I need to port this across.
Disabling the model validation did not help for us. The model
argument is still null.
Upvotes: 2
Views: 1941
Reputation: 30056
For FromBody
, it will bind the request body
to Model
by JsonInputFormatter
.
For JsonInputFormatter
, it will call return InputFormatterResult.Success(model)
when there is no error, and call return InputFormatterResult.Failure();
when there is any error. For return InputFormatterResult.Failure();
, it will not bind the valid property.
For a solution, you could implement custom formatter to return return InputFormatterResult.Success(model)
.
CustomFormatter
based on JsonInputFormatter. Replace InputFormatterResult.Failure()
with InputFormatterResult.Success(model)
.
if (!(exception is JsonException || exception is OverflowException))
{
var exceptionDispatchInfo = ExceptionDispatchInfo.Capture(exception);
exceptionDispatchInfo.Throw();
}
return InputFormatterResult.Success(model);
Inject CustomFormatter
in Startup.cs
services.AddMvc(o =>
{
var serviceProvider = services.BuildServiceProvider();
var customJsonInputFormatter = new CustomFormatter(
serviceProvider.GetRequiredService<ILoggerFactory>().CreateLogger<CustomFormatter>(),
serviceProvider.GetRequiredService<IOptions<MvcJsonOptions>>().Value.SerializerSettings,
serviceProvider.GetRequiredService<ArrayPool<char>>(),
serviceProvider.GetRequiredService<ObjectPoolProvider>(),
o,
serviceProvider.GetRequiredService<IOptions<MvcJsonOptions>>().Value
);
o.InputFormatters.Insert(0, customJsonInputFormatter);
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
Upvotes: 1
Reputation: 9205
Actually, your problem is related to Data Binding, not to validation, that's why disabling the model validation did not help. You can implement custom Binder and configure it to manually bind your properties, e.g.:
public class PostModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
string valueFromBody = string.Empty;
using (var sr = new StreamReader(bindingContext.HttpContext.Request.Body))
{
valueFromBody = sr.ReadToEnd();
}
if (string.IsNullOrEmpty(valueFromBody))
{
return Task.CompletedTask;
}
string idString = Convert.ToString(((JValue)JObject.Parse(valueFromBody)["id"]).Value);
string categoryString = Convert.ToString(((JValue)JObject.Parse(valueFromBody)["category"]).Value);
if (string.IsNullOrEmpty(idString) || !int.TryParse(idString, out int id))
{
return Task.CompletedTask;
}
Category? category = null;
if(Enum.TryParse(categoryString, out Category parsedCategory))
{
category = parsedCategory;
}
bindingContext.Result = ModelBindingResult.Success(new PostModel()
{
Id = id,
Category = category
});
return Task.CompletedTask;
}
}
Then you can apply this binder to your class:
[ModelBinder(BinderType = typeof(PostModelBinder))]
public class PostModel
{
public int Id { get; set; }
public Category? Category { get; set; }
}
or to action:
[HttpPost]
public async Task<Response> Post([ModelBinder(BinderType = typeof(PostModelBinder))][FromBody]PostModel model)
=> this.Service.Execute(model);
or create CustomModelBinderProvider:
public class CustomModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType == typeof(PostModel))
return new PostModelBinder();
return null;
}
}
and register it in ConfigureServices methods of Startup class:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddMvc(
config => config.ModelBinderProviders.Insert(0, new CustomModelBinderProvider())
);
...
}
Upvotes: 1
Reputation: 77934
No you can't since the property tied to an enum. if you really want to be what you posted then change the model to be
public class PostModel
{
public int Id { get; set; }
public string Category { get; set; }
}
Then in your endpoint parse the string to enum like
Enum.TryParse("All", out Category cat);
Upvotes: 0