Receive an array of json objects and validate in the controller - asp.net-mvc

I'm sending a JSON like this:
[
{col1: 'value', col2: 'value'},
{col1: 'value2', col2: 'value2'},
...
]
The action in my controller has a List parameter that is requiring a custom model binder, like this:
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var contentType = controllerContext.HttpContext.Request.ContentType;
String bodyText;
Stream stream = null;
try
{
stream = controllerContext.HttpContext.Request.InputStream;
stream.Seek(0, SeekOrigin.Begin);
using (var reader = new StreamReader(stream))
{
stream = null;
bodyText = reader.ReadToEnd();
}
}
finally
{
if (stream != null)
stream.Dispose();
}
if (string.IsNullOrEmpty(bodyText))
{
return null;
}
var model = new JavaScriptSerializer().Deserialize<T>(bodyText);
return model;
// return base.BindModel(controllerContext, bindingContext);
}
It's working, except that it is not considering the data annotations for validations (required, range, etc).
How can I get this working with validation?
UPDATE
Controller Action
[HttpPost]
public ActionResult ActionName([ModelBinder(typeof(JsonArrayValidationModelBinder<List<EntityName>>))]List<EntityName> viewModel)
Entity
public class EntityName
{
[Display(Name = "Data Entrada")]
[DataType(DataType.Date)]
[Required]
public DateTime? DataEntrada { get; set; }
// ....
}

I have revised my answer. There were a few problems that I ran into when trying to get this working. Detailed below is the problem and the solution I used.
The json: The json you provided did not match the Model you provided. So I assumed the json string should have included something like this:
`DataEntrada: "1/1/2014"`
The model: Your model describes only EntityName. The deserialized json is a list. These are two different things. So I modified the json to be an object that defines EntityNames (list of EntityName), like this:
`data = { EntityNames: [{ DataEntrada: "1/1/2014" }] };`
and then I implemented this class..this will be the result of deserialization:
public class EntityInfo
{
public EntityName[] EntityNames { get; set; }
}
and finally, modified the ActionMethod like so:
public JsonResult SaveActionName([ModelBinder(typeof(JsonArrayValidationModelBinder<EntityInfo>))]EntityInfo viewModel)
Validation: Validating EntityNames was not as easy to implement as I thought it would be. I could not get the validation attribute for EntityName to fire during model binding (being a member of a list). So, I implemented a custom validator derived from 'ValidationAttribute' like this:
public class EntityNamesValidation : ValidationAttribute
{
public override bool IsValid(object value)
{
EntityName[] list = (EntityName[])value;
foreach (EntityName e in list)
{
if (string.IsNullOrEmpty(e.DataEntrada.ToString()))
return false;
// more checks performed here
}
return true;
}
}
and then I applied EntityNamesValidation attribute to EntityNames and EntityInfo, like so:
[EntityNamesValidation]
public EntityName[] EntityNames { get; set; }
Incorrect model during bind: The JsonArrayValidationModelBinder was using a bindingContext that did not have an instance of anything. If you debug BindModel before base.BindModel you will see that bindingContext.Model is null. So what I did was set bindingContext.ModelMetadata.Model = model after deserialization and before the call to base.BindModel. I also moved base.BindModel in the code to fire just before model is returned...see below
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
[...]
bindingContext.ModelMetadata.Model = model;
base.BindModel(controllerContext, bindingContext);
return model;
}
Verification: I did not unit test this, but I did place a breakpoint in the ActionMethod. I then used the following json:
data = { EntityNames: [{ DataEntrada: "1/1/2014" }, { DataEntrada: null }] };
when the code reached the breakpoint, ModelState.IsValid is false. I then changed json to this:
data = { EntityNames: [{ DataEntrada: "1/1/2014" }, { DataEntrada: "2/19/2014" }] };
when the code reached the breakpoint, ModelState.IsValid is true.
This approach works, but is not ideal. I think you want validation to occur without creating custom code and use MVC to handle this.
I hope this gets you a step further.
ALL THE CODE
javascript
data = { EntityNames: [{ DataEntrada: "1/1/2014" }, { DataEntrada: null }] };
var jsonOfLog = JSON.stringify(data);
$.ajax({
type: 'POST',
dataType: 'text',
url: "/EntityData/SaveActionName",
data: jsonOfLog,
success: function (data) {
alert(data);
},
error: function (result) {
alert(result);
}
,
async: false
});
models
public class EntityInfo
{
[EntityNamesValidation]
public EntityName[] EntityNames { get; set; }
}
public class EntityName
{
[Display(Name = "Data Entrada")]
[DataType(DataType.Date)]
[Required]
public DateTime? DataEntrada { get; set; }
}
custom validator
public class EntityNamesValidation : ValidationAttribute
{
public override bool IsValid(object value)
{
EntityName[] list = (EntityName[])value;
foreach (EntityName e in list)
{
if (string.IsNullOrEmpty(e.DataEntrada.ToString()))
return false;
// more checks performed here
}
return true;
}
}
BindModel
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var contentType = controllerContext.HttpContext.Request.ContentType;
String bodyText;
Stream stream = null;
try
{
stream = controllerContext.HttpContext.Request.InputStream;
stream.Seek(0, SeekOrigin.Begin);
using (var reader = new StreamReader(stream))
{
stream = null;
bodyText = reader.ReadToEnd();
}
}
finally
{
if (stream != null)
stream.Dispose();
}
if (string.IsNullOrEmpty(bodyText))
{
return null;
}
var model = new JavaScriptSerializer().Deserialize<T>(bodyText);
bindingContext.ModelMetadata.Model = model;
base.BindModel(controllerContext, bindingContext);
return model;
}
ActionMethod
[HttpPost]
public JsonResult SaveActionName([ModelBinder(typeof(JsonArrayValidationModelBinder<EntityInfo>))]EntityInfo viewModel)

