Pass JSON object with a TimeSpan property to C# WebAPI - c#

I have a WebAPI (written in C#), a POST-method accepting a complex object with a System.TimeSpan-Property named TriggerDelay, and a React Native application from where I am passing this object in JSON format.
However, this TimeSpan-property is not serializing properly and I keep getting 00:00:00-value on the API side.
I am trying like this:
"triggerDelay":{
"hours": "30",
"minutes": "10",
"seconds": "0"
},
OR like this:
"triggerDelay": "30:10:00"
But still no luck... In the API, it is always 00:00:00.
I would appreciate any help!
UPD Here is my Model:
public class Alarm
{
public Guid Id { get; set; } = Guid.NewGuid();
[...other properties...]
public TimeSpan TriggerDelay {get; set;}
}
My WebAPI Method:
public async Task<IActionResult> Publish([FromBody] Alarm alarm) {}
And here is my raw JSON object, set in the body of the request in Postman:
{
"id": "d17ef748-f378-4728-c6c2-9dfab1efce5b",
[...other properties...]
"triggerDelay":{
"hours": "30",
"minutes": "10",
"seconds": "0"
}
}

Newtonsoft's Json.NET supports TimeSpan serialization/deserializion out of the box (how to switch to Newtonsoft.Json in an ASP.NET Core 3.0 MVC project if you decide to) :
public class MyClass
{
public TimeSpan Interval { get; set; }
}
var json = #"{ ""Interval"":""00:00:42""}";
Console.WriteLine(JsonConvert.DeserializeObject<MyClass>(json).Interval.TotalSeconds); // prints 42
System.Text.Json (the default json handling tool in ASP.NET Core since 3.0 which, it seems, you are using) does not have built-in support for TimeSpan at the moment, so you will need to implement custom converter. Simplest one would look like this:
public class TimeSpanConverter : System.Text.Json.Serialization.JsonConverter<TimeSpan>
{
public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return TimeSpan.Parse(reader.GetString());
}
public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString());
}
}
And usage:
public class MyClass
{
[System.Text.Json.Serialization.JsonConverterAttribute(typeof(TimeSpanConverter))]
public TimeSpan Interval { get; set; }
}
Console.WriteLine(System.Text.Json.JsonSerializer.Deserialize<MyClass>(json).Interval.TotalSeconds); // prints 42

Related

Custom System.Text JsonConverter<DateTimeOffset> not getting invoked

