MVC 4 .net send generic object (not necessarily json) - c#

I have an mvc 4 application.
In one of the post actions I would like to have a parameter of type 'object'.
It should be able to accept from the client an number, a string, and also generic json.
I tried implementing it with the following model binder:
public class ObjectModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (value == null || string.IsNullOrEmpty(value.AttemptedValue))
{
return null;
}
else
{
var js = new JavaScriptSerializer();
var result = js.Deserialize<object>((string)value.ConvertTo(typeof(string)));
return result;
}
}
}
In the client I use jquery ajax to post the data, and also if the value is a javascript object I use JSON.stringify.
When I send a json or an int it works, but if I try sending a string it will throws an exception - "Invalid JSON primitive: THE_STRING_VALUE"
Should I use something else?
Thanks for the help.

The problem is that JSON is a string, so you need to be able to differentiate between strings that are JSON and those that aren't. Try something like this instead:
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (value == null || string.IsNullOrEmpty(value.AttemptedValue))
{
return null;
}
else
{
int n;
string s = (string)value.ConvertTo(typeof(string));
if (s.StartsWith("{") || s.StartsWith("["))
{
var js = new JavaScriptSerializer();
var result = js.Deserialize<object>(s);
return result;
}
else if (int.TryParse(s, out n))
{
return n;
}
return s;
}
}

Related

Convert int values (1,0) to boolean using FromQueryAttribute

I have this API method:
[HttpGet("foo")]
public IActionResult Foo([FromQuery] bool parameter)
{
// ...
}
And I know I can call my method like this and it will work:
.../foo?parameter=true
But I also want to support numeric values 0 and 1 and call my method like this:
.../foo?parameter=1
But when I try, I get this exception inside System.Private.CoreLib
System.FormatException: 'String '1' was not recognized as a valid Boolean.'
Is this even possible?
Because the default ModelBinder didn't support 1 to bool, we can try to create our own binding logic by implementing IModelBinder interface.
public class BoolModelBinder : IModelBinder {
public Task BindModelAsync(ModelBindingContext bindingContext) {
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName).FirstValue;
if (int.TryParse(value, out var intValue)) {
bindingContext.Result = ModelBindingResult.Success(intValue == 1);
} else if (bool.TryParse(value, out var boolValue)) {
bindingContext.Result = ModelBindingResult.Success(boolValue);
} else if (string.IsNullOrWhiteSpace(value)) {
bindingContext.Result = ModelBindingResult.Success(false);
}
return Task.CompletedTask;
}
}
then we can use ModelBinderAttribute to assign our BoolModelBinder type as this binder.
[HttpGet("foo")]
public IActionResult Foo([ModelBinder(typeof(BoolModelBinder))] bool parameter)
{
// ...
}
the easiest way is to change an input parameter type to string
[HttpGet("foo")]
public IActionResult Foo([FromQuery] string parameter)
{
if( parameter=="true" || parameter=="1") ....
else if( parameter=="false" || parameter=="0") ....
else ....
}

Model property bindings with [FromBody] attribute

