As of .NET 6 in ASP.NET API, if you want to get DateOnly
(or TimeOnly
) as query parameter, you need to separately specify all it's fields instead of just providing a string ("2021-09-14", or "10:54:53" for TimeOnly
) like you can for DateTime
.
I was able to fix that if they are part of the body by adding adding custom JSON converter (AddJsonOptions(o => o.JsonSerializerOptions.Converters.Add(...))
), but it doesn't work for query parameters.
I know that could be fixed with model binder, but I don't want to create a model binder for every model that contains DateOnly/TimeOnly
. Is there a way to fix this application wide?
Demo:
Lets assume you have a folowwing action:
[HttpGet] public void Foo([FromQuery] DateOnly date, [FromQuery] TimeOnly time, [FromQuery] DateTime dateTime)
Here's how it would be represented in Swagger:
I want it represented as three string fields: one for DateOnly
, one for TimeOnly
and one for DateTime
(this one is already present).
PS: It's not a Swagger problem, it's ASP.NET one. If I try to pass ?date=2021-09-14
manually, ASP.NET wouldn't understand it.
Turns out, there are two solutions:
I went with TypeConverter
, and everything worked! Since .Net team are not planning to add full support for DateOnly/TimeOnly
in .Net 6, I've decided to create a NuGet to do so:
https://www.nuget.org/packages/DateOnlyTimeOnly.AspNet (source code)
After adding it to the project and configuring Program.cs
as described, Swagger for the action described in the question's description will look like this:
First you need to declare type convertor from string
to DateOnly
(and one from string
to TimeOnly
):
using System.ComponentModel;
using System.Globalization;
namespace DateOnlyTimeOnly.AspNet.Converters;
public class DateOnlyTypeConverter : 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 str)
{
return DateOnly.Parse(str);
}
return base.ConvertFrom(context, culture, value);
}
public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType)
{
if (destinationType == typeof(string))
{
return true;
}
return base.CanConvertTo(context, destinationType);
}
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
{
if (destinationType == typeof(string) && value is DateOnly date)
{
return date.ToString("O");
}
return base.ConvertTo(context, culture, value, destinationType);
}
}
(one for DateOnly
is the same, but DateOnly
is replaced with TimeOnly
)
Than TypeConverterAttribute
needs to be added on DateOnly
and TimeOnly
. It can be done like this:
TypeDescriptor.AddAttributes(typeof(DateOnly), new TypeConverterAttribute(typeof(DateOnlyTypeConverter)));
TypeDescriptor.AddAttributes(typeof(TimeOnly), new TypeConverterAttribute(typeof(TimeOnlyTypeConverter)));
To make it a bit cleaner this code can be wrapped in extension method:
using DateOnlyTimeOnly.AspNet.Converters;
using Microsoft.AspNetCore.Mvc;
using System.ComponentModel;
namespace Microsoft.Extensions.DependencyInjection;
public static class MvcOptionsExtensions
{
public static MvcOptions UseDateOnlyTimeOnlyStringConverters(this MvcOptions options)
{
TypeDescriptor.AddAttributes(typeof(DateOnly), new TypeConverterAttribute(typeof(DateOnlyTypeConverter)));
TypeDescriptor.AddAttributes(typeof(TimeOnly), new TypeConverterAttribute(typeof(TimeOnlyTypeConverter)));
return options;
}
}
Usage:
builder.Services.AddControllers(options => options.UseDateOnlyTimeOnlyStringConverters())
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With