I am creating a custom JsonConverterto parse datetimeoffset, to fix utc issue with offset. I am following MS doc
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace SystemTextJsonSamples
{
public class DateTimeOffsetJsonConverter : JsonConverter<DateTimeOffset>
{
public override DateTimeOffset Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options) =>
DateTimeOffset.ParseExact(reader.GetString()!,
"MM/dd/yyyy", CultureInfo.InvariantCulture);
public override void Write(
Utf8JsonWriter writer,
DateTimeOffset dateTimeValue,
JsonSerializerOptions options) =>
writer.WriteStringValue(dateTimeValue.ToString(
"MM/dd/yyyy", CultureInfo.InvariantCulture));
}
}
I have registered the converter in the startup like so
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
options.JsonSerializerOptions.Converters.Add(new DateTimeConverter());
options.JsonSerializerOptions.Converters.Add(new DateTimeOffsetConverter());
})
and here is my model
[Serializable()]
public class Travel
{
public DateTimeOffset TravelTime { get; set; }
}
When i make call to my api, my custom converter for datetimeoffset is not getting called. Please note that i also have a customdate converter which is working as expected.
Why is my offsetdatetime converter not getting invoked when i serialize/deserialize.
I am using .Net core 6
It's not enough to define a JsonConverter, you also have to apply it to the property, like this:
[JsonConverter(typeof(DateTimeOffsetJsonConverter))]
public DateTimeOffset TravelTime { get; set; }
Then it will be used for serialization and deserialization.
The reason for this (like why can't it pick up the type?) is that you can have several converters defined for the same type and apply them to the properties that need them. Now you can actually make different 'string to string' converters (for example).
Firstly, it should be DateTimeOffsetJsonConverter instead of DateTimeOffsetConverter, change your code to:
builder.Services.AddControllersWithViews().AddJsonOptions(options =>
{
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
options.JsonSerializerOptions.Converters.Add(new DateTimeConverter());
//options.JsonSerializerOptions.Converters.Add(new DateTimeOffsetConverter());
options.JsonSerializerOptions.Converters.Add(new DateTimeOffsetJsonConverter());
});
Then, be sure you post the data with content type application/json. For example:
Besides, you can also add [FromBody] to specify the source because it binds the form data by default if you use asp.net core MVC project:
[HttpPost]
public IActionResult Index([FromBody]Travel model)
{
//do your stuff...
}

Cannot convert string to TimeSpan

So, i am creating a service (using ASP.NETCore 3.1) that receives information about the operation schedule of a shop that can be registered in the system. For that i have an entity which communicates to the database and a model which communicates with this entity, as follows:
Entity:
public class OperationSchedule
{
public int Id { get; set; }
public TimeSpan Open { get; set; }
public TimeSpan Close { get; set; }
public DayOfWeek DatofWeek { get; set; }
}
Model:
public class OperationScheduleModel
{
public TimeSpan Open { get; set; }
public TimeSpan Close { get; set; }
public DayOfWeek DatofWeek { get; set; }
}
Of course they were already mapped using automapper. I'm using swagger to test such communications. The problem begins when i try to post a new "operation schedule", swagger shows me the following output:
Error converting value "string" to type 'System.TimeSpan'
In my first attemption to resolve this problem i'd tryied to use Microsoft.AspNetCore.Mvc.NewtonsoftJson, and configured it in startup this way: services.AddMvc().AddNewtonsoftJson();
As long it does not resolved my problem i was thinking in map the types string and TimeSpan on Automapper by doing something like this:
public class AutoMapperProfile : Profile
{
public AutoMapperProfile()
{
// Custom types mapping
CreateMap<string, TimeSpan>.ConvertUsing<StringToTimeSpanConverter>();
// OperationSchedule mapping
CreateMap<OperationSchedule, OperationScheduleModel>();
CreateMap<OperationScheduleModel, OperationSchedule>();
}
}
But VisualStudio isn't letting me do so, saying that a cannot use "StringToTimeSpanConverter" as a generic property, something like ConvertUsing(new StringToTimeSpanConverter()); doesn't make any difference.
I really can't even imagine another way to resolve it, but may i missing something? Help please
Error converting value "string" to type 'System.TimeSpan'
The reason for this is that System.Text.Json doesn't support TimeSpan.
One solution is to go back to JSON.NET. Here are the steps:
Add a package reference to Microsoft.AspNetCore.Mvc.NewtonsoftJson.
Update Startup.ConfigureServices to call AddNewtonsoftJson.
services.AddControllers()
.AddNewtonsoftJson();
Another option is to use a custom converter for that TimeSpan:
public class CustomConverter : JsonConverter<TimeSpan>
{
public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var value = reader.GetString();
return TimeSpan.Parse(value);
}
public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString());
}
}
Register it in Startup.ConfigureServices with AddJsonOptions:
services.AddControllers()
.AddJsonOptions(options =>
options.JsonSerializerOptions.Converters.Add(new CustomConverter())
);
Result:

Date Only Attribute in .NET Core 3.1

I am migrating a C# API from .NET Framework to .NET Core 3.1.
I have a requirement that some fields return yyyyMMdd only (no time)
and other fields that would return the full DateTime Value (Date and Time).
In the old .NET Framework world, we could make a quick converter like this:
public class OnlyDateConverter : IsoDateTimeConverter
{
public OnlyDateConverter()
{
DateTimeFormat = "yyyyMMdd";
}
}
and use it in my model like
[JsonConverter(typeof(DateTimeConverter))]
public DateTime OrderDate { get; set; }
That isn't working in .NET Core 3.1.
When I call it via Swagger, my JSON that is returned is:
"OrderDate": "2002-05-22T00:00:00"
I know you can add a JsonSerializerOption in Startup.cs, however that will force all dates to use the same formatting. I need to pick and choose.
I have tried:
making multiple json converters, however they never get called/work
[DataType(DataType.Date)]
[JsonConverter(typeof(DateTimeConverter))]
I have spent all day on this. I'm hoping someone has done this and can point out my silly mistake.
This code work for me
in your output model add this :
[DataType(DataType.Date)]
[JsonConverter(typeof(JsonDateConverterExtension))]
public DateTime? DateOfBirth { get; set; }
where JsonDateConverterExtension is :
public class JsonDateConverterExtension : JsonConverter<DateTime?>
{
public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> DateTime.ParseExact(reader.GetString(),
"yyyy-MM-dd", CultureInfo.InvariantCulture);
public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options)
=> writer.WriteStringValue(value?.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
}

