Search code examples
c#asp.net-core-webapimodel-binding.net-6.0typeconverter

How can I use DateOnly/TimeOnly query parameters in ASP.NET Core 6?


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:

enter image description here

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.


Solution

  • 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:

    enter image description here

    How does it work

    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())