My controller method looks like this:
public ActionResult SomeMethod(Dictionary<int, string> model)
{
}
Is it possible to call this method and populate "model" using only query string? I mean, typing something like this:
ControllerName/SomeMethod?model.0=someText&model.1=someOtherText
in our browser address bar. Is it possible?
EDIT:
It would appear my question was misunderstood - I want to bind the query string, so that the Dictionary method parameter is populated automatically. In other words - I don't want to manually create the dictionary inside my method, but have some automathic .NET binder do it form me, so I can access it right away like this:
public ActionResult SomeMethod(Dictionary<int, string> model)
{
var a = model[SomeKey];
}
Is there an automatic binder, smart enough to do this?
In ASP.NET Core, you can use the following syntax (without needing a custom binder):
?dictionaryVariableName[KEY]=VALUE
Assuming you had this as your method:
public ActionResult SomeMethod([FromQuery] Dictionary<int, string> model)
And then called the following URL:
?model[0]=firstString&model[1]=secondString
Your dictionary would then be automatically populated. With values:
(0, "firstString")
(1, "secondString")
For .NET Core 2.1, you can do this very easily.
public class SomeController : ControllerBase
{
public IActionResult Method([FromQuery]IDictionary<int, string> query)
{
// Do something
}
}
And the url
/Some/Method?1=value1&2=value2&3=value3
It will bind that to the dictionary. You don't even have to use the parameter name query.
try custom model binder
public class QueryStringToDictionaryBinder: IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var collection = controllerContext.HttpContext.Request.QueryString;
var modelKeys =
collection.AllKeys.Where(
m => m.StartsWith(bindingContext.ModelName));
var dictionary = new Dictionary<int, string>();
foreach (string key in modelKeys)
{
var splits = key.Split(new[]{'.'}, StringSplitOptions.RemoveEmptyEntries);
int nummericKey = -1;
if(splits.Count() > 1)
{
var tempKey = splits[1];
if(int.TryParse(tempKey, out nummericKey))
{
dictionary.Add(nummericKey, collection[key]);
}
}
}
return dictionary;
}
}
in controller action use it on model
public ActionResult SomeMethod(
[ModelBinder(typeof(QueryStringToDictionaryBinder))]
Dictionary<int, string> model)
{
//return Content("Test");
}
More specific to mvc model binding is to construct the query string as
/somemethod?model[0].Key=1&model[0].Value=One&model[1].Key=2&model[1].Value=Two
Custom Binder would simply follow DefaultModelBinder
public class QueryStringToDictionary<TKey, TValue> : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var modelBindingContext = new ModelBindingContext
{
ModelName = bindingContext.ModelName,
ModelMetadata = new ModelMetadata(new EmptyModelMetadataProvider(), null,
null, typeof(Dictionary<TKey, TValue>), bindingContext.ModelName),
ValueProvider = new QueryStringValueProvider(controllerContext)
};
var temp = new DefaultModelBinder().BindModel(controllerContext, modelBindingContext);
return temp;
}
}
Apply custom model binder in model as
public ActionResult SomeMethod(
[ModelBinder(typeof(QueryStringToDictionary<int, string>))] Dictionary<int, string> model)
{
// var a = model[SomeKey];
return Content("Test");
}
Related
I have one generic-controller (similar to this: .Net Core Override Controller Route for Generic Controller) which registers generic implementations for all dynamic types, I have.
This works very well. But while trying to implement the support navigation-routing with additional filter-values I have some issues. This example:
http://localhost/odata/EntityA(4711)/SubEntity?$filter=category eq 'ABC'
works theoretically, but I need to extract the ODataQueryOptions.
So this is what I have so far:
ExternalControllerFeatureProvider
public class ExternalControllerFeatureProvider : IApplicationFeatureProvider<ControllerFeature>
{
public void PopulateFeature(IEnumerable<ApplicationPart> parts, ControllerFeature feature)
{
foreach (var candidate in _entityCompiler.GetTypes())
{
feature.Controllers.Add(
typeof(GenericController<>).MakeGenericType(candidate).GetTypeInfo()
);
}
}
}
GenericController
[Produces("application/json")]
[GenericControllerNameConvention]
[EnableQuery]
public class GenericController<T> : ODataController
{
public async Task<IQueryable<T>> Get([FromServices] ODataQueryOptions odataQueryOptions)
{
var parameters = ExtractQueryParameter(odataQueryOptions);
return await InternalGet(parameters);
}
public async Task<IQueryable<T>> Get([FromServices] ODataQueryOptions odataQueryOptions, [FromODataUri] object key)
{
var parameters = ExtractQueryParameter(odataQueryOptions);
AppendKeyAttributeFilter(parameters, key);
return await InternalGet(parameters);
}
public async Task<IActionResult> GetNavigation(Guid key, string propertyName)
{
var parameters = new Dictionary<string, object>();
AppendKeyAttributeFilter(parameters, key);
AppendExpandFilter(parameters, propertyName);
var rootObject = await InternalGet(parameters);
if (rootObject.Any())
{
var info = typeof(T).GetProperty(propertyName);
object value = info.GetValue(rootObject.FirstOrDefault());
return Ok(value);
}
return NotFound();
}
Similar to this (http://odata.github.io/WebApi/03-04-custom-routing-convention/) I created a NavigationRoutingConvention, which extracts the navigation-property and calls the GetNavigation-method from the GenericController with the correct propertyName.
The problem is that this GenericController-method can not return IQueryable nor IEnumerable, but only some untyped types like IActionResult.
In order to manually filter my datasource in the backend I need the ODataQueryOptions, like in the both Get-methods. The problem is that it seems that the underleying framework needs to know the correct returned type.
If I add [FromServices] ODataQueryOptions to the method-head I get following exception:
System.InvalidOperationException: Cannot create an EDM model as the
action 'GetNavigation' on controller 'EntityA' has a return type
'System.Threading.Tasks.Task`1[[Microsoft.AspNetCore.Mvc.IActionResult,
Microsoft.AspNetCore.Mvc.Abstractions, Version=2.1.1.0,
Culture=neutral, PublicKeyToken=adb9793829ddae60]]' that does not
implement IEnumerable. at
Microsoft.AspNet.OData.ODataQueryParameterBindingAttribute.ODataQueryParameterBinding.GetEntityClrTypeFromActionReturnType(ActionDescriptor
actionDescriptor) at
Microsoft.AspNet.OData.ODataQueryParameterBindingAttribute.ODataQueryParameterBinding.BindModelAsync(ModelBindingContext
bindingContext) at
Microsoft.AspNetCore.Mvc.ModelBinding.Binders.BinderTypeModelBinder.BindModelAsync(ModelBindingContext
bindingContext) at
Microsoft.AspNetCore.Mvc.ModelBinding.ParameterBinder.BindModelAsync(ActionContext
actionContext, IModelBinder modelBinder, IValueProvider valueProvider,
ParameterDescriptor parameter, ModelMetadata metadata, Object value)
at
Microsoft.AspNetCore.Mvc.Internal.ControllerBinderDelegateProvider.<>c__DisplayClass0_0.<g__Bind|0>d.MoveNext()
So I found the solution. I have abstained from the idea of my own routing convention and added a further Generic-Controller especially for sub-navigation properties. Below the abstract not working code, cleaned by some private parts... :-)
GenericSubNavigationController
[Produces("application/json")]
[GenericControllerNameConvention]
[EnableQuery]
public class GenericSubNavigationController<TBaseType, TSubType, TSubTypeDeclared> : GenericControllerBase<TBaseType>
{
public GenericSubNavigationController(ISubTypeEnricher subTypeEnricher) : base(subTypeEnricher)
{
}
public async Task<IQueryable<TSubTypeDeclared>> GetNavigation([FromServices] ODataQueryOptions odataQueryOptions, Guid key)
{
PropertyInfo propertyInfo = typeof(TBaseType).GetProperties().FirstOrDefault(x => x.PropertyType == typeof(TSubType));
string propertyName = propertyInfo.Name;
var parameters = new Dictionary<string, string>();
AppendKeyAttributeFilter(parameters, key);
AppendExpandFilter(parameters, propertyName);
var subParameters = new Tuple<string, Dictionary<string, string>>(propertyName, ExtractQueryParameter(odataQueryOptions));
var rootObject = await InternalGet<TBaseType>(parameters, subParameters);
if (rootObject.Any())
{
var info = typeof(TBaseType).GetProperty(propertyName);
object value = info.GetValue(rootObject.FirstOrDefault());
return new EnumerableQuery<TSubTypeDeclared>((IEnumerable<TSubTypeDeclared>) value);
}
return null;
}
}
In order to work, you have to instantiate this controller in the ExternalControllerFeatureProvider, which was already mentioned in my initial question
ExternalControllerFeatureProvider
public class ExternalControllerFeatureProvider : IApplicationFeatureProvider<ControllerFeature>
{
private readonly IExternalCompiler _entityCompiler;
public ExternalControllerFeatureProvider(IExternalCompiler entityCompiler)
{
_entityCompiler = entityCompiler;
}
public void PopulateFeature(IEnumerable<ApplicationPart> parts, ControllerFeature feature)
{
var types = _entityCompiler.GetTypes().ToList();
foreach (var candidate in types)
{
feature.Controllers.Add(
typeof(GenericController<>).MakeGenericType(candidate).GetTypeInfo()
);
foreach (var propertyInfo in candidate.GetProperties())
{
Type targetType = propertyInfo.PropertyType.GenericTypeArguments.Any()
? propertyInfo.PropertyType.GenericTypeArguments.First()
: propertyInfo.PropertyType;
if (types.Contains(targetType))
{
var typeInfo = typeof(GenericSubNavigationController<,,>).MakeGenericType(candidate, propertyInfo.PropertyType, targetType).GetTypeInfo();
feature.Controllers.Add(typeInfo);
}
}
}
}
}
And finally we have to change the used attribute GenericControllerNameConvention to change the action-name of the methods to reflact the default OData requirements
GenericControllerNameConvention
public class GenericControllerNameConvention : Attribute, IControllerModelConvention
{
public void Apply(ControllerModel controller)
{
if (!controller.ControllerType.IsGenericType || (controller.ControllerType.GetGenericTypeDefinition() !=
typeof(GenericController<>) && controller.ControllerType.GetGenericTypeDefinition() !=
typeof(GenericSubNavigationController<,,>)))
{
// Not a GenericController, ignore.
return;
}
var entityType = controller.ControllerType.GenericTypeArguments[0];
controller.ControllerName = $"{entityType.Name}";
if (controller.ControllerType.GetGenericTypeDefinition() ==
typeof(GenericSubNavigationController<,,>))
{
foreach (var controllerAction in controller.Actions)
{
if (controllerAction.ActionName == "GetNavigation")
{
var subType = controller.ControllerType.GenericTypeArguments[1];
PropertyInfo propertyInfo = entityType.GetProperties().FirstOrDefault(x => x.PropertyType == subType);
controllerAction.ActionName = $"Get{propertyInfo.Name}";
}
}
}
}
}
I'm trying to grab the "status" and "all" key, value from the requested URL, and can't figure out how to build my class object.
The JSON API specification I'm referring to can be found here:
http://jsonapi.org/recommendations/#filtering
// requested url
/api/endpoint?filter[status]=all
// my attempt at model binding
public class FilterParams
{
public Dictionary<string, string> Filter { get; set; }
}
[HttpGet]
public string Get([FromUri] FilterParams filter)
{
// never gets populated...
var filterStatus = filter.Filter["status"];
}
If you're building json:api apps on .Net Core, I strongly recommend checking out this library: https://github.com/json-api-dotnet/JsonApiDotNetCore
It handles all of the heavy lifting for you and for this specific example, (you need to get the filter value) the solution looks like:
public FooController : JsonApiController<Foo> {
private readonly IQueryAccessor _queryAccessor;
public FooController(IQueryAccessor queryAccessor, /* ... */)
: base(/* ... */) {
_queryAccessor = queryAccessor;
}
[HttpGet]
public override async Task<IActionResult> GetAsync() {
var status = _queryAccessor.GetRequired<string>("status");
// ...
}
}
You could use IModelBinder for that:
Define a model binder:
public class FilterParamsModelBinder : IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType != typeof(FilterParams)) return false;
Dictionary<string, string> result = new Dictionary<string, string>();
var parameters = actionContext.Request.RequestUri.Query.Substring(1);
if(parameters.Length == 0) return false;
var regex = new Regex(#"filter\[(?<key>[\w]+)\]=(?<value>[\w^,]+)");
parameters
.Split('&')
.ToList()
.ForEach(_ =>
{
var groups = regex.Match(_).Groups;
if(groups.Count == 0)
bindingContext.ModelState.AddModelError(bindingContext.ModelName, "Cannot convert value.");
result.Add(groups["key"].Value, groups["value"].Value);
});
bindingContext.Model = new FilterParams { Filter = result};
return bindingContext.ModelState.IsValid;
}
}
Use it:
[HttpGet]
public string Get([ModelBinderAttribute(typeof(FilterParamsModelBinder))] FilterParams filter)
{
//your code
}
If you could define a route like "/api/endpoint?filter=status,all" instead, than you could use a TypeConverter for that:
Define a converter:
public class FilterConverter : TypeConverter
{
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if (!(value is string)) return base.ConvertFrom(context, culture, value);
var keyValue = ((string)value).Split(',');
return new FilterParams
{
Filter = new Dictionary<string, string> { [keyValue[0]] = keyValue[1] }
};
}
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
}
}
Use it:
[TypeConverter(typeof(FilterConverter))]
public class FilterParams
{
public Dictionary<string, string> Filter { get; set; }
}
[HttpGet]
public string Get(FilterParams filter)
{
var filterStatus = filter.Filter["status"];
}
Hi I am new to asp net mvc programming and I am wondering why the OnPropertyValidating method for my CustomModelBinder class is not being called.
Here is my declaration for the CUstomModelBinder.
public class TestModelBinder : DefaultModelBinder
{
protected override bool OnPropertyValidating(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value)
{
if (value is string && (controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase)))
{
if (controllerContext.Controller.ValidateRequest && bindingContext.PropertyMetadata[propertyDescriptor.Name].RequestValidationEnabled)
{
int index;
if (IsDangerousString(value.ToString(), out index))
{
throw new HttpRequestValidationException("Dangerous Input Detected");
}
}
}
return base.OnPropertyValidating(controllerContext, bindingContext, propertyDescriptor, value);
}
}
and here is what I added to the Global.asax
ModelBinders.Binders.Add(typeof(TestModelBinder), new TestModelBinder());
ModelBinders.Binders.DefaultBinder = new TestModelBinder();
now I am assuming that this OnPropertyValidating method will get called everytime I call a controller action something like :
[HttpPost]
public JsonResult TestMethod(int param1, string param2, string param3)
{
...
}
but the OnPropertyValidating method on my customModelBinder never gets called.
Can anyone help me to understand why? Is there any good tutorial sites for this?
Thank in advance!
Instead of this:
ModelBinders.Binders.Add(typeof(TestModelBinder), new TestModelBinder());
Do this:
ModelBinders.Binders.Add(typeof(<your model>), new TestModelBinder());
I need to create a dynamic input form based on a derived type but I cannot get complex properties bound properly when passed to the POST method of my controller. Other properties bind fine. Here is a contrived example of what I have:
Model
public abstract class ModelBase {}
public class ModelDerivedA : ModelBase
{
public string SomeProperty { get; set; }
public SomeType MySomeType{ get; set; }
public ModelDerivedA()
{
MySomeType = new SomeType();
}
}
public class SomeType
{
public string SomeTypeStringA { get; set; }
public string SomeTypeStringB { get; set; }
}
Custom Model Binder
The binder is based on this answer: polymorphic-model-binding
public class BaseViewModelBinder : DefaultModelBinder
{
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
var typeValue = bindingContext.ValueProvider.GetValue("ModelType");
var type = Type.GetType(
(string)typeValue.ConvertTo(typeof(string)),
true
);
if (!typeof(ModelBase).IsAssignableFrom(type))
{
throw new InvalidOperationException("The model does not inherit from mode base");
}
var model = Activator.CreateInstance(type);
bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, type);
return model;
}
}
Controller
[HttpPost]
public ActionResult GetDynamicForm([ModelBinder(typeof(BaseViewModelBinder))] ModelBase model)
{
// model HAS values for SomeProperty
// model has NO values for MySomeType
}
View Excerpt
#Html.Hidden("ModelType", Model.GetType())
#Html.Test(Model);
JavaScript
The form is posted using $.ajax using data: $(this).serialize(), which, if I debug shows the correct populated form data.
All properties are populated in the model excluding those of SomeType. What do I need to change to get them populated?
Thanks
Values are not being populated because you are creating new instance of type like following:
var model = Activator.CreateInstance(type);
bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, type);
return model;
and returning the same model instead which is not correct.
do something like below.
ValueProviderResult valueResult;
bindingContext.ModelState.SetModelValue("ModelType", valueResult);
return valueResult;
Here is very good discussion on modelBinder.
http://odetocode.com/blogs/scott/archive/2009/05/05/iterating-on-an-asp-net-mvc-model-binder.aspx
I have solved my immediate issue by:
get an instance of FormvalueProvider (to get access to what has been posted)
recursively going through my model and setting each property value to the matching value in the FormValueProvider
private FormValueProvider vp;
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
var typeValue = bindingContext.ValueProvider.GetValue("ModelType");
var type = Type.GetType(
(string)typeValue.ConvertTo(typeof(string)),
true
);
if (!typeof(ModelBase).IsAssignableFrom(type))
{
throw new InvalidOperationException("Bad Type");
}
var model = Activator.CreateInstance(type);
vp = new FormValueProvider(controllerContext);
bindingContext.ValueProvider = vp;
SetModelPropertValues(model);
return model;
}
And the recursion, based on this answer here to print properties in nested objects
private void SetModelPropertValues(object obj)
{
Type objType = obj.GetType();
PropertyInfo[] properties = objType.GetProperties();
foreach (PropertyInfo property in properties)
{
object propValue = property.GetValue(obj, null);
var elems = propValue as IList;
if (elems != null)
{
foreach (var item in elems)
{
this.SetModelPropertValues(item);
}
}
else
{
if (property.PropertyType.Assembly == objType.Assembly)
{
this.SetModelPropertValues(propValue);
}
else
{
property.SetValue(obj, this.vp.GetValue(property.Name).AttemptedValue, null);
}
}
}
}
Anyone using this may need to make it more robust for their needs.
I would be very intersted to hear of any drawbacks to this as a general approach to this kind of problem.
However, I'm hoping that this post helps in some situations.
Try to add default constructor to your ModelDerivedA to initialize MySomeType
public class ModelDerivedA : ModelBase
{
public ModelDerivedA()
{
MySomeType = new SomeType();
}
}
I have written a Custom Model Binder which is supposed to map Dates, coming from URL-Strings (GET) according to the current culture (a sidenote here: the default model binder does not consider the current culture if you use GET as http-call...).
public class DateTimeModelBinder : IModelBinder
{
#region IModelBinder Members
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (controllerContext.HttpContext.Request.HttpMethod == "GET")
{
string theDate = controllerContext.HttpContext.Request.Form[bindingContext.ModelName];
DateTime dt = new DateTime();
bool success = DateTime.TryParse(theDate, System.Globalization.CultureInfo.CurrentUICulture, System.Globalization.DateTimeStyles.None, out dt);
if (success)
{
return dt;
}
else
{
return null;
}
}
return null; // Oooops...
}
#endregion
}
I registered the model binder in global.asax:
ModelBinders.Binders.Add(typeof(DateTime?), new DateTimeModelBinder());
Now the problem occurs in the last return null;. If I use other forms with POST, it would overwrite the already mapped values with null. How can I avoid this?
Thx for any inputs.
sl3dg3
Derive from DefaultModelBinder and then invoke the base method:
public class DateTimeModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
// ... Your code here
return base.BindModel(controllerContext, bindingContext);
}
}
Well, it is actually a trivial solution: I create a new instance of the default binder and pass the task to him:
public class DateTimeModelBinder : IModelBinder
{
#region IModelBinder Members
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (controllerContext.HttpContext.Request.HttpMethod == "GET")
{
string theDate = controllerContext.HttpContext.Request.Form[bindingContext.ModelName];
DateTime dt = new DateTime();
bool success = DateTime.TryParse(theDate, System.Globalization.CultureInfo.CurrentUICulture, System.Globalization.DateTimeStyles.None, out dt);
if (success)
{
return dt;
}
else
{
return null;
}
}
DefaultModelBinder binder = new DefaultModelBinder();
return binder.BindModel(controllerContext, bindingContext);
}
#endregion
}
One more possible solution is pass some of the best default model bidners into custom and call it there.
public class BaseApiRequestModelBinder : IModelBinder
{
private readonly IModelBinder _modelBinder;
public BaseApiRequestModelBinder(IModelBinder modelBinder)
{
_modelBinder = modelBinder;
}
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
//calling best default model binder
await _modelBinder.BindModelAsync(bindingContext);
var model = bindingContext.Result.Model as BaseApiRequestModel;
//do anything you want with a model that was bind with default binder
}
}
public class BaseApiRequestModelBinderProvider : IModelBinderProvider
{
private IList<IModelBinderProvider> _modelBinderProviders { get; }
public BaseApiRequestModelBinderProvider(IList<IModelBinderProvider> modelBinderProviders)
{
_modelBinderProviders = modelBinderProviders;
}
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.ModelType == typeof(BaseApiRequestModel) || context.Metadata.ModelType.IsSubclassOf(typeof(BaseApiRequestModel)))
{
//Selecting best default model binder. Don't forget to exlude the current one as it is also in list
var defaultBinder = _modelBinderProviders
.Where(x => x.GetType() != this.GetType())
.Select(x => x.GetBinder(context)).FirstOrDefault(x => x != null);
if (defaultBinder != null)
{
return new BaseApiRequestModelBinder(defaultBinder);
}
}
return null;
}
//Register model binder provider in ConfigureServices in startup
services
.AddMvc(options => {
options.ModelBinderProviders.Insert(0, new BaseApiRequestModelBinderProvider(options.ModelBinderProviders));
})