For example, a user creates a new question on a forum.
I send ajax to the server, then I use HtmlEncode to exclude the HTML code before saving it in the database.
Is it possible that HtmlEncode would be used automatically when receiving a request?
Also, when using the attribute (for example [HtmlAllowed]) you can allow html code in request.
Thanks
You can achieve it using custom model binder, every string property or string parameter will go through this method when ASP.NET attempts to bind request to parameters of action method
public class StringBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
ValueProviderResult value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, value);
var str = (string)value?.ConvertTo(typeof(string));
str = HttpUtility.HtmlEncode(str);
return str;
}
}
And in Application_Start()
ModelBinders.Binders.Add(typeof(string), new StringBinder());
Thanks to Yegor Androsov, for pointing me to right direction.
This ModelBinder automaticaly encode all string properties, except that has [SafeHtml] attribute
public class SafeStringModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
HttpRequestBase request = controllerContext.HttpContext.Request;
string name = bindingContext.ModelName;
string value = request.Unvalidated[name];
Type holderType = bindingContext.ModelMetadata.ContainerType;
if (holderType != null)
{
PropertyInfo propertyType = holderType.GetProperty(bindingContext.ModelMetadata.PropertyName);
if (propertyType == null) return value;
object[] attributes = propertyType.GetCustomAttributes(true);
bool hasAttribute = attributes.Cast<Attribute>().Any(a => a.GetType() == typeof (SafeHtmlAttribute));
if (!hasAttribute && !string.IsNullOrEmpty(value))
{
value = HttpUtility.HtmlEncode(value);
}
}
return value;
}
}
[AttributeUsage(AttributeTargets.Property)]
public class SafeHtmlAttribute : Attribute { }
Related
Actually on my .NET Core project i'm using a simple modelbinder to trim input string
public class StringModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
ValueProviderResult valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (valueProviderResult != ValueProviderResult.None)
{
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
string valueAsString = string.IsNullOrWhiteSpace(valueAsString) ? null : valueAsString.Trim();
bindingContext.Result = ModelBindingResult.Success(valueAsString);
}
return Task.CompletedTask;
}
}
I use it through the IModelBinderProvider and it works perfectly on input of type string. However I notice that it's not fired when on input I have a collection of string like this
public IActionResult CollectionTrimTest([FromBody] List<string> values)
To perform it I think I should use something like
if (context.Metadata.IsCollectionType)
return new BinderTypeModelBinder(typeof(CollectionModelBinder));
but I really don't know how to implement CollectionModelBinder to trim result based on string collection. Centralizing the trim logic between StringModelBinder and CollectionModelBinder would be greatly appreciated of course.
Solved, seems [FromBody] must require a JsonConverter. More information at https://stackoverflow.com/a/54663595/4963176
Note sure but i am looking stupid for this.I have created a simple model binder as shown below.
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
HttpRequestBase request = controllerContext.HttpContext.Request;
Customer obj = (Customer)base
.BindModel(controllerContext, bindingContext);
obj.CustomerName = request.Form["Text1"];
return obj;
}
I have a required field validator on the Customer model
public class Customer
{
private string _CustomerName;
[Required]
public string CustomerName
{
get { return _CustomerName; }
set { _CustomerName = value; }
}
}
in Global.asax i have tied up the model with the binder
ModelBinders.Binders.Add(typeof(Customer), new MyBinder());
But when i check the ModelState.IsValid its always false. What am i missing here ?
By directly accessing the property, you're bypassing the data annotations binding invoked by the default model binder (which happens as part of BindModel method).
You'll either need to let the base handle this behavior by having the request item have the same name as your CustomerName property, or invoke it yourself: http://odetocode.com/blogs/scott/archive/2011/06/29/manual-validation-with-data-annotations.aspx
Here is a snippet from the above linked site (adapted for your code):
var cust = new Customer();
var context = new ValidationContext(cust, serviceProvider: null, items: request);
var results = new List<ValidationResult>();
var isValid = Validator.TryValidateObject(cust, context, results);
if (!isValid)
{
foreach (var validationResult in results)
{
Console.WriteLine(validationResult.ErrorMessage);
}
}
We know that MVC returns DateTime for JsonResult in this format: /Date(1240718400000)/, and we know how to parse it in JS.
However, It seems that MVC doesn't accept DateTime parameter being sent in this way. For example, I have the following Action.
[HttpGet]
public ViewResult Detail(BookDetail details) { //... }
The BookDetail class contains a DateTime field named CreateDate, and I passed a JSON object from JS in this format:
{"CreateDate": "/Date(1319144453250)/"}
CreateDate is recognized as null.
If I passed the JSON in this way, it works as expected:
{"CreateDate": "2011-10-10"}
The problem is that I cannot change client side code in an easy way, have to stick to /Date(1319144453250)/ this format. I have to make changes in server side.
How to solve this problem? Is that anything related to ModelBinder?
Thanks so much in advance!
The problem, as you suspected, is a model binding issue.
To work around it, create a custom type, and let's call it JsonDateTime. Because DateTime is a struct, you cannot inherit from it, so create the following class:
public class JsonDateTime
{
public JsonDateTime(DateTime dateTime)
{
_dateTime = dateTime;
}
private DateTime _dateTime;
public DateTime Value
{
get { return _dateTime; }
set { _dateTime = value; }
}
}
Change CreateDate to this type. Next, we need a custom model binder, like so:
public class JsonDateTimeModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName).ToString();
return new DateTime(Int64.Parse(
value.Substring(6).Replace(")/",String.Empty))); // "borrowed" from skolima's answer
}
}
Then, in Global.asax.cs, in Application_Start, register your custom ModelBinder:
ModelBinders.Binders.Add(typeof(JsonDateTime), new JsonDateTimeModelBinder());
In your model, use this to parse the date:
// property
String CreateDate;
DateTime CreateDateAsDate;
// drop prefix, drop suffix, parse as long and read as ticks
CreateDateAsDate date = new DateTime(Int64.Parse(
CreateDate.Substring(6).Replace(")/",String.Empty)));
I think using custom Model Binder will do the trick. The below model binder class will work on both cases. It will parse all dot net recognizable date string as well as JSON formatted date string. No need to change any existing code.
public class DateTimeModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var model = base.BindModel(controllerContext, bindingContext);
if (model == null && (bindingContext.ModelType == typeof(DateTime) || bindingContext.ModelType == typeof(DateTime?)))
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (value == null || String.IsNullOrEmpty(value.AttemptedValue))
model = (bindingContext.ModelType == typeof(DateTime?)) ? null : (object)DateTime.MinValue;
else if (Regex.IsMatch(value.AttemptedValue, #"\/Date\(\d+\)\/"))
model = new DateTime(1970, 1, 1).AddMilliseconds(Int64.Parse(value.AttemptedValue.Substring(6).Replace(")/", String.Empty))).ToLocalTime();
//else //Any other format
}
return model;
}
}
Configure Model Binder in Application_Start of Global.asax
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
//Your Existing Code....
ModelBinders.Binders.Add(typeof(DateTime), new DateTimeModelBinder());
ModelBinders.Binders.Add(typeof(DateTime?), new DateTimeModelBinder());
}
}
Vote If it helps
I have a few questions regarding custom model binding, model state, and data annotations.
1) Is it redundant to do validation in the custom model binder if I have data annotations on my model, because that's what I thought the point of data annotations were.
2) Why is my controller treating the model state as valid even when it's not, mainly I make the Name property null or too short.
3) Is it ok to think of custom model binders as constructor methods, because that's what they remind me of.
First here is my model.
public class Projects
{
[Key]
[Required]
public Guid ProjectGuid { get; set; }
[Required]
public string AccountName { get; set; }
[Required(ErrorMessage = "Project name required")]
[StringLength(128, ErrorMessage = "Project name cannot exceed 128 characters")]
[MinLength(3, ErrorMessage = "Project name must be at least 3 characters")]
public string Name { get; set; }
[Required]
public long TotalTime { get; set; }
}
Then I'm using a custom model binder to bind some properties of the model. Please don't mind that it's quick and dirty just trying to get it functioning and then refactoring it.
public class ProjectModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
if (bindingContext == null)
{
throw new ArgumentNullException("bindingContext");
}
var p = new Project();
p.ProjectGuid = System.Guid.NewGuid();
p.AccountName = controllerContext.HttpContext.User.Identity.Name;
p.Name = controllerContext.HttpContext.Request.Form.Get("Name");
p.TotalTime = 0;
//
// Is this redundant because of the data annotations?!?!
//
if (p.AccountName == null)
bindingContext.ModelState.AddModelError("Name", "Name is required");
if (p.AccountName.Length < 3)
bindingContext.ModelState.AddModelError("Name", "Minimum length is 3 characters");
if (p.AccountName.Length > 128)
bindingContext.ModelState.AddModelError("Name", "Maximum length is 128 characters");
return p;
}
}
Now my controller action.
[HttpPost]
public ActionResult CreateProject([ModelBinder(typeof(ProjectModelBinder))]Project project)
{
//
// For some reason the model state comes back as valid even when I force an error
//
if (!ModelState.IsValid)
return Content(Boolean.FalseString);
//_projectRepository.CreateProject(project);
return Content(Boolean.TrueString);
}
EDIT
I Found some code on another stackoverflow question but I'm not sure at which point I would inject the following values into this possible solution.
What I want to inject when a new object is created:
var p = new Project();
p.ProjectGuid = System.Guid.NewGuid();
p.AccountName = controllerContext.HttpContext.User.Identity.Name;
p.Name = controllerContext.HttpContext.Request.Form.Get("Name");
p.TotalTime = 0;
How do I get the above code into what's below (Possible solution):
public class ProjectModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType == typeof(Project))
{
ModelBindingContext newBindingContext = new ModelBindingContext()
{
ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(
() => new Project(), // construct a Project object,
typeof(Project) // using the Project metadata
),
ModelState = bindingContext.ModelState,
ValueProvider = bindingContext.ValueProvider
};
// call the default model binder this new binding context
return base.BindModel(controllerContext, newBindingContext);
}
else
{
return base.BindModel(controllerContext, bindingContext);
}
}
}
}
You will find things work much easier if you inherit from the DefaultModelBinder, override the BindModel method, call the base.BindModel method and then make the manual changes (setting the guid, account name and total time).
1) It is redundant to validate as you have done it. You could write code to reflect the validation metadata much like the default does, or just remove the data annotations validation since you are not using it in your model binder.
2) I don't know, it seems correct, you should step through the code and make sure your custom binder is populating all of the applicable rules.
3) It's a factory for sure, but not so much a constructor.
EDIT: you couldn't be any closer to the solution, just set the properties you need in the model factory function
public class ProjectModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType == typeof(Project))
{
ModelBindingContext newBindingContext = new ModelBindingContext()
{
ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(
() => new Project() // construct a Project object
{
ProjectGuid = System.Guid.NewGuid(),
AccountName = controllerContext.HttpContext.User.Identity.Name,
// don't set name, thats the default binder's job
TotalTime = 0,
},
typeof(Project) // using the Project metadata
),
ModelState = bindingContext.ModelState,
ValueProvider = bindingContext.ValueProvider
};
// call the default model binder this new binding context
return base.BindModel(controllerContext, newBindingContext);
}
else
{
return base.BindModel(controllerContext, bindingContext);
}
}
}
Or you could alternately override the CreateModel method:
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, System.Type modelType)
{
if (modelType == typeof(Project))
{
Project model = new Project()
{
ProjectGuid = System.Guid.NewGuid(),
AccountName = controllerContext.HttpContext.User.Identity.Name,
// don't set name, thats the default binder's job
TotalTime = 0,
};
return model;
}
throw new NotSupportedException("You can only use the ProjectModelBinder on parameters of type Project.");
}
I had a nice function that took my FormCollection (provided from the controller). Now I want to do a model bind instead and have my model binder call that function and it needs the FormCollection. For some reason I can find it. I thought it would have been
controllerContext.HttpContext.Request.Form
Try this:
var formCollection = new FormCollection(controllerContext.HttpContext.Request.Form)
FormCollection is a type we added to ASP.NET MVC that has its own ModelBinder. You can look at the code for FormCollectionBinderAttribute to see what I mean.
Accessing the form collection directly appears to be frowned on. The following is an example from an MVC4 project where I have a custom Razor EditorTemplate that captures the date and time in separate form fields. The custom binder retrieves the values of the individual fields and combines them into a DateTime.
public class DateTimeModelBinder : DefaultModelBinder
{
private static readonly string DATE = "Date";
private static readonly string TIME = "Time";
private static readonly string DATE_TIME_FORMAT = "dd/MM/yyyy HH:mm";
public DateTimeModelBinder() { }
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (bindingContext == null) throw new ArgumentNullException("bindingContext");
var provider = new FormValueProvider(controllerContext);
var keys = provider.GetKeysFromPrefix(bindingContext.ModelName);
if (keys.Count == 2 && keys.ContainsKey(DATE) && keys.ContainsKey(TIME))
{
var date = provider.GetValue(string.Format("{0}.{1}", bindingContext.ModelName, DATE)).AttemptedValue;
var time = provider.GetValue(string.Format("{0}.{1}", bindingContext.ModelName, TIME)).AttemptedValue;
if (!string.IsNullOrWhiteSpace(date) && !string.IsNullOrWhiteSpace(time))
{
DateTime dt;
if (DateTime.TryParseExact(string.Format(System.Globalization.CultureInfo.CurrentCulture, "{0} {1}", date, time),
DATE_TIME_FORMAT,
System.Globalization.CultureInfo.CurrentCulture,
System.Globalization.DateTimeStyles.AssumeLocal,
out dt))
return dt;
}
}
return base.BindModel(controllerContext, bindingContext);
}
}
Use bindingContext.ValueProvider (and bindingContext.ValueProvider.TryGetValue, etc.) to get values directly.