Asp net core rc2. Abstract class model binding - c#

In the RC1 I use the following code for abstract classes or interfaces binding:
public class MessageModelBinder : IModelBinder {
public Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext) {
if(bindingContext.ModelType == typeof(ICommand)) {
var msgTypeResult = bindingContext.ValueProvider.GetValue("messageType");
if(msgTypeResult == ValueProviderResult.None) {
return ModelBindingResult.FailedAsync(bindingContext.ModelName);
}
var type = Assembly.GetAssembly(typeof(MessageModelBinder )).GetTypes().SingleOrDefault(t => t.FullName == msgTypeResult.FirstValue);
if(type == null) {
return ModelBindingResult.FailedAsync(bindingContext.ModelName);
}
var metadataProvider = (IModelMetadataProvider)bindingContext.OperationBindingContext.HttpContext.RequestServices.GetService(typeof(IModelMetadataProvider));
bindingContext.ModelMetadata = metadataProvider.GetMetadataForType(type);
}
return ModelBindingResult.NoResultAsync;
}
}
This binder only reads model type (messageType parameter) from query string and overrides metadata type. And the rest of the work performed by standard binders such as BodyModelBinder.
In Startup.cs I just add first binder:
services.AddMvc().Services.Configure<MvcOptions>(options => {
options.ModelBinders.Insert(0, new MessageModelBinder());
});
Controller:
[Route("api/[controller]")]
public class MessageController : Controller {
[HttpPost("{messageType}")]
public ActionResult Post(string messageType, [FromBody]ICommand message) {
}
}
How can I perform this in RC2?
As far as I understand, now I have to use IModelBinderProvider. OK, I tried.
Startup.cs:
services.AddMvc().Services.Configure<MvcOptions>(options => {
options.ModelBinderProviders.Insert(0, new MessageModelBinderProvider());
});
ModelBinderProvider:
public class MessageModelBinderProvider : IModelBinderProvider {
public IModelBinder GetBinder(ModelBinderProviderContext context) {
if(context == null) {
throw new ArgumentNullException(nameof(context));
}
return context.Metadata.ModelType == typeof(ICommand) ? new MessageModelBinder() : null;
}
}
ModelBinder:
public class MessageModelBinder : IModelBinder {
public Task BindModelAsync(ModelBindingContext bindingContext) {
if(bindingContext.ModelType == typeof(ICommand)) {
var msgTypeResult = bindingContext.ValueProvider.GetValue("messageType");
if(msgTypeResult == ValueProviderResult.None) {
bindingContext.Result = ModelBindingResult.Failed(bindingContext.ModelName);
return Task.FromResult(0);
}
var type = typeof(MessageModelBinder).GetTypeInfo().Assembly.GetTypes().SingleOrDefault(t => t.FullName == msgTypeResult.FirstValue);
if(type == null) {
bindingContext.Result = ModelBindingResult.Failed(bindingContext.ModelName);
return Task.FromResult(0);
}
var metadataProvider = (IModelMetadataProvider)bindingContext.OperationBindingContext.HttpContext.RequestServices.GetService(typeof(IModelMetadataProvider));
bindingContext.ModelMetadata = metadataProvider.GetMetadataForType(type);
bindingContext.Result = ModelBindingResult.Success(bindingContext.ModelName, Activator.CreateInstance(type));
}
return Task.FromResult(0);
}
}
But I cannot specify NoResult. If I do not specify bindingContext.Result, I get null model in controller.
If I specify bindingContext.Result, I get empty model without setting model fields.

