ASP.NET Web API decimal validation - c#

We have the following
public class Model
{
[Required]
public decimal Cost { get;set; }
}
When we receive an invalid value, such as "dasd" or whatever else, I return a BadRequest:
public HttpResponseMessage Post(Model data)
{
if (!ModelState.IsValid)
{
return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
}
return Request.CreateResponse(HttpStatusCode.OK);
}
But in the response, the json looks like:
{
"message": "The request is invalid.",
"modelState": {
"data.cost": ["Could not convert string to decimal: dasd. Path 'cost', line 1, position 181."],
}
}
First of all, how can I get rid of the data. prefix?
Also, I would like to implement a custom validator, something like MustBeDecimal attribute that could allow me to return more userfriendly error
message.

The Web API framework binds "dasd" to a decimal and it fails because it is not possible to convert "dasd" to decimal. Obviously, binding has to happen before validation. If you change decimal to string, binding will be okay and your regex validation will run and ModelState will be invalid but in this case also, the prefix that do you do not want to be present will be present.
The Validate method of DefaultBodyModelValidator has this keyPrefix parameter, which is used to create the prefix. Unfortunately, this method is not marked virtual for you to override. So, you will need to do something like this (see the first part).

You can set an errormessage on the RegularExpression data-annotation attribute. Is there any reason you can't use this?
public class Model
{
[Required]
[RegularExpression(#"^\d+.\d{0,2}$",ErrorMessage = "You didn't enter a decimal!")]
public decimal Cost { get;set; }
}

Related

Single Web API request which accepts all type of inputs in C#

I designe a request which will accept all the types of inputs like XML, JSON, etc.
Accordingly the method will respond and will give the corresponding output. Is there any example on that?
I have tried the below code.
When I call it from Postman it is giving a
415 ERROR.
[HttpPost("/GetOutput", Name = nameof(GetOutput))]
[Consumes("application/xml","application/json", "text/plain")]
public IActionResult GetOutput(dynamic request)
{
//process
return new ObjectResult(res.ToString());
}
Allowing a user to submit all the types of input can be a super-dangerous idea. You never know what your users will submit.
If you just want to accept a text-based input, like what your example is trying to do , accepting json/xml/text, you can try this:
1. Define a binding model. No dynamic:
public class MyRequestBindingModel
{
// the type of Content.
public string Type { get; set; }
// the serialized json/xml/text content.
public string Content { get; set; }
}
2. Your controller action:
[HttpPost("/GetOutput")]
public IActionResult GetOutput([FromBody] MyRequestBindingModel request)
{
// process
switch(request.Type.ToLower())
{
case "json": _processJsonInput(request.Content);
case "xml": _processXmlInput(request.Content);
default: _processTextInput(request.Content);
}
// do something else you want
}
The switch statement in the above example is just showing an idea on how to handle the string content base on the Type provided.
If you want to return XML if the Content in the API call is XML, this answer may be helpful:
Helpful Answer.

Proper way to return calculation result from Web Api

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.

Is it possible to use Web API model validation on query parameters?

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)
{
}

ASP.NET WebAPI 2: handle empty string query parameters

We have an api used to get data of products:
public IHttpActionResult GetProducts(ProductFilter productFilter)
{
try
{
.....
}
catch(Exception)
{
throw new OurCustomException();
}
}
and
public class ProductFilter
{
[RegularExpression(#"^[a-zA-Z0-9]{11}$")]
public String Number { get; set; }
.....
}
Here is what i want:
When send GET /api/products?number=Test1234567 it will return information of a product with the number "Test1234567"
When send GET /api/products?number= it will return error because the empty string does not match the regex
When send GET /api/products it will return information of all products
So can you suggest me any way to do this by using just Validation Attribute, since we have a common method to handle ValidationException and we cannot throw ValidationException from method GetProducts. I have tried to use [Required] and [DisplayFormat(ConvertEmptyStringToNull = false)] on Number but none of them worked.
And please also inform me if it is impossible.
EDIT: I think your problem is that you are not passing full model as parameter - your are getting null once binder tries to bind query string. This is not a problem with your regex. To get it working you should make q request like GET /api/products?number=&prop1=some_value&prop2=some_value
Original answer:
I think changing your regex to:
public class ProductFilter
{
[RegularExpression(#"^(|[a-zA-Z0-9]{11})$")]
public String Number { get; set; }
.....
}
should do a trick.
However MSDN documentation states that:
If the value of the property is null or an empty string (""), the
value automatically passes validation for the
RegularExpressionAttribute attribute.
So it should work anyway. Additionally we can check RegularExpression code:
public override bool IsValid(object value)
{
this.SetupRegex();
string input = Convert.ToString(value, (IFormatProvider) CultureInfo.CurrentCulture);
if (string.IsNullOrEmpty(input))
return true;
Match match = this.Regex.Match(input);
if (match.Success && match.Index == 0)
return match.Length == input.Length;
return false;
}
as you can see, it allows null or empty input so your problem is rather different.

Why does an ASP.NET MVC 4 Custom validator cause a routing error?

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.

Categories

Resources