Entity Framework всегда сохраняет DateTimeOffset как UTC

Есть ли способ, которым я могу указать Entity Framework всегда хранить DateTimeOffset как значение UTC. Технически для этого нет причин, но я предпочитаю, чтобы все данные в моей базе данных были согласованы.

В настоящее время он сохраняет DateTimeOffset, полученный от клиента, который может быть любым в зависимости от языкового стандарта пользователя.

Я думал о том, чтобы, возможно, перехватить весь метод SaveChanges, прокручивать изменения, искать типы DateTimeOffset и выполнять преобразование явно, но это похоже на большую работу, которую можно было бы смягчить, если бы в EntityFramework было что-то встроенное.

Мы будем очень признательны за любые идеи или предложения.

EDIT: Разница между моим сообщением и другими, посвященными этой теме, заключается в том, что мое свойство C# имеет тип DateTimeOffset, а поле моей базы данных также имеет значение DateTimeOffset(0). Нет никаких технических причин для преобразования, кроме согласованности. Если я сохраняю значение как «17:00 +5:00» или «10:00 -2:00», это не имеет значения, это одинаковые моменты времени. Однако я хотел бы, чтобы все мои даты сохранялись как «12:00 +0:00», чтобы их было легче читать в SSMS или других инструментах БД.

РЕДАКТИРОВАТЬ 2: Видя, что не существует "простого" способа сделать это, я теперь пытаюсь решить проблему в функции DbContext.SaveChanges(). Проблема в том, что независимо от того, что я делаю, значение не изменится? Вот код на данный момент:

public new void SaveChanges()
{
    foreach (var entry in this.ChangeTracker.Entries())
    {
        foreach (var propertyInfo in entry.Entity.GetType().GetProperties().Where(p => p.CanWrite && p.PropertyType == typeof(DateTimeOffset)))
        {
            var value = entry.CurrentValues.GetValue<DateTimeOffset>(propertyInfo.Name);

            var universalTime = value.ToUniversalTime();
            entry.CurrentValues[propertyInfo.Name] = universalTime;
            // entry.Property(propertyInfo.Name).CurrentValue = universalTime;

            // Adding a watch to "entry.CurrentValues[propertyInfo.Name]" reveals that it has not changed??
        }

        // TODO: DateTimeOffset?
    }

    base.SaveChanges();
}

EDIT 3 Мне наконец удалось решить эту проблему, см. ответ ниже


person Talon    schedule 10.10.2016    source источник
comment
Вы имеете в виду хранить все как UTC+0? Как вы будете конвертировать его обратно, когда вам нужно отобразить формат локали пользователя? Рассмотрите также настройки часового пояса, такие как BST.   -  person musefan    schedule 10.10.2016
comment
@musefan Именно поэтому хранение в UTC имеет смысл. Да, вам понадобится локальный формат/часовой пояс, но он будет работать глобально, а не только в часовом поясе, где было введено время и дата.   -  person Maarten    schedule 10.10.2016
comment
@Igor, этот пост полезен, но он касается даты и времени против даты и времени. Я универсально имею дело с datetimeoffset как в коде, так и в БД.   -  person Talon    schedule 11.10.2016
comment
@Maarten нет технической причины для преобразования. Не имеет значения, имеет ли база данных 17:00 +2:00 или 14:00 -1:00. Они оба одинаковы. Для меня это о постоянстве.   -  person Talon    schedule 11.10.2016
comment
Инструменты базы данных без проблем справляются с datetimeoffset. Почему вы думаете, что вам нужно преобразовать значение и потерять исходную информацию о смещении? Здесь нет согласованности, которую нужно поддерживать, просто потеря данных.   -  person Panagiotis Kanavos    schedule 11.10.2016
comment
@PanagiotisKanavos Исходный часовой пояс не имеет для меня значения. Моя проблема заключается в том, что при просмотре базы данных ее труднее читать (с точки зрения человека, а не машины).   -  person Talon    schedule 11.10.2016
comment
Я повторяю свой комментарий здесь, так как мой ответ не применяется, и я удалил его: @Talon, вы пытались установить фактическое свойство объекта (используя что-то вроде: propertyInfo.SetValue(entry.Entity, universalTime))?. Я не думаю, что когда-либо пытался калечить CurrentValues   -  person Jcl    schedule 11.10.2016
comment
Какой смысл использовать datetimeoffset, если тогда вы отбросите смещение? Кроме того, с точки зрения человека/разработчика то, что волшебным образом меняет значения и отбрасывает данные, не приветствуется. Гораздо чище изменить смещение при установке свойства, а не за кулисами.   -  person Panagiotis Kanavos    schedule 11.10.2016
comment
Кстати, EF может отбросить новое значение просто потому, что два значения равны. Свойство помечается как изменение, только если новое значение отличается. Вам придется фактически изменить значение, например, добавив миллисекунду   -  person Panagiotis Kanavos    schedule 11.10.2016
comment
@PanagiotisKanavos Проверьте это prnt.sc/csffv0 , этого я хочу избежать. TimeZone хранится для размещения систем в разных часовых поясах. Мне удалось решить мою проблему. Большое спасибо, что нашли время, чтобы помочь!   -  person Talon    schedule 11.10.2016
comment
Нет, смещение не предназначено для систем с разными часовыми поясами. Он предназначен для того, чтобы вы могли сохранять время отправления и прибытия с одного побережья на другое, не изменяя времени, указанного авиакомпанией. Вот почему запрос очень, очень нелогичен   -  person Panagiotis Kanavos    schedule 11.10.2016
comment
Я должен согласиться с @PanagiotisKanavos в этом. Я не вижу смысла хранить все ваши DateTimeOffset в часовом поясе UTC, кроме того, что это выглядит красивее в SSMS. Если вам нужно показать их в определенном часовом поясе, конвертируйте их для отображения (в вашем приложении), а не для хранения.   -  person Jcl    schedule 11.10.2016
comment
@PanagiotisKanavos, если бы я вместо этого использовал DateTime, предполагалось, что часовой пояс соответствует серверу, выполняющему запрос к базе данных. Статьи приходят со всего мира, дата публикации устанавливается журналистом в своем часовом поясе. Затем сервер выполняет getNews, где publishDate ‹ now. Без настроенного смещения эта publishDate становится неоднозначной. Конечно, я мог бы убедиться, что я всегда сохраняю DateTime как UtcTime и выдаю его как системное правило. Но, используя DateTimeOffset, я устраняю необходимость в этом правиле, так как нет двусмысленности.   -  person Talon    schedule 11.10.2016
comment
@Jcl, если бы к этой базе данных подключалась только одна система, мы могли бы хранить как DateTime, и преобразования могли бы выполняться кодом, предполагающим определенный часовой пояс (UTC). Но чтобы обеспечить надежность системы в будущем, сделать ее переносимой (переместить на новый сервер в другом часовом поясе) и позволить другим приложениям подключаться к базе данных, нам необходимо сделать данные полностью однозначными.   -  person Talon    schedule 11.10.2016
comment
@Talon, по крайней мере, смещение времени в полях datetimeoffset в SQL Server всегда сохраняется, индексируется и сравнивается в формате UTC. Смещение предназначено только для извлечения (чтобы вы знали, в какое время были введены данные). Сравнения всегда производятся в формате UTC.   -  person Jcl    schedule 11.10.2016
comment
На самом деле MSDN формулирует это так же: The data is stored in the database and processed, compared, sorted, and indexed in the server as in UTC. The time zone offset will be preserved in the database for retrieval. . В принципе, это не имеет значения ни для чего, кроме отображения   -  person Jcl    schedule 11.10.2016
comment
Опять же, в зависимости от ваших дизайнерских решений, если вы играете с несколькими часовыми поясами (и часовыми поясами! = смещениями времени), я бы, вероятно, просто сохранил часовой пояс пользователя отдельно (из даты и часового пояса вы можете получить смещение: из смещения вы не можете получить часовой пояс)   -  person Jcl    schedule 11.10.2016
comment
@Jcl извинения, я использовал TimeZone и Offset взаимозаменяемо, так что это может быть путаницей. Смещение - это то, что я имею в виду. Поэтому я хочу, чтобы все в базе данных было преобразовано в +0 для удобства чтения. исходное смещение не имеет значения для системы после того, как оно было преобразовано в +0   -  person Talon    schedule 11.10.2016
comment
Хорошо, если смещение бесполезно в вашем приложении, и вы хотите более чистый вид... это нормально, но я бы не стал с радостью игнорировать его, если вы не убедитесь, что знаете, что нет никаких последствий и не будет в будущем (которое всегда трудно предсказать :-)). Особенно, если вы изменяете возможный пользовательский ввод объектов перед сохранением. Опять же: это дизайнерские решения, которые вам нужно принять :-)   -  person Jcl    schedule 11.10.2016
comment
Давайте продолжим обсуждение в чате.   -  person Talon    schedule 11.10.2016


Ответы (1)


Благодаря помощи, которую я получил. Наконец-то мне удалось получить ответ благодаря Jcl