The JSON value could not be converted to System.Nullable[System.Int32]

I updated an ASP.NET Core 2.2 API to ASP.NET Core 3.0 and I am using System.Json:
services
.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_3_0)
.AddJsonOptions(x => {})
I then tried to post JSON data using Angular 8, which was working before:
{
"name": "John"
"userId": "1"
}
The model in the ASP.NET Core 3.0 API is:
public class UserModel {
public String Name { get; set; }
public Int32? UserId { get; set; }
}
And the API Controller action is as follows:
[HttpPost("users")]
public async Task<IActionResult> Create([FromBody]PostModel) {
}
When I submit the model I get the following error:
The JSON value could not be converted to System.Nullable[System.Int32].
Do I need to do something else when using System.Json instead of Newtonsoft?
Microsoft has removed Json.NET dependency from ASP.NET Core 3.0 onwards and using System.Text.Json namespace now for serialization, deserialization and more.
You can still configure your application to use Newtonsoft.Json. For this -
Install Microsoft.AspNetCore.Mvc.NewtonsoftJson NuGet package
In ConfigureServices() add a call to AddNewtonsoftJson()-
services.AddControllers().AddNewtonsoftJson();
Read more on https://devblogs.microsoft.com/dotnet/try-the-new-system-text-json-apis/
https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-migrate-from-newtonsoft-how-to
Here Via the json you pass a string value for UserId but your model refer a int32? value for UserId. Then how your value convert from string to int32?
I faced this issue! and all those solutions did not work for me! so I did the following:-
First of all the data returned to me as the following:-
I need to make year as an int and also want to make value as double!
You should make custom JsonConverter, it worked for me after a lot of search, and here is sample of:-
StringToDoubleConverter
public sealed class StringToDoubleConverter : JsonConverter<double>
{
public override double Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
double.TryParse(reader.GetString(),out double value);
return value;
}
public override void Write(
Utf8JsonWriter writer,
double value,
JsonSerializerOptions options)
{
throw new NotImplementedException();
}
}
Then you can save it to db! play around as you like
In my scenario I was just sending the "", not the null :)

Keeping same rest endpoint for complex types in asp.net core api