I want to apply some preprocessing to raw data before it assigned to model properties. Namely to replace comma with dot to allow converting both this strings "324.32" and "324,32" into double. So I wrote this model binder
public class MoneyModelBinder: IModelBinder
{
private readonly Type _modelType;
public MoneyModelBinder(Type modelType)
{
_modelType = modelType;
}
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
string modelName = bindingContext.ModelName;
ValueProviderResult providerResult = bindingContext.ValueProvider.GetValue(modelName);
if (providerResult == ValueProviderResult.None)
{
return TaskCache.CompletedTask;
}
bindingContext.ModelState.SetModelValue(modelName, providerResult);
string value = providerResult.FirstValue;
if (string.IsNullOrEmpty(value))
{
return TaskCache.CompletedTask;
}
value = value.Replace(",", ".");
object result;
if(_modelType == typeof(double))
{
result = Convert.ToDouble(value, CultureInfo.InvariantCulture);
}
else if(_modelType == typeof(decimal))
{
result = Convert.ToDecimal(value, CultureInfo.InvariantCulture);
}
else if(_modelType == typeof(float))
{
result = Convert.ToSingle(value, CultureInfo.InvariantCulture);
}
else
{
throw new NotSupportedException($"binder doesn't implement this type {_modelType}");
}
bindingContext.Result = ModelBindingResult.Success(result);
return TaskCache.CompletedTask;
}
}
then appropriate provider
public class MoneyModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if(context.Metadata?.ModelType == null)
{
return null;
}
if (context.Metadata.ModelType.In(typeof(double), typeof(decimal), typeof(float)))
{
return new MoneyModelBinder(context.Metadata.ModelType);
}
return null;
}
}
and registering it inside Startup.cs
services.AddMvc(options =>
{
options.ModelBinderProviders.Insert(0, new MoneyModelBinderProvider());
});
but I noticed some strange behavior or maybe I missed something. If I use this kind of action
public class Model
{
public string Str { get; set; }
public double Number { get; set; }
}
[HttpPost]
public IActionResult Post(Model model)
{
return Ok("ok");
}
and supply parameters inside query string everything works fine: first provider is called for model itself then for every property of the model. But if I use [FromBody] attribute and supply parameters by JSON, provider is called for model but never called for properties of this model. But why? How can I use binders with FromBody?
I've found solution. As it described here [FromBody] behaves differently in comparing to other value providers - it converts complex objects all at once via JsonFormatters. So in addition model binders we should write separate logic just for FromBody. And of course we can catch some points during json processing:
public class MoneyJsonConverter : JsonConverter
{
public override bool CanWrite => false;
public override bool CanConvert(Type objectType)
{
return objectType == typeof(double);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
string value = (reader.Value ?? "").Replace(" ", "").Replace(",", ".");
TypeConverter converter = TypeDescriptor.GetConverter(modelType);
object result = converter.ConvertFromInvariantString(value);
return result;;
}
}
and using
services.AddMvc(options =>
{
options.ModelBinderProviders.Insert(0, new MoneyModelBinderProvider());
}).AddJsonOptions(options =>
{
options.SerializerSettings.Converters.Add(new MoneyJsonConverter());
});

Custom Data Type for "HttpGet" Route in Asp.Net Web Api Project