Deriving from DefaultModelBinder will give you what you are looking for. In your override, call base method, like so
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
// base BindModel should validate your model
base.BindModel(controllerContext, bindingContext);
// (optional) Capture validation result
bool ModelIsValid = bindingContext.ModelState.IsValid;
var contentType = controllerContext.HttpContext.Request.ContentType;
[...]
}

Related

CustomModelBinder on properties of a Dto class

I have a DtoClass which has properties of a specific class, I don't want to have a CustomModelBinder for the DtoClass but for the class of its properties; I am using asp.net core 3.1.
My ModelBinder Class is:
public class SessionIdModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
Guard.Against.Null(bindingContext, nameof(bindingContext));
var modelName = bindingContext.ModelName;
var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
if (valueProviderResult == ValueProviderResult.None)
return Task.CompletedTask;
var sessionId = SessionId.Parse(valueProviderResult.FirstValue);
if (sessionId.IsFailure)
{
bindingContext.ModelState.AddModelError(modelName, sessionId.Errors.First().Message);
bindingContext.Result = ModelBindingResult.Failed();
return Task.CompletedTask;
}
bindingContext.Result = ModelBindingResult.Success(sessionId.Data);
return Task.CompletedTask;
}
}
The Dto class is like:
public class MergeSessionsDto
{
[ModelBinder(BinderType = typeof(SessionIdModelBinder), Name = nameof(OldSession))]
public SessionId OldSession { get; set; }
[ModelBinder(BinderType = typeof(SessionIdModelBinder), Name = nameof(NewSession))]
// [BindProperty(BinderType = typeof(SessionIdModelBinder), Name = nameof(NewSession))]
public SessionId NewSession { get; set; }
}
The action in my controller is:
public async Task<IActionResult> MergeSessions([FromBody] MergeSessionsDto dto)
{
var result = DoTheMerge(dto.OldSession, dto.NewSession);
return result;
}
in the startup class I also registered the ModelBinderProvider :
services.AddControllers(options=> options.ModelBinderProviders.Insert(0, new MyCustomModelBinderProvider()))
which is like:
public sealed class MyCustomModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
Guard.Against.Null(context, nameof(context));
if (context.Metadata.ModelType == typeof(SessionId))
return new BinderTypeModelBinder(typeof(SessionIdModelBinder));
return null;
}
}
No matter which approach I am using, either [ModelBinder], [BindProperty] attributes, or global registration, SessionModelBinder is not called, and I am getting this error:
Exception: Invalid error serialization: 'The dto field is required.'

