I have an Asp.Net WebApi controller with the following action:
public void Post([FromBody]object value)
Now, sometimes this value parameter is sent as a String containing a ISO8601-formatted date, and sometimes as a DateTime. The data is sent in JSON format.
For each of those two options I have to do different things, so I need to make that distinction, but I always end up with a DateTime value.
I am aware of the fact that my source DateTime values are being serialized to JSON as strings in ISO8601 format when they are sent to the above action (I am using an HttpClient for the actual sending), and hence my action cannot differentiate between the two.
My question is whether it is possible to include some kind of type hint to the sent values, in order to tell my WebApi endpoint which specific type is being sent (and enforce that type on the deserialized value).
Solved this eventually by implementing a custom JsonConverter, which serializes string values along with a type hint (similar to Newtonsoft.Json's MetadataPropertyHandling setting in JsonSerializerSettings).
Here is the implementation which does the trick:
public class DateStringTypeConservingConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType.IsAssignableFrom(typeof(string));
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var token = JToken.ReadFrom(reader);
var typeMetadata = token["$type"];
if (typeMetadata?.Value<string>() == typeof(string).FullName)
{
return token.Value<string>("$value");
}
// Default behavior
return serializer.Deserialize(reader, objectType);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value is string)
{
WriteJsonStringWithType(writer, value as string);
}
else
{
// Default behavior
serializer.Serialize(writer, value);
}
}
// Write the given string to the given JSON writer with type info:
// { $type: "System.String", $value: "<string content>" }
private void WriteJsonStringWithType(JsonWriter writer, string value)
{
writer.WriteStartObject();
writer.WritePropertyName("$type");
writer.WriteValue(typeof(string).FullName);
writer.WritePropertyName("$value");
writer.WriteValue(value);
writer.WriteEndObject();
}
}
And a little usage example:
static void Main(string[] args)
{
var dateString = new Wrapper
{
Value = "2017-01-08T21:24:48.114Z"
};
var converters = new JsonConverter[] { new DateStringTypeConservingConverter() };
var serializedDateString = JsonConvert.SerializeObject(dateString, new JsonSerializerSettings
{
Converters = converters
});
var deserializedDateString =
JsonConvert.DeserializeObject<Wrapper>(serializedDateString, converters);
// Output: System.String
Console.WriteLine($"The type of deserialized value is: { deserializedDateString.Value.GetType() }");
Console.ReadKey();
}
class Wrapper
{
public object Value { get; set; }
}
Related
I'm trying to selectively parse (only certain and not all) valid date strings into DateTimeOffset during deserialization. Has anyone achieved that?
Here's the Json I'm trying to deseralize into a JObject:
{
"SomeDate": "2019-09-19T01:21:00.747Z",
"SomeString": "2019-09-19T01:21:00.747Z"
}
Here's the deserializer I'm using:
JsonSerializer Deserializer = new JsonSerializer
{
DateParseHandling = DateParseHandling.DateTimeOffset
};
As you can expect, here's the JObject I get back:
{
"SomeDate": "2019-09-19T01:21:00.747+00:00",
"SomeString": "2019-09-19T01:21:00.747+00:00"
}
But I'd like to deserialize only SomeDate into a DateTimeOffset and retain SomeString as is:
{
"SomeDate": "2019-09-19T01:21:00.747+00:00",
"SomeString": "2019-09-19T01:21:00.747Z"
}
I can't decorate SomeString property with a custom converter since I'm deserializing to a JObject and therefore I don't have a class to put the decorator
I assume that you want the behavior to be dependent on the serialized value.
If so, you may have to provide your own serializer. See: https://blog.kulman.sk/custom-datetime-deserialization-with-json-net/
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.Value == null)
{
return null;
}
var s = reader.Value.ToString();
if (s.Contains("+"))
{
// DateTimeOffset
}
else
{
// DateTime
}
}
Once you do that, you can register it as a serializer setting.
I am using webapi2. I have a property in model is start date whose datatype is datetime. I want to pass the date as "dd-mm-yyyy" format. But if i send, i am getting 400 bad request. Could you please help me out. Note, I am using Fluent validation for the model validation.
public class Item
{
public DateTime? StartDate { get; set; }
public string Id { get; set; }
}
I want to pass the date as "dd-mm-yyyy"`
You have 3 options.
Option ISO8601
Don't pass it as "dd-mm-yyyy". Pass it instead in ISO8601 format (yyyy-MM-dd). That is the correct way to serialize DateTimes to string and also for then communicating that string representation between tiers. This format is a standard, widely used, unambiguous, and almost all frameworks that I am aware of have built in mechanisms for outputting DateTimes to that format and parsing them from that format.
Displaying a DateTime formatted as "dd-mm-yyyy" is a presentation layer concern and it should stay there and not "bleed" into the other application layers.
Option Formatters
Use custom code, like a Json Converte or an ActionFilterAttribute, to read the incoming DateTime.
Option String
Accept a string parameter instead and handle your own parsing inside the controller's method.
I honestly do not recommend the last 2 options. Instead use ISO8601: a standard, unambiguous, widely accepted means of communicating a DateTime.
I have created a custom value provider factory and am using the default model binding.
public class OrderValueProviderFactory<T> : ValueProviderFactory where T : class
{
public override IValueProvider GetValueProvider(HttpActionContext actionContext)
{
var querystring = actionContext.Request.GetQueryNameValuePairs().ToDictionary(x => x.Key.ToLower(), x => x.Value);
return new OrderValueProvider<T>(querystring);
}
}
public class OrderValueProvider<T> : IValueProvider
{
readonly Dictionary<string, string> querystring;
public OrderValueProvider(Dictionary<string, string> _querystring)
{
querystring = _querystring;
}
public bool ContainsPrefix(string prefix)
{
return true;
}
public ValueProviderResult GetValue(string key)
{
T obj = (T)Activator.CreateInstance(typeof(T));
PropertyInfo[] properties = typeof(T).GetProperties();
foreach (var property in properties)
{
if (property.PropertyType == typeof(string))
{
property.SetValue(obj, querystring.GetStringValue(property.Name.ToLower()));
}
else if (property.PropertyType == typeof(DateTime?))
{
property.SetValue(obj, querystring.GetDateTimeValue(property.Name.ToLower()));
}
else if (property.PropertyType == typeof(int))
{
property.SetValue(obj, querystring.GetIntValue(property.Name.ToLower()));
}
}
return new ValueProviderResult(obj, "", CultureInfo.InvariantCulture);
}
}
I have created a simple custom JsonConverter - code below - to simply change the error message generated when an invalid date is passed.
This is being used in an ASP.Net MVC site and also ASP.Net Web API for Ajax calls.
I am posting a DTO (partial code below) via ajax. When i do this on my local environment everything functions as it should; when the date is invalid I receive a bad request along with the correct error message
When i publish this to a production server it's totally ignored and i receive the default ModelState list of errors.
I have verified my web.config and Newtonsoft versions; they're the same both locally and on the server.
Why would the converter be ignored on production?
Converter is:
public class CustomDateValidationConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return true;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (string.IsNullOrEmpty(reader.Value.ToString()))
return null;
DateTime d;
DateTime.TryParse(reader.Value.ToString(), out d);
if (d == null || d == DateTime.MinValue)
throw new Exception(string.Format("{0} is invalid", reader.Path.AddSpacesBeforeUppercase(true)));
return d;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
serializer.Serialize(writer, DateTime.Parse(value as string));
}
}
I am using it in my DTO as:
.....
[JsonConverter(typeof(CustomDateValidationConverter))]
[DateLessThanOrEqual("CreateDateTo", "Create Date From must be on or before Create Date To")]
public DateTime? CreateDateFrom { get; set; }
.....
Check Your Production Server Date format, It may differ from your Development environment. so It may accept you input date format as valid
I've written a REST endpoint with ASP.NET Web API 2 that receives dates. The backend system has no concept of UTC times or DST, the dates stored in the database are just the UK date and time.
The website feeding the endpoint, however, is including UTC offset values in the data sent to the end point (e.g. a date looks like "1939-01-08T00:00:00+01:00"). This happens when a summer date is entered in the winter or vice-versa (because of the adjustment for DST).
Is it possible for me to set the JSON deserializer to completely ignore those UTC offset values? I've looked at the docs here and the examples here and tried all of the different enum options, but none of them are giving me the behaviour I'm looking for.
If the date parsing isn't working the way you want, you can turn it off and use a custom JsonConverter to handle the dates instead.
Here is a converter that should do the job:
class OffsetIgnoringDateConverter : 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)
{
string rawDate = (string)reader.Value;
if (rawDate == null) return existingValue;
if (rawDate.Length > 19) rawDate = rawDate.Substring(0, 19);
DateTime date = DateTime.ParseExact(rawDate, "yyyy'-'MM'-'dd'T'HH':'mm':'ss",
CultureInfo.CurrentCulture, DateTimeStyles.AssumeLocal);
return date;
}
public override bool CanWrite
{
get { return false; }
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
To install this converter into the Web API pipeline, add the following to your Application_Start() method in Global.asax:
var config = GlobalConfiguration.Configuration;
var jsonSettings = config.Formatters.JsonFormatter.SerializerSettings;
jsonSettings.DateParseHandling = DateParseHandling.None;
jsonSettings.Converters.Add(new OffsetIgnoringDateConverter());
Here is a demo (console app): https://dotnetfiddle.net/kt9pFl
In trying to troubleshoot some code, I looked to the log where I am capturing any unhandled exceptions.
In my log function, I have the following line of code:
data = JsonConvert.DeserializeObject(exception)
Part of the value of data reads, \"Message\":null. When I debug and look at the exception, the Message property of the exception has a value (which accurately describes the issue). If I try to convert a different exception to JSON using the line above, I see the value of the Message property in the resulting JSON.
What am I missing?
This looks like a bug in AutoMapperMappingException. Json.NET supports ISerializable, and the base class System.Exception implements this interface. As can be seen from the reference source, the base class version of GetObjectData simply adds the base field value not the property value to the serialization stream:
info.AddValue("Message", _message, typeof(String));
Any subclass of Exception that holds additional data above and beyond the base class must needs override GetObjectData and serialize its own data. For instance, here is the implementation of ArgumentOutOfRangeException:
public override void GetObjectData(SerializationInfo info, StreamingContext context) {
if (info==null) {
throw new ArgumentNullException("info");
}
Contract.EndContractBlock();
base.GetObjectData(info, context);
info.AddValue("ActualValue", m_actualValue, typeof(Object));
}
Now, from its source code, AutoMapperMappingException overrides Exception.Message to output additional data. And in fact, the base message might be null:
public override string Message
{
get
{
string message = null;
var newLine = Environment.NewLine;
if (Context != null)
{
message = _message + newLine + newLine + "Mapping types:";
message += newLine + string.Format("{0} -> {1}", Context.SourceType.Name, Context.DestinationType.Name);
message += newLine + string.Format("{0} -> {1}", Context.SourceType.FullName, Context.DestinationType.FullName);
var destPath = GetDestPath(Context);
message += newLine + newLine + "Destination path:" + newLine + destPath;
message += newLine + newLine + "Source value:" + newLine + (Context.SourceValue ?? "(null)");
return message;
}
if (_message != null)
{
message = _message;
}
message = (message == null ? null : message + newLine) + base.Message;
return message;
}
}
It does not, however, override GetObjectData(), which is the bug.
To work around the problem, you could write your own custom JsonConverter and serialize the message property instead of the base field:
public class ExceptionConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(AutoMapperMappingException).IsAssignableFrom(objectType);
}
public override bool CanRead { get { return false; } }
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var exception = (Exception)value;
var obj = JObject.FromObject(exception);
obj["Message"] = exception.Message;
obj.WriteTo(writer);
}
}
This will at least ensure the message is present. However, the ResolutionContext property of AutoMapperMappingException still won't be serialized, so the resulting deserialized exception will have lost some data.
Alternatively, you could try serializing and deserializing the ResolutionContext with Json.NET's default serialization and adding it as a property in the converter. Not sure if it would work, but it might. Or try to report an issue.
(Or, just log the ToString() output of the exception, that's usually good enough.)