So, is it possible to combine the 3?
When I send a request to the server that does not conform to the model annotated validation (empty email in this case) ModelState.IsValid is true.
Model
public class UpdateModel
{
[Required]
public string Email { get; set; }
public string Name { get; set; }
}
Model Binder
public class MyModelBinder : IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
var data = actionContext.Request.Content.ReadAsStringAsync().Result;
var model = JsonConvert.DeserializeObject<UpdateModel>(data);
bindingContext.Model = model;
return true;
}
}
Action in ApiController
public IHttpActionResult Update(UpdateModel model)
{
if (!ModelState.IsValid)
{
// this never happens :-(
}
}
Related but for MVC not WebApi: Custom model binding, model state, and data annotations
Validation through DataAnnotation was treated in Model Binder. If you use your own ModelBinder, you must explicit call validation over DataAnnotation in the ModelBinder - if you wish automatic validation over DA.
Related
I've been attempting to create a custom model binder and am running into an issue where the object after going through the binder is ALWAYS empty.
I have a good reason for using a custom model binder. For the purposes of this question, assume I HAVE to use a custom model binder. In every case where i've used a custom binder provider with either stock or custom modelbinders I've run into this issue, so I've dumbed this down a LOT to demonstrate, but it happens in this specific example as well, and I really need to know why.
I have a simple controller action and DTO class:
[HttpPost]
[Route("{id}")]
public async Task<ActionResult<QueryServicesDto>> UpdateQueryService(int id,
[FromBody] QueryServicesDtoLight dto)
{
}
public class QueryServicesDtoLight
{
public long QueryServicesId { get; set; }
public DateTime? CreationDate { get; set; }
public long? ModifiedDate { get; set; }
public long? PropagationDate { get; set; }
public int? CabinetListNumber { get; set; }
public string GameCode { get; set; }
public string Status { get; set; }
}
Can anyone tell me why when posting valid JSON to this action with no custom binder providert I get a DTO with the proper values, but if I inject the custom modelBinderProvider below I get a newed up model with no values?
public class QueryServiceModelBinderBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType== typeof(QueryServicesDtoLight))
{
var propertyBinders = new Dictionary<ModelMetadata, IModelBinder>();
for (var i = 0; i < context.Metadata.Properties.Count; i++)
{
ModelMetadata theProp = context.Metadata.Properties[i];
var binder = context.CreateBinder(theProp);
propertyBinders.Add(theProp, binder);
}
var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
return new ComplexTypeModelBinder(propertyBinders, loggerFactory);
}
else
{
return null;
}
}
}
Binder provider is added like this::
services.AddControllersWithViews(o=>o.ModelBinderProviders.Insert(0, new QueryServiceModelBinderBinderProvider())).AddNewtonsoftJson();
Sample JSON Data
{"queryServicesId":14,"creationDate":"2021-03-08T17:06:36.053","modifiedDate":16176433093000000,"propagationDate":0,"cabinetListNumber":996,"gameCode":"PGA2006","status":"AC"}
I'm looking for a way to have more than one action method with the same name in controller without changing Url (route).
[HTTPPost]
Public ActionResult Method1 (Dto1 param)
{
}
[HTTPPost]
Public ActionResult Method2 (Dto2 param)
{
}
[HTTPPost]
Public ActionResult Method3 (Dto3 param)
{
}
This throws error -
Microsoft.AspNetCore.Routing.Matching.AmbiguousMatchException: The request matched multiple endpoints
Dto1, Dto2 and Dto3 derive from a base Dto, each have properties specific to different request methods. I am trying to avoid having a single method with a common Dto which will require multiple validations such as validating mandatory fields based on the value of other fields, etc. If we can have 3 different Post methods with different Dtos, it would makes things much easier
Adding Dtos (Simplified)
public class BaseDto
{
public string CommonProp1 { get; set; }
public string CommonProp2 { get; set; }
}
public class Dto1: BaseDto
{
public enumType Type = enumType.Type1;
public string Property1 { get; set; }
}
public class Dto2 : BaseDto
{
public enumType Type = enumType.Type2;
public string Property2 { get; set; }
}
public class Dto3 : BaseDto
{
public enumType Type = enumType.Type3;
public string Property3 { get; set; }
}
You can use Routes or calling a private method from the three above methods, you shouldn't do this as you want. I think your problem is more deep.
But.... if you still want it, here is a workaround.
Instead of receiving an object, receive a string with json content and parse the object.
But you will have to have a property inside the "json object" or another parameter that defines you wich object it is (Dto1, Dto2 or Dto3). In any case will be the same that use different routes or methods because objects are different.
[HTTPPost]
Public ActionResult Method (string param)
{
//Decode your string called param with JSON with a property inside
}
or
[HTTPPost]
Public ActionResult Method (string param, int type)
{
//Decode your string called param with JSON switching "type" as 1, 2 or 3
}
UPDATE after your update:
I suggest you receive BaseDto and the type in other parameter.
[HTTPPost]
Public ActionResult Method (BaseDto param, int type)
{
}
I have a contact Razor page implemented in ASP.NET Core 2.0. I am using model binding and custom validation.
If I use custom validation on a separate model class, the validation method is called. If I use custom validation on a property on the PageModel, the validation method is not called. However, all properties are successfully bound.
Here's the PageModel class and the separate model class:
public class ContactModel : PageModel
{
[BindProperty]
public ContactMessageModel ContactMessageModel { get; set; }
[BindProperty, CustomValidation]
public string SomeData { get; set; }
public IActionResult OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
return RedirectToPage("MessageSent");
}
}
public class ContactMessageModel
{
[Required]
public string Name { get; set; }
[Required]
public string Email { get; set; }
[Required, CustomValidation]
public string Message { get; set; }
}
A test validation attribute class is as follows:
public class CustomValidationAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
return ValidationResult.Success;
}
}
The validation attribute is called for the ContactMessageModel.Message property, but it isn't called for ContactModel.SomeData property.
Why is this and how do I fix it?
I was running into this too. In my case my issue was because I missed the addition of the [BindProperty] attribute when I moved the properties from a wrapper class model into the PageModel directly. I see that's not the case for you.
In your case I do see that you have the [Required] attribute on all properties except the ContactModel.SomeData. Maybe adding [Required] there would get things acting as you would expect?
I know I can validate a model object by implementing IValidateObject but unfortunately this doesn't give you the nice errors that state the line and the type that has failed json validation when converting the json request to the object when your controller is decorated with the FromBody attribute.
My question is, is it possible to validate an objects properties conditionally in an ApiController and get the nice error format you get for free? As an arbitrary example, say the Account class below needed to validate Roles had more than one item if IsAdmin was true?
public class Account
{
[JsonRequired]
public bool IsAdmin { get; set; }
public IList<string> Roles { get; set; }
}
is it possible to validate an objects properties conditionally in an ApiController and get the nice error format you get for free? As an arbitrary example, say the Account class below needed to validate Roles had more than one item if IsAdmin was true?
Try this:
1.Controller(be sure to add [ApiController] otherwise you need to judge the ModelState):
[Route("api/[controller]")]
[ApiController]
public class ValuesController : Controller
{
[HttpPost]
public IActionResult Post([FromBody]Account account)
{
return Ok(account);
}
}
2.Model:
public class Account: IValidatableObject
{
[JsonRequired]
public bool IsAdmin { get; set; }
public IList<string> Roles { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var count = 0;
if (IsAdmin == true)
{
if (Roles != null)
{
foreach (var role in Roles)
{
count++;
}
}
}
if (count <= 1)
{
yield return new ValidationResult(
$"Roles had more than one item", new[] { "Roles" });
}
}
}
I have a simple model for my asp.net core controller:
[HttpPost]
public async Task<DefaultResponse> AddCourse([FromBody]CourseDto dto)
{
var response = await _courseService.AddCourse(dto);
return response;
}
My model is :
public class CourseDto
{
public int Id { get; set; }
public string Name { get; set; }
public string Genre { get; set; }
public string Duration { get; set; }
public string Level { get; set; }
public string AgeRange { get; set; }
public string Notes { get; set; }
public bool Active { get; set; }
public string OrganisationCode { get; set; }
}
I'm trying to set value of "OrganisationCode" using a custom mode binder or action filter, but had no success.
I would be thnakful if you advise whats the right way to updat ethe model before executing the action.
Thanks.
I will show you here a very simple custom model binder I have just written (and tested in .Net Core 2.0):
My model binder:
public class CustomModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
var value = valueProviderResult.FirstValue; // get the value as string
var model = value.Split(",");
bindingContext.Result = ModelBindingResult.Success(model);
return Task.CompletedTask;
}
}
My model (and notice, only one property has my custom model binder annotation):
public class CreatePostViewModel
{
[Display(Name = nameof(ContentText))]
[MinLength(10, ErrorMessage = ValidationErrors.MinLength)]
public string ContentText { get; set; }
[BindProperty(BinderType = typeof(CustomModelBinder))]
public IEnumerable<string> Categories { get; set; } // <<<<<< THIS IS WHAT YOU ARE INTERESTER IN
#region View Data
public string PageTitle { get; set; }
public string TitlePlaceHolder { get; set; }
#endregion
}
What it does is: it receives some text like "aaa,bbb,ccc", and converts it into array, and return it to the ViewModel.
I hope that helps.
DISCLAIMER: I am not an expert in model binders writing, I have learnt that 15 minutes ago, and I found your question (with no helpful answer), so I tried to help. This is a very basic model binder, some improvements are surely required. I learned how to write it from the official documentation page.
The [FromBody] attribute you are using on the action parameter. means that you direct the default behavior of Model Binding to use the formatters instead. That is why your custom Model Binder does not work.
And [FromBody] is reading the content (request body). So you won't get the request body from your Action Filter, as the request body is a non-rewindable stream, so it suppose to be read only once (I'm assuming that you are trying to read the request body from Action Filter).
My suggestion is to use your custom model binder and remove the FromBody Attribute.