What is the best way to prohibit integer value for Enum action's parameter

I use some Enum as parameter in some action. For example we have the following code
public enum SomeEnum { SomeVal1 = 1, SomeVal2 = 2 }
[HttpGet]
public void SomeAction(SomeEnum someParameter) { }
By default asp.net engine allows to use both string and integer values that's why we are able to call it like this 'http://host/SomeController/SomeAction/SomeVal1' or this 'http://host/SomeController/SomeAction/1' or even this 'http://host/SomeController/SomeAction/54'! I would like to stay with the first sample using string value. For this I've implemented the following model binder:
public class RequireStringsAttribute : ModelBinderAttribute
{
public RequireStringsAttribute() : base(typeof(ModelBinder))
{
}
private class ModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.FieldName).FirstValue;
var isValid = Enum.GetNames(bindingContext.ModelType).Any(name =>
name.Equals(value, StringComparison.OrdinalIgnoreCase));
if (isValid)
{
bindingContext.Result = ModelBindingResult.Success(Enum.Parse(bindingContext.ModelType, value, ignoreCase: true));
}
else
{
bindingContext.ActionContext.ModelState.AddModelError(bindingContext.FieldName, $"The value '{value}' is not valid.");
}
return Task.CompletedTask;
}
}
}
And I've applied it:
[HttpGet]
public void SomeAction([RequireStrings]SomeEnum someParameter) { }
It works fine but I just want to know is there a better way to do it?

Get custom attribute for parameter when model binding