I try to add a variable of "custom data type" in my HttpGet Route.
I have this code:
[HttpGet("{idObject}")]
public ResponseSchema Get(ObjectId idObject)
{
if (idObject == null) {
throw new BodyParseException();
}
var user = _usersLogic.GetById(idObject);
if (user == null) {
_response.Success = false;
_response.ErrorCode = "UserDoesNotExist";
}
else {
_response.Objects.Add(user);
}
return _response;
}
ObjectId is a Datatype defined in using MongoDB.Bson.
For the Json Serialization and Deserialization we already have the code to automatically convert on both sides. But can this be similarly done in the Url itself.
We are right now using this Mvc version:
"Microsoft.AspNet.Mvc": "6.0.0-beta8"
So the URL looks like this:
GET Users/55b795827572761a08d735ac
The code to parse it from "string" to "ObjectId" is:
ObjectId.TryParse(idString, out idObject);
The question is where to put that TryParse code. Because I need to tell ASP.NET how it should parse the idObject from String to ObjectId. Since the URL basically is a string.
For Post or Put JSON Payload I already found a solution. I know that this is something different. But Probably it is helpful to understand the scenario, or find a solution to this scenario:
public class EntityBaseDocument
{
[JsonConverter(typeof(ObjectIdConverter))]
public ObjectId Id { get; set; }
}
// Since we have this value converter. We can use ObjectId everywhere
public class ObjectIdConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
serializer.Serialize(writer, value.ToString());
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
JToken token = JToken.Load(reader);
return new ObjectId(token.ToObject<string>());
}
public override bool CanConvert(Type objectType)
{
return typeof(ObjectId).IsAssignableFrom(objectType);
}
}
This will bind it from Uri objects:
public ResponseSchema Get([FromUri]ObjectId idObject)
So: ?param1=something&param2=sometingelse
This will bind it from the body (e.g. a JSon object)
public ResponseSchema Get([FromBody]ObjectId idObject)
Or you can roll your own:
public ResponseSchema Get([ModelBinder(typeof(MyObjectBinder))]ObjectId idObject)
The example on asp.net of a model binder is:
public class GeoPointModelBinder : IModelBinder
{
// List of known locations.
private static ConcurrentDictionary<string, GeoPoint> _locations
= new ConcurrentDictionary<string, GeoPoint>(StringComparer.OrdinalIgnoreCase);
static GeoPointModelBinder()
{
_locations["redmond"] = new GeoPoint() { Latitude = 47.67856, Longitude = -122.131 };
_locations["paris"] = new GeoPoint() { Latitude = 48.856930, Longitude = 2.3412 };
_locations["tokyo"] = new GeoPoint() { Latitude = 35.683208, Longitude = 139.80894 };
}
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType != typeof(GeoPoint))
{
return false;
}
ValueProviderResult val = bindingContext.ValueProvider.GetValue(
bindingContext.ModelName);
if (val == null)
{
return false;
}
string key = val.RawValue as string;
if (key == null)
{
bindingContext.ModelState.AddModelError(
bindingContext.ModelName, "Wrong value type");
return false;
}
GeoPoint result;
if (_locations.TryGetValue(key, out result) || GeoPoint.TryParse(key, out result))
{
bindingContext.Model = result;
return true;
}
bindingContext.ModelState.AddModelError(
bindingContext.ModelName, "Cannot convert value to Location");
return false;
}
}
I believe NikoliaDante's answer works if you have a route such as /api/users?id={{idHere}}. However, if you are looking to have more RESTful routes, the solution below will do the trick for you. I just tested this out in a Web API 2 application and it works well. This will handle the use case where you may have a route such as /api/users/{{userId}}/something/{{somethingId}}.
//Http Parameter Binding Magic
public class ObjectIdParameterBinding : HttpParameterBinding
{
public ObjectIdParameterBinding(HttpParameterDescriptor p) : base(p){ }
public override Task ExecuteBindingAsync(System.Web.Http.Metadata.ModelMetadataProvider metadataProvider, HttpActionContext actionContext, System.Threading.CancellationToken cancellationToken)
{
var value = actionContext.ControllerContext.RouteData.Values[Descriptor.ParameterName].ToString();
SetValue(actionContext, ObjectId.Parse(value));
var tsc = new TaskCompletionSource<object>();
tsc.SetResult(null);
return tsc.Task;
}
}
//Binding Attribute
public class ObjectIdRouteBinderAttribute : ParameterBindingAttribute
{
public override HttpParameterBinding GetBinding(HttpParameterDescriptor parameter)
{
return new ObjectIdParameterBinding(parameter);
}
}
//Controller Example
[Route("api/users/{id}")]
public async Task<User> Get([ObjectIdRouteBinder] ObjectId id)
{
//Yay!
}
ASP.NET Web API provides several approaches for do that. Take a look for
Parameter Binding in Web API documentation.
Summary:
FromUriAttribute - for simple DTO classes
TypeConverter - to help Web API treat your class as simple type
HttpParameterBinding - allow to create behaviour attribute
ValueProvider - for more complex case
IActionValueBinder - to write own parameter-binding process at all

WebAPI JSON Deserialization with property of Object

I have the following problem with the Asp.net Web Api.
I try to use the following object as parameter of my action
[DataContract]
public class MyObject
{
[DataMember]
public object MyIdProperty {get;set;}
}
the property MyIdProperty can contains either a Guid or an Int32
In MVC I did an ModelBinder and it works like a charm so I did one for the WebApi like this
public class HttpObjectIdPropertyModelBinder : IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType != ObjectType
|| (bindingContext.ModelName.TrimHasValue()
&& !bindingContext.ModelName.EndsWith("Id", StringComparison.OrdinalIgnoreCase)))
{
return false;
}
ValueProviderResult result = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (result == null || result.RawValue == null || result.RawValue.GetType() == ObjectType)
{
bindingContext.Model = null;
return true;
}
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, result);
string stringValue = result.RawValue as string;
if (stringValue == null)
{
string[] stringValues = result.RawValue as string[];
if (stringValues != null && stringValues.Length == 1)
{
stringValue = stringValues[0];
}
if (stringValue == null)
{
return false;
}
}
Guid guid;
int integer;
if (Guid.TryParse(stringValue, out guid))
{
bindingContext.Model = guid;
}
else if (int.TryParse(stringValue, out integer))
{
bindingContext.Model = integer;
}
else
{
return false;
}
return true;
}
private static readonly Type ObjectType = typeof(object);
private static HttpParameterBinding EvaluateRule(HttpObjectIdPropertyModelBinder binder, HttpParameterDescriptor parameter)
{
if (parameter.ParameterType == ObjectType
&& parameter.ParameterName.EndsWith("Id", StringComparison.OrdinalIgnoreCase))
{
return parameter.BindWithModelBinding(binder);
}
return null;
}
public static void Register()
{
var binder = new HttpObjectIdPropertyModelBinder();
GlobalConfiguration.Configuration.Services.Insert(typeof(ModelBinderProvider), 0, new SimpleModelBinderProvider(typeof(object), binder));
GlobalConfiguration.Configuration.ParameterBindingRules.Insert(0, param => EvaluateRule(binder, param));
}
}
It's the first time I'm doing a model binder for the WebApi so I'm not even sure if I'm doing it well and if it's a good way to fix this problem.
Anyway with this model binder if I have an action like this
public IEnumerable<MyObject> Get(object id)
{
// code here...
}
The parameter id is deserialized properly using the Json formatter or the Xml formatter and the model binder
But if I use the following action
public void Post(MyObject myObject)
{
// code here...
}
The parameter myObject is deserialized perfectly when I use the Xml formatter but when I use the Json formatter the property MyIdProperty contains a string instead of a Guid or Int32.
And in both case my model binder is NOT used at all. It's like it stop the evaluation of the model at the action paramaters compare to MVC that use the model binders for each property with a complex type.
Note: I would like to not use the true type or use a internal or protected property with the true type because I have this kind of property in a lot of different of object and the code will become really hard to maintain if I have to duplicate them each time

ASP.NET MVC (3) Binding POST value to 'object' type makes the object a System.String[], why?

So, I have a search form in my website which posts Search.Value as a parameter where in the project there is a class called Search with a property Value which is an object. When the value is bound by the model binder, object is always System.String[] regardless if I pass in a single number, a single word or anything else.
Can someone explain to me why that is, or if I can make it stop doing that?
Here's the code:
// Search.cs
public sealed class Search {
private object _Value = null;
public object Value {
get {
return this._Value;
}
set {
if (value != null) {
this._Value = value;
this.IsInt32 = (this._Value is Int32);
this.IsString = (this._Value is String);
};
}
}
public bool IsInt32 { get; set; }
public bool IsString { get; set; }
}
// SearchController.cs
[HttpPost]
public ActionResult List(
[Bind(Prefix = "Search", Include = "Value")] Search Search) {
return this.View();
}
// Form HTML
<form action="/Administration/Search" method="post">
#Html.TextBox("Search.Value", Model.Search.Value, new { type = "search", placeholder = "Search", size = 48, required = string.Empty })
<input type="submit" value="⌕" />
</form>
UPDATE
Per #Darin's suggestion and example I've made a custom binder and it seems to be working so far. Here's the code if anyone else runs into this, modify as needed of course:
public class SearchModelBinder : DefaultModelBinder {
public override object BindModel(
ControllerContext ControllerContext,
ModelBindingContext BindingContext) {
if (BindingContext == null) {
throw new ArgumentNullException("BindingContext");
};
ValueProviderResult ValueResult = BindingContext.ValueProvider.GetValue(BindingContext.ModelName + ".Value");
if (ValueResult == null) {
return (null);
};
string Value = ValueResult.AttemptedValue;
if (String.IsNullOrEmpty(Value)) {
return (null);
};
int Int;
if (int.TryParse(Value, out Int)) {
return new Search {
Value = Convert.ToInt32(Value)
};
};
long Long;
if (long.TryParse(Value, out Long)) {
return new Search {
Value = Convert.ToInt64(Value)
};
};
return new Search {
Value = Value
};
}
}
No idea what you are trying to achieve, why are you using object, ... but you could write a custom model binder for this Search model. This model binder will assign directly the parameter that is sent in the request to the Value property:
public class SearchModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName + ".Value");
if (value != null)
{
return new Search
{
Value = value.AttemptedValue
};
}
return base.BindModel(controllerContext, bindingContext);
}
}
which you could register in Global.asax:
ModelBinders.Binders.Add(typeof(Search), new SearchModelBinder());

Categories

Resources