public new void SaveChanges()
{
    foreach (var entry in this.ChangeTracker.Entries().Where(p => p.State == EntityState.Added || p.State == EntityState.Modified))
        {
        foreach (var propertyInfo in entry.Entity.GetType().GetTypeProperties().Where(p => p.PropertyType == typeof(DateTimeOffset)))
        {
            propertyInfo.SetValue(entry.Entity, entry.CurrentValues.GetValue<DateTimeOffset>(propertyInfo.Name).ToUniversalTime());
        }
        foreach (var propertyInfo in entry.Entity.GetType().GetTypeProperties().Where(p => p.PropertyType == typeof(DateTimeOffset?)))
        {
            var dateTimeOffset = entry.CurrentValues.GetValue<DateTimeOffset?>(propertyInfo.Name);
            if (dateTimeOffset != null) propertyInfo.SetValue(entry.Entity, dateTimeOffset.Value.ToUniversalTime());
        }
    }

     base.SaveChanges();
}

ИЗМЕНИТЬ:

По совету Jcl я написал небольшой помощник для кэширования отражения:

public static class TypeReflectionExtension
{
    public static Dictionary<Type, PropertyInfo[]> PropertyInfoCache;

    static TypeReflectionHelper()
    {
        PropertyInfoCache = new Dictionary<Type, PropertyInfo[]>();
    }

    public static PropertyInfo[] GetTypeProperties(this Type type)
    {
        if (!PropertyInfoCache.ContainsKey(type))
        {
            PropertyInfoCache[type] = type.GetProperties();
        }
        return PropertyInfoCache[type];
    }
}

EDIT 2: Чтобы немного очистить код, я создал общую функцию, которую вы можете использовать для перехвата и изменения любого типа данных:

public static class DbEntityEntryExtension
{
    public static void ModifyTypes<T>(this DbEntityEntry dbEntityEntry, Func<T, T> method)
    {
        foreach (var propertyInfo in dbEntityEntry.Entity.GetType().GetTypeProperties().Where(p => p.PropertyType == typeof(T) && p.CanWrite))
        {
            propertyInfo.SetValue(dbEntityEntry.Entity, method(dbEntityEntry.CurrentValues.GetValue<T>(propertyInfo.Name)));
        }
    }
}

Использование этого (внутри вашего DbContext):

public new void SaveChanges()
{
    foreach (var entry in this.ChangeTracker.Entries().Where(p => p.State == EntityState.Added || p.State == EntityState.Modified))
    {
        entry.ModifyTypes<DateTimeOffset>(ConvertToUtc);
        entry.ModifyTypes<DateTimeOffset?>(ConvertToUtc);
    }

    base.SaveChanges();
}

private static DateTimeOffset ConvertToUtc(DateTimeOffset dateTimeOffset)
{
    return dateTimeOffset.ToUniversalTime();
}

private static DateTimeOffset? ConvertToUtc(DateTimeOffset? dateTimeOffset)
{
    return dateTimeOffset?.ToUniversalTime();
}

Отказ от ответственности Это решает мою проблему, но не рекомендуется. Я даже не знаю, буду ли я использовать это, потому что сейчас это кажется неправильным (спасибо Jcl;) . Поэтому, пожалуйста, используйте это с осторожностью. Возможно, будет лучше, если вы последуете ответу Адама Бенсона

person Talon    schedule 11.10.2016
comment
Убедитесь, что вы кэшируете все эти вызовы GetType().GetProperties(): они ОЧЕНЬ медленные (вы можете сделать любое решение для кэширования, которое соотносит Type и PropertyName с фактическим PropertyInfo) - person Jcl; 11.10.2016
comment
@Jcl Я посмотрю на это прямо сейчас, все эти проблемы только для удобочитаемости заставляют меня задуматься, но во имя опыта я буду упорствовать. - person Talon; 11.10.2016
comment
честно говоря, если бы я сделал это (чего я бы не сделал), я бы также сохранил часовой пояс, в котором пользователь поместил дату и время (и я имею в виду фактический часовой пояс, что было бы лучше, если вам не нужна производительность, а просто сохранение смещения): я просто пытался помочь вам с вашим вопросом; ваши дизайнерские решения - это совсем другая история :-) Удачи! - person Jcl; 11.10.2016
comment
@Jcl, теперь я беспокоюсь о производительности. Мне нужно поболтать с моей командой, чтобы узнать, можем ли мы придумать альтернативы или нам следует просто отказаться от читабельности и оставить смещение в покое. - person Talon; 11.10.2016
comment
Простой Dictionary<Type, Dictionary<string, PropertyInfo>> будет работать как простой кеш для этих вызовов отражения, если у вас нет более сложных требований (очистка памяти, ленивое вычисление и так далее). PropertyInfo.SetValue не очень быстрый, но и не такой уж медленный - person Jcl; 11.10.2016