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:
Related
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...
}
since upgrading to automapper 11 trying to map a string to an Uri no longer works
Entity
public sealed class Website
{
public int Id { get; set; }
public string Name {get; set; }
public string BaseAddress { get; set; }
}
object I'm mapping to
public sealed class WebsiteDto
{
public int Id { get; set; }
public string Name { get; set; }
public Uri BaseAddress { get; set; }
}
mapping profile contains
CreateMap<Website, WebsiteDto>();
and how I'm calling it
_mapper.Map<List<WebsiteDto>>(listOfWebsiteEntities);
This was working fine before upgrading to 11, I think it might be a bug but before I raise it as an issue Automapper suggests raising a question here, am I missing something?
after downgrading to automapper 10.1.1 it works again. the upgrade guide doesn't seem to mention anything related to this
The issue affects only relative uris and happens due to the next breaking change in 11 version:
System.ComponentModel.TypeConverter is no longer supported
It was removed for performance reasons. So it’s best not to use it anymore. But if you must, there is a sample in the test project.
You can add TypeConverterMapper to mappers so Automapper will behave as previously. Note that this implementation is very primitive and very unperformant:
var cfg = new MapperConfiguration(cfg => cfg.Internal().Mappers.Insert(0, new TypeConverterMapper()))
public class TypeConverterMapper : ObjectMapper<object, object>
{
public override bool IsMatch(TypePair context)
{
return GetConverter(context.SourceType).CanConvertTo(context.DestinationType) ||
GetConverter(context.DestinationType).CanConvertFrom(context.SourceType);
}
public override object Map(object source, object destination, Type sourceType, Type destinationType, ResolutionContext context)
{
var typeConverter = GetConverter(sourceType);
return typeConverter.CanConvertTo(destinationType) ? typeConverter.ConvertTo(source, destinationType) : GetConverter(destinationType).ConvertFrom(source);
}
private TypeConverter GetConverter(Type type) => TypeDescriptor.GetConverter(type);
}
Or create a mapping from string to Uri which, I would say, is much better option. Quick and dirty one simulating behaviour of previous one can look like this:
cfg.CreateMap<string, Uri>().ConvertUsing(s => (Uri)new UriTypeConverter().ConvertFrom(s));
Or just:
cfg.CreateMap<string, Uri>().ConvertUsing(s => new Uri(s, UriKind.RelativeOrAbsolute));
Related github issue
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
The system i am developing is using DataContractJsonSerializer.
The service looks like this:
[HttpPost, Route("RunQuery")]
public List<BIResultRecord> RunQuery(BIQuery query) {
// Logic
}
The BIQuery class hierarchy is as follows:
[DataContract]
[KnownType(typeof(BIQuery1))]
public class BIQuery
{
[DataMember]
public string Member { get; set; }
[DataMember]
public QueryTypeEnum QueryType { get; set; }
}
[DataContract]
public class BIQuery1 : BIQuery
{
public BIQuery1()
{
QueryType = QueryTypeEnum.Type1;
}
[DataMember]
public ClassSpecificObject Object { get; set; }
}
THE PROBLEM:
Although i am sending BIQuery1 as a json object it is always deserialized (in my RunQuery method) as the parent object (BIQuery).
WHAT I'VE TRIED:
I've removed the default formatter and added a new one:
public class DataContractJsonFormatter : JsonMediaTypeFormatter
{
public override DataContractJsonSerializer CreateDataContractSerializer(Type type)
{
return new DataContractJsonSerializer(type, new DataContractJsonSerializerSettings() { EmitTypeInformation = EmitTypeInformation.AsNeeded });
}
}
I serialized the object in .NET to see the JSON structure and
cloning it to the request. I saw that it adds
"__type" : "BIQuery1"
to the JSON, which i'm using as well and it changed nothing.
Please help!
MORE DETAILS
The .NET system (client and server) runs in production as is, so only minor changes currently allowed. I'm making also a web client that would work with the existing services over REST.
[HttpPost, Route("RunQuery")]
public List<BIResultRecord> RunQuery(BIQuery query) {
// Logic
}
Your argument in the RunQuery method is the of type BIQuery object. It is deserializing as a BIQuery object because that is the type you are telling the method it will be passed. If you are passing an object of the type BIQuery1 to the method, do this instead:
[HttpPost, Route("RunQuery")]
public List<BIResultRecord> RunQuery(BIQuery1 query) {
// Logic
}
I have a class User and I need to work with them in web services.
Then problem is that if I try to serialize Id that is type of BsonObjectId, I see
that have an empty property, that have an empty property, and so on ...
I have write this workaround in order, it's is a good solution?
public partial class i_User
{
[BsonId(IdGenerator = typeof(BsonObjectIdGenerator))]
[NonSerialized]
public BsonObjectId _id;
public String Id
{
get
{
return this._id.ToString();
}
}
}
In this way, I can keep _Id as BsonObjectId but I send an string representation over the web in the property Id.
Another solution is to work with StringObjectIdGenerator
public partial class i_User
{
[BsonId(IdGenerator = typeof(StringObjectIdGenerator))]
public String id;
}
But is see that MongoDB will store a string into database instead of ObjectId.
What is the best approach in order to work in a serialization environmental like web services and/or an client-server (Flash+C#)?
If I understand you correctly, you want to access the Id property as a string, but have the Id saved as an ObjectId in MongoDB. This can be accomplished using BsonRepresentation with BsonId.
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }
Details can be found here.
If you want to do it with a Class Map - this is the way to do it:
BsonClassMap.RegisterClassMap<i_User>(cm =>
{
cm.AutoMap();
cm.SetIdMember(cm.GetMemberMap(x => x.Id)
.SetIdGenerator(StringObjectIdGenerator.Instance));
});
There is also a more generic approach using conventions.
This approach allows your to setup rules for all models in one place.
First. Add a convention for ID generator
public class IdGeneratorConvention : ConventionBase, IPostProcessingConvention
{
public void PostProcess(BsonClassMap classMap)
{
var idMemberMap = classMap.IdMemberMap;
if (idMemberMap == null || idMemberMap.IdGenerator != null)
{
return;
}
idMemberMap.SetIdGenerator(StringObjectIdGenerator.Instance);
}
}
Second. Register our convention. Register method must be called before the first query.
var conventionPack = new ConventionPack { new IdGeneratorConvention() };
ConventionRegistry.Register("Pack", conventionPack, x => true);
Working with the ASP minimal APIs and with record types for models, here is what works for me.
Basic custom converter:
using MongoDB.Bson;
using System.Text.Json;
using System.Text.Json.Serialization;
public class ObjectIdJsonConverter : JsonConverter<ObjectId> {
public override ObjectId Read(ref Utf8JsonReader reader, Type typeToConvert,
JsonSerializerOptions options) => new ObjectId(reader.GetString());
public override void Write(Utf8JsonWriter writer, ObjectId value,
JsonSerializerOptions options) => writer.WriteStringValue(value.ToString());
}
The record:
using System.Text.Json.Serialization;
using MongoDB.Bson;
public record User(
[property: JsonConverter(typeof(ObjectIdJsonConverter))]
ObjectId Id)
In the controller:
app.MapGet("/user/{id}",
async (string id, UserService userService) => {
var user = await userService.Find(id);
return Results.Json(user);
});