I have a Rest endpoint, lets call it tags
http://api/tags
which creates tags objects passing this json format:
[{
"TagName" : "IntegerTag",
"DataType" : 1,
"IsRequired" : true
}]
If I would like to maintain the same endpoint to create new tags but with different json format. Lets say I want to create a ListTag
[{
"TagName" : "ListTag",
"DataType" : 5,
"Values" : ["Value1", "Value2", "Value3"]
"IsRequired" : true
}]]
or a RangeTag
[{
"TagName" : "RangeTag",
"DataType" : 6,
"Min": 1,
"Max": 10,
"IsRequired" : true
}]
I am not having any problem with C# to create a new Dto on my controller api and pass it as a different parameter because C# admits methods overloads for that:
void CreateTags(TagForCreateDto1 dto){…}
void CreateTags(TagForCreateDto2 dto){…}
But when I need to maintain in the same controller both methods with a POST request to create the tags, mvc does not allow for the same route to have both.
[HttpPost]
void CreateTags(TagForCreateDto1 dto){…}
[HttpPost]
void CreateTags(TagForCreateDto2 dto){…}
An unhandled exception occurred while processing the request.
AmbiguousActionException: Multiple actions matched. The following
actions matched route data and had all constraints satisfied.
Please advise
You can take leverage of the Factory pattern that will return the tags you want to create based on the JSON input. Create a factory, call it TagsFactory, that implements the following interface:
public interface ITagsFactory
{
string CreateTags(int dataType, string jsonInput);
}
Create a TagsFactory like below:
public class TagsFactory : ITagsFactory
{
public string CreateTags(int dataType, string jsonInput)
{
switch(dataType)
{
case 1:
var intTagsDto = JsonConvert.DeserializeObject<TagForCreateDto1(jsonInput);
// your logic to create the tags below
...
var tagsModel = GenerateTags();
return the JsonConvert.SerializeObject(tagsModel);
case 5:
var ListTagsDto = JsonConvert.DeserializeObject<TagForCreateDto2>(jsonInput);
// your logic to create the tags below
...
var tagsModel = GenerateTags();
return the JsonConvert.SerializeObject(tagsModel);
}
}
}
For little more separation of concerns, you can move the GenerateTags logic out of the factory to its own class.
Once the above is in place, I would suggest making a slight change to the design of your
TagsController. Add the following parameters to the CreateTags action
data-type or tag-name. Whatever is easier to handle and read it using [FromHeader]
jsonInput and read it using [FromBody]
Your controller will then look like below, making use of the ITagsFactory injected via DI
[Route("api")]
public class TagsController : Controller
{
private readonly ITagsFactory _tagsFactory;
public TagsController(ITagsFactory tagsFactory)
{
_tagsFactory= tagsFactory;
}
[HttpPost]
[Route("tags")]
public IActionResult CreateTags([FromHeader(Name = "data-type")] string dataType, [FromBody] string jsonInput)
{
var tags = _tagsFactory.CreateTags(dataType, jsonInput);
return new ObjectResult(tags)
{
StatusCode = 200
};
}
}
The work is almost done. However, in order to read the raw JSON input from the body, you need to add the CustomInputFormatter and register it at the Startup
public class RawRequestBodyInputFormatter : InputFormatter
{
public RawRequestBodyInputFormatter()
{
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json"));
}
public override bool CanRead(InputFormatterContext context)
{
return true;
}
public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
{
var request = context.HttpContext.Request;
using (var reader = new StreamReader(request.Body))
{
var content = await reader.ReadToEndAsync();
return await InputFormatterResult.SuccessAsync(content);
}
}
}
Register the formatter and the TagsFactory in the Startup like below:
services.AddSingleton<ITagsFactory, TagsFactory>();
services.AddMvc(options =>
{
options.InputFormatters.Insert(0, new RawRequestBodyInputFormatter());
}
That way your endpoint will remain the same. If you need to add more TagTypes, you just need to add that case to the TagsFactory. You can probably think that it's the violation of OCP. However, the Factory needs to know what kind of object it needs to create. If you like to abstract it more, you can make use of AbstractFactory, but I think that would be overkill.
One way to accomplish what you want, having a single POST endpoint while being able to post different "versions" of Tags is by creating a custom JsonConverter.
Basically, since you already have a property DataType that can be used to identify which type of Tag it is, it's easy to serialize it into the correct type. So, in code it looks like this:
BaseTag > ListTag, RangeTag
public class BaseTag
{
public string TagName { get; set; }
public int DataType { get; set; }
public bool IsRequired { get; set; }
}
public sealed class ListTag : BaseTag
{
public ICollection<string> Values { get; set; }
}
public sealed class RangeTag: BaseTag
{
public int Min { get; set; }
public int Max { get; set; }
}
Then, the custom PolymorphicTagJsonConverter
public class PolymorphicTagJsonConverter : JsonConverter
{
public override bool CanWrite => false;
public override bool CanConvert(Type objectType)
=> typeof(BaseTag).IsAssignableFrom(objectType);
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
=> throw new NotImplementedException();
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader == null) throw new ArgumentNullException("reader");
if (serializer == null) throw new ArgumentNullException("serializer");
if (reader.TokenType == JsonToken.Null)
return null;
var jObject = JObject.Load(reader);
var target = CreateTag(jObject);
serializer.Populate(jObject.CreateReader(), target);
return target;
}
private BaseTag CreateTag(JObject jObject)
{
if (jObject == null) throw new ArgumentNullException("jObject");
if (jObject["DataType"] == null) throw new ArgumentNullException("DataType");
switch ((int)jObject["DataType"])
{
case 5:
return new ListTag();
case 6:
return new RangeTag();
default:
return new BaseTag();
}
}
}
The heavy work is done in ReadJson and Create methods. Create receives an JObject and inside it inspects the DataType property to figure out which type of Tag it is. Then, ReadJson just proceeds calling the Populate on the JsonSerializer for the appropriate Type.
You need to tell the framework to use your custom converter then:
[JsonConverter(typeof(PolymorphicTagJsonConverter))]
public class BaseTag
{
// the same as before
}
Finally, you can just have one POST endpoint that will accept all types of tags:
[HttpPost]
public IActionResult Post(ICollection<BaseTag> tags)
{
return Ok(tags);
}
One downside is that switch on the converter. You might be okay or not with it.. you could do some smart work and try to make the tag classes implement somehow some interface so you could just call Create on the BaseTag and it would forward the call to the correct one at runtime, but I guess you can get started with this, and if complexity increases then you can think on a smarter/more automatic way of finding the correct Tag classes.

Categories