Superfluous comment: I cannot believe I couldn't find a clear answer for this anywhere yet!
Using ASP.NET MVC model binding requires use of dot notation (variableName.propertyName) when using a query string. However, jQuery will use bracket notation when using a GET request, such as variableName[propertyName]=value&. ASP.NET MVC cannot understand this notation.
If I issued a POST request ASP.NET is able to properly bind the model because it uses dot notation in the posted body.
Is there any way to force ASP.NET to bind to a model that is a complex object when bracketed notation is used within a query string?
I'm not sure if this is the ideal solution, but I solved this using some reflection magic by implementing a generic implementation of IModelBinder. The stipulations on this implementation is that it assumes the elements from JavaScript in the query string are in camelCase and the class in C# is in PascalCase per standard styles. Additionally, it only functions on public [set-able] properties. Here's my implementation below:
public class BracketedQueryStringModelBinder<T> : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var properties = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(p => p.CanWrite);
Dictionary<string, object> values = new Dictionary<string, object>();
foreach (var p in properties)
{
if (!IsNullable(p.PropertyType))
{
object val = TryGetValueType(p.PropertyType, bindingContext, p.Name);
if (val != null)
{
values.Add(p.Name, val);
}
}
else
{
object val = GetRefernceType(p.PropertyType, bindingContext, p.Name);
values.Add(p.Name, val);
}
}
if (values.Any())
{
object boundModel = Activator.CreateInstance<T>();
foreach (var p in properties.Where(i => values.ContainsKey(i.Name)))
{
p.SetValue(boundModel, values[p.Name]);
}
return boundModel;
}
return null;
}
private static bool IsNullable(Type t)
{
if (t == null)
throw new ArgumentNullException("t");
if (!t.IsValueType)
return true;
return Nullable.GetUnderlyingType(t) != null;
}
private static object TryGetValueType(Type type, ModelBindingContext ctx, string key)
{
if (string.IsNullOrEmpty(key))
throw new ArgumentNullException("key");
key = ConvertToPascalCase(key);
ValueProviderResult result = ctx.ValueProvider.GetValue(string.Concat(ctx.ModelName, "[", key, "]"));
if (result == null && ctx.FallbackToEmptyPrefix)
result = ctx.ValueProvider.GetValue(key);
if (result == null)
return null;
try
{
object returnVal = result.ConvertTo(type);
ctx.ModelState.SetModelValue(key, result);
return returnVal;
}
catch (Exception ex)
{
ctx.ModelState.AddModelError(ctx.ModelName, ex);
return null;
}
}
private static object GetRefernceType(Type type, ModelBindingContext ctx, string key)
{
if (string.IsNullOrEmpty(key))
throw new ArgumentNullException("key");
key = ConvertToPascalCase(key);
ValueProviderResult result = ctx.ValueProvider.GetValue(string.Concat(ctx.ModelName, "[", key, "]"));
if (result == null && ctx.FallbackToEmptyPrefix)
result = ctx.ValueProvider.GetValue(key);
if (result == null)
return null;
try
{
object returnVal = result.ConvertTo(type);
ctx.ModelState.SetModelValue(key, result);
return returnVal;
}
catch (Exception ex)
{
ctx.ModelState.AddModelError(ctx.ModelName, ex);
return null;
}
}
private static string ConvertToPascalCase(string str)
{
char firstChar = str[0];
if (char.IsUpper(firstChar))
return char.ToLower(firstChar) + str.Substring(1);
return str;
}
}
Then in your controller you can use it like this:
[HttpGet]
public ActionResult myAction([ModelBinder(typeof(BracketedQueryStringModelBinder<MyClass>))] MyClass mc = null)
{
...
}
The main downfall to this method is that if you do get a query string in dot notation this binding will fail since it doesn't revert back to the standard model binder.
Related
Is there a way to get a dynamic object from query parameters in an ASP.NET Core WebAPI controller action?
When I try the following I get queries as an empty object
public object Action([FromQuery] dynamic queries)
{
...
}
Here is a workaround of customizing a model binder to bind the query string to Dictionary type:
DynamicModelBinder
public class DynamicModelBinder:IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
var result = new Dictionary<string, dynamic> { };
var query = bindingContext.HttpContext.Request.Query;
if (query == null)
{
bindingContext.ModelState.AddModelError("QueryString", "The data is null");
return Task.CompletedTask;
}
foreach (var k in query.Keys)
{
StringValues v = string.Empty;
var flag = query.TryGetValue(k, out v);
if (flag)
{
if (v.Count > 1)
{
result.Add(k, v);
}
else {
result.Add(k, v[0]);
}
}
}
bindingContext.Result = ModelBindingResult.Success(result);
return Task.CompletedTask;
}
}
Controller
public object Action([ModelBinder(BinderType = typeof(DynamicModelBinder))]dynamic queries)
{
return queries;
}
Result
I'm a french student and I have to do a project for the semester.
I have to get medias from Instagram (with the api) and put them in a database. I use instasharp for now and I can do a request with the HttpClient class.
This returns a string with the content of the JSON request. But I've seen a class named Mapper.cs, which should match the result of the request with the different class of instasharp.
For example, if I search for a media, the mapper class should read the JSON string a create an appropriate instance of Media.
I do this kind of request: https://api.instagram.com/v1/media/search?lat=45.759723&lng=4.842223&distance=5000&client_id=My-Id&count=5
But I'm not sure of it, is somebody able to tell me what the Mapper class really does.
Here is the Mapper class, I do not understand all of the method, so I can't use it.
class Mapper {
public static object Map<T>(string json) where T : new() {
//var t = new T();
var j = JObject.Parse(json);
var t = typeof(T);
try {
var instance = Map(t, j);
// add the pure json back
if (instance != null) {
var prop = instance.GetType().GetProperty("Json");
if (prop != null) {
prop.SetValue(instance, json, null);
}
}
return instance;
}
catch (Exception ex) {
Debug.WriteLine(ex.Message);
return null;
}
}
private static object Map(Type t, JObject json) {
var instance = Activator.CreateInstance(t);
Array.ForEach(instance.GetType().GetProperties(), prop => {
var attribute = prop.GetCustomAttributes(typeof(Model.JsonMapping), false);
if (attribute.Length > 0) {
var propertyType = prop.PropertyType;
var mapsTo = ((Model.JsonMapping)attribute[0]).MapsTo;
var mappingType = ((Model.JsonMapping)attribute[0]).MapType;
switch (mappingType) {
case Model.JsonMapping.MappingType.Class:
if (json[mapsTo] != null) {
if (json[mapsTo].HasValues) {
var obj = Map(propertyType, (JObject)json[mapsTo]);
prop.SetValue(instance, obj, null);
}
}
break;
case Model.JsonMapping.MappingType.Collection:
var col = Map(propertyType, (JArray)json[mapsTo]);
prop.SetValue(instance, col, null);
break;
default:
if (json != null) {
if (json[mapsTo] != null) {
// special case for datetime because it comes in Unix format
if (prop.PropertyType == typeof(DateTime))
prop.SetValue(instance, UnixTimeStampToDateTime(json[mapsTo].ToString()), null);
else
prop.SetValue(instance, Convert.ChangeType(json[mapsTo].ToString(), prop.PropertyType), null);
}
}
break;
}
}
});
return instance;
}
private static IList Map(Type t, JArray json) {
var type = t.GetGenericArguments()[0];
// This will produce List<Image> or whatever the original element type is
var listType = typeof(List<>).MakeGenericType(type);
var result = (IList)Activator.CreateInstance(listType);
if (json != null) {
foreach (var j in json)
if (type.Name == "String" || type.Name == "Int32")
result.Add(j.ToString());
else result.Add(Map(type, (JObject)j));
}
return result;
}
private static DateTime UnixTimeStampToDateTime(string unixTimeStamp) {
// Unix timestamp is seconds past epoch
double unixTime = Convert.ToDouble(unixTimeStamp);
System.DateTime dtDateTime = new DateTime(1970, 1, 1, 0, 0, 0, 0);
dtDateTime = dtDateTime.AddSeconds(unixTime).ToLocalTime();
return dtDateTime;
}
private static void SetPropertyValue(PropertyInfo prop, object instance, object value) {
prop.SetValue(instance, Convert.ChangeType(value, prop.PropertyType), null);
}
}
And that is what I tried, but it doesn't work, "Erreur 540 'InstaSharp.Mapper.Map(string)' is not accessible" (something like this, I translated the error myself)
And this one "Error 539 The type or namespace name 'media' is not found ( a using directive or an assembly reference is it missing ?"
InstaSharp.Model.Media media = new InstaSharp.Model.Media();
InstaSharp.Mapper.Map<media>(reponse);
I really don't know how to use the Mapper class because I never saw a class like that (I started to use C# only 6 month ago)
The Map<T> method is expecting a type, so the type of media, not the instance media. Your usage should be something like:
InstaSharp.Mapper.Map<InstaSharp.Model.Media>(response);
As for what the class does, it exposes one static method Map<T>(string) and looks like it uses JSON.net to deserialize the string parameter into an instance of the type T with data values mapped from the JSON.
If you wanted a List<T> of Media, you'd do much the same thing as before:
InstaSharp.Mapper.Map<List<InstaSharp.Model.Media>>(response);
Lastly, because the result of Map<T> is an object, you'll need to cast it or use a dynamic variable to actually do anything.
var x = (List<Model.Media>)Mapper.Map<List<Model.Media>>(response);
To specify what class properties map to what json property, you'll need to add the Model.JsonMapping attribute to your properties and specify the MapTo attribute option.
[Model.JsonMapping(MapTo="someJsonProperty")]
public string SomeProperty {get;set;}
I've written a custom error message localization logic in my custom DataAnnotationsModelMetadataProvider class. It's working just fine with build-in StringLengthAttribute or RequiredAttribute validation error messages. But i am having trouble with my custom derived RegularExpressionAttribute classes. The logic i am using is something like below :
public class AccountNameFormatAttribute : RegularExpressionAttribute {
public AccountNameFormatAttribute()
: base(Linnet.Core.Shared.RegExPatterns.AccountNamePattern) {
}
public override string FormatErrorMessage(string name) {
return string.Format("{0} field must contain only letters, numbers or | . | - | _ | characters.", name);
}
}
public class SignUpViewModel {
[AccountNameFormat()]
[StringLength(16, MinimumLength = 3)]
[Required]
[DisplayName("Account Name")]
public string AccountName { get; set; }
[Required]
[DisplayName("Password")]
[StringLength(32, MinimumLength = 6)]
[DataType(System.ComponentModel.DataAnnotations.DataType.Password)]
public string Password { get; set; }
// .... and other properties, quite similar ... //
}
public class MvcDataAnnotationsModelValidatorProvider : DataAnnotationsModelValidatorProvider {
protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes) {
MyMvcController myMvcController = context.Controller as MyMvcController; /* custom mvc controller, that contains methods for wcf service activations and common properties. */
if (myMvcController == null) {
return base.GetValidators(metadata, context, attributes);
}
List<Attribute> newAttributes = new List<Attribute>();
foreach (Attribute att in attributes) {
if (att.GetType() != typeof(ValidationAttribute) && !att.GetType().IsSubclassOf(typeof(ValidationAttribute))) {
// if this is not a validation attribute, do nothing.
newAttributes.Add(att);
continue;
}
ValidationAttribute validationAtt = att as ValidationAttribute;
if (!string.IsNullOrWhiteSpace(validationAtt.ErrorMessageResourceName) && validationAtt.ErrorMessageResourceType != null) {
// if resource key and resource type is already set, do nothing.
newAttributes.Add(validationAtt);
continue;
}
string translationKey = "MvcModelMetaData.ValidationMessages." + metadata.ModelType.Name + (metadata.PropertyName != null ? "." + metadata.PropertyName : string.Empty) + "." + validationAtt.GetType().Name;
string originalText = validationAtt.FormatErrorMessage("{0}"); /* non-translated default english text */
// clonning current attiribute into a new attribute
// not to ruin original attribute for later usage
// using Activator.CreateInstance and then mapping with AutoMapper inside..
var newAtt = this.CloneValidationAttiribute(validationAtt);
// fetching translation from database via WCF service...
// At this point, i can see error strings are always translated.
// And it works perfect with [Required], [StringLength] and [DataType] attributes.
// But somehow it does not work with my AccountNameFormatAttribute on the web page, even if i give it the translated text as expected..
// Even if its ErrorMessage is already set to translated text,
// it still displays the original english text from the overridden FormatErrorMessage() method on the web page.
// It is the same both with client side validation or server side validation.
// Seems like it does not care the ErrorMessage that i manually set.
newAtt.ErrorMessage = myMvcController.Translations.GetTranslation(translationKey, originalText);
newAttributes.Add(newAtt);
}
IEnumerable<ModelValidator> result = base.GetValidators(metadata, context, newAttributes);
return result;
}
private ValidationAttribute CloneValidationAttiribute(ValidationAttribute att) {
if (att == null) {
return null;
}
Type attType = att.GetType();
ConstructorInfo[] constructorInfos = attType.GetConstructors();
if (constructorInfos == null || constructorInfos.Length <= 0) {
// can not close..
return att;
}
if (constructorInfos.Any(ci => ci.GetParameters().Length <= 0)) {
// clone with no constructor paramters.
return CloneManager.CloneObject(att) as ValidationAttribute;
}
// Validation attributes that needs constructor paramters...
if (attType == typeof(StringLengthAttribute)) {
int maxLength = ((StringLengthAttribute)att).MaximumLength;
return CloneManager.CloneObject(att, maxLength) as StringLengthAttribute;
}
return att;
}
}
public class CloneManager {
public static object CloneObject(object input) {
return CloneObject(input, null);
}
public static object CloneObject(object input, params object[] constructorParameters) {
if (input == null) {
return null;
}
Type type = input.GetType();
if (type.IsValueType) {
return input;
}
ConstructorInfo[] constructorInfos = type.GetConstructors();
if (constructorInfos == null || constructorInfos.Length <= 0) {
throw new LinnetException("0b59079b-3dc4-4763-b26d-651bde93ba56", "Object type does not have any constructors.", false);
}
if ((constructorParameters == null || constructorParameters.Length <= 0) && !constructorInfos.Any(ci => ci.GetParameters().Length <= 0)) {
throw new LinnetException("f03be2b9-b629-4a72-b025-c7a87924d9a4", "Object type does not have any constructor without parameters.", false);
}
object newObject = null;
if (constructorParameters == null || constructorParameters.Length <= 0) {
newObject = Activator.CreateInstance(type);
} else {
newObject = Activator.CreateInstance(type, constructorParameters);
}
return MapProperties(input, newObject);
}
private static object MapProperties(object source, object destination) {
if (source == null) {
return null;
}
Type type = source.GetType();
if (type != destination.GetType()) {
throw new LinnetException("e67bccfb-235f-42fc-b6b9-55f454c705a8", "Use 'MapProperties' method only for object with same types.", false);
}
if (type.IsValueType) {
return source;
}
var typeMap = AutoMapper.Mapper.FindTypeMapFor(type, type);
if (typeMap == null) {
AutoMapper.Mapper.CreateMap(type, type);
}
AutoMapper.Mapper.Map(source, destination, type, type);
return destination;
}
}
Seems like my logic was actually an odd approach.
I've discovered making custom DataAnnotationsModelValidators for each type of validation attiributes. And then translating the ErrorMessages inside Validate() and GetClientValidationRules() methods.
public class MvcRegularExpressionAttributeAdapter : RegularExpressionAttributeAdapter {
public MvcRegularExpressionAttributeAdapter(ModelMetadata metadata, ControllerContext context, RegularExpressionAttribute attribute)
: base(metadata, context, attribute) {
}
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules() {
return MvcValidationResultsTranslation.TranslateClientValidationRules(base.GetClientValidationRules(), this.Metadata, this.ControllerContext, this.Attribute);
}
public override IEnumerable<ModelValidationResult> Validate(object container) {
return MvcValidationResultsTranslation.TranslateValidationResults(base.Validate(container), this.Metadata, this.ControllerContext, this.Attribute);
}
}
public class MvcValidationResultsTranslation {
public static IEnumerable<ModelClientValidationRule> TranslateClientValidationRules(IEnumerable<ModelClientValidationRule> validationRules, ModelMetadata metadata, ControllerContext context, ValidationAttribute validationAttribute) {
if (validationRules == null) {
return validationRules;
}
MvcController mvcController = context.Controller as MvcController;
if (mvcController == null) {
return validationRules;
}
if (!string.IsNullOrWhiteSpace(validationAttribute.ErrorMessageResourceName) && validationAttribute.ErrorMessageResourceType != null) {
// if resource key and resource type is set, do not override..
return validationRules;
}
string translatedText = GetTranslation(metadata, mvcController, validationAttribute);
foreach (var validationRule in validationRules) {
List<string> msgParams = new List<string>();
msgParams.Add(!string.IsNullOrEmpty(metadata.DisplayName) ? metadata.DisplayName : metadata.PropertyName);
if (validationRule.ValidationParameters != null) {
msgParams.AddRange(validationRule.ValidationParameters.Where(p => p.Value.GetType().IsValueType || p.Value.GetType().IsEnum).Select(p => p.Value.ToString()));
}
validationRule.ErrorMessage = string.Format(translatedText, msgParams.ToArray());
}
return validationRules;
}
public static IEnumerable<ModelValidationResult> TranslateValidationResults(IEnumerable<ModelValidationResult> validationResults, ModelMetadata metadata, ControllerContext context, ValidationAttribute validationAttribute) {
if (validationResults == null) {
return validationResults;
}
MvcController mvcController = context.Controller as MvcController;
if (mvcController == null) {
return validationResults;
}
if (!string.IsNullOrWhiteSpace(validationAttribute.ErrorMessageResourceName) && validationAttribute.ErrorMessageResourceType != null) {
// if resource key and resource type is set, do not override..
return validationResults;
}
string translatedText = GetTranslation(metadata, mvcController, validationAttribute);
List<ModelValidationResult> newValidationResults = new List<ModelValidationResult>();
foreach (var validationResult in validationResults) {
ModelValidationResult newValidationResult = new ModelValidationResult();
newValidationResult.Message = string.Format(translatedText, (!string.IsNullOrEmpty(metadata.DisplayName) ? metadata.DisplayName : metadata.PropertyName));
newValidationResults.Add(newValidationResult);
}
return newValidationResults;
}
}
You can use my Griffin.MvcContrib to get easier localization.
Use nuget to download griffin.mvccontrib
Define a string table as described here.
Use the regular [RegularExpression] attribute directly in your view model.
Add this to your string table:
SignUpViewModel_AccountName_RegularExpression "{0} field must contain only letters, numbers or | . | - | _ | characters.
That's it..
I made a test website to debug an issue I'm having, and it appears that either I'm passing in the JSON data wrong or MVC just can't bind nullable longs. I'm using the latest MVC 3 release, of course.
public class GetDataModel
{
public string TestString { get; set; }
public long? TestLong { get; set; }
public int? TestInt { get; set; }
}
[HttpPost]
public ActionResult GetData(GetDataModel model)
{
// Do stuff
}
I'm posting a JSON string with the correct JSON content type:
{ "TestString":"test", "TestLong":12345, "TestInt":123 }
The long isn't bound, it's always null. It works if I put the value in quotes, but I shouldn't have to do that, should I? Do I need to have a custom model binder for that value?
I created a testproject just to test this. I put your code into my HomeController and added this to index.cshtml:
<script type="text/javascript">
$(function () {
$.post('Home/GetData', { "TestString": "test", "TestLong": 12345, "TestInt": 123 });
});
</script>
I put a breakpoint in the GetData method, and the values were binded to the model like they should:
So I think there's something wrong with the way you send the values. Are you sure the "TestLong" value is actually sent over the wire? You can check this using Fiddler.
If you don't want to go with Regex and you only care about fixing long?, the following will also fix the problem:
public class JsonModelBinder : DefaultModelBinder {
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder)
{
var propertyType = propertyDescriptor.PropertyType;
if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
{
var provider = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (provider != null
&& provider.RawValue != null
&& Type.GetTypeCode(provider.RawValue.GetType()) == TypeCode.Int32)
{
var value = new System.Web.Script.Serialization.JavaScriptSerializer().Deserialize(provider.AttemptedValue, bindingContext.ModelMetadata.ModelType);
return value;
}
}
return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder);
}
}
My colleague came up with a workaround for this. The solution is to take the input stream and use a Regex to wrap all numeric variables in quotes to trick the JavaScriptSerializer into deserialising the longs properly. It's not a perfect solution, but it takes care of the issue.
This is done in a custom model binder. I used Posting JSON Data to ASP.NET MVC as an example. You have to take care, though, if the input stream is accessed anywhere else.
public class JsonModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (!IsJSONRequest(controllerContext))
return base.BindModel(controllerContext, bindingContext);
// Get the JSON data that's been posted
var jsonStringData = new StreamReader(controllerContext.HttpContext.Request.InputStream).ReadToEnd();
// Wrap numerics
jsonStringData = Regex.Replace(jsonStringData, #"(?<=:)\s{0,4}(?<num>[\d\.]+)\s{0,4}(?=[,|\]|\}]+)", "\"${num}\"");
// Use the built-in serializer to do the work for us
return new JavaScriptSerializer().Deserialize(jsonStringData, bindingContext.ModelMetadata.ModelType);
}
private static bool IsJSONRequest(ControllerContext controllerContext)
{
var contentType = controllerContext.HttpContext.Request.ContentType;
return contentType.Contains("application/json");
}
}
Then put this in the Global:
ModelBinders.Binders.DefaultBinder = new JsonModelBinder();
Now the long gets bound successfully. I would call this a bug in the JavaScriptSerializer. Also note that arrays of longs or nullable longs get bound just fine without the quotes.
You can use this model binder class
public class LongModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (string.IsNullOrEmpty(valueResult.AttemptedValue))
{
return (long?)null;
}
var modelState = new ModelState { Value = valueResult };
object actualValue = null;
try
{
actualValue = Convert.ToInt64(
valueResult.AttemptedValue,
CultureInfo.InvariantCulture
);
}
catch (FormatException e)
{
modelState.Errors.Add(e);
}
bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
return actualValue;
}
}
In Global.asax Application_Start add these lines
ModelBinders.Binders.Add(typeof(long), new LongModelBinder());
ModelBinders.Binders.Add(typeof(long?), new LongModelBinder());
I wanted to incorporate the solution presented by Edgar but still have the features of the DefaultModelBinder. So instead of creating a new model binder I went with a different approach and replaced the JsonValueProviderFactory with a custom one. There's only a minor change in the code from the original MVC3 source code:
public sealed class NumericJsonValueProviderFactory : ValueProviderFactory
{
private static void AddToBackingStore(Dictionary<string, object> backingStore, string prefix, object value)
{
IDictionary<string, object> d = value as IDictionary<string, object>;
if (d != null)
{
foreach (KeyValuePair<string, object> entry in d)
{
AddToBackingStore(backingStore, MakePropertyKey(prefix, entry.Key), entry.Value);
}
return;
}
IList l = value as IList;
if (l != null)
{
for (int i = 0; i < l.Count; i++)
{
AddToBackingStore(backingStore, MakeArrayKey(prefix, i), l[i]);
}
return;
}
// primitive
backingStore[prefix] = value;
}
private static object GetDeserializedObject(ControllerContext controllerContext)
{
if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
{
// not JSON request
return null;
}
StreamReader reader = new StreamReader(controllerContext.HttpContext.Request.InputStream);
string bodyText = reader.ReadToEnd();
if (String.IsNullOrEmpty(bodyText))
{
// no JSON data
return null;
}
JavaScriptSerializer serializer = new JavaScriptSerializer();
// below is the code that Edgar proposed and the only change to original source code
bodyText = Regex.Replace(bodyText, #"(?<=:)\s{0,4}(?<num>[\d\.]+)\s{0,4}(?=[,|\]|\}]+)", "\"${num}\"");
object jsonData = serializer.DeserializeObject(bodyText);
return jsonData;
}
public override IValueProvider GetValueProvider(ControllerContext controllerContext)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
object jsonData = GetDeserializedObject(controllerContext);
if (jsonData == null)
{
return null;
}
Dictionary<string, object> backingStore = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
AddToBackingStore(backingStore, String.Empty, jsonData);
return new DictionaryValueProvider<object>(backingStore, CultureInfo.CurrentCulture);
}
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;
}
}
Then to register the new value provider you need to add the following lines to your Global.asax:
ValueProviderFactories.Factories.Remove(ValueProviderFactories.Factories.OfType<JsonValueProviderFactory>().FirstOrDefault());
ValueProviderFactories.Factories.Add(new NumericJsonValueProviderFactory());
Given the following objects:
public class Customer {
public String Name { get; set; }
public String Address { get; set; }
}
public class Invoice {
public String ID { get; set; }
public DateTime Date { get; set; }
public Customer BillTo { get; set; }
}
I'd like to use reflection to go through the Invoice to get the Name property of a Customer. Here's what I'm after, assuming this code would work:
Invoice inv = GetDesiredInvoice(); // magic method to get an invoice
PropertyInfo info = inv.GetType().GetProperty("BillTo.Address");
Object val = info.GetValue(inv, null);
Of course, this fails since "BillTo.Address" is not a valid property of the Invoice class.
So, I tried writing a method to split the string into pieces on the period, and walk the objects looking for the final value I was interested in. It works okay, but I'm not entirely comfortable with it:
public Object GetPropValue(String name, Object obj) {
foreach (String part in name.Split('.')) {
if (obj == null) { return null; }
Type type = obj.GetType();
PropertyInfo info = type.GetProperty(part);
if (info == null) { return null; }
obj = info.GetValue(obj, null);
}
return obj;
}
Any ideas on how to improve this method, or a better way to solve this problem?
EDIT after posting, I saw a few related posts... There doesn't seem to be an answer that specifically addresses this question, however. Also, I'd still like the feedback on my implementation.
I use following method to get the values from (nested classes) properties like
"Property"
"Address.Street"
"Address.Country.Name"
public static object GetPropertyValue(object src, string propName)
{
if (src == null) throw new ArgumentException("Value cannot be null.", "src");
if (propName == null) throw new ArgumentException("Value cannot be null.", "propName");
if(propName.Contains("."))//complex type nested
{
var temp = propName.Split(new char[] { '.' }, 2);
return GetPropertyValue(GetPropertyValue(src, temp[0]), temp[1]);
}
else
{
var prop = src.GetType().GetProperty(propName);
return prop != null ? prop.GetValue(src, null) : null;
}
}
Here is the Fiddle: https://dotnetfiddle.net/PvKRH0
I know I'm a bit late to the party, and as others said, your implementation is fine
...for simple use cases.
However, I've developed a library that solves exactly that use case, Pather.CSharp.
It is also available as Nuget Package.
Its main class is Resolver with its Resolve method.
You pass it an object and the property path, and it will return the desired value.
Invoice inv = GetDesiredInvoice(); // magic method to get an invoice
var resolver = new Resolver();
object result = resolver.Resolve(inv, "BillTo.Address");
But it can also resolve more complex property paths, including array and dictionary access.
So, for example, if your Customer had multiple addresses
public class Customer {
public String Name { get; set; }
public IEnumerable<String> Addresses { get; set; }
}
you could access the second one using Addresses[1].
Invoice inv = GetDesiredInvoice(); // magic method to get an invoice
var resolver = new Resolver();
object result = resolver.Resolve(inv, "BillTo.Addresses[1]");
I actually think your logic is fine. Personally, I would probably change it around so you pass the object as the first parameter (which is more inline with PropertyInfo.GetValue, so less surprising).
I also would probably call it something more like GetNestedPropertyValue, to make it obvious that it searches down the property stack.
You have to access the ACTUAL object that you need to use reflection on. Here is what I mean:
Instead of this:
Invoice inv = GetDesiredInvoice(); // magic method to get an invoice
PropertyInfo info = inv.GetType().GetProperty("BillTo.Address");
Object val = info.GetValue(inv, null);
Do this (edited based on comment):
Invoice inv = GetDesiredInvoice(); // magic method to get an invoice
PropertyInfo info = inv.GetType().GetProperty("BillTo");
Customer cust = (Customer)info.GetValue(inv, null);
PropertyInfo info2 = cust.GetType().GetProperty("Address");
Object val = info2.GetValue(cust, null);
Look at this post for more information:
Using reflection to set a property of a property of an object
In hopes of not sounding too late to the party, I would like to add my solution:
Definitely use recursion in this situation
public static Object GetPropValue(String name, object obj, Type type)
{
var parts = name.Split('.').ToList();
var currentPart = parts[0];
PropertyInfo info = type.GetProperty(currentPart);
if (info == null) { return null; }
if (name.IndexOf(".") > -1)
{
parts.Remove(currentPart);
return GetPropValue(String.Join(".", parts), info.GetValue(obj, null), info.PropertyType);
} else
{
return info.GetValue(obj, null).ToString();
}
}
You don't explain the source of your "discomfort," but your code basically looks sound to me.
The only thing I'd question is the error handling. You return null if the code tries to traverse through a null reference or if the property name doesn't exist. This hides errors: it's hard to know whether it returned null because there's no BillTo customer, or because you misspelled it "BilTo.Address"... or because there is a BillTo customer, and its Address is null! I'd let the method crash and burn in these cases -- just let the exception escape (or maybe wrap it in a friendlier one).
Here is another implementation that will skip a nested property if it is an enumerator and continue deeper. Properties of type string are not affected by the Enumeration Check.
public static class ReflectionMethods
{
public static bool IsNonStringEnumerable(this PropertyInfo pi)
{
return pi != null && pi.PropertyType.IsNonStringEnumerable();
}
public static bool IsNonStringEnumerable(this object instance)
{
return instance != null && instance.GetType().IsNonStringEnumerable();
}
public static bool IsNonStringEnumerable(this Type type)
{
if (type == null || type == typeof(string))
return false;
return typeof(IEnumerable).IsAssignableFrom(type);
}
public static Object GetPropValue(String name, Object obj)
{
foreach (String part in name.Split('.'))
{
if (obj == null) { return null; }
if (obj.IsNonStringEnumerable())
{
var toEnumerable = (IEnumerable)obj;
var iterator = toEnumerable.GetEnumerator();
if (!iterator.MoveNext())
{
return null;
}
obj = iterator.Current;
}
Type type = obj.GetType();
PropertyInfo info = type.GetProperty(part);
if (info == null) { return null; }
obj = info.GetValue(obj, null);
}
return obj;
}
}
based on this question and on
How to know if a PropertyInfo is a collection
by Berryl
I use this in a MVC project to dynamically Order my data by simply passing the Property to sort by
Example:
result = result.OrderBy((s) =>
{
return ReflectionMethods.GetPropValue("BookingItems.EventId", s);
}).ToList();
where BookingItems is a list of objects.
> Get Nest properties e.g., Developer.Project.Name
private static System.Reflection.PropertyInfo GetProperty(object t, string PropertName)
{
if (t.GetType().GetProperties().Count(p => p.Name == PropertName.Split('.')[0]) == 0)
throw new ArgumentNullException(string.Format("Property {0}, is not exists in object {1}", PropertName, t.ToString()));
if (PropertName.Split('.').Length == 1)
return t.GetType().GetProperty(PropertName);
else
return GetProperty(t.GetType().GetProperty(PropertName.Split('.')[0]).GetValue(t, null), PropertName.Split('.')[1]);
}
if (info == null) { /* throw exception instead*/ }
I would actually throw an exception if they request a property that doesn't exist. The way you have it coded, if I call GetPropValue and it returns null, I don't know if that means the property didn't exist, or the property did exist but it's value was null.
public static string GetObjectPropertyValue(object obj, string propertyName)
{
bool propertyHasDot = propertyName.IndexOf(".") > -1;
string firstPartBeforeDot;
string nextParts = "";
if (!propertyHasDot)
firstPartBeforeDot = propertyName.ToLower();
else
{
firstPartBeforeDot = propertyName.Substring(0, propertyName.IndexOf(".")).ToLower();
nextParts = propertyName.Substring(propertyName.IndexOf(".") + 1);
}
foreach (var property in obj.GetType().GetProperties())
if (property.Name.ToLower() == firstPartBeforeDot)
if (!propertyHasDot)
if (property.GetValue(obj, null) != null)
return property.GetValue(obj, null).ToString();
else
return DefaultValue(property.GetValue(obj, null), propertyName).ToString();
else
return GetObjectPropertyValue(property.GetValue(obj, null), nextParts);
throw new Exception("Property '" + propertyName.ToString() + "' not found in object '" + obj.ToString() + "'");
}
I wanted to share my solution although it may be too late. This solution is primarily to check if the nested property exists. But it can be easily tweaked to return the property value if needed.
private static PropertyInfo _GetPropertyInfo(Type type, string propertyName)
{
//***
//*** Check if the property name is a complex nested type
//***
if (propertyName.Contains("."))
{
//***
//*** Get the first property name of the complex type
//***
var tempPropertyName = propertyName.Split(".", 2);
//***
//*** Check if the property exists in the type
//***
var prop = _GetPropertyInfo(type, tempPropertyName[0]);
if (prop != null)
{
//***
//*** Drill down to check if the nested property exists in the complex type
//***
return _GetPropertyInfo(prop.PropertyType, tempPropertyName[1]);
}
else
{
return null;
}
}
else
{
return type.GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
}
}
I had to refer to few posts to come up with this solution. I think this will work for multiple nested property types.
My internet connection was down when I need to solve the same problem, so I had to 're-invent the wheel':
static object GetPropertyValue(Object fromObject, string propertyName)
{
Type objectType = fromObject.GetType();
PropertyInfo propInfo = objectType.GetProperty(propertyName);
if (propInfo == null && propertyName.Contains('.'))
{
string firstProp = propertyName.Substring(0, propertyName.IndexOf('.'));
propInfo = objectType.GetProperty(firstProp);
if (propInfo == null)//property name is invalid
{
throw new ArgumentException(String.Format("Property {0} is not a valid property of {1}.", firstProp, fromObject.GetType().ToString()));
}
return GetPropertyValue(propInfo.GetValue(fromObject, null), propertyName.Substring(propertyName.IndexOf('.') + 1));
}
else
{
return propInfo.GetValue(fromObject, null);
}
}
Pretty sure this solves the problem for any string you use for property name, regardless of extent of nesting, as long as everything's a property.
Based on the original code from #jheddings, I have created a extension method version with generic type and verifications:
public static T GetPropertyValue<T>(this object sourceObject, string propertyName)
{
if (sourceObject == null) throw new ArgumentNullException(nameof(sourceObject));
if (string.IsNullOrWhiteSpace(propertyName)) throw new ArgumentException(nameof(propertyName));
foreach (string currentPropertyName in propertyName.Split('.'))
{
if (string.IsNullOrWhiteSpace(currentPropertyName)) throw new InvalidOperationException($"Invalid property '{propertyName}'");
PropertyInfo propertyInfo = sourceObject.GetType().GetProperty(currentPropertyName);
if (propertyInfo == null) throw new InvalidOperationException($"Property '{currentPropertyName}' not found");
sourceObject = propertyInfo.GetValue(sourceObject);
}
return sourceObject is T result ? result : default;
}
I wrote a method that received one object type as the argument from the input and returns dictionary<string,string>
public static Dictionary<string, string> GetProperties(Type placeHolderType)
{
var result = new Dictionary<string, string>();
var properties = placeHolderType.GetProperties();
foreach (var propertyInfo in properties)
{
string name = propertyInfo.Name;
string description = GetDescriptionTitle(propertyInfo);
if (IsNonString(propertyInfo.PropertyType))
{
var list = GetProperties(propertyInfo.PropertyType);
foreach (var item in list)
{
result.Add($"{propertyInfo.PropertyType.Name}_{item.Key}", item.Value);
}
}
else
{
result.Add(name, description);
}
}
return result;
}
public static bool IsNonString(Type type)
{
if (type == null || type == typeof(string))
return false;
return typeof(IPlaceHolder).IsAssignableFrom(type);
}
private static string GetDescriptionTitle(MemberInfo memberInfo)
{
if (Attribute.GetCustomAttribute(memberInfo, typeof(DescriptionAttribute)) is DescriptionAttribute descriptionAttribute)
{
return descriptionAttribute.Description;
}
return memberInfo.Name;
}
public static object GetPropertyValue(object src, string propName)
{
if (src == null) throw new ArgumentException("Value cannot be null.", "src");
if (propName == null) throw new ArgumentException("Value cannot be null.", "propName");
var prop = src.GetType().GetProperty(propName);
if (prop != null)
{
return prop.GetValue(src, null);
}
else
{
var props = src.GetType().GetProperties();
foreach (var property in props)
{
var propInfo = src.GetType().GetProperty(property.Name);
if (propInfo != null)
{
var propVal = propInfo.GetValue(src, null);
if (src.GetType().GetProperty(property.Name).PropertyType.IsClass)
{
return GetPropertyValue(propVal, propName);
}
return propVal;
}
}
return null;
}
usage: calling part
var emp = new Employee() { Person = new Person() { FirstName = "Ashwani" } };
var val = GetPropertyValue(emp, "FirstName");
above can search the property value at any level
Try inv.GetType().GetProperty("BillTo+Address");