Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 968
maxc137 Avatar asked Jan 25 '23 06:01

maxc137


1 Answers

Turns out, there are two solutions:

  • Custom ModelBinder + Swagger configuration
  • Custom TypeConverter (Swagger will automatically see it, and update UI accordingly)

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())
like image 59
maxc137 Avatar answered Jan 29 '23 22:01

maxc137