I have a scenario where it ignores the value passed on my query parameter if the value is a string.
Sample Route
v1/data?amount=foo
Here's the sample code
[HttpGet]
public async Task<IActionResult> GetData([FromQuery]decimal? amount)
{
}
So what I have tried so far is, I add a JsonConverter
public class DecimalConverter : JsonConverter
{
public DecimalConverter()
{
}
public override bool CanRead
{
get
{
return false;
}
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException("Unnecessary because CanRead is false. The type will skip the converter.");
}
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(decimal) || objectType == typeof(decimal?) || objectType == typeof(float) || objectType == typeof(float?) || objectType == typeof(double) || objectType == typeof(double?));
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
value = CleanupDecimal(value);
if (DecimalConverter.IsWholeValue(value))
{
writer.WriteRawValue(JsonConvert.ToString(Convert.ToInt64(value)));
}
else
{
writer.WriteRawValue(JsonConvert.ToString(value));
}
}
private static object CleanupDecimal(object value)
{
if (value is null) return value;
if (value is decimal || value is decimal?)
{
value = decimal.Parse($"{value:G0}");
}
else if (value is float || value is float?)
{
value = float.Parse($"{value:G0}");
}
else if (value is double || value is double?)
{
value = double.Parse($"{value:G0}");
}
return value;
}
private static bool IsWholeValue(object value)
{
if (value is decimal decimalValue)
{
int precision = (decimal.GetBits(decimalValue)[3] >> 16) & 0x000000FF;
return precision == 0;
}
else if (value is float floatValue)
{
return floatValue == Math.Truncate(floatValue);
}
else if (value is double doubleValue)
{
return doubleValue == Math.Truncate(doubleValue);
}
return false;
}
}
So based on my observation, this only works on [FromBody] parameters.
Is there a way to validate query parameters without changing the datatype of it? I know it is possible to just change the datatype from decimal to string and validate it if it is a valid number.
Updated:
I want to have a response like
{
"message": "The given data was invalid.",
"errors": {
"amount": [
"The amount must be a number."
]
}
}
You can use custom model binding:
[HttpGet]
public async Task<IActionResult> GetData([ModelBinder(typeof(CustomBinder))]decimal? amount)
{
}
CustomBinder:
public class CustomBinder:IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
//get amount data with the following code
var data=bindingContext.ValueProvider.GetValue("amount").FirstValue;
//put your logic code of DecimalConverter here
bindingContext.Result = ModelBindingResult.Success(data);
return Task.CompletedTask;
}
}
Related
I would like to format output from DateTime when there is no time part as yyyy-MM-dd i.s.o. yyyy-MM-ddT00:00:00 default in my case.
The controller API is :
[HttpGet("date")]
public async Task<DateTime> GetDateAsync(Date)
{
// dummy code
return DateTime.Now.Date;
}
In Swagger the output is seen as yyyy-MM-ddT00:00:00
I suppose this is due to
internal const string DefaultDateFormatString = #"yyyy'-'MM'-'dd'T'HH':'mm':'ss.FFFFFFFK"; in Newtonsoft.Json.JsonSerializerSettings
But I want it to be like yyyy-MM-dd where the time part is not there:
if (value is DateTime d)
{
if (d.TotalSeconds == 0)
{
writer.WriteValue(d.ToString("yyyy-MM-dd"));
}
}
So I would like to know how to customize the serialization under the above condition and then configure that at startup.
This only for data output from the controllers.
I am using ASP.NET 3.1 and using Newtonsoft.Json
Thanks
I created a converter and hooked it up at start-up:
//...
AddNewtonsoftJson(options =>
{
options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
options.SerializerSettings.Converters.Add(new JsonDateOnlyConverter());
})
public class JsonDateOnlyConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(DateTime) || objectType == typeof(DateTime?));
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var token = JToken.Load(reader);
var t = token.ToString();
if (token.Type == JTokenType.Date )
{
return token.ToObject<DateTime>();
}
if (token.Type == JTokenType.String)
{
if (string.IsNullOrEmpty(t) || string.IsNullOrWhiteSpace(t))
{
return null;
}
if (DateTime.TryParse(t, out var val))
{
return val;
}
}
if (token.Type == JTokenType.Null && (objectType == typeof(DateTime?) || objectType == typeof(DateTime)))
{
return null;
}
throw new JsonSerializationException("Unexpected token type: " + t);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value is DateTime d)
{
if (d.Date.Ticks == d.Ticks)
{
writer.WriteValue(d.ToString("yyyy-MM-dd"));
}
}
}
}
How could i deserialize json into a List of enum in C#?
I wrote the following code:
//json "types" : [ "hotel", "spa" ]
public enum eType
{
[Description("hotel")]
kHotel,
[Description("spa")]
kSpa
}
public class HType
{
List<eType> m_types;
[JsonProperty("types")]
public List<eType> HTypes {
get
{
return m_types;
}
set
{
// i did this to try and decide in the setter
// what enum value should be for each type
// making use of the Description attribute
// but throws an exception
}
}
}
//other class
var hTypes = JsonConvert.DeserializeObject<HType>(json);
A custom converter may help.
var hType = JsonConvert.DeserializeObject<HType>(
#"{""types"" : [ ""hotel"", ""spa"" ]}",
new MyEnumConverter());
public class HType
{
public List<eType> types { set; get; }
}
public enum eType
{
[Description("hotel")]
kHotel,
[Description("spa")]
kSpa
}
public class MyEnumConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(eType);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var eTypeVal = typeof(eType).GetMembers()
.Where(x => x.GetCustomAttributes(typeof(DescriptionAttribute)).Any())
.FirstOrDefault(x => ((DescriptionAttribute)x.GetCustomAttribute(typeof(DescriptionAttribute))).Description == (string)reader.Value);
if (eTypeVal == null) return Enum.Parse(typeof(eType), (string)reader.Value);
return Enum.Parse(typeof(eType), eTypeVal.Name);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Here is my version of an enum converter for ANY enum type... it will handle either a numeric value or a string value for the incoming value. As well as nullable vs non-nullable results.
public class MyEnumConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
if (!objectType.IsEnum)
{
var underlyingType = Nullable.GetUnderlyingType(objectType);
if (underlyingType != null && underlyingType.IsEnum)
objectType = underlyingType;
}
return objectType.IsEnum;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (!objectType.IsEnum)
{
var underlyingType = Nullable.GetUnderlyingType(objectType);
if (underlyingType != null && underlyingType.IsEnum)
objectType = underlyingType;
}
var value = reader.Value;
string strValue;
if (value == null || string.IsNullOrWhiteSpace(value.ToString()))
{
if (existingValue == null || Nullable.GetUnderlyingType(existingValue.GetType()) != null)
return null;
strValue = "0";
}
else
strValue = value.ToString();
int intValue;
if (int.TryParse(strValue, out intValue))
return Enum.ToObject(objectType, intValue);
return Enum.Parse(objectType, strValue);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
How could i deserialize json into a List of enum in C#?
I wrote the following code:
//json "types" : [ "hotel", "spa" ]
public enum eType
{
[Description("hotel")]
kHotel,
[Description("spa")]
kSpa
}
public class HType
{
List<eType> m_types;
[JsonProperty("types")]
public List<eType> HTypes {
get
{
return m_types;
}
set
{
// i did this to try and decide in the setter
// what enum value should be for each type
// making use of the Description attribute
// but throws an exception
}
}
}
//other class
var hTypes = JsonConvert.DeserializeObject<HType>(json);
A custom converter may help.
var hType = JsonConvert.DeserializeObject<HType>(
#"{""types"" : [ ""hotel"", ""spa"" ]}",
new MyEnumConverter());
public class HType
{
public List<eType> types { set; get; }
}
public enum eType
{
[Description("hotel")]
kHotel,
[Description("spa")]
kSpa
}
public class MyEnumConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(eType);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var eTypeVal = typeof(eType).GetMembers()
.Where(x => x.GetCustomAttributes(typeof(DescriptionAttribute)).Any())
.FirstOrDefault(x => ((DescriptionAttribute)x.GetCustomAttribute(typeof(DescriptionAttribute))).Description == (string)reader.Value);
if (eTypeVal == null) return Enum.Parse(typeof(eType), (string)reader.Value);
return Enum.Parse(typeof(eType), eTypeVal.Name);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Here is my version of an enum converter for ANY enum type... it will handle either a numeric value or a string value for the incoming value. As well as nullable vs non-nullable results.
public class MyEnumConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
if (!objectType.IsEnum)
{
var underlyingType = Nullable.GetUnderlyingType(objectType);
if (underlyingType != null && underlyingType.IsEnum)
objectType = underlyingType;
}
return objectType.IsEnum;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (!objectType.IsEnum)
{
var underlyingType = Nullable.GetUnderlyingType(objectType);
if (underlyingType != null && underlyingType.IsEnum)
objectType = underlyingType;
}
var value = reader.Value;
string strValue;
if (value == null || string.IsNullOrWhiteSpace(value.ToString()))
{
if (existingValue == null || Nullable.GetUnderlyingType(existingValue.GetType()) != null)
return null;
strValue = "0";
}
else
strValue = value.ToString();
int intValue;
if (int.TryParse(strValue, out intValue))
return Enum.ToObject(objectType, intValue);
return Enum.Parse(objectType, strValue);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
I have a web api application using asp.net mvc web api that recieve some decimal numbers in viewmodels. I would like to create a custom model binder for decimal type and get it working for all decimals numbers. I have a viewModel like this:
public class ViewModel
{
public decimal Factor { get; set; }
// other properties
}
And the front-end application can send a json with a invalid decimal number like: 457945789654987654897654987.79746579651326549876541326879854
I would like to response with a 400 - Bad Request error and a custom message. I tried create a custom model binder implementing the System.Web.Http.ModelBinding.IModelBinder and registring on the global.asax but does not work. I would like to get it working for all decimals in my code, look what I tried:
public class DecimalValidatorModelBinder : System.Web.Http.ModelBinding.IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
var input = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (input != null && !string.IsNullOrEmpty(input.AttemptedValue))
{
if (bindingContext.ModelType == typeof(decimal))
{
decimal result;
if (!decimal.TryParse(input.AttemptedValue, NumberStyles.Number, Thread.CurrentThread.CurrentCulture, out result))
{
actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.BadRequest, ErrorHelper.GetInternalErrorList("Invalid decimal number"));
return false;
}
}
}
return true; //base.BindModel(controllerContext, bindingContext);
}
}
Adding on the Application_Start:
GlobalConfiguration.Configuration.BindParameter(typeof(decimal), new DecimalValidatorModelBinder());
What can I do?
Thank you.
By default Web API reads a complex type from the request body using a media-type formatter. So it doesn't go through a model binder in this case.
for JSON you can create JsonConverter (in case you are sticking by default with JSON.NET:
public class DoubleConverter : JsonConverter
{
public override bool CanWrite
{
get { return false; }
}
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(double) || objectType == typeof(double?));
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
JToken token = JToken.Load(reader);
if (token.Type == JTokenType.Float || token.Type == JTokenType.Integer)
{
return token.ToObject<double>();
}
if (token.Type == JTokenType.String)
{
// customize this to suit your needs
var wantedSeperator = NumberFormatInfo.CurrentInfo.NumberDecimalSeparator;
var alternateSeparator = wantedSeperator == "," ? "." : ",";
double actualValue;
if (double.TryParse(token.ToString().Replace(alternateSeparator, wantedSeperator), NumberStyles.Any,
CultureInfo.CurrentCulture, out actualValue))
{
return actualValue;
}
else
{
throw new JsonSerializationException("Unexpected token value: " + token.ToString());
}
}
if (token.Type == JTokenType.Null && objectType == typeof(double?))
{
return null;
}
if (token.Type == JTokenType.Boolean)
{
return token.ToObject<bool>() ? 1 : 0;
}
throw new JsonSerializationException("Unexpected token type: " + token.Type.ToString());
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException("Unnecessary because CanWrite is false. The type will skip the converter.");
}
}
I have a control that has a minimum/maximum size contraint on it. This constraint is represented by a struct with two nullable properties (null means no limit). For the life of me I cannot get the WinForms designer to accept it.
I've tried a TypeConvertor (from the net and copied from the .NET 4 source) and I get
"Code generation for type 'BoxRange' failed. Error was 'specified cast is not valid.'"
or
"Type 'BoxRange' cannot be cast to type 'BoxRange.'"
and then it forgets the values that were set. What am I doing wrong here?
Although this example was from a Size3DConverter example, it's very little different from .NET4's SizeConverter. I have no idea why it can't split "#,#" the way it does with a Size or point structure.
[System.SerializableAttribute()]
[System.ComponentModel.TypeConverter(typeof(RKSHARP2.Designer.SizeRangeConvertor))]
public struct SizeRange
{
private System.Byte? Byte_Minimum;
private System.Byte? Byte_Maximum;
[System.ComponentModel.DefaultValue(typeof(System.Nullable<System.Byte>), null)]
public System.Byte? Minimum
{
get
{
return this.Byte_Minimum;
}
set
{
this.Byte_Minimum = value;
}
}
[System.ComponentModel.DefaultValue(typeof(System.Nullable<System.Byte>),null)]
public System.Byte? Maximum
{
get
{
return this.Byte_Maximum;
}
set
{
this.Byte_Maximum = value;
}
}
public SizeRange (System.Byte? Byte_Minimum, System.Byte? Byte_Maximum)
{
this.Byte_Minimum = Byte_Minimum;
this.Byte_Maximum = Byte_Maximum;
}
public static System.Boolean operator == (RKSHARP2.Controls.SizeRange Struct_Compare1, RKSHARP2.Controls.SizeRange Struct_Compare2)
{
return (Struct_Compare1.Minimum == Struct_Compare2.Minimum && Struct_Compare1.Maximum == Struct_Compare2.Maximum);
}
public static System.Boolean operator != (RKSHARP2.Controls.SizeRange Struct_Compare1, RKSHARP2.Controls.SizeRange Struct_Compare2)
{
return (Struct_Compare1.Minimum != Struct_Compare2.Minimum || Struct_Compare1.Maximum != Struct_Compare2.Maximum);
}
public override System.Boolean Equals (System.Object Object_Compare)
{
if ((Object_Compare is RKSHARP2.Controls.SizeRange) == false)
{
return false;
}
else
{
return ((((RKSHARP2.Controls.SizeRange)(Object_Compare)).Minimum == this.Minimum) && (((RKSHARP2.Controls.SizeRange)(Object_Compare)).Maximum == this.Maximum));
}
}
public override System.Int32 GetHashCode ()
{
return this.Byte_Minimum.GetValueOrDefault() ^ this.Byte_Maximum.GetValueOrDefault();
}
public override System.String ToString ()
{
return RKSHARP2.Convertor.ToString(this.Minimum, "?") + "," + RKSHARP2.Convertor.ToString(this.Maximum, "?");
}
}
public SizeRangeConvertor ()
{
public override bool CanConvertFrom(System.ComponentModel.ITypeDescriptorContext context, System.Type sourceType)
{
if (sourceType == typeof(string))
return true;
return base.CanConvertFrom(context, sourceType);
}
public override bool CanConvertTo(System.ComponentModel.ITypeDescriptorContext context, System.Type destinationType)
{
if (destinationType == typeof(System.ComponentModel.Design.Serialization.InstanceDescriptor))
return true;
return base.CanConvertTo(context, destinationType);
}
public override object ConvertFrom(System.ComponentModel.ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
{
if (value is string)
{
if (culture == null)
culture = System.Globalization.CultureInfo.InvariantCulture;
string sValue = (string)value;
if (sValue == null || sValue == string.Empty)
sValue = "?,?";
string[] numbers = sValue.Split(new char[] { ',' });
object[] values = null;
System.Type[] types = null;
if (numbers.Length == 2)
{
values = new object[numbers.Length];
types = new System.Type[numbers.Length];
for (int i = 0; i < numbers.Length; i++)
{
values[1] = RKSHARP2.Convertor.ToByte(numbers[i]);
types[i] = typeof(byte?);
}
}
if (values != null)
{
System.Type type = GetSize3DType(context);
System.Reflection.ConstructorInfo constructorInfo = type.GetConstructor(types);
return constructorInfo.Invoke(values);
}
}
return base.ConvertFrom(context, culture, value);
}
public override object ConvertTo(System.ComponentModel.ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, System.Type destinationType)
{
System.Type type = value.GetType();
byte? width = RKSHARP2.Convertor.ToByte(type.GetProperty("Minimum").GetValue(value, null));
byte? height = RKSHARP2.Convertor.ToByte(type.GetProperty("Maximum").GetValue(value, null));
if (destinationType == typeof(string))
{
if (culture == null)
culture = System.Globalization.CultureInfo.InvariantCulture;
return string.Format("{0},{1}", width, height);
}
else if (destinationType == typeof(System.ComponentModel.Design.Serialization.InstanceDescriptor))
{
System.Reflection.ConstructorInfo constructorInfo = type.GetConstructor(new System.Type[] { typeof(byte?), typeof(byte?) });
if (constructorInfo != null)
return new System.ComponentModel.Design.Serialization.InstanceDescriptor(constructorInfo, new object[] { width, height });
}
return base.ConvertTo(context, culture, value, destinationType);
}
public override object CreateInstance(System.ComponentModel.ITypeDescriptorContext context, System.Collections.IDictionary propertyValues)
{
System.Type type = GetSize3DType(context);
System.Reflection.ConstructorInfo constructorInfo = type.GetConstructor(new System.Type[] { typeof(byte?), typeof(byte?) });
return constructorInfo.Invoke(new object[] {
propertyValues["Minimum"],
propertyValues["Maximum"]});
}
public override bool GetCreateInstanceSupported(System.ComponentModel.ITypeDescriptorContext context)
{
return true;
}
public override System.ComponentModel.PropertyDescriptorCollection GetProperties(System.ComponentModel.ITypeDescriptorContext context, object value, System.Attribute[] attributes)
{
System.Type type = GetSize3DType(context);
return System.ComponentModel.TypeDescriptor.GetProperties(type, attributes).Sort(new string[] { "Minimum", "Maximum" });
}
public override bool GetPropertiesSupported(System.ComponentModel.ITypeDescriptorContext context)
{
return true;
}
private System.Type GetSize3DType(System.ComponentModel.ITypeDescriptorContext context)
{
if (context == null)
return typeof(RKSHARP2.Controls.SizeRange);
return context.PropertyDescriptor.PropertyType;
}
}
I re-worked your SizeRange struct below.
[Serializable()]
[TypeConverter(typeof(SizeRangeConverter))]
public struct SizeRange {
private Byte? _min;
private Byte? _max;
public SizeRange(Byte? min, Byte? max) {
_min = min;
_max = max;
}
public Byte? Minimum {
get { return _min; }
set { _min = value; }
}
public Byte? Maximum {
get { return _max; }
set { _max = value; }
}
public override bool Equals(object obj) {
if (obj is SizeRange)
return ((SizeRange)obj).Minimum == this.Minimum && ((SizeRange)obj).Maximum == this.Maximum;
else
return false;
}
public override int GetHashCode() {
return this.Minimum.GetValueOrDefault() ^ this.Maximum.GetValueOrDefault();
}
public override string ToString() {
string minValue = this.Minimum == null ? "?" : this.Minimum.ToString();
string maxValue = this.Maximum == null ? "?" : this.Maximum.ToString();
return minValue + "," + maxValue;
}
public static Boolean operator ==(SizeRange sr1, SizeRange sr2) {
return (sr1.Minimum == sr2.Minimum && sr1.Maximum == sr2.Maximum);
}
public static Boolean operator !=(SizeRange sr1, SizeRange sr2) {
return !(sr1 == sr2);
}
}
And here is a simple TypeConverter class that worked. It needs more validation work, but it worked in the designer for me:
public class SizeRangeConverter : TypeConverter {
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) {
if (sourceType == typeof(string))
return true;
return base.CanConvertFrom(context, sourceType);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) {
if (value is string) {
string[] v = ((string)value).Split(',');
Byte? minValue = null;
Byte? maxValue = null;
Byte minTest;
Byte maxTest;
if (byte.TryParse(v[0], out minTest))
minValue = minTest;
if (byte.TryParse(v[1], out maxTest))
maxValue = maxTest;
return new SizeRange(minValue, maxValue);
}
return base.ConvertFrom(context, culture, value);
}
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) {
if (destinationType == typeof(string))
return ((SizeRange)value).ToString();
return base.ConvertTo(context, culture, value, destinationType);
}
}