The new datatypes of DateOnly and TimeOnly are a good addition to C# but it feels as if they were released too soon without proper integration with other features. An example of this is their integration with API controllers, when added as a datatype to an input or output DTO the controller and other related services like Swagger expect them to be provided as an object rather than as a string.

In order to allow the API to accept a date in the expected format “2022-08-24” and for Swagger to display the examples correctly it’s necessary to first add a converter.

public sealed class DateOnlyJsonConverter : JsonConverter<DateOnly>
{
    public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        return DateOnly.FromDateTime(reader.GetDateTime());
    }

    public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options)
    {
        var isoDate = value.ToString("O");
        writer.WriteStringValue(isoDate);
    }
}
public sealed class NullableDateOnlyJsonConverter : JsonConverter<DateOnly?>
{
    public override DateOnly? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TryGetDateTime(out DateTime dateTime))
            return DateOnly.FromDateTime(dateTime);
        else
            return null;
    }

    public override void Write(Utf8JsonWriter writer, DateOnly? value, JsonSerializerOptions options)
    {
        if (value == null)
            writer.WriteNullValue();
        else
            writer.WriteStringValue(((DateOnly)value).ToString("O"));
    }
}

These then need to be added to your service collection in Startup.cs.

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers().AddJsonOptions(x =>
    {
        // serialize DateOnly as strings
        x.JsonSerializerOptions.Converters.Add(new DateOnlyJsonConverter());
        x.JsonSerializerOptions.Converters.Add(new NullableDateOnlyJsonConverter());
    });
}

This will allow your API controllers to work properly with DateOnly datatypes now but if you want Swagger to work as expected you’ll need to update the mappings there as well.

services.AddSwaggerGen(c =>
{
    c.MapType<DateOnly>(() => new OpenApiSchema
        {
            Type = "string",
            Format = "date"
        });
});

The above code works for remapping string to DateOnly in the request body but won’t work if DateOnly query parameters are being passed.

The below code is based on that in the DateOnlyTimeOnly.AspNet project.

public abstract class StringTypeConverterBase<T> : TypeConverter
{
    protected abstract T Parse(string s);

    protected abstract string ToIsoString(T source);

    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 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 T typedValue)
        {
            return ToIsoString(typedValue);
        }
        return base.ConvertTo(context, culture, value, destinationType);
    }
}
public class DateOnlyTypeConverter : StringTypeConverterBase<DateOnly>
{
    protected override DateOnly Parse(string s) => DateOnly.Parse(s);

    protected override string ToIsoString(DateOnly source) => source.ToString("O");
}

This type converter can then be added in the ConfigureServices method of your startup after you’ve setup Swagger with AddSwaggerGen.

TypeConverterAttribute typeConverterAttribute = new TypeConverterAttribute(typeof(DateOnlyTypeConverter));
TypeDescriptor.AddAttributes(typeof(DateOnly), typeConverterAttribute);

0 Comments

Leave a Reply

Avatar placeholder

Your email address will not be published. Required fields are marked *