I've seen a lot of similar posts on this, but haven't found the answer specific to controller parameters.
I've written a custom attribute called AliasAttribute that allows me to define aliases for parameters during model binding. So for example if I have: public JsonResult EmailCheck(string email) on the server and I want the email parameter to be bound to fields named PrimaryEmail or SomeCrazyEmail I can "map" this using the aliasattribute like this: public JsonResult EmailCheck([Alias(Suffix = "Email")]string email).
The problem: In my custom model binder I can't get a hold of the AliasAttribute class applied to the email parameter. It always returns null.
I've seen what the DefaultModelBinder class is doing to get the BindAttribute in reflector and its the same but doesn't work for me.
Question: How do I get this attribute during binding?
AliasModelBinder:
public class AliasModelBinder : DefaultModelBinder
{
public static ICustomTypeDescriptor GetTypeDescriptor(Type type)
{
return new AssociatedMetadataTypeTypeDescriptionProvider(type).GetTypeDescriptor(type);
}
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var value = base.BindModel(controllerContext, bindingContext);
var descriptor = GetTypeDescriptor(bindingContext.ModelType);
/*************************/
// this next statement returns null!
/*************************/
AliasAttribute attr = (AliasAttribute)descriptor.GetAttributes()[typeof(AliasAttribute)];
if (attr == null)
return null;
HttpRequestBase request = controllerContext.HttpContext.Request;
foreach (var key in request.Form.AllKeys)
{
if (string.IsNullOrEmpty(attr.Prefix) == false)
{
if (key.StartsWith(attr.Prefix, StringComparison.InvariantCultureIgnoreCase))
{
if (string.IsNullOrEmpty(attr.Suffix) == false)
{
if (key.EndsWith(attr.Suffix, StringComparison.InvariantCultureIgnoreCase))
{
return request.Form.Get(key);
}
}
return request.Form.Get(key);
}
}
else if (string.IsNullOrEmpty(attr.Suffix) == false)
{
if (key.EndsWith(attr.Suffix, StringComparison.InvariantCultureIgnoreCase))
{
return request.Form.Get(key);
}
}
if (attr.HasIncludes)
{
foreach (var include in attr.InlcludeSplit)
{
if (key.Equals(include, StringComparison.InvariantCultureIgnoreCase))
{
return request.Form.Get(include);
}
}
}
}
return null;
}
}
AliasAttribute:
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class AliasAttribute : Attribute
{
private string _include;
private string[] _inlcludeSplit = new string[0];
public string Prefix { get; set; }
public string Suffix { get; set; }
public string Include
{
get
{
return _include;
}
set
{
_include = value;
_inlcludeSplit = SplitString(_include);
}
}
public string[] InlcludeSplit
{
get
{
return _inlcludeSplit;
}
}
public bool HasIncludes { get { return InlcludeSplit.Length > 0; } }
internal static string[] SplitString(string original)
{
if (string.IsNullOrEmpty(original))
{
return new string[0];
}
return (from piece in original.Split(new char[] { ',' })
let trimmed = piece.Trim()
where !string.IsNullOrEmpty(trimmed)
select trimmed).ToArray<string>();
}
}
Usage:
public JsonResult EmailCheck([ModelBinder(typeof(AliasModelBinder)), Alias(Suffix = "Email")]string email)
{
// email will be assigned to any field suffixed with "Email". e.g. PrimaryEmail, SecondaryEmail and so on
}
Gave up on this and then stumbled across the Action Parameter Alias code base that will probably allow me to do this. It's not as flexible as what I started out to write but probably can be modified to allow wild cards.
what I did was make my attribute subclass System.Web.Mvc.CustomModelBinderAttribute which then allows you to return a version of your custom model binder modified with the aliases.
example:
public class AliasAttribute : System.Web.Mvc.CustomModelBinderAttribute
{
public AliasAttribute()
{
}
public AliasAttribute( string alias )
{
Alias = alias;
}
public string Alias { get; set; }
public override IModelBinder GetBinder()
{
var binder = new AliasModelBinder();
if ( !string.IsNullOrEmpty( Alias ) )
binder.Alias = Alias;
return binder;
}
}
which then allows this usage:
public ActionResult Edit( [Alias( "somethingElse" )] string email )
{
// ...
}

What's the best way to bind ExtJs 4 Grid filter info to asp.net mvc action parameters?

What's the best way to bind ExtJs 4 Grid filter info to asp.net mvc action parameters?
I wrote these helper classes:
public class ExtFilterInfo
{
public string Field { get; set; }
public ExtFilterData Data { get; set; }
}
public class ExtFilterData
{
public string Type { get; set; }
public string Value { get; set; }
}
Here is the Action:
public ActionResult Grid(int start, int limit, string sort, ExtFilterInfo[] filter)
The QueryString looks something like this:
_dc:1306799668564
filter%5B0%5D%5Bfield%5D:Nome
filter%5B0%5D%5Bdata%5D%5Btype%5D:string
filter%5B0%5D%5Bdata%5D%5Bvalue%5D:nu
page:1
start:0
limit:20
A custom model binder looks like it could fit the bill:
public class ExtFilterInfoModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var filter = (ExtFilterInfo)base.BindModel(controllerContext, bindingContext);
var field = bindingContext.ValueProvider.GetValue(bindingContext.ModelName + "[field]");
if (field != null)
{
filter.Field = field.AttemptedValue;
}
var type = bindingContext.ValueProvider.GetValue(bindingContext.ModelName + "[data][type]");
if (type != null)
{
if (filter.Data == null)
{
filter.Data = new ExtFilterData();
}
filter.Data.Type = type.AttemptedValue;
}
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName + "[data][value]");
if (value != null)
{
if (filter.Data == null)
{
filter.Data = new ExtFilterData();
}
filter.Data.Value = value.AttemptedValue;
}
return filter;
}
}
which could be registered in Application_Start:
ModelBinders.Binders.Add(typeof(ExtFilterInfo), new ExtFilterInfoModelBinder());
and now the filter collection which your controller action takes as argument should be bound correctly.

