I am trying to create Web API model binder that will bind URL parameters sent by a grid component of a javascript framework.
Grid sends URL parameters indicating standard page, pageSize, and JSON formatted sorters, filters, and groupers.
The URL string looks like this:
http://localhost/api/inventory?page=1&start=0&limit=10sort=[{"property":"partName","direction":"desc"},{"property":"partStatus","direction":"asc"}]&group=[{"property":"count","direction":"asc"}]
The model in question is Inventory which has simple, Count (int) property and a reference, Part (Part) peoperty (which in turn has Name, Status).
The view model/dto is flattened (InventoryViewModel .Count, .PartName, .PartStatus, etc, etc.)
I use Dynamic Expression Api then to query domain model, map the result to view model and send it back as JSON.
During model binding I need to build the expressions by examining model and view model that are being used.
In order to keep model binder reusable, how can I pass/specify model and view model types being used?
I need this in order to build valid sort,filter,and grouping expsessions
Note: I don't want to pass these as part of the grid url params!
One idea I had was to make StoreRequest generic (e.g. StoreRequest) but I am not sure if or how model binder would work.
Sample API Controller
// 1. model binder is used to transform URL params into StoreRequest. Is there a way to "pass" types of model & view model to it?
public HttpResponseMessage Get(StoreRequest storeRequest)
{
int total;
// 2. domain entites are then queried using StoreRequest properties and Dynamic Expression API (e.g. "Order By Part.Name DESC, Part.Status ASC")
var inventoryItems = _inventoryService.GetAll(storeRequest.Page, out total, storeRequest.PageSize, storeRequest.SortExpression);
// 3. model is then mapped to view model/dto
var inventoryDto = _mapper.MapToDto(inventoryItems);
// 4. response is created and view model is wrapped into grid friendly JSON
var response = Request.CreateResponse(HttpStatusCode.OK, inventoryDto.ToGridResult(total));
response.Content.Headers.Expires = DateTimeOffset.UtcNow.AddMinutes(5);
return response;
}
StoreRequestModelBinder
public class StoreRequestModelBinder : IModelBinder
{
private static readonly ILog Logger = LogManager.GetCurrentClassLogger();
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
Logger.Debug(m => m("Testing model binder for type: {0}", bindingContext.ModelType));
if (bindingContext.ModelType != typeof(StoreRequest))
{
return false;
}
var storeRequest = new StoreRequest();
// ----------------------------------------------------------------
int page;
if (TryGetValue(bindingContext, StoreRequest.PageParameter, out page))
{
storeRequest.Page = page;
}
// ----------------------------------------------------------------
int pageSize;
if (TryGetValue(bindingContext, StoreRequest.PageSizeParameter, out pageSize))
{
storeRequest.PageSize = pageSize;
}
// ----------------------------------------------------------------
string sort;
if (TryGetValue(bindingContext, StoreRequest.SortParameter, out sort))
{
try
{
storeRequest.Sorters = JsonConvert.DeserializeObject<List<Sorter>>(sort);
// TODO: build sort expression using model and viewModel types
}
catch(Exception e)
{
Logger.Warn(m=>m("Unable to parse sort parameter: \"{0}\"", sort), e);
}
}
// ----------------------------------------------------------------
bindingContext.Model = storeRequest;
return true;
}
private bool TryGetValue<T>(ModelBindingContext bindingContext, string key, out T result)
{
var valueProviderResult = bindingContext.ValueProvider.GetValue(key);
if (valueProviderResult == null)
{
result = default(T);
return false;
}
result = (T)valueProviderResult.ConvertTo(typeof(T));
return true;
}
}
just change your controller signature like
public HttpResponseMessage Get([ModelBinder(typeof(StoreRequestModelBinder)]StoreRequest storeRequest)
Regards
Related
In my application, I have a few modal windows. Each of them takes the same partial view and the same view model, but displays other data. For that, I generate a dynamic HtmlFieldPrefix as to not have multiple IDs of the same name. Example:
#foreach (var product in Model.Products)
{
string buyModalId = product.BuyModel.BindingPrefix;
#await Html.PartialForAsync("_BuyForm", product.BuyModel, buyModalId)
}
BindingPrefix contains a dynamically added string (for example buy-product-{ID}). In my view I also have a hidden field that is supposed to POST the binding prefix back:
#Html.Hidden(nameof(Model.BindingPrefix), Model.BindingPrefix)
(Source: Asp.Net MVC Dynamic Model Binding Prefix)
That does not work, however, since the binding prefix is null when POSTing, too. Hence await TryUpdateModelAsync(model, model.BindingPrefix); in my controller fails.
The code for the Html.PartialForAsync method is the following:
public static Task<IHtmlContent> PartialForAsync(this IHtmlHelper htmlHelper, string partialViewName, object model, string prefix)
{
var viewData = new ViewDataDictionary(htmlHelper.ViewData);
var htmlPrefix = viewData.TemplateInfo.HtmlFieldPrefix;
viewData.TemplateInfo.HtmlFieldPrefix += !Equals(htmlPrefix, string.Empty) ? $".{prefix}" : prefix;
var part = htmlHelper.PartialAsync(partialViewName, model, viewData);
return part;
}
(Source: MVC 6 VNext how to set HtmlFieldPrefix?)
What am I missing? Why is my model still null? When removing the binding prefix, the binding works flawlessly - but the browser throws warnings regarding multiple same IDs.
Found the answer by using a custom model binder inside my model:
public override void BindModel(ModelBindingContext bindingContext)
{
var providers = bindingContext.ValueProvider as System.Collections.IList;
var formProvider = providers?.OfType<JQueryFormValueProvider>().FirstOrDefault();
if (formProvider != null)
{
var (_, value) = formProvider.GetKeysFromPrefix(string.Empty).First();
bindingContext.BinderModelName = value;
bindingContext.FieldName = value;
bindingContext.ModelName = value;
}
base.BindModel(bindingContext);
}
I've been developing with MVC for a few years now and one think I was originally taught was in a HttpPost action, the first thing you always do is perform a check on ModelState.IsValid, so...
[HttpPost]
public ActionResult Edit(ViewModel form) {
if(ModelState.IsValid) {
// Do post-processing here
}
}
I've come to an issue now where I have a hashed ID passed through the form. If it's 0, its a new record, if its above 0 it means I'm editing. Should my ModelState.IsValid ever return false, I need to setup some dropdown list data again before returning to the Edit view with the same model. To set some of these items that are returned to the form after failure, I need to know the unhashed number, but unhashing it inside the ModelState.IsValid would make it not available in the else statement.
So, is it acceptable to do the following:-
[HttpPost]
public ActionResult Edit(ViewModel form) {
int myID = 0;
if(/** unhashing is successful...**/)
{
if(ModelState.IsValid) {
// Do post-processing here
}
else
{
// Setup dropdowns using unhashed ID and return original view...
}
}
}
Note that ModelState.IsValid is not the first test inside the HttpPost. Is that acceptable? If not, is there a more appropriate way of doing such logic?
Thanks!
In your source you seem to have written some comment about unhashing but such term doesn't exist. The purpose of a hash function is to be irreversible. I think that on the other hand you have meant decrypting the query string value. Ideally this decryption should happen in a custom model binder for your view model, setting the ModelState.IsValid value to false for this parameter. So that inside your controller action all you need to check is this boolean parameter. A controller action should not be decrypting any query string or whatever parameters. This should be done much earlier in the MVC execution pipeline. A custom model binder or even a custom authorization filter would be a much better fit for this scenario.
So let's take an example:
public class ViewModel
{
public int Id { get; set; }
... some other stuff around
}
Now you could write a custom model binder for this view model:
public class MyDecryptingBinder : DefaultModelBinder
{
protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor)
{
if (propertyDescriptor.Name == "Id")
{
var idParam = bindingContext.ValueProvider.GetValue("Id");
if (idParam != null)
{
string rawValue = idParam.RawValue as string;
int value;
if (this.DecryptId(rawValue, out value))
{
propertyDescriptor.SetValue(bindingContext.Model, value);
return;
}
}
}
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
}
private bool DecryptId(string raw, out int id)
{
// TODO: you know what to do here: decrypt the raw value and
// set it to the id parameter, or just return false if decryption fails
}
}
Now you can register this custom model binder with your view model at bootstrapping time (Application_Start):
ModelBinders.Binders.Add(typeof(ViewModel), new MyDecryptingBinder());
and then your controller action will be as simple as:
[HttpPost]
public ActionResult Index(ViewModel model)
{
if (ModelState.IsValid)
{
// The decryption of the id parameter has succeeded, you can use model.Id here
}
else
{
// Decryption failed or the id parameter was not provided: act accordingly
}
}
Need help on this one. I have a WebAPI who can receive multiple ids as parameters. The user can call the API using 2 route:
First route:
api/{controller}/{action}/{ids}
ex: http://localhost/api/{controller}/{action}/id1,id2,[...],idN
Method signature
public HttpResponseMessage MyFunction(
string action,
IList<string> values)
Second route:
"api/{controller}/{values}"
ex: http://localhost/api/{controller}/id1;type1,id2;type2,[...],idN;typeN
public HttpResponseMessage MyFunction(
IList<KeyValuePair<string, string>> ids)
Now I need to pass a new parameter to the 2 existing route. The problem is this parameter is optional and tightly associated with the id value. I made some attempt like a method with KeyValuePair into KeyValuePair parameter but its results in some conflict between routes.
What I need is something like that :
ex: http://localhost/api/{controller}/{action}/id1;param1,id2;param2,[...],idN;paramN
http://localhost/api/{controller}/id1;type1;param1,id2;type2;param2,[...],idN;typeN;paramN
You might be able to deal with it by accepting an array:
public HttpResponseMessage MyFunction(
string action,
string[] values)
Mapping the route as:
api/{controller}/{action}
And using the query string to supply values:
GET http://server/api/Controller?values=1&values=2&values=3
Assumption: You are actually doing some command with the data.
If your payload to the server is getting more complex than a simple route can handle, consider using a POST http verb and send it to the server as JSON instead of mangling the uri to shoehorn it in as a GET.
Different assumption: You are doing a complex fetch and GET is idiomatically correct for a RESTFUL service.
Use a querystring, per the answer posted by #TrevorPilley
Looks like a good scenario for a custom model binder. You can handle your incoming data and detect it your self and pass it to your own type to use in your controller. No need to fight with the built in types.
See here.
From the page (to keep the answer on SO):
Model Binders
A more flexible option than a type converter is to create a custom
model binder. With a model binder, you have access to things like the
HTTP request, the action description, and the raw values from the
route data.
To create a model binder, implement the IModelBinder interface. This
interface defines a single method, BindModel:
bool BindModel(HttpActionContext actionContext, ModelBindingContext
bindingContext);
Here is a model binder for GeoPoint objects.
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;
} } A model binder gets raw input values from a value provider. This design separates two distinct functions:
The value provider takes the HTTP request and populates a dictionary
of key-value pairs. The model binder uses this dictionary to populate
the model. The default value provider in Web API gets values from the
route data and the query string. For example, if the URI is
http://localhost/api/values/1?location=48,-122, the value provider
creates the following key-value pairs:
id = "1" location = "48,122" (I'm assuming the default route template,
which is "api/{controller}/{id}".)
The name of the parameter to bind is stored in the
ModelBindingContext.ModelName property. The model binder looks for a
key with this value in the dictionary. If the value exists and can be
converted into a GeoPoint, the model binder assigns the bound value to
the ModelBindingContext.Model property.
Notice that the model binder is not limited to a simple type
conversion. In this example, the model binder first looks in a table
of known locations, and if that fails, it uses type conversion.
Setting the Model Binder
There are several ways to set a model binder. First, you can add a
[ModelBinder] attribute to the parameter.
public HttpResponseMessage
Get([ModelBinder(typeof(GeoPointModelBinder))] GeoPoint location)
You
can also add a [ModelBinder] attribute to the type. Web API will use
the specified model binder for all parameters of that type.
[ModelBinder(typeof(GeoPointModelBinder))] public class GeoPoint {
// .... }
I found a solution.
First, I created a class to override the
KeyValuePair<string, string>
type to add a third element (I know it's not really a pair!). I could have use Tuple type also:
public sealed class KeyValuePair<TKey, TValue1, TValue2>
: IEquatable<KeyValuePair<TKey, TValue1, TValue2>>
To use this type with parameter, I create an
ActionFilterAttribute
to split (";") the value from the url and create a KeyValuePair (third element is optional)
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (actionContext.ActionArguments.ContainsKey(ParameterName))
{
var keyValuePairs = /* function to split parameters */;
actionContext.ActionArguments[ParameterName] =
keyValuePairs.Select(
x => x.Split(new[] { "," }, StringSplitOptions.None))
.Select(x => new KeyValuePair<string, string, string>(x[0], x[1], x.Length == 3 ? x[2] : string.Empty))
.ToList();
}
}
And finally, I add the action attribute filter to the controller route and change the parameter type:
"api/{controller}/{values}"
ex: http://localhost/api/{controller}/id1;type1;param1,id2;type2,[...],idN;typeN;param3
[MyCustomFilter("ids")]
public HttpResponseMessage MyFunction(
IList<KeyValuePair<string, string, string>> ids)
I could use some url parsing technique, but the ActionFilterAttribute is great and the code is not a mess finally!
I've been looking around the internet for an answer or example, but could not find one yet. I simply would like to change the default JSON serializer which is used to deserialize JSON while modelbinding to JSON.NET library.
I've found this SO post, but cannot implement it so far, I can't even see the System.Net.Http.Formatters namespace, nor can I see GlobalConfiguration.
What am I missing?
UPDATE
I have an ASP.NET MVC project, it was basically an MVC3 project. Currently I'm targetting .NET 4.5 and using the ASP.NET MVC 5 and related NuGet packages.
I don't see the System.Web.Http assembly, nor any similar namespace. In this context I would like to inject JSON.NET to be used as the default model binder for JSON type of requests.
I've finally found an answer. Basically I don't need the MediaTypeFormatter stuff, that's not designed to be used in MVC environment, but in ASP.NET Web APIs, that's why I do not see those references and namespaces (by the way, those are included in the Microsoft.AspNet.WeApi NuGet package).
The solution is to use a custom value provider factory. Here is the code required.
public class JsonNetValueProviderFactory : ValueProviderFactory
{
public override IValueProvider GetValueProvider(ControllerContext controllerContext)
{
// first make sure we have a valid context
if (controllerContext == null)
throw new ArgumentNullException("controllerContext");
// now make sure we are dealing with a json request
if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
return null;
// get a generic stream reader (get reader for the http stream)
var streamReader = new StreamReader(controllerContext.HttpContext.Request.InputStream);
// convert stream reader to a JSON Text Reader
var JSONReader = new JsonTextReader(streamReader);
// tell JSON to read
if (!JSONReader.Read())
return null;
// make a new Json serializer
var JSONSerializer = new JsonSerializer();
// add the dyamic object converter to our serializer
JSONSerializer.Converters.Add(new ExpandoObjectConverter());
// use JSON.NET to deserialize object to a dynamic (expando) object
Object JSONObject;
// if we start with a "[", treat this as an array
if (JSONReader.TokenType == JsonToken.StartArray)
JSONObject = JSONSerializer.Deserialize<List<ExpandoObject>>(JSONReader);
else
JSONObject = JSONSerializer.Deserialize<ExpandoObject>(JSONReader);
// create a backing store to hold all properties for this deserialization
var backingStore = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
// add all properties to this backing store
AddToBackingStore(backingStore, String.Empty, JSONObject);
// return the object in a dictionary value provider so the MVC understands it
return new DictionaryValueProvider<object>(backingStore, CultureInfo.CurrentCulture);
}
private static void AddToBackingStore(Dictionary<string, object> backingStore, string prefix, object value)
{
var d = value as IDictionary<string, object>;
if (d != null)
{
foreach (var entry in d)
{
AddToBackingStore(backingStore, MakePropertyKey(prefix, entry.Key), entry.Value);
}
return;
}
var l = value as IList;
if (l != null)
{
for (var i = 0; i < l.Count; i++)
{
AddToBackingStore(backingStore, MakeArrayKey(prefix, i), l[i]);
}
return;
}
// primitive
backingStore[prefix] = value;
}
private static string MakeArrayKey(string prefix, int index)
{
return prefix + "[" + index.ToString(CultureInfo.InvariantCulture) + "]";
}
private static string MakePropertyKey(string prefix, string propertyName)
{
return (String.IsNullOrEmpty(prefix)) ? propertyName : prefix + "." + propertyName;
}
}
And you can use it like this in your Application_Start method:
// remove default implementation
ValueProviderFactories.Factories.Remove(ValueProviderFactories.Factories.OfType<JsonValueProviderFactory>().FirstOrDefault());
// add our custom one
ValueProviderFactories.Factories.Add(new JsonNetValueProviderFactory());
Here is the post which pointed me to the right direction, and also this one gave a good explanation on value providers and modelbinders.
I had such a problem with this as well. I was posting JSON to an action, yet my JsonProperty names were ignored. Thus, my model properties were always empty.
public class MyModel
{
[JsonProperty(PropertyName = "prop1")]
public int Property1 { get; set; }
[JsonProperty(PropertyName = "prop2")]
public int Property2 { get; set; }
[JsonProperty(PropertyName = "prop3")]
public int Property3 { get; set; }
public int Foo { get; set; }
}
I am posting to an action using this custom jquery function:
(function ($) {
$.postJSON = function (url, data, dataType) {
var o = {
url: url,
type: 'POST',
contentType: 'application/json; charset=utf-8'
};
if (data !== undefined)
o.data = JSON.stringify(data);
if (dataType !== undefined)
o.dataType = dataType;
return $.ajax(o);
};
}(jQuery));
And I call it like this:
data = {
prop1: 1,
prop2: 2,
prop3: 3,
foo: 3,
};
$.postJSON('/Controller/MyAction', data, 'json')
.success(function (response) {
...do whatever with the JSON I got back
});
Unfortunately, only foo was ever getting bound (odd, since the case is not the same, but I guess the default modelbinder isn't case-sensitive)
[HttpPost]
public JsonNetResult MyAction(MyModel model)
{
...
}
The solution ended up being rather simple
I just implemented a generic version of Dejan's model binder which works very nicely for me. It could probably use some dummy checks (like making sure the request is actually application/json), but it's doing the trick right now.
internal class JsonNetModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
controllerContext.HttpContext.Request.InputStream.Position = 0;
var stream = controllerContext.RequestContext.HttpContext.Request.InputStream;
var readStream = new StreamReader(stream, Encoding.UTF8);
var json = readStream.ReadToEnd();
return JsonConvert.DeserializeObject(json, bindingContext.ModelType);
}
}
When I want to use it on a specific action, I simply tell it that I want to use my custom Json.Net model binder instead:
[HttpPost]
public JsonNetResult MyAction([ModelBinder(typeof(JsonNetModelBinder))] MyModel model)
{
...
}
Now my [JsonProperty(PropertyName = "")] attributes are no longer ignored on MyModel and everything is bound correctly!
In my case, I had to deserialize complex objects including interfaces and dynamically loaded types etc. so providing a custom value provider does not work as MVC still needs tries to figure out how to instantiate interfaces and then fails.
As my objects were already properly annotated to work with Json.NET, I took a different route: I've implemented a custom model binder and used Json.NET to explicitly deserialize the request body data like this:
internal class CustomModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
// use Json.NET to deserialize the incoming Position
controllerContext.HttpContext.Request.InputStream.Position = 0; // see: http://stackoverflow.com/a/3468653/331281
Stream stream = controllerContext.RequestContext.HttpContext.Request.InputStream;
var readStream = new StreamReader(stream, Encoding.UTF8);
string json = readStream.ReadToEnd();
return JsonConvert.DeserializeObject<MyClass>(json, ...);
}
}
The custom model binder is registered in Global.asax.cs:
ModelBinders.Binders.Add(typeof(MyClass), new CustomModelBinder();
Hi this is my convert a Ilist of model to a Ilist of ViewModel method
public static IList<PostViewModel> ConvertToPostViewModelList(this IList<Post> posts)
{
return posts.Select(ConvertToPostViewModel).ToList();
}
and also this is the ConvertToPostViewModel
public static PostViewModel ConvertToPostViewModel(this Post post)
{
var blogPostViewModel = new PostViewModel
{
Id = post.Id,
Body = post.Body,
Summary = post.Summary,
Title = post.Title,
Category = post.Category,
CreationDate = post.CreationDate,
SelectedCategory = post.CategoryId,
SelectedTag = post.TagId,
Tag = post.Tag,
UrlSlug = post.UrlSlug
};
return blogPostViewModel;
}
what is the problem with this , I got this error View :
The model item passed into the dictionary is of type 'System.Collections.Generic.List`1[Blog.Domain.Model.Post]', but this dictionary requires a model item of type 'System.Collections.Generic.IList`1[Blog.Web.UI.ViewModels.PostViewModel]'.
then ?? I convert the Ilist of Model to ViewModel via this :
return posts.Select(ConvertToPostViewModel).ToList();
then what is going on ??
what I have done in action
public ActionResult Posts()
{
var blogPost = _blogRepository.GetAllPost();
var blogPostViewModel = blogPost.ConvertToPostViewModelList();
return View("Posts", blogPostViewModel);
}
and in View
#model IList<Blog.Web.UI.ViewModels.PostViewModel>
Two possibilities:
The method you posted is not the one being matched. You can verify this easily by throwing an exception in your controller action and seeing if it gets thrown. This could be the result of either: (a) an overloaded Posts controller action, where the other is being matched; or, (b) a custom route that is intercepting the request.
You originally returned the domain object in testing, but after changing the controller action to set the model to the PostViewModel, you forgot to recompile your MVC project. Try recompiling your solution and see if the results change.