Использование DateTimeStyles.RoundtripKind при связывании DateTime с IConfiguration - PullRequest
2 голосов
/ 08 марта 2020

TL; DR: хотелось бы, чтобы результат связывания DateTime значений из IConfiguration был таким же, как DateTime.Parse(stringValue, null, DateTimeStyles.RoundtripKind), но по умолчанию он совпадает с DateTime.Parse(stringValue).


Я использую функцию привязки IConfiguration для извлечения DateTime значений из конфигурации.

Допустим, у меня есть конфигурация JSON, которая выглядит следующим образом:

{
    testValue: "2020-03-08T14:37:46.9793888Z"
}

Я могу вывести значение как DateTime одним из нескольких способов. Вот первые два, о которых я подумал:

// Directly from IConfiguration
var dateTimeValue = configuration.GetValue<DateTime>("testValue");
// By configuring options

// Assuming this class exists
private class TestOptions
{
    public DateTime TestValue { get; set; }
}

// Set up options on a service provider
var services = new ServiceCollection();
services.Configure<TestOptions>(configuration);
var provider = services.BuildServiceProvider();

// Get as a service
var options = provider.GetRequiredService<IOptions<TestOptions>>().Value;
return options.TestValue;

Результат их выполнения, по крайней мере по умолчанию, более или менее эквивалентен применению обычного разбора:

// Bad, ignores Kind
var dateTimeValue = DateTime.Parse(stringValue)

Я работаю над кодом, который обращает внимание на свойство Kind DateTime. Обычный анализ игнорирует время "Z" и делает что-либо с указанием часового пояса Local. Подобный вызов со стилем RoundtripKind устраняет проблему:

// Good, uses Kind if available
var dateTimeValue = DateTime.Parse(stringValue, null, DateTimeStyles.RoundtripKind);

Есть ли способ заставить связыватель конфигурации использовать этот вызов вместо значения по умолчанию?

1 Ответ

0 голосов
/ 09 марта 2020

У меня возникла мысль: я мог бы заменить поле DateTime в классе параметров типом оболочки, а затем реализовать анализ. Мы меняем тип поля следующим образом:

private class TestOptions
{
    //public DateTime TestValue { get; set; }
    public RoundtripDateTime TestValue { get; set; }
}

Мы создаем тип оболочки (я сделал его struct, но class также работает), включая неявные преобразования в и из DateTime (так что по крайней мере некоторый код не нужно менять) и хорошие Parse() и ToString() методы. Мы включили атрибут [TypeConverter], чтобы связыватель конфигурации обнаружил класс преобразователя.

    [TypeConverter(typeof(RoundtripDateTimeTypeConverter))]
    public struct RoundtripDateTime
    {
        public RoundtripDateTime(DateTime value)
        {
            Value = value;
        }

        public DateTime Value { get; set; }

        public static implicit operator DateTime(RoundtripDateTime roundTripDateTime) => roundTripDateTime.Value;
        public static implicit operator RoundtripDateTime(DateTime dateTime) => new RoundtripDateTime(dateTime);

        public override string ToString() => Value.ToString("o");
        public static RoundtripDateTime Parse(string s) => DateTime.Parse(s, null, DateTimeStyles.RoundtripKind);
        public static RoundtripDateTime Parse(string s, CultureInfo culture) => DateTime.Parse(s, culture, DateTimeStyles.RoundtripKind);
    }

Наконец, мы включили новую реализацию TypeConverter, которая будет обрабатывать экземпляры оболочки. (Я обманул и посмотрел на источник на DateTimeConverter, чтобы узнать, что он уже делал, но на самом деле это гораздо проще , потому что он всегда работает в незнании культуры. [Редактировать: Для преобразования из string, добавил культуру, чтобы помочь Parse() иметь дело с датами, которые не в формате ISO8601. Это все еще довольно просто.])

    public class RoundtripDateTimeTypeConverter : TypeConverter
    {
        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
        {
            return (sourceType == typeof(string)) || base.CanConvertFrom(context, sourceType);
        }

        public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
        {
            if (value is string stringValue)
            {
                var text = stringValue.Trim();

                if (text == string.Empty)
                {
                    return (RoundtripDateTime)DateTime.MinValue;
                }
                else
                {
                    try
                    {
                        return RoundtripDateTime.Parse(text, culture ?? CultureInfo.InvariantCulture);
                    }
                    catch (FormatException e)
                    {
                        throw new FormatException($"'{value}' is not a valid value for {nameof(RoundtripDateTime)}.", e);
                    }
                }
            }
            else
            {
                return base.ConvertFrom(context, culture, value);
            }
        }

        public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
        {
            if (destinationType == typeof(string) && value is RoundtripDateTime roundtripDateTimeValue)
            {
                return (roundtripDateTimeValue.Value == DateTime.MinValue)
                    ? string.Empty
                    : roundtripDateTimeValue.ToString();
            }
            else
            {
                return base.ConvertTo(context, culture, value, destinationType);
            }
        }
    }

Все это может потребовать некоторой настройки, но я думаю, что у меня есть что-то, с чем я могу работать.

Если у вас есть идеи получше, не стесняйтесь добавить еще один ответ.

...