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"];
}
Related
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);
}
}
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 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());
});
Is it possible to implicitly convert route argument by controller method from it's string representation into instance of an object with the default Binder?
Let's say I have class BusinessObjectId that contains two properties and can be converted from/to string
public class BusinessObjectId
{
private static readonly IDictionary<bool, char> IdTypeMap = new Dictionary<bool, char> { [false] = 'c', [true] = 'd' };
private static readonly Regex StrIdPattern = new Regex("^(?<type>[cd]{1})(?<number>\\d+)$", RegexOptions.Compiled);
public long Id { get; set; }
public bool IsDraft { get; set; }
public BusinessObjectId() { }
public BusinessObjectId(long id, bool isDraft)
{
Id = id;
IsDraft = isDraft;
}
public BusinessObjectId(string strId)
{
if (string.IsNullOrEmpty(strId)) return;
var match = StrIdPattern.Match(strId);
if (!match.Success) throw new ArgumentException("Argument is not in correct format", nameof(strId));
Id = long.Parse(match.Groups["number"].Value);
IsDraft = match.Groups["type"].Value == "d";
}
public override string ToString()
{
return $"{IdTypeMap[IsDraft]}{Id}";
}
public static implicit operator string(BusinessObjectId busId)
{
return busId.ToString();
}
public static implicit operator BusinessObjectId(string strBussId)
{
return new BusinessObjectId(strBussId);
}
}
These actionlinks are translated into nice urls:
#Html.ActionLink("xxx", "Sample1", "HomeController", new { oSampleId = new BusinessObjectId(123, false) } ... url:"/sample1/c123"
#Html.ActionLink("xxx", "Sample1", "HomeController", new { oSampleId = new BusinessObjectId(123, true) } ... url:"/sample1/d123"
Then I'd like to use parameters in controller methods like this:
public class HomeController1 : Controller
{
[Route("sample1/{oSampleId:regex(^[cd]{1}\\d+$)}")]
public ActionResult Sample1(BusinessObjectId oSampleId)
{
// oSampleId is null
throw new NotImplementedException();
}
[Route("sample2/{sSampleId:regex(^[cd]{1}\\d+$)}")]
public ActionResult Sample2(string sSampleId)
{
BusinessObjectId oSampleId = sSampleId;
// oSampleId is initialized well by implicit conversion
throw new NotImplementedException();
}
}
Method Sample1 doesn't recognize incoming argument and the instance oSampleId is null. In method Sample2 implicit conversion from string represetation works well, but I'd prefer not to call it manually.
Well I've found the answer ... You should write custom TypeConverter that can convert BusinessObjectId class from string and Decorate it with attribute
[TypeConverter(typeof(BusinessObjectIdConverter))]
public class BusinessObjectId
{ ... }
public class BusinessObjectIdConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
return new BusinessObjectId((string)value);
}
}
From now on you can use BusinessObjectId as parameter in controller methods and it will be initialized like a charm :-)
public class HomeController1 : Controller
{
[Route("sample1/{oSampleId:regex(^[cd]{1}\\d+$)}")]
public ActionResult Sample1(BusinessObjectId oSampleId)
{
// TODO: oSampleId is successfully parsed from it's string representations
}
}
I was hoping to use a dynamically typed object to write to a CSV file.
I'm receiving a 'CsvHelper.CsvWriterException' within the CsvWriter.WriteObject method with this message: "No properties are mapped for type 'WpmExport.DynamicEntry'."
Here is the class that I'm trying use :
public class DynamicEntry : DynamicObject
{
private Dictionary<string, object> dictionary = new Dictionary<string, object>();
public override bool TryGetMember(
GetMemberBinder binder, out object result)
{
string name = binder.Name.ToLower();
return dictionary.TryGetValue(name, out result);
}
public override bool TrySetMember(
SetMemberBinder binder, object value)
{
dictionary[binder.Name.ToLower()] = value;
return true;
}
public override IEnumerable<string> GetDynamicMemberNames()
{
return dictionary.Keys.AsEnumerable();
}
}
Anyone with any ideas or working examples? The documentation at http://joshclose.github.io/CsvHelper/ hints that it is possible but doesn't provide any guidance.
TIA
The functionality does not exist yet. You can write dynamic but not DynamicObject. You can view a thread on the subject here. https://github.com/JoshClose/CsvHelper/issues/187
When the functionality get implemented, I'll update the answer with the version it's in.
Update
This functionality will be available in 3.0. You can currently try out the 3.0-beta from NuGet.
Because I cannot wait for Version 3.0 (and CsvHelper.Excel to support it), I have found a interim-solution.
Got the class to export:
public partial class EntryReportInventory
{
public Guid DeviceId { get; set; }
[ReportProperty]
public string DeviceName { get; set; }
public Dictionary<string, object> InventoryValues { get; set; }
public EntryReportInventory(Device device, Dictionary<string, object> inventoryValues)
{
this.DeviceId = device.Id;
this.DeviceName = device.Name;
this.InventoryValues = inventoryValues;
}
}
Created Mapper:
Type genericClass = typeof(DefaultCsvClassMap<>);
Type constructedClass = genericClass.MakeGenericType(typeof(EntryReportInventory));
return (CsvClassMap)Activator.CreateInstance(constructedClass);
And now the magic. I iterate all properties.
foreach (PropertyInfo property in mapping)
{
...
if (isInventoryReportBaseType && typeof(Dictionary<string, object>).IsAssignableFrom(property.PropertyType))
{
var dataSource = (ReportInventoryBase)Activator.CreateInstance(entityType, dbContext);
foreach (var item in dataSource.ColumnNameAndText)
{
var columnName = item.Key;
var newMap = new CsvPropertyMap(property);
newMap.Name(columnName);
newMap.TypeConverter(new InventoryEntryListSpecifiedTypeConverter(item.Key));
customMap.PropertyMaps.Add(newMap);
}
...
}
And my converter is:
public class InventoryEntryListSpecifiedTypeConverter : CsvHelper.TypeConversion.ITypeConverter
{
private string indexKey;
public InventoryEntryListSpecifiedTypeConverter(string indexKey)
{
this.indexKey = indexKey;
}
public bool CanConvertFrom(Type type)
{
return true;
}
public bool CanConvertTo(Type type)
{
return true;
}
public object ConvertFromString(TypeConverterOptions options, string text)
{
throw new NotImplementedException();
}
public string ConvertToString(TypeConverterOptions options, object value)
{
var myValue = value as Dictionary<string, object>;
if (value == null || myValue.Count == 0) return null;
return myValue[indexKey] + "";
}
}
Don't know why, but it works to pass the same property several times.
That's it :)
You only have to have a list before (here: dataSource.ColumnNameAndText, filled from an external source) to identify the columns/values.