.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
using System.ComponentModel.Design.Serialization;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
namespace System.ComponentModel;
<summary>
<see cref='System.DateOnly'/>
</summary>
public class DateOnlyConverter : TypeConverter
{
<summary>
<see cref='System.DateOnly'/>
</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>
<see cref='System.DateOnly'/>
</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
{
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>
<see cref='System.DateOnly'/>
</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
using System.ComponentModel.Design.Serialization;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
namespace System.ComponentModel;
<summary>
<see cref='System.TimeOnly'/>
</summary>
public class TimeOnlyConverter : TypeConverter
{
<summary>
<see cref='System.TimeOnly'/>
</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>
<see cref='System.TimeOnly'/>
</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
{
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>
<see cref='System.TimeOnly'/>
</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();