I am currently trying to write a Web API application where one of the parameters I'd like to validate is a query parameter (that is, I wish to pass it in in the form /route?offset=0&limit=100):
[HttpGet]
public async Task<HttpResponseMessage> GetItems(
int offset = 0,
int limit = 100)
{
if (!ModelState.IsValid)
{
// Handle error
}
// Handle request
}
In particular, I want to ensure that "offset" is greater than 0, since a negative number will cause the database to throw an exception.
I went straight for the logical approach of attaching a ValidationAttribute to it:
[HttpGet]
public async Task<HttpResponseMessage> GetItems(
[Range(0, int.MaxValue)] int offset = 0,
int limit = 100)
{
if (!ModelState.IsValid)
{
// Handle error
}
// Handle request
}
This does not cause any errors at all.
After a lot of painful debugging into ASP.NET, it appears to me that this may be simply impossible. In particular, because the offset parameter is a method parameter rather than a field, the ModelMetadata is created using GetMetadataForType rather than GetMetadataForProperty, which means that the PropertyName will be null. In turn, this means that AssociatedValidatorProvider calls GetValidatorsForType, which uses an empty list of attributes even though the parameter had attributes on it.
I don't even see a way to write a custom ModelValidatorProvider in such a way as to get at that information, because the information that this was a function parameter seems to have been lost long ago. One way to do that might be to derive from the ModelMetadata class and use a custom ModelMetadataProvider as well but there's basically no documentation for any of this code so it would be a crapshoot that it actually works correctly, and I'd have to duplicate all of the DataAnnotationsModelValidatorProvider logic.
Can someone prove me wrong? Can someone show me how to get validation to work on a parameter, similar to how the BindAttribute works in MVC? Or is there an alternative way to bind query parameters that will allow the validation to work correctly?
You can create a view request model class with those 2 properties and apply your validation attributes on the properties.
public class Req
{
[Range(1, Int32.MaxValue, ErrorMessage = "Enter number greater than 1 ")]
public int Offset { set; get; }
public int Limit { set; get; }
}
And in your method, use this as the parameter
public HttpResponseMessage Post(Req model)
{
if (!ModelState.IsValid)
{
// to do :return something. May be the validation errors?
var errors = new List<string>();
foreach (var modelStateVal in ModelState.Values.Select(d => d.Errors))
{
errors.AddRange(modelStateVal.Select(error => error.ErrorMessage));
}
return Request.CreateResponse(HttpStatusCode.OK, new { Status = "Error",
Errors = errors });
}
// Model validation passed. Use model.Offset and Model.Limit as needed
return Request.CreateResponse(HttpStatusCode.OK);
}
When a request comes, the default model binder will map the request params(limit and offset, assuming they are part of the request) to an object of Req class and you will be able to call ModelState.IsValid method.
For .Net 5.0 and validating query parameters:
using System.ComponentModel.DataAnnotations;
namespace XXApi.Models
{
public class LoginModel
{
[Required]
public string username { get; set; }
public string password { get; set; }
}
}
namespace XXApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class LoginController : ControllerBase
{
[HttpGet]
public ActionResult login([FromQuery] LoginModel model)
{
//.Net automatically validates model from the URL string
//and gets here after validation succeeded
}
}
}
if (Offset < 1)
ModelState.AddModelError(string.Empty, "Enter number greater than 1");
if (ModelState.IsValid)
{
}
Related
I have a simple controller which allows a route path constrained by a regex pattern in a 'complex object'. When I try to read the single property from the object it is always null.
The ModelState is showing an errors collection saying:
The ActorId field is required
So it appears to be a model binding issue and not a validation issue.
I feel like I'm missing a pattern in the [HttpGet] block or something.
ActorIdParameter.cs
public sealed class ActorIdParameter
{
[Required]
[RegularExpression(#"^.*nm(\d{5}|\d{7})$")]
public string ActorId { get; set; }
}
ActorsController.cs
[HttpGet("{actorIdParameter}")]
public async Task<IActionResult> GetActorByIdAsync([FromRoute]ActorIdParameter actorIdParameter)
{
_ = actorIdParameter ?? throw new ArgumentNullException(nameof(actorIdParameter));
// validation result
if (!ModelState.IsValid) //<---this is always false
return ValidationProcessor.GetAndLogInvalidActorIdParameter(method, logger);
}
Code is called using this example: http://localhost:4120/api/actors/nm0000206
There are several other posts which deal with the [FromQuery] and [FromBody] but I can't find anything that deals with the route path. It seems like the {actorIdParameter} needs something to say "get the ActorId property within that object" or something.
I feel like I need the complex object for the regex matching. Alternatively I could switch from the ActorIdParameter object to a string and possibly decorate it inline on the GetActorByIdAsync method but I'm curious if anyone has any suggestions?
Code below works for me using
https://localhost:5001/api/actors/nm0000206
and validation fails correctly for this one
https://localhost:5001/api/actors/42
You don't need any custom handling for this.
public class ActorIdParameter
{
[Required]
[RegularExpression(#"^.*nm(\d{5}|\d{7})$")]
[FromRoute]
public string ActorId { get; set; }
}
[Route("api/actors")]
public class ActorsController : ControllerBase
{
[HttpGet("{actorId}")]
public async Task<IActionResult> GetActorByIdAsync(ActorIdParameter model)
{
if (!ModelState.IsValid)
{
return new BadRequestResult();
}
return new OkResult();
}
}
Try to do GET request to those three, change actors to your controller route :
http://url/actors/nm11111
http://url/actors?actorIdParameter=nm11111
http://url/actors/?actorIdParameter=nm11111
Is any worked ?
Check you model errors ( to understand why it is false ) :
var errors = ModelState
.Where(x => x.Value.Errors.Count > 0)
.Select(x => new { x.Key, x.Value.Errors })
.ToArray();
ModelState.IsValid == false, why?
I am using https://learn.microsoft.com/en-us/aspnet/core/tutorials/first-web-api?view=aspnetcore-2.2&tabs=visual-studio as a reference and am interested in the way updates should be handled.
Given the following code for an update method:
public class Todo
{
public int Id { get; set; }
public string Title { get; set; }
public bool Completed { get; set; }
}
public class UpdateTodoRequest
{
public string Title { get; set; }
public bool Completed { get; set; }
}
public async Task<ActionResult> UpdateAsync([FromRoute] id, UpdateTodoRequest todoUpdateRequest)
{
if (todo == null)
{
return BadRequest();
}
var result = await _todoService.UpdateAsync(id, todoUpdateRequest);
// Here I can have three possible results
// 1. Todo with that Id was not found - 404
// 2. Todo didn't pass validation rules - 400 (together with list of rules that didn't pass)
// 3. Everything was successful - 204
}
How should (can) I model _todoService.UpdateAsync method signature so that it could handle all of three cases I've listed?
I could refactor that as follows:
public async Task<ActionResult> UpdateAsync([FromRoute] id, UpdateTodoRequest todoUpdateRequest)
{
if (todo == null)
{
return BadRequest();
}
// Getting the whole Todo by id coming from route
var todo = await _todoService.GetById(id);
if (todo == null)
{
// Returning 404 here
return NotFound();
}
// Updating values coming from request to todo taken from service
todo.Title = todoUpdateRequest.Title;
todo.Completed = todo.Completed;
// Result = boolean for success/failure
var result = await _todoService.UpdateAsync(todo);
return result ? NoContent() : BadRequest();
}
In this version I can almost handle cases I wanted:
I can get 404 if there's no todo with that Id
I can return 400 for validation issues (HOWEVER without validation criteria)
I can get 204 if update was successful
So how could I model my service so that controller could handle all of these three cases above? Also I'm open to any kind of suggestions to do things differently or links to SO questions where this has been discussed to death.
I've also thought that it would make sense to use a Either Monad from functional world: https://mikhail.io/2016/01/validation-with-either-data-type-in-csharp/ to indicate success/validation errors and return correct response type based on its' result.
Because there are multiple return types and paths in the action, liberal use of the [ProducesResponseType] attribute is necessary. This attribute produces more descriptive response details for API help pages generated by tools like Swagger. [ProducesResponseType] indicates the known types and HTTP status codes to be returned by the action. Refer to here.
You could use below code to return three kinds of responses' type.
[HttpPut("{id}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> UpdateAsync([FromRoute] id, UpdateTodoRequest todoUpdateRequest)
I'm creating an API that will take parameters (type double) and return the result.
ex.
http://localhost:54897/api/Power/Nominal/6/-2
In the browser, I'll see:
4.0
Here's the code I have so far:
Model
public class PowerModel
{
[Required]
[Range(0,50)]
public double PowerFront { get; set; }
[Required]
[Range(-50,-1)]
public double PowerBack { get; set; }
[Required]
public double Result { get; set; }
}
Controller
[Produces("application/json")]
[Route("api/Power")]
public class PowerController : Controller
{
[HttpGet("Nominal/{powerFront}/{powerBack}")]
public double NominalPower(PowerModel powerModel)
{
if (ModelState.IsValid)
{
powerModel.Result = Power.NominalPower(powerModel.PowerFront, powerModel.PowerBack);
return powerModel.Result;
}
else
{
return 0;
}
}
}
With the above code, I can take in parameters, validate them and return a numeric result.
Something doesn't seem right though. I don't want to return 0 on an invalid model state, I want to return a message in the event of invalid input.
What would be the best way to take in parameters (preferably named), validate those parameters with attributes, perform a calculation, return a value on successful input or show error message on incorrect input?
This is something that HTTP Status Codes are used for. In your successful case, 200 is returned to indicate that the request was successful. For an invalid ModelState, it's common to return 400 (which indicates a bad request was made).
To achieve this in ASP.NET Core, you can take advantage of ActionResult<T>. Here's a complete example of how this would affect NominalPower:
[HttpGet("Nominal/{powerFront}/{powerBack}")]
public ActionResult<double> NominalPower(PowerModel powerModel)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
powerModel.Result = Power.NominalPower(powerModel.PowerFront, powerModel.PowerBack);
return powerModel.Result;
}
In the example above, we pass ModelState into the BadRequest method, which will be serialised as JSON to show a list of errors that occurred when validing the model. If you'd prefer not to include this, you can just omit the ModelState argument when calling BadRequest.
Alternatively, you could simply decorate your PowerController class with the ApiController attribute, which will cause any requests that result in an invalid ModelState to automatically return a 400 with JSON-serialised errors. Here's an example of that approach:
[Produces("application/json")]
[Route("api/Power")]
[ApiController]
public class PowerController : Controller
{
[HttpGet("Nominal/{powerFront}/{powerBack}")]
public double NominalPower(PowerModel powerModel)
{
powerModel.Result = Power.NominalPower(powerModel.PowerFront, powerModel.PowerBack);
return powerModel.Result;
}
}
In this version, there's no need to check ModelState as that's already been checked thanks to the presence of the ApiController attribute. You can even customise the automatic response that gets returned, if needed, as I've detailed in another answer, here.
I have an ASP.NET MVC 4 app that seems to work fine. I write a custom ValidatorAttribute to make sure the value of one property is not smaller than another. Since there are two properties involved, I override IsValid(object, context).
I write unit tests using Validator.TryValidateObject and the Validate(object, context) member of the attribute, and they pass as expected. I include tests for the expected use with values that are valid and values that are invalid. I include tests where the attribute is applied to a property that is the right type, and get expected behavior (My design choice is to pass if either property type is wrong.)
I add the attribute to my model, hooking it in to the app. Something like:
public abstract class DataElement
{
...
[Required]
public string Version { get; set; }
[StringLength(8, ErrorMessage = "8 characters or less")]
[Required(ErrorMessage = "Required")]
[DisplayName("ID")]
public string DataElementNumber { get; set; }
...
}
public abstract class SimpleElement : DataElement
{
[Required]
[DisplayName("Minimum")]
public int MinimumLength { get; set; }
[Required]
[DisplayName("Maximum")]
[NotSmallerThan("MinimumLength")]
public int MaximumLength { get; set; }
}
public class CodeList: SimpleElement
{
public Collection<CodeValue> Values { get; set; }
}
I have a controller something like
[HttpGet]
public ActionResult Edit(string elementId, string version)
{
CodeList model = Store.GetCodeList(elementId, version);
return View(model);
}
[HttpPost]
public ActionResult Edit(CodeList model)
{
ActionResult result;
if (ModelState.IsValid)
{
Store.Upsert(model);
result = RedirectToAction("Index", "SomeOtherController");
}
else
{
result = View(model.DataElementNumber, model.Version);
}
return result;
}
Simple, I think. If the model is valid, commit to the data store. If it's not valid, re-display the form, with a validation message. In cases where I enter valid values in the form, the validator behaves as expected, that is, the application commits values to the data store and move on.
In the case where I enter a value for Minimum that is smaller than Maximum, the case I am guarding against, instead of seeing my view, again, I see an error screen, something like this for the case where DataElementNumber="XML-25" and Version="201301"
The view 'XML-25' or its master was not found or no view engine supports the searched locations. The following locations were searched:
~/Views/CodeListBuilder/XML-25.aspx
~/Views/CodeListBuilder/XML-25.ascx
~/Views/Shared/XML-25.aspx
~/Views/Shared/XML-25.ascx
~/Views/CodeListBuilder/201301.master
~/Views/Shared/201301.master
~/Views/CodeListBuilder/XML-25.cshtml
~/Views/CodeListBuilder/XML-25.vbhtml
~/Views/Shared/XML-25.cshtml
~/Views/Shared/XML-25.vbhtml
~/Views/CodeListBuilder/201301.cshtml
~/Views/CodeListBuilder/201301.vbhtml
~/Views/Shared/201301.cshtml
~/Views/Shared/201301.vbhtml
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.
Exception Details: System.InvalidOperationException:...
I can comment out the custom NotSmallerThanAttribute and the system behaves as I expect, apart form being able to enter number fo maximum that are smaller than minimum. I am not sure how to diagnose this. What kind of behavior in a validator can confuse the routing engine? How do I find it? TIA
Your problem has nothing to do with your validator.
With the result = View(model.DataElementNumber, model.Version); you are using the following overload of the View method:
protected internal ViewResult View(
string viewName,
string masterName
)
So the framework thinks that your model.DataElementNumber is your viewName and your model.Version your masterName that is why you get this strange view missing exception.
To fix this you just need to use the correct overload with passing in your model
result = View(model);
and MVC will take care of re-displaying your previously posted DataElementNumber and Version values.
I have a small problem with the WebApi.
Problem:
If I want to post a model using JSON, I can add as many members I want, as long as the members defined in model are present.
Question:
How can I trigger an exception, if an undefined member is present in my Json object. Is this achievable without a custom JsonConverter?
What I'm looking for is a generic solution, not a convertion for every different model.
Example:
Model:
public class Person
{
[Required]
public string Name { get; set; }
}
Api Controller:
public class PersonController : ApiController
{
public HttpResponseMessage Post(Person person)
{
if (person != null)
{
if (ModelState.IsValid)
{
//do some stuff
return new HttpResponseMessage(HttpStatusCode.OK);
}
}
return new HttpResponseMessage(HttpStatusCode.BadRequest);
}
}
Json posts (body)
{"Name":"Joe"} --> valid
{"Name":"Joe","InvalidMember","test","Name","John"} --> also valid. In this case I want to trigger an Exception. Because if you look at it, it doesn't match my modeldefinition exactly.
One thing you could try is playing around with this setting:
config.Formatters.JsonFormatter.SerializerSettings.MissingMemberHandling = MissingMemberHandling.Error;
It should give you an invalid model state when there are extra properties that aren't recognized in the JSON.