简体   繁体   中英

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:

在此处输入图像描述

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:

在此处输入图片说明

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

I also met your issue in my side and it seems the constructor itself doesn't support parameter-less mode. As the code below :

public DateOnly(int year, int month, int day)
        {
            throw null;
        }

        [NullableContext(1)]
        public DateOnly(int year, int month, int day, Calendar calendar)
        {
            throw null;
        }

While the DateTime supports:

public DateTime Date
    {
        get
        {
            throw null;
        }
    }

Therefore, I'm afraid before some updating on DateOnly and TimeOnly , we all need to use string instead and divide the string into year, month and day and then new DateOnly(int year, int month, int day) .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM