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
Related
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 { }
I am working in MVC4 and want to define a model using an Uppercase attribute. The idea would be that the presence of the Uppercase attribute would cause the model value to be converted to uppercase when it arrived at the server.
At the moment I have the following code within the model:
[Required]
[Display(Name="Account Code")]
[StringValidation(RegExValidation.AccountCode, Uppercase=true)]
public string Account
{
get { return _account; }
set
{
if (value != null)
_account = value.ToUpper();
}
}
But what I would really like is this:
[Required]
[Display(Name="Account Code")]
[StringValidation(RegExValidation.AccountCode)]
[Uppercase]
public string Account { get; set; }
I think that I may need to create the Uppercase attribute as a ValidationAttribute to ensure it gets fired when the model hits the server. But that seems a bit wrong, as I'm not really validating the data. Is there a better way?
Also, is there any way to ensure the invocation order on the attributes? I really want to convert the data to uppercase before the custom StringValidation attribute fires, as this checks the case of the text in the regex pattern.
To add a bit of background to this, I want to reduce the need to add code to uppercase the data. The nirvana would be a single attribute, which updates the data on the way into the server, either in the model binding or validation stage. This attribute can then be referenced in the StringValidation attribute to amend the RegEx value used in its checks. I can also then lookup this attribute in a custom TextBoxFor helper method, such that I can add text-transform: uppercase so it looks correct on the client side.
Does anyone have any ideas out there?
I have managed to get this working, to a point, so here's my solution for others to appraise.
Once point to note was that the full solution couldn't be achieved because I couldn't get the Modelmetadata inside the StringValidation.IsValid() attribute. The particular issue I had here was that I could get the Metadata, however I could not get the PropertyName from it, only the DisplayName. There were multiple options out there, but the fact that some of my properties have the same DisplayName means that I couldn't be sure that the ProprtyName was the one I was actually validating.
Here's the code for the ValidationAttribute:
public class StringValidationAttribute : ValidationAttribute, IClientValidatable, IMetadataAware {
private bool _uppercase;
public StringValidationAttribute(bool uppercase = false) {
_uppercase = uppercase;
}
...
public void OnMetadataCreated(ModelMetadata metadata)
{
metadata.AdditionalValues["Uppercase"] = _uppercase;
}
}
I then created a new IModelBinder implementation:
public class StringBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
ValueProviderResult result = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (result == null)
return null;
if (bindingContext.ModelMetadata.AdditionalValues.ContainsKey("Uppercase")) {
if ((bool)bindingContext.ModelMetadata.AdditionalValues["Uppercase"]])
return result.AttemptedValue.ToUpper();
}
return result.AttemptedValue;
}
}
And registered that in myGlobal.asax file:
ModelBinders.Binders.Add(typeof(string), new StringBinder());
The code so far will cause any string input coming into MVC to be converted to Uppercase if it has StringValidationAttribute attached to it on the model, and where the uppercase indicator has been set.
Next, to achieve my desire of making the html forms be uppercase too, I implemented a new EditorTemplate named string.cshtml. In this view I added:
RouteValueDictionary htmlAttributes = new RouteValueDictionary();
if ((bool)ViewData.ModelMetadata.AdditionalValues["Uppercase"]) {
htmlAttributes.Add("class", "Uppercase");
}
#Html.TextBox("", Model, htmlAttributes)
With the CSS as;
.Uppercase {
text-transform: uppercase;
}
Hope this post helps some others out there.
For Web API purpose it is better to convert the incoming json to uppercase or lowercase.
public class ToUpperCase : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(string);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
return reader.Value.ToString().ToUpper();
}
}
[Display(Name = "PNR NAME")]
[JsonConverter(typeof(Annotations.ToUpperCase))]
public string PNR { get; set; }
OR Globally;
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
//.......... others
JsonMediaTypeFormatter jsonFormatter = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
JsonSerializerSettings jSettings = new Newtonsoft.Json.JsonSerializerSettings();
jSettings.Converters.Add(new UpperCaseStringConverter());
jsonFormatter.SerializerSettings = jSettings;
}
You're right, ValidationAttribute is not the right fit. It seems like doing this at the Model Binding stage would be a better idea. See this article for a detailed explanation of how to customize this behavior.
Based on the information provided there, I believe you should be able to create an attribute based on CustomModelBinderAttribute like this:
[AttributeUsage(AttributeTargets.Property)]
public class UppercaseAttribute : CustomModelBinderAttribute
{
public override IModelBinder GetBinder()
{
return new UppercaseModelBinder();
}
private class UppercaseModelBinder : DefaultModelBinder
{
public override object BindModel(
ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
var value = base.BindModel(controllerContext, bindingContext);
var strValue = value as string;
if (strValue == null)
return value;
return strValue.ToUpperInvariant();
}
}
}
I have not tested this. Let me know if it works or not.
NOTE:
I'm adding on to this post because until I discovered the approach I now use, I read this and tried all above unsuccessfully.
I generally use a two part process when dealing with forcing text data to be formatted as uppercase. 1. at the view and 2. at the controller
At the view layer so that the user knows data is going to be used in the uppercase form. This can be down through htmlAttributes used in the EditorFor HTML helper.
#HTML.EditorFor(model => model.Access_Code, new { htmlAttributes = new Style= "text-transform:uppercase"}})
Now this only forces the data seen and entered by the user to uppercase and not the data sent to the server. To do that requires some code in the associated method in the controller.
I add the ToUpper() method to the target attribute of the object being passed back to the contoller. Here is hypothetical example showing this.
public ActionResult verify(int? id)
{
var userData = db.user.Where (i=> i.userID == id).Single();
userData.Access_Code = userData.Access_Code.ToUpper();
...
}
I know how to create a model class that mirrors query string variables so that when it comes into my Web API controller action, the model is populated.
However, is there a way to make it so that I'm not locked into the query string variable names as the properties on my model class?
Example:
public class MyModel {
public string o {get;set;}
}
public class MyController {
public string Get(MyModel model) {
}
}
Then, if my query string looks like:
GET http://domain.com/?o=12345
Is there a way to name that model property "Order" or something instead of "o" and then have it populated with the value from "o="?
You can create custom model binder that will bind data to model as you wish. To use it you should:
public string Get([ModelBinder(typeof(MyComplexTypeModelBinder))]MyModel model)
{
...
}
To create custom model binder you can inherit from IModelBinder or from DefaultModelBinder.
public class MyComplexTypeModelBinder : IModelBinder
{
public Object BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException("bindingContext");
// Create the model instance (using the ctor you like best)
var obj = new MyComplexType();
// Set properties reading values from registered value providers
obj.Order = FromPostedData<string>(bindingContext, "o");
...
return obj;
}
private T FromPostedData<T>(ModelBindingContext context, String key)
{
// Get the value from any of the input collections
ValueProviderResult result;
context.ValueProvider.TryGetValue(key, out result);
// Set the state of the model property resulting from
context.ModelState.SetModelValue(key, result);
// Return the value converted (if possible) to the target type
return (T) result.ConvertTo(typeof(T));
}
Solution for this scenario is custom IValueProvider. This ASP.NET MVC extension point is the correct place, where we can bridge the QueryString keys into Model.Property names. In comparison with ModelBinder, this will target exactly what we need (while not introducing later issues, when even other value providers (FORM) accidently contains that key...)
There is good tutorial how to introduce the custom IValueProvider:
http://donovanbrown.com/post/How-to-create-a-custom-Value-Provider-for-MVC.aspx
And there is an simple example which is able to provide values for Model "Order" property, coming as QueryString "o" key:
Factory
// Factory
public class MyValueProviderFactory : ValueProviderFactory
{
public override IValueProvider GetValueProvider(ControllerContext ctx)
{
return new MyValueProvider(ctx);
}
}
Provider
// Provider
class MyValueProvider : IValueProvider
{
protected HttpRequestBase Request { get; set; }
public MyValueProvider(ControllerContext ctx)
{
Request = ctx.HttpContext.Request;
}
// our custom logic to test QueryString keys, and expected prefixes
public bool ContainsPrefix(string prefix)
{
var containsSpecial =
"Order".Equals(prefix, StringComparison.OrdinalIgnoreCase)
&& Request.QueryString.AllKeys.Contains("o"
, StringComparer.InvariantCultureIgnoreCase);
return containsSpecial;
}
// Handling "Order" key
public ValueProviderResult GetValue(string key)
{
if (!ContainsPrefix(key))
{
return null;
}
var values = Request.QueryString.GetValues("o");
if (values.Any())
{
return new ValueProviderResult(values, values.First()
, CultureInfo.CurrentCulture);
}
return null;
}
}
And in the global.asax we have to inject it:
protected void Application_Start()
{
ValueProviderFactories.Factories.Add(new MyValueProviderFactory());
...
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've found a lot of information on implementing a custom model binder for validation purposes but I haven't seen much about what I'm attempting to do.
I want to be able to manipulate the values that the model binder is going to set based on attributes on the property in the view model. For instance:
public class FooViewModel : ViewModel
{
[AddBar]
public string Name { get; set; }
}
AddBar is just
public class AddBarAttribute : System.Attribute
{
}
I've not been able to find a clean way to find the attributes on a view model property in the custom model binder's BindModel method. This works but it feels like there should be a simpler solution:
public class FooBarModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var value = base.BindModel(controllerContext, bindingContext);
var hasBarAttribute = false;
if(bindingContext.ModelMetadata.ContainerType != null)
{
var property = bindingContext.ModelMetadata.ContainerType.GetProperties()
.Where(x => x.Name == bindingContext.ModelMetadata.PropertyName).FirstOrDefault();
hasBarAttribute = property != null && property.GetCustomAttributes(true).Where(x => x.GetType() == typeof(AddBarAttribute)).Count() > 0;
}
if(value.GetType() == typeof(String) && hasBarAttribute)
value = ((string)value) + "Bar";
return value;
}
}
Is there a cleaner way to view the attributes on the view model property or a different kind of attribute I could be using? The DataAnnotation attributes really seem to be for a different problem.
UPDATE
Craig's answer got me to the right place but I thought I'd put some examples in here for others.
The metadata provider I ended up with looks like
public class FooBarModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
{
var metaData = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
if(attributes.OfType<AddBarAttribute>().Any())
metaData.AdditionalValues.Add("AddBarKey", true);
return metaData;
}
}
The model binder looks like:
public class FooBarModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var value = base.BindModel(controllerContext, bindingContext);
if(bindingContext.ModelMetadata.AdditionalValues.ContainsKey("AddBarKey"))
value = ((string)value) + "Bar";
return value;
}
}
The "correct" way (per the guy who wrote it) is to write a model metadata provider. There's an example at the link. Not precisely "simple," but it works, and you'll be doing what the rest of MVC does.