.NET 6 で DateOnly
および TimeOnly
型が追加されました。日付や時間のみを扱う (若干残念な名前を除けば) 待望の子ですね。
ところで、アプリケーション構成として日付や時間 (特に時間) を扱うことはちょくちょくあるのではないかと思います。現代の .NET (Core 系) でアプリケーションの構成情報を利用すると言えば Microsoft.Extensions.Configuration
の IConfiguration
ですが、実はここから DateOnly
や TimeOnly
に直接マッピングできません、残念ながら。DateTime
や DateTimeOffset
などはできるのに!
Microsoft.Extensions.Configuration
の実装を追いかけてみると TypeConverter
を通してマッピングを図るようになっているのですが、.NET 6 時点では DateOnly
や TimeOnly
に対応する TypeConverter
が登録されていないために動作しないんですね。この問題は .NET Runtime Team も認識していて、.NET 7 Preview 6 で既に修正されています。
でも我々は (というか僕は) .NET 6 でも使いたい!ということで .NET 6 でも DateOnlyConverter
と TimeOnlyConverter
を利用できるようにしてしまいましょう。
TypeConverter を実装
まず .NET 7 で実装された DateOnlyConverter
と TimeOnlyConverter
を持ってきます。一部コンパイルが通らない箇所があるのでそこだけ修正します。若干ずるいけどこれがベストなのだ...(ゴゴゴ
修正箇所
- .NET 7 以降ではこの実装は不要なので
#if NET6_0
を追加 - String Resources を直接展開
- .NET 7 で新規に追加された
TimeOnly.Microsecond
プロパティに関する箇所を削除
実装の全貌
そこそこ実装が長いので折り畳み状態にしました。ご了承ください。
DateOnlyConverter.cs
#if NET6_0 // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.ComponentModel.Design.Serialization; using System.Diagnostics.CodeAnalysis; using System.Globalization; namespace System.ComponentModel; /// <summary> /// Provides a type converter to convert <see cref='System.DateOnly'/> objects to and from various other representations. /// </summary> public class DateOnlyConverter : TypeConverter { /// <summary> /// Gets a value indicating whether this converter can convert an object in the given source type to a <see cref='System.DateOnly'/> /// object using the specified context. /// </summary> /// <inheritdoc /> public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) { return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType); } /// <inheritdoc /> public override bool CanConvertTo(ITypeDescriptorContext? context, [NotNullWhen(true)] Type? destinationType) { return destinationType == typeof(InstanceDescriptor) || base.CanConvertTo(context, destinationType); } /// <summary> /// Converts the given value object to a <see cref='System.DateOnly'/> object. /// </summary> /// <inheritdoc /> public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) { if (value is string text) { text = text.Trim(); if (text.Length == 0) { return DateOnly.MinValue; } try { // See if we have a culture info to parse with. If so, then use it. DateTimeFormatInfo? formatInfo = null; if (culture != null) { formatInfo = (DateTimeFormatInfo?)culture.GetFormat(typeof(DateTimeFormatInfo)); } if (formatInfo != null) { return DateOnly.Parse(text, formatInfo); } else { return DateOnly.Parse(text, culture); } } catch (FormatException e) { var message = $"{text} is not a valid value for {nameof(DateOnly)}."; throw new FormatException(message, e); } } return base.ConvertFrom(context, culture, value); } /// <summary> /// Converts the given value object from a <see cref='System.DateOnly'/> object using the arguments. /// </summary> /// <inheritdoc /> public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType) { if (destinationType == typeof(string) && value is DateOnly dateOnly) { if (dateOnly == DateOnly.MinValue) { return string.Empty; } culture ??= CultureInfo.CurrentCulture; DateTimeFormatInfo? formatInfo = (DateTimeFormatInfo?)culture.GetFormat(typeof(DateTimeFormatInfo)); if (culture == CultureInfo.InvariantCulture) { return dateOnly.ToString("yyyy-MM-dd", culture); } string format = formatInfo!.ShortDatePattern; return dateOnly.ToString(format, CultureInfo.CurrentCulture); } if (destinationType == typeof(InstanceDescriptor) && value is DateOnly date) { return new InstanceDescriptor(typeof(DateOnly).GetConstructor(new Type[] { typeof(int), typeof(int), typeof(int) }), new object[] { date.Year, date.Month, date.Day }); } return base.ConvertTo(context, culture, value, destinationType); } } #endif
TimeOnlyConverter.cs
#if NET6_0 // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.ComponentModel.Design.Serialization; using System.Diagnostics.CodeAnalysis; using System.Globalization; namespace System.ComponentModel; /// <summary> /// Provides a type converter to convert <see cref='System.TimeOnly'/> objects to and from various other representations. /// </summary> public class TimeOnlyConverter : TypeConverter { /// <summary> /// Gets a value indicating whether this converter can convert an object in the given source type to a <see cref='System.TimeOnly'/> /// object using the specified context. /// </summary> /// <inheritdoc /> public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) { return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType); } /// <inheritdoc /> public override bool CanConvertTo(ITypeDescriptorContext? context, [NotNullWhen(true)] Type? destinationType) { return destinationType == typeof(InstanceDescriptor) || base.CanConvertTo(context, destinationType); } /// <summary> /// Converts the given value object to a <see cref='System.TimeOnly'/> object. /// </summary> /// <inheritdoc /> public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) { if (value is string text) { text = text.Trim(); if (text.Length == 0) { return TimeOnly.MinValue; } try { // See if we have a culture info to parse with. If so, then use it. DateTimeFormatInfo? formatInfo = null; if (culture != null) { formatInfo = (DateTimeFormatInfo?)culture.GetFormat(typeof(DateTimeFormatInfo)); } if (formatInfo != null) { return TimeOnly.Parse(text, formatInfo); } else { return TimeOnly.Parse(text, culture); } } catch (FormatException e) { var message = $"{text} is not a valid value for {nameof(TimeOnly)}."; throw new FormatException(message, e); } } return base.ConvertFrom(context, culture, value); } /// <summary> /// Converts the given value object from a <see cref='System.TimeOnly'/> object using the arguments. /// </summary> /// <inheritdoc /> public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType) { if (destinationType == typeof(string) && value is TimeOnly timeOnly) { if (timeOnly == TimeOnly.MinValue) { return string.Empty; } culture ??= CultureInfo.CurrentCulture; DateTimeFormatInfo formatInfo = (DateTimeFormatInfo)culture.GetFormat(typeof(DateTimeFormatInfo))!; return timeOnly.ToString(formatInfo.ShortTimePattern, CultureInfo.CurrentCulture); } if (destinationType == typeof(InstanceDescriptor) && value is TimeOnly time) { if (time.Ticks == 0) { return new InstanceDescriptor(typeof(TimeOnly).GetConstructor(new Type[] { typeof(long) }), new object[] { time.Ticks }); } return new InstanceDescriptor(typeof(TimeOnly).GetConstructor(new Type[] { typeof(int), typeof(int), typeof(int), typeof(int) }), new object[] { time.Hour, time.Minute, time.Second, time.Millisecond }); } return base.ConvertTo(context, culture, value, destinationType); } } #endif
参照元
TypeConverter を登録
TypeConverter
の porting ができたら、.NET Runtime が認識できるよう登録していきましょう。ザックリ以下のようにします。
#if NET6_0 public static class TypeConverterShims { public static void Register() { register<DateOnly, DateOnlyConverter>(); register<TimeOnly, TimeOnlyConverter>(); static void register<TObject, TConverter>() { var attribute = new TypeConverterAttribute(typeof(TConverter)); TypeDescriptor.AddAttributes(typeof(TObject), attribute); } } } #endif
最後にこれをアプリケーション起動時に一度呼び出せば OK です。IConfiguration.Get<T>();
などの前に呼び出すことを忘れずに!
TypeConverterShims.Register();