I had a similar requirement with custom model binding and abstract classes and the suggestions posted by dougbu on github AspNet/Mvc/issues/4703 worked for me. I upgraded from RC1 to ASP.NET Core 1.0 and needed to modify my custom model binder with his recommendations. I've copy & pasted his code below in case the link to the github issue breaks. Read the comments in the github issue for security considerations around code that creates objects of a requested type on the server.
MessageModelBinderProvider
public class MessageModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.ModelType != typeof(ICommand))
{
return null;
}
var binders = new Dictionary<string, IModelBinder>();
foreach (var type in typeof(MessageModelBinderProvider).GetTypeInfo().Assembly.GetTypes())
{
var typeInfo = type.GetTypeInfo();
if (typeInfo.IsAbstract || typeInfo.IsNested)
{
continue;
}
if (!(typeInfo.IsClass && typeInfo.IsPublic))
{
continue;
}
if (!typeof(ICommand).IsAssignableFrom(type))
{
continue;
}
var metadata = context.MetadataProvider.GetMetadataForType(type);
var binder = context.CreateBinder(metadata);
binders.Add(type.FullName, binder);
}
return new MessageModelBinder(context.MetadataProvider, binders);
}
}
MessageModelBinder
public class MessageModelBinder : IModelBinder
{
private readonly IModelMetadataProvider _metadataProvider;
private readonly Dictionary<string, IModelBinder> _binders;
public MessageModelBinder(IModelMetadataProvider metadataProvider, Dictionary<string, IModelBinder> binders)
{
_metadataProvider = metadataProvider;
_binders = binders;
}
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
var messageTypeModelName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, "messageType");
var messageTypeResult = bindingContext.ValueProvider.GetValue(messageTypeModelName);
if (messageTypeResult == ValueProviderResult.None)
{
bindingContext.Result = ModelBindingResult.Failed();
return;
}
IModelBinder binder;
if (!_binders.TryGetValue(messageTypeResult.FirstValue, out binder))
{
bindingContext.Result = ModelBindingResult.Failed();
return;
}
// Now know the type exists in the assembly.
var type = Type.GetType(messageTypeResult.FirstValue);
var metadata = _metadataProvider.GetMetadataForType(type);
ModelBindingResult result;
using (bindingContext.EnterNestedScope(metadata, bindingContext.FieldName, bindingContext.ModelName, model: null))
{
await binder.BindModelAsync(bindingContext);
result = bindingContext.Result;
}
bindingContext.Result = result;
}
}

Related

ModelBinder converting existing values in properties to null

I have a modelbinder like this:
public class CustomQuarantineModelBinder : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.ModelType.GetInterfaces().Contains(typeof(IQuarantineControl))
{
return new QuarantineModelBinder();
}
return null;
}
}
public class QuarantineModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext modelBindingContext)
{
char[] delimeter = { '|' };
if (modelBindingContext == null)
{
throw new ArgumentNullException(nameof(modelBindingContext));
}
var model = Activator.CreateInstance(modelBindingContext.ModelType);
if (modelBindingContext.ModelType.GetInterfaces().Contains(typeof(IQuarantineControl)))
{
var qc = model as IQuarantineControl;
if (qc != null)
{
var request = modelBindingContext.HttpContext.Request;
string QuarantineControl = request.Form["QuarantineControl"];
if (!string.IsNullOrEmpty(QuarantineControl))
{
string[] components = QuarantineControl.Split(delimeter);
qc.QuarantineClear();
qc.QuarantineControlID = Convert.ToInt32(components[0]);
qc.QuarantineState = (QuarantineState)Convert.ToInt32(components[1]);
for (int i = 2; i < components.Length; i++)
{
qc.QuarantineReasons.Add(components[i]);
}
}
}
}
modelBindingContext.Result = ModelBindingResult.Success(model);
return Task.CompletedTask;
}
However, other fields in the model is getting turned as null or empty. I would like to set QuarantineState, QuarantineControlId etc.. without affecting other values. Thanks
When your custom IModelBinderProvider returns a binder, that binder is responsible for binding the entire type. If you wish to fall back to the default MVC binder for other properties, you will need to do so explicitly.
Perhaps something like;
public class CustomQuarantineModelBinder : IModelBinderProvider
{
private readonly IModelBinderProvider baseProvider;
public CustomQuarantineModelBinder(IModelBinderProvider baseProvider){
this.baseProvider = baseProvider;
}
public IModelBinder GetBinder(ModelBinderProviderContext context){
...
return new QuarantineModelBinder(BaseProvider.GetBinder(context));
}
}
public class QuarantineModelBinder : IModelBinder
{
private readonly IModelBinder binder;
public QuarantineModelBinder(IModelBinder binder){
this.binder = binder;
}
public Task BindModelAsync(ModelBindingContext modelBindingContext)
{
...
binder.BindModelAsync(modelBindingContext);
...
}
}
services.AddMvc(options =>
{
var baseProvider = options.ModelBinderProviders
.OfType<ComplexObjectModelBinderProvider>()
.First();
options.ModelBinderProviders.Insert(0, new CustomQuarantineModelBinder(baseProvider));
});
When the model object is posted the string values that are empty are converted to null. This is the default behavior of the MVC model binder.
You can try a workaround like this,
public sealed class EmptyStringModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
bindingContext.ModelMetadata.ConvertEmptyStringToNull = false;
//Binders = new ModelBinderDictionary() { DefaultBinder = this };
return base.BindModel(controllerContext, bindingContext);
}
}

Custom JsonFormatter in Utf8Json is ignored

I have this simple JsonFormatter:
public sealed class Int64StringConversionFormatter : IJsonFormatter<long> {
public void Serialize(ref JsonWriter writer, long value, IJsonFormatterResolver formatterResolver) {
writer.WriteString(value.ToString(NumberFormatInfo.InvariantInfo));
}
public long Deserialize(ref JsonReader reader, IJsonFormatterResolver formatterResolver) {
var token = reader.GetCurrentJsonToken();
if (token == JsonToken.String) {
var s = reader.ReadString();
return
long.TryParse(s, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out var l)
? l
: 0;
}
if (token != JsonToken.Number)
throw new ValueProviderException("The provided value is not String or Int64.");
var value = reader.ReadInt64();
return value;
}
}
which is an implementation of Utf8Json.IJsonFormatter<> for working with long values. I've added this formatter to an AspNetCore WebApi application like this:
public static MvcOptions SetupCustomJsonFormatter(
this MvcOptions options) {
CompositeResolver.RegisterAndSetAsDefault(_formatters, _resolvers);
options.InputFormatters.Insert(0, new Utf8JsonInputFormatter());
options.OutputFormatters.Insert(0, new Utf8JsonOutputFormatter());
return options;
}
And here is my _formatters and _resolvers:
static readonly IJsonFormatterResolver[] resolvers = {
StandardResolver.ExcludeNullCamelCase,
ImmutableCollectionResolver.Instance,
EnumResolver.Default,
DynamicGenericResolver.Instance,
};
static readonly IJsonFormatter[] _formatters
= new [] {new Int64StringConversionFormatter()};
Also, here is my Utf8JsonInputFormatter:
internal sealed class Utf8JsonInputFormatter : IInputFormatter {
private readonly IJsonFormatterResolver _resolver;
public Utf8JsonInputFormatter() : this(null) {
}
public Utf8JsonInputFormatter(IJsonFormatterResolver resolver) {
_resolver = resolver ?? JsonSerializer.DefaultResolver;
}
public bool CanRead(InputFormatterContext context)
=> context.HttpContext.Request.ContentType?.StartsWith("application/json") == true;
public async Task<InputFormatterResult> ReadAsync(InputFormatterContext context) {
var request = context.HttpContext.Request;
if (request.Body.CanSeek && request.Body.Length == 0) return await InputFormatterResult.NoValueAsync();
var result = await JsonSerializer.NonGeneric.DeserializeAsync(context.ModelType, request.Body, _resolver);
return await InputFormatterResult.SuccessAsync(result);
}
}
Everything seems to should be OK. But Int64StringConversionFormatter.Serialize and Int64StringConversionFormatter.Deserializ methods never get called. I tested the configuration with another simple formatter (say UnixDateTimeFormatter) and it works just fine. But I cannot figure it out why this one isn't getting called. Do you have any idea what am I missing here?

OData Navigation-Routing for a Generic-Controller with ODataQueryOptions-support

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}";
}
}
}
}
}

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());
});

dotnet core webapi json-api compliant querystring route

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"];
}

Categories

Resources