asp.net MVC 1.0 and 2.0 currency model binding

I would like to create model binding functionality so a user can enter ',' '.' etc for currency values which bind to a double value of my ViewModel.
I was able to do this in MVC 1.0 by creating a custom model binder, however since upgrading to MVC 2.0 this functionality no longer works.
Does anyone have any ideas or better solutions for performing this functionality? A better solution would be to use some data annotation or custom attribute.
public class MyViewModel
{
public double MyCurrencyValue { get; set; }
}
A preferred solution would be something like this...
public class MyViewModel
{
[CurrencyAttribute]
public double MyCurrencyValue { get; set; }
}
Below is my solution for model binding in MVC 1.0.
public class MyCustomModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
object result = null;
ValueProviderResult valueResult;
bindingContext.ValueProvider.TryGetValue(bindingContext.ModelName, out valueResult);
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueResult);
if (bindingContext.ModelType == typeof(double))
{
string modelName = bindingContext.ModelName;
string attemptedValue = bindingContext.ValueProvider[modelName].AttemptedValue;
string wantedSeperator = NumberFormatInfo.CurrentInfo.NumberDecimalSeparator;
string alternateSeperator = (wantedSeperator == "," ? "." : ",");
try
{
result = double.Parse(attemptedValue, NumberStyles.Any);
}
catch (FormatException e)
{
bindingContext.ModelState.AddModelError(modelName, e);
}
}
else
{
result = base.BindModel(controllerContext, bindingContext);
}
return result;
}
}
You might try something among the lines:
// Just a marker attribute
public class CurrencyAttribute : Attribute
{
}
public class MyViewModel
{
[Currency]
public double MyCurrencyValue { get; set; }
}
public class CurrencyBinder : DefaultModelBinder
{
protected override object GetPropertyValue(
ControllerContext controllerContext,
ModelBindingContext bindingContext,
PropertyDescriptor propertyDescriptor,
IModelBinder propertyBinder)
{
var currencyAttribute = propertyDescriptor.Attributes[typeof(CurrencyAttribute)];
// Check if the property has the marker attribute
if (currencyAttribute != null)
{
// TODO: improve this to handle prefixes:
var attemptedValue = bindingContext.ValueProvider
.GetValue(propertyDescriptor.Name).AttemptedValue;
return SomeMagicMethodThatParsesTheAttemptedValue(attemtedValue);
}
return base.GetPropertyValue(
controllerContext,
bindingContext, propertyDescriptor,
propertyBinder
);
}
}
public class HomeController: Controller
{
[HttpPost]
public ActionResult Index([ModelBinder(typeof(CurrencyBinder))] MyViewModel model)
{
return View();
}
}
UPDATE:
Here's an improvement of the binder (see TODO section in previous code):
if (!string.IsNullOrEmpty(bindingContext.ModelName))
{
var attemptedValue = bindingContext.ValueProvider
.GetValue(bindingContext.ModelName).AttemptedValue;
return SomeMagicMethodThatParsesTheAttemptedValue(attemtedValue);
}
In order to handle collections you will need to register the binder in Application_Start as you will no longer be able to decorate the list with the ModelBinderAttribute:
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterRoutes(RouteTable.Routes);
ModelBinders.Binders.Add(typeof(MyViewModel), new CurrencyBinder());
}
And then your action could look like this:
[HttpPost]
public ActionResult Index(IList<MyViewModel> model)
{
return View();
}
Summarizing the important part:
bindingContext.ValueProvider.GetValue(bindingContext.ModelName)
A further improvement step of this binder would be to handle validation (AddModelError/SetModelValue)

Resources