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
Related
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());
});
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¶m2=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
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;
}
}
I have created strongly typed ID classes in my project as there is currently confusion with interchangeable string ID's which is causing bugs which are easy to miss.
I have changed all the string id parameters in my action methods to the new strongly typed to realise that the MVC model binder can not now bind the strings to the new type (despite implicit string conversion operators existing for this type).
e.g.
public ActionResult Index(JobId jobId)
{
//...
}
I have read around about creating custom model binders, but all the tutorials are about binding a POCO class when we know the names of the query parameters / form values.
I just want to be able to tell the framework 'if parameter is strongly typed id type, instantiate using this constructor', so it will always work no matter what the name of the parameter is.
Can anyone point me in the right direction?
EDIT
This is the base class that the strongly typed ID's inherit from:
public class StronglyTypedId
{
private readonly string _id;
public StronglyTypedId(string id)
{
_id = id;
}
public static bool operator ==(StronglyTypedId a, StronglyTypedId b)
{
if (ReferenceEquals(a, b))
{
return true;
}
if (((object)a != null) && ((object)b == null) || ((object)a == null))
{
return false;
}
return a._id == b._id;
}
public static bool operator !=(StronglyTypedId a, StronglyTypedId b)
{
return !(a == b);
}
public override string ToString()
{
return _id;
}
public override bool Equals(object obj)
{
if (!(obj is StronglyTypedId))
{
return false;
}
return ((StronglyTypedId)obj)._id == _id;
}
public Guid ToGuid()
{
return Guid.Parse(_id);
}
public bool HasValue()
{
return !string.IsNullOrEmpty(_id);
}
}
I figured out a way to do this just now using a custom model binder. This way will work no matter what the name of the parameter that needs to be bound:
public class JobIdModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType == typeof(JobId))
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
string id = value.AttemptedValue;
return new JobId(id);
}
else
{
return base.BindModel(controllerContext, bindingContext);
}
}
}
So it's pretty much the same as if you are implementing a custom binder for a specific model type, except this is the bit of magic var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName) which means it will work regardless of naming of parameters.
Then you simply register the binder in your Global.cs like so:
ModelBinders.Binders.Add(typeof(JobId), new JobIdModelBinder());
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());