I have created a ModelBinder, which only gets triggered if an object has a [Decimal] attribute assigned, yet for some reason, despite it actually sanitising the data it does not seem to update the posted model.
I wonder if someone could see from my code below, where I maybe going wrong.
Startup.cs
public void ConfigureServices(IServiceCollection serviceCollection)
{
serviceCollection.AddMvc(config => config.ModelBinderProviders.Insert(0, new DecimalModelBinderProvider()));
}
DecimalModelBinderProvider.cs
public class DecimalModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext modelBinderProviderContext)
{
if (modelBinderProviderContext == null)
{
throw new ArgumentNullException(nameof(modelBinderProviderContext));
}
if (!modelBinderProviderContext.Metadata.IsComplexType)
{
try
{
var propertyName = modelBinderProviderContext.Metadata.PropertyName;
var property = modelBinderProviderContext.Metadata.ContainerType.GetProperty(propertyName);
if (property != null)
{
var attribute = property.GetCustomAttributes(typeof(DecimalAttribute), false).FirstOrDefault();
if (attribute != null)
{
return new DecimalModelBinder(modelBinderProviderContext.Metadata.ModelType, attribute as IDecimalAttribute);
}
}
}
catch (Exception exception)
{
var message = exception.Message;
return null;
}
}
return null;
}
}
DecimalModelBinder.cs
public class DecimalModelBinder : IModelBinder
{
private readonly IDecimalAttribute _decimalAttribute;
private readonly SimpleTypeModelBinder _simpleTypeModelBinder;
public DecimalModelBinder(Type type, IDecimalAttribute decimalAttribute)
{
if (type == null)
{
throw new ArgumentNullException(nameof(type));
}
_decimalAttribute = decimalAttribute;
_simpleTypeModelBinder = new SimpleTypeModelBinder(type);
}
public Task BindModelAsync(ModelBindingContext modelBindingContext)
{
if (modelBindingContext == null)
{
throw new ArgumentNullException(nameof(modelBindingContext));
}
var valueProviderResult = modelBindingContext.ValueProvider.GetValue(modelBindingContext.ModelName);
if (valueProviderResult != ValueProviderResult.None)
{
modelBindingContext.ModelState.SetModelValue(modelBindingContext.ModelName, valueProviderResult);
var value = valueProviderResult.FirstValue;
bool success;
var result = _decimalAttribute.Decimal(value, out success);
if (success)
{
modelBindingContext.Result = ModelBindingResult.Success(result);
return Task.CompletedTask;
}
}
return _simpleTypeModelBinder.BindModelAsync(modelBindingContext);
}
}
IDecimalAttribute.cs
public interface IDecimalAttribute
{
object Decimal(string value, out bool success);
}
DecimalAttribute.cs
[AttributeUsage(AttributeTargets.Property)]
public class DecimalAttribute : Attribute, IDecimalAttribute
{
public object Decimal(string value, out bool success)
{
var tryModelValue = string.IsNullOrEmpty(value) ? "0.00" : value.Replace("£", "").Replace("%", "");
decimal #decimal;
success = decimal.TryParse(tryModelValue, out #decimal);
return #decimal;
}
}
Test.cs
public class Test
{
[Display(Name = "Name", Description = "Name")]
public string Name { get; set; }
[Decimal]
[Display(Name = "Amount", Description = "Amount")]
public double Amount { get; set; }
}
HomeController
[ValidateAntiForgeryToken]
[HttpPost]
public IActionResult Index(Test test)
{
if (ModelState.IsValid)
{
}
return View(test);
}
For the purpose of testing I will enter the value £252.83 into the Amount field and submit the form.
If I then place a brakepoint on the line var value = valueProviderResult.FirstValue; I can see that value is £252.83 and if I place a breakpoint on the line modelBindingContext.Result = ModelBindingResult.Success(result); I can see that the result is 252.83M.
However if I step through the code further and place a breakpoint on the line if (ModelState.IsValid) the valid state is false and if I inspect the model test the object Amount is 0.
If anyone can help it would be much appreciated :-)
Try inspect further on the ModelState Error, the Amount property should be invalid and there must be an exception.
I am guessing it should be InvalidCastException. I notice that the Amount property in Test class is Double while you are producing Decimal in your DecimalAttribute.
So the build-in Model Binder that processing the Test class (should be ComplexTypeModelBinder) unable to set the Amount property as it is different type.
Related
I have a controller like below having circular reference in class B.
This is happening because Swashbuckle's jsonserilalizer's setting is set to
ReferenceLoopHandling = ReferenceLoopHandling.Error and I didn't find any way to override this setting.
I am using Swashbuckle 5.6.0 in an ASP.NET MVC application.
public class IssueController : ApiController
{
[HttpGet]
[Route("A")]
public A Get(A input)
{
return new A();
}
}
public class A
{
public virtual B prop1 { get; set; }
}
public class B
{
public virtual B Parent { get; set; }
}
In my case it turned out, that the Stackoverflow exception wasn't caused by the jsonserializer, but by the previous step (when the Swashbuckle schema gets created before json gets serialized). Circle References doesn't seem to work yet with Swashbuckle (in Swashbuckle for it seems to be fixed btw). To fix this, you have to copy the HandleFromUriParamsRecurseSave and add this (with the other filters) to the operations:
private static SwaggerDocument BuildSwaggerDocument()
{
...
var operationFilters = new List<IOperationFilter> {
new HandleFromUriParamsRecurseSave(20),
...
};
}
In the copied HandleFromUriParamsRecurseSave just add a maxrecurselength property, which fits your case and you shouldn't have the StackOverflow error any more:
private void ExtractAndAddQueryParams(
Schema sourceSchema,
string sourceQualifier,
bool? sourceRequired,
SchemaRegistry schemaRegistry,
ICollection<Parameter> operationParams)
{
foreach (var property in sourceSchema.properties)
{
...
var recurseCount = sourceQualifier.Count(t => t == '.');
if (schema.#ref != null && recurseCount < _maxRecurseLength)
{
ExtractAndAddQueryParams(schemaRegistry.Definitions[schema.#ref.Replace("#/definitions/", "")], sourceQualifier + ToCamelCase(property.Key) + ".", flag, schemaRegistry, operationParams);
}
else
{
...
}
}
}
}
A further workaround I tried, but unfortunately didn't solve the issue, was to add a stack and everytime I detected a loop, added the right schema definition just one time (maybe someone sees the problem):
private void ExtractAndAddQueryParams(
Stack<string> sourceSchemaNames,
Schema sourceSchema,
string sourceQualifier,
bool? sourceRequired,
SchemaRegistry schemaRegistry,
ICollection<Parameter> operationParams)
{
if (sourceSchemaNames.Count > _maxRecurseLength) {
return;
}
foreach (var property in sourceSchema.properties)
{
var schema = property.Value;
var readOnly = schema.readOnly;
if (readOnly != true)
{
var flag = sourceRequired == true && sourceSchema.required != null && sourceSchema.required.Contains(property.Key);
var recursionDetected = _disableRecursion && sourceSchemaNames.Contains(schema.#ref);
if (schema.#ref != null && !recursionDetected)
{
sourceSchemaNames.Push(schema.#ref);
ExtractAndAddQueryParams(sourceSchemaNames, schemaRegistry.Definitions[schema.#ref.Replace("#/definitions/", "")],
sourceQualifier + ToCamelCase(property.Key) + ".", flag, schemaRegistry,
operationParams);
sourceSchemaNames.Pop();
}
else
{
...
if (recursionDetected) {
partialSchema.type = "object";
partialSchema.#ref = schema.#ref;
}
operationParams.Add(partialSchema);
}
}
}
}
}
I'm looking for a way to add error code alongside the error message to ModelState.
for example
ModelState.AddModelError("ErrorKey", new { Code = 4001, Message = "Some error message" });
For some bad requests client should do an action and comparing error message is not an ideal solution for making a decision. ModelState.AddModelError method only accepts two parameters, an error key and a message. Is there a way to achieve this or something similar?
No, there is not a way to achieve what you are looking for, in your code when you're trying to do something like this:
return BadRequest(ModelState);
You’ll receive a 400 bad request response back with message you've already added (as you can see, the error code has been presented already here). So, there is neither a usage nor a way of adding the Error Code in your case.
I found a way to add the error code to ValidationProblemDetails:
public class CustomValidationProblemDetails : ValidationProblemDetails
{
public CustomValidationProblemDetails()
{
}
[JsonPropertyName("errors")]
public new IEnumerable<ValidationError> Errors { get; } = new List<ValidationError>();
}
ValidationProblemDetails has an Error property that is IDictionary<string, string[]> and replace this property with our version to add code error.
public class ValidationError
{
public int Code { get; set; }
public string Message { get; set; }
}
Constructor of ValidationProblemDetails accepts ModelStateDictionary and need to convert it to list of ValidationError:
public CustomValidationProblemDetails(IEnumerable<ValidationError> errors)
{
Errors = errors;
}
public CustomValidationProblemDetails(ModelStateDictionary modelState)
{
Errors = ConvertModelStateErrorsToValidationErrors(modelState);
}
private List<ValidationError> ConvertModelStateErrorsToValidationErrors(ModelStateDictionary modelStateDictionary)
{
List<ValidationError> validationErrors = new();
foreach (var keyModelStatePair in modelStateDictionary)
{
var errors = keyModelStatePair.Value.Errors;
switch (errors.Count)
{
case 0:
continue;
case 1:
validationErrors.Add(new ValidationError { Code = 100, Message = errors[0].ErrorMessage });
break;
default:
var errorMessage = string.Join(Environment.NewLine, errors.Select(e => e.ErrorMessage));
validationErrors.Add(new ValidationError { Message = errorMessage });
break;
}
}
return validationErrors;
}
Create custom ProblemDetailsFactory to create CustomValidationProblemDetails when we want to return bad request response:
public class CustomProblemDetailsFactory : ProblemDetailsFactory
{
public override ProblemDetails CreateProblemDetails(HttpContext httpContext, int? statusCode = null, string title = null,
string type = null, string detail = null, string instance = null)
{
var problemDetails = new ProblemDetails
{
Status = statusCode,
Title = title,
Type = type,
Detail = detail,
Instance = instance,
};
return problemDetails;
}
public override ValidationProblemDetails CreateValidationProblemDetails(HttpContext httpContext,
ModelStateDictionary modelStateDictionary, int? statusCode = null, string title = null, string type = null,
string detail = null, string instance = null)
{
statusCode ??= 400;
type ??= "https://tools.ietf.org/html/rfc7231#section-6.5.1";
instance ??= httpContext.Request.Path;
var problemDetails = new CustomValidationProblemDetails(modelStateDictionary)
{
Status = statusCode,
Type = type,
Instance = instance
};
if (title != null)
{
// For validation problem details, don't overwrite the default title with null.
problemDetails.Title = title;
}
var traceId = Activity.Current?.Id ?? httpContext?.TraceIdentifier;
if (traceId != null)
{
problemDetails.Extensions["traceId"] = traceId;
}
return problemDetails;
}
}
And at the end register the factory:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddTransient<ProblemDetailsFactory, CustomProblemDetailsFactory>();
}
Read the Extending ProblemDetails - Add error code to ValidationProblemDetails for more detail.
I have an WebAPI project that takes in ISO date strings inside the input model. I have been parsing these using DateTimeOffset?. I want to banish BCL datetimes from my project so I want to find a way to directly bind these strings to Instant.
public class MyInputModel
{
public DateTimeOffset? OldTime { get; set; }
public Instant NewTime { get; set; }
}
An example JSON input model looks like this:
{
"oldtime":"2016-01-01T12:00:00Z",
"newtime":"2016-01-01T12:00:00Z"
}
And my controller code is:
[HttpPost]
public async Task<IActionResult> PostTimesAsync([FromBody]MyInputModel input)
{
Instant myOldTime = Instant.FromDateTimeUtc(input.oldTime.Value.UtcDateTime);
Instant myNewTime = input.newTime; // This cannot be used with ISO date strings.
}
I attempted to build a custom model binder as follows. This works for models in the query string but not for those in the body of a POST request. How can I bind date input in an ISO 8601 string format to a NodaTime Instant?
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (!String.IsNullOrWhiteSpace(bindingContext.ModelName) &&
bindingContext.ModelType == typeof(Instant?) &&
bindingContext.ValueProvider.GetValue(bindingContext.ModelName) != null)
{
Instant? value;
var val = bindingContext.ValueProvider
.GetValue(bindingContext.ModelName).FirstValue as string;
if (String.IsNullOrWhiteSpace(val))
{
bindingContext.Result = ModelBindingResult.Failed();
return Task.FromResult(0);
}
else if (InstantExtensions.TryParse(val, out value))
{
bindingContext.Result = ModelBindingResult.Success(value);
return Task.FromResult(0);
}
else
{
bindingContext.ModelState.AddModelError(bindingContext.ModelName,
"The date is invalid.");
}
}
bindingContext.Result = ModelBindingResult.Failed();
return Task.FromResult(0);
}
public static bool TryParse(string value, out Instant? result)
{
result = null;
// If this is date-only, add Utc Midnight to the value.
if (value.Length.Equals(10))
{
value += "T00:00:00Z";
}
// Trim milliseconds if present
var decimalPointIndex = value.IndexOf('.');
if (decimalPointIndex > 0)
{
value = value.Substring(0, decimalPointIndex) + "Z";
}
// Attempt to parse
var parseResult = InstantPattern.GeneralPattern.Parse(value);
if (parseResult.Success)
{
result = parseResult.Value;
return true;
}
return false;
}
You should add your model binder like this: ( in WebApiConfig Register method )
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.BindParameter(typeof(Instant), new InstantModelBinder())
...
}
}
WebApiConfig.Register is called in Configuration function in Startup.cs file. In most cases like this:
var config = new HttpConfiguration();
WebApiConfig.Register(config);
If it isn't called, you can just add this line:
config.BindParameter(typeof(Instant), new InstantModelBinder())
where HttpConfiguration object is being created in Startup.cs.
i am using asp.net MVC 3 , in my module there are two types of payment modes 1. Wire transfer and 2. PayPal . Now depending on this type 1 and 2 the properties are to be kept Required or other data annotations ! how to do this ?
for eg :
There is a Radio button for payment type ,
If type 1- i.e Wire Transfer is selected then these fields should be validated - First name , last name , email,beneficiary name , bank name , bank no , ifsc code etc
if it is type 2- i.e PayPal then these fields are required - PayPal email .
This could be done by manual validation but is there some way to do it the right way with DataAnnotations?
Simon Ince's blog post seems to be outdated.
There is no need to use DataAnnotationsModelValidator or do a DataAnnotationsModelValidator registration.
You can use the following code:
[AttributeUsage(AttributeTargets.Property, AllowMultiple=false)]
public class RequiredIfAttribute : ValidationAttribute, IClientValidatable {
private const string _defaultErrorMessage = "'{0}' is required when {1} equals {2}.";
public string DependentProperty { get; set; }
public object TargetValue { get; set; }
public RequiredIfAttribute(string dependentProperty, object targetValue):base(_defaultErrorMessage) {
this.DependentProperty = dependentProperty;
this.TargetValue = targetValue;
}
public override string FormatErrorMessage(string name) {
return String.Format(CultureInfo.CurrentCulture, ErrorMessageString, name, DependentProperty, TargetValue);
}
protected override ValidationResult IsValid(object value, ValidationContext context) {
if (context.ObjectInstance != null) {
Type type = context.ObjectInstance.GetType();
PropertyInfo info = type.GetProperty(DependentProperty);
object dependentValue;
if (info != null) {
dependentValue = info.GetValue(context.ObjectInstance, null);
if (object.Equals(dependentValue, TargetValue)) {
if (string.IsNullOrWhiteSpace(Convert.ToString(value))) {
return new ValidationResult(ErrorMessage);
}
}
}
}
return ValidationResult.Success;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context) {
ModelClientValidationRule rule = new ModelClientValidationRule();
rule.ErrorMessage = this.FormatErrorMessage(metadata.PropertyName);
rule.ValidationType = "requiredif";
rule.ValidationParameters.Add("depedentproperty", DependentProperty);
rule.ValidationParameters.Add("targetvalue", TargetValue);
yield return rule;
}
}
and the javascript side: if you are using jquery:
$.validator.unobtrusive.adapters.add('requiredif', ['depedentproperty', 'targetvalue'], function (options) {
options.rules["required"] = function (element) {
return $('#' + options.params.depedentproperty).val() == options.params.targetvalue
};
if (options.message) {
options.messages["required"] = options.message;
}
$('#' + options.params.depedentproperty).blur(function () {
$('#' + options.element.name).valid();
});
});
I've updated my example to use MVC 3, so that one is more up to date.
http://blogs.msdn.com/b/simonince/archive/2011/02/04/conditional-validation-in-asp-net-mvc-3.aspx
You could write a custom validator attribute and decorate your model with it:
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class CustomValidationAttribute : ValidationAttribute
{
public override bool IsValid(object value)
{
var model = value as MyViewModel;
if (model == null)
{
return false;
}
if (model.WireTransfer == 1)
{
return !string.IsNullOrEmpty(model.FirstName) &&
!string.IsNullOrEmpty(model.LastName);
}
else if (model.WireTransfer == 2)
{
return !string.IsNullOrEmpty(model.PaypalEmail);
}
return false;
}
}
and then in your main Model:
[CustomValidation]
public class MyViewModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
...
}
I have used the approach from Simon Ince's blog post and it works well. Basically he creates a RequiredIf data attribute where you can specify the other property and value that must be true in order to make the current field required.
I have two fields in my form
AccountNumber
ReverseAccountNumber
Can i use data annotations to validate that the value of "ReverseAccountNumber" textbox is equal to the reversed value of "AccountNumber".
i.e.
AccountNumber = 12345
ReverseAccountNumber = 54321
i expect the validation to occur on the lostFocus event of the ReverseAccountNumber textbox.
I think i can do this using IDataErrorInfo, However I believe this would require a POST first before validation occurs, and i consider it a last resort.
Simply add a validation attribute to the class (not the properties) and evaluate the class object to compare the two properties. As for the client side, ASP.NET MVC 3 should be able to generate proper client-side validation for this (although I have not tried it myself since Iam still using xVal).
CustomAttribute
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
public sealed class ReversStringMatchAttribute : ValidationAttribute
{
public string Property { get; set; }
public ReversStringMatchAttribute()
{ }
public override bool IsValid(object value)
{
return true;
}
}
CustomValidator
public class ReversStringValidator : DataAnnotationsModelValidator<ReversStringMatchAttribute>
{
string property;
public ReversStringValidator(ModelMetadata metadata, ControllerContext context, ReversStringMatchAttribute attribute)
: base(metadata, context, attribute)
{
property = attribute.Property;
}
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
{
var rule = new ModelClientValidationRule
{
ErrorMessage = Attribute.ErrorMessage,
ValidationType = "reversStringValidator"
};
rule.ValidationParameters.Add("propertyname", property);
return new[] { rule };
}
}
Java Script
Sys.Mvc.ValidatorRegistry.validators["reversStringValidator"] = function (rule) {
//initialization
//return validator function
return function (value, context) {
var field = $get(rule.ValidationParameters['propertyname']);
if (field == null)
return "Property name is invalid!";
var s1 = field.value;
if (s1) {
if (value) {
var reverse = value.split("").reverse().join("");
if (s1 != reverse.toString()) {
return rule.ErrorMessage;
}
} else {
return rule.ErrorMessage;
}
}
return true;
}
};
then use it on your property
public class AccountViewModel
{
[Required(ErrorMessage="Account Number is Required")]
public string AccountNumber { get; set; }
[ReversStringMatch(ErrorMessage = "The value doesn't match the Account Number", Property="AccountNumber")]
public string ReverseAccountNumber { get; set; }
}
i have some doubts on the $get validation method in javascript but it works, for now.