Entity Framework DateTime и UTC

98

Возможно ли, чтобы Entity Framework (в настоящее время я использую подход Code First с CTP5) сохраняла все значения DateTime как UTC в базе данных?

Или, может быть, есть способ указать его в сопоставлении, например, в этом столбце last_login:

modelBuilder.Entity<User>().Property(x => x.Id).HasColumnName("id");
modelBuilder.Entity<User>().Property(x => x.IsAdmin).HasColumnName("admin");
modelBuilder.Entity<User>().Property(x => x.IsEnabled).HasColumnName("enabled");
modelBuilder.Entity<User>().Property(x => x.PasswordHash).HasColumnName("password_hash");
modelBuilder.Entity<User>().Property(x => x.LastLogin).HasColumnName("last_login");
Fionn
источник

Ответы:

147

Вот один из подходов, который вы можете рассмотреть:

Сначала определите следующий атрибут:

[AttributeUsage(AttributeTargets.Property)]
public class DateTimeKindAttribute : Attribute
{
    private readonly DateTimeKind _kind;

    public DateTimeKindAttribute(DateTimeKind kind)
    {
        _kind = kind;
    }

    public DateTimeKind Kind
    {
        get { return _kind; }
    }

    public static void Apply(object entity)
    {
        if (entity == null)
            return;

        var properties = entity.GetType().GetProperties()
            .Where(x => x.PropertyType == typeof(DateTime) || x.PropertyType == typeof(DateTime?));

        foreach (var property in properties)
        {
            var attr = property.GetCustomAttribute<DateTimeKindAttribute>();
            if (attr == null)
                continue;

            var dt = property.PropertyType == typeof(DateTime?)
                ? (DateTime?) property.GetValue(entity)
                : (DateTime) property.GetValue(entity);

            if (dt == null)
                continue;

            property.SetValue(entity, DateTime.SpecifyKind(dt.Value, attr.Kind));
        }
    }
}

Теперь подключите этот атрибут к вашему контексту EF:

public class MyContext : DbContext
{
    public DbSet<Foo> Foos { get; set; }

    public MyContext()
    {
        ((IObjectContextAdapter)this).ObjectContext.ObjectMaterialized +=
            (sender, e) => DateTimeKindAttribute.Apply(e.Entity);
    }
}

Теперь для любого свойства DateTimeили DateTime?вы можете применить этот атрибут:

public class Foo
{
    public int Id { get; set; }

    [DateTimeKind(DateTimeKind.Utc)]
    public DateTime Bar { get; set; }
}

Таким образом, всякий раз, когда Entity Framework загружает объект из базы данных, он устанавливает указанное DateTimeKindвами значение, например UTC.

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

Мэтт Джонсон-Пинт
источник
7
Если у вас не получается заставить это работать, возможно, вам не хватает одного из следующих способов использования: using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using System.Reflection;
Saustrup 06
8
@Saustrup - вы найдете большинство примеров на SO, которые для краткости опускают использование, если они не имеют прямого отношения к вопросу. Но спасибо.
Мэтт Джонсон-Пинт
4
@MattJohnson без использования операторов @Saustrup, вы получите несколько бесполезных ошибок компиляции, таких как'System.Array' does not contain a definition for 'Where'
Джейкоб Эггерс
7
Как сказал @SilverSideDown, это работает только с .NET 4.5. Я создал несколько расширений, чтобы сделать его совместимым с .NET 4.0, по адресу gist.github.com/munr/3544bd7fab6615290561 . Также следует отметить, что это не будет работать с проекциями, только с полностью загруженными объектами.
Мун
5
Есть какие-нибудь предложения по работе с прогнозами?
Джафин
33

Мне очень нравится подход Мэтта Джонсона, но в моей модели ВСЕ мои члены DateTime - это UTC, и я не хочу, чтобы их все украшали атрибутом. Итак, я обобщил подход Мэтта, чтобы позволить обработчику событий применять значение Kind по умолчанию, если только член явно не украшен атрибутом.

Конструктор класса ApplicationDbContext включает этот код:

/// <summary> Constructor: Initializes a new ApplicationDbContext instance. </summary>
public ApplicationDbContext()
        : base(MyApp.ConnectionString, throwIfV1Schema: false)
{
    // Set the Kind property on DateTime variables retrieved from the database
    ((IObjectContextAdapter)this).ObjectContext.ObjectMaterialized +=
      (sender, e) => DateTimeKindAttribute.Apply(e.Entity, DateTimeKind.Utc);
}

DateTimeKindAttribute выглядит так:

/// <summary> Sets the DateTime.Kind value on DateTime and DateTime? members retrieved by Entity Framework. Sets Kind to DateTimeKind.Utc by default. </summary>
[AttributeUsage(AttributeTargets.Property)]
public class DateTimeKindAttribute : Attribute
{
    /// <summary> The DateTime.Kind value to set into the returned value. </summary>
    public readonly DateTimeKind Kind;

    /// <summary> Specifies the DateTime.Kind value to set on the returned DateTime value. </summary>
    /// <param name="kind"> The DateTime.Kind value to set on the returned DateTime value. </param>
    public DateTimeKindAttribute(DateTimeKind kind)
    {
        Kind = kind;
    }

    /// <summary> Event handler to connect to the ObjectContext.ObjectMaterialized event. </summary>
    /// <param name="entity"> The entity (POCO class) being materialized. </param>
    /// <param name="defaultKind"> [Optional] The Kind property to set on all DateTime objects by default. </param>
    public static void Apply(object entity, DateTimeKind? defaultKind = null)
    {
        if (entity == null) return;

        // Get the PropertyInfos for all of the DateTime and DateTime? properties on the entity
        var properties = entity.GetType().GetProperties()
            .Where(x => x.PropertyType == typeof(DateTime) || x.PropertyType == typeof(DateTime?));

        // For each DateTime or DateTime? property on the entity...
        foreach (var propInfo in properties) {
            // Initialization
            var kind = defaultKind;

            // Get the kind value from the [DateTimekind] attribute if it's present
            var kindAttr = propInfo.GetCustomAttribute<DateTimeKindAttribute>();
            if (kindAttr != null) kind = kindAttr.Kind;

            // Set the Kind property
            if (kind != null) {
                var dt = (propInfo.PropertyType == typeof(DateTime?))
                    ? (DateTime?)propInfo.GetValue(entity)
                    : (DateTime)propInfo.GetValue(entity);

                if (dt != null) propInfo.SetValue(entity, DateTime.SpecifyKind(dt.Value, kind.Value));
            }
        }
    }
}
Bob.at.Indigo.Health
источник
1
Это очень полезное расширение принятого ответа!
Learner
Возможно, мне что-то не хватает, но как это по умолчанию DateTimeKind.Utc, а не DateTimeKind.Unspecified?
Rhonage 08
1
@Rhonage Извините за это. По умолчанию устанавливается в конструкторе ApplicationDbContext. Я обновил ответ, включив это.
Bob.at.Indigo.Health
1
@ Bob.at.AIPsychLab Спасибо, дружище, теперь стало намного яснее. Пытался выяснить, есть ли какое-то отражение веса, но нет, очень просто!
Rhonage
Это не работает, если модель имеет DateTImeатрибут без (общедоступного) метода установки. Изменить предложено. См. Также stackoverflow.com/a/3762475/2279059
Флориан Винтер
15

Этот ответ работает с Entity Framework 6

Принятый ответ не работает для проектируемого или анонимного объекта. Производительность тоже может быть проблемой.

Для этого нам нужно использовать DbCommandInterceptorобъект, предоставляемый EntityFramework.

Создать перехватчик:

public class UtcInterceptor : DbCommandInterceptor
{
    public override void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
    {
        base.ReaderExecuted(command, interceptionContext);

        if (interceptionContext?.Result != null && !(interceptionContext.Result is UtcDbDataReader))
        {
            interceptionContext.Result = new UtcDbDataReader(interceptionContext.Result);
        }
    }
}

interceptionContext.Result это DbDataReader, который мы заменяем нашим

public class UtcDbDataReader : DbDataReader
{
    private readonly DbDataReader source;

    public UtcDbDataReader(DbDataReader source)
    {
        this.source = source;
    }

    public override DateTime GetDateTime(int ordinal)
    {
        return DateTime.SpecifyKind(source.GetDateTime(ordinal), DateTimeKind.Utc);
    }        

    // you need to fill all overrides. Just call the same method on source in all cases

    public new void Dispose()
    {
        source.Dispose();
    }

    public new IDataReader GetData(int ordinal)
    {
        return source.GetData(ordinal);
    }
}

Зарегистрируйте перехватчик в своем DbConfiguration

internal class MyDbConfiguration : DbConfiguration
{
    protected internal MyDbConfiguration ()
    {           
        AddInterceptor(new UtcInterceptor());
    }
}

Наконец, зарегистрируйте конфигурацию на вашем DbContext

[DbConfigurationType(typeof(MyDbConfiguration ))]
internal class MyDbContext : DbContext
{
    // ...
}

Вот и все. Ура.

Для простоты вот полная реализация DbReader:

using System;
using System.Collections;
using System.Data;
using System.Data.Common;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace MyNameSpace
{
    /// <inheritdoc />
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1010:CollectionsShouldImplementGenericInterface")]
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")]
    public class UtcDbDataReader : DbDataReader
    {
        private readonly DbDataReader source;

        public UtcDbDataReader(DbDataReader source)
        {
            this.source = source;
        }

        /// <inheritdoc />
        public override int VisibleFieldCount => source.VisibleFieldCount;

        /// <inheritdoc />
        public override int Depth => source.Depth;

        /// <inheritdoc />
        public override int FieldCount => source.FieldCount;

        /// <inheritdoc />
        public override bool HasRows => source.HasRows;

        /// <inheritdoc />
        public override bool IsClosed => source.IsClosed;

        /// <inheritdoc />
        public override int RecordsAffected => source.RecordsAffected;

        /// <inheritdoc />
        public override object this[string name] => source[name];

        /// <inheritdoc />
        public override object this[int ordinal] => source[ordinal];

        /// <inheritdoc />
        public override bool GetBoolean(int ordinal)
        {
            return source.GetBoolean(ordinal);
        }

        /// <inheritdoc />
        public override byte GetByte(int ordinal)
        {
            return source.GetByte(ordinal);
        }

        /// <inheritdoc />
        public override long GetBytes(int ordinal, long dataOffset, byte[] buffer, int bufferOffset, int length)
        {
            return source.GetBytes(ordinal, dataOffset, buffer, bufferOffset, length);
        }

        /// <inheritdoc />
        public override char GetChar(int ordinal)
        {
            return source.GetChar(ordinal);
        }

        /// <inheritdoc />
        public override long GetChars(int ordinal, long dataOffset, char[] buffer, int bufferOffset, int length)
        {
            return source.GetChars(ordinal, dataOffset, buffer, bufferOffset, length);
        }

        /// <inheritdoc />
        public override string GetDataTypeName(int ordinal)
        {
            return source.GetDataTypeName(ordinal);
        }

        /// <summary>
        /// Returns datetime with Utc kind
        /// </summary>
        public override DateTime GetDateTime(int ordinal)
        {
            return DateTime.SpecifyKind(source.GetDateTime(ordinal), DateTimeKind.Utc);
        }

        /// <inheritdoc />
        public override decimal GetDecimal(int ordinal)
        {
            return source.GetDecimal(ordinal);
        }

        /// <inheritdoc />
        public override double GetDouble(int ordinal)
        {
            return source.GetDouble(ordinal);
        }

        /// <inheritdoc />
        public override IEnumerator GetEnumerator()
        {
            return source.GetEnumerator();
        }

        /// <inheritdoc />
        public override Type GetFieldType(int ordinal)
        {
            return source.GetFieldType(ordinal);
        }

        /// <inheritdoc />
        public override float GetFloat(int ordinal)
        {
            return source.GetFloat(ordinal);
        }

        /// <inheritdoc />
        public override Guid GetGuid(int ordinal)
        {
            return source.GetGuid(ordinal);
        }

        /// <inheritdoc />
        public override short GetInt16(int ordinal)
        {
            return source.GetInt16(ordinal);
        }

        /// <inheritdoc />
        public override int GetInt32(int ordinal)
        {
            return source.GetInt32(ordinal);
        }

        /// <inheritdoc />
        public override long GetInt64(int ordinal)
        {
            return source.GetInt64(ordinal);
        }

        /// <inheritdoc />
        public override string GetName(int ordinal)
        {
            return source.GetName(ordinal);
        }

        /// <inheritdoc />
        public override int GetOrdinal(string name)
        {
            return source.GetOrdinal(name);
        }

        /// <inheritdoc />
        public override string GetString(int ordinal)
        {
            return source.GetString(ordinal);
        }

        /// <inheritdoc />
        public override object GetValue(int ordinal)
        {
            return source.GetValue(ordinal);
        }

        /// <inheritdoc />
        public override int GetValues(object[] values)
        {
            return source.GetValues(values);
        }

        /// <inheritdoc />
        public override bool IsDBNull(int ordinal)
        {
            return source.IsDBNull(ordinal);
        }

        /// <inheritdoc />
        public override bool NextResult()
        {
            return source.NextResult();
        }

        /// <inheritdoc />
        public override bool Read()
        {
            return source.Read();
        }

        /// <inheritdoc />
        public override void Close()
        {
            source.Close();
        }

        /// <inheritdoc />
        public override T GetFieldValue<T>(int ordinal)
        {
            return source.GetFieldValue<T>(ordinal);
        }

        /// <inheritdoc />
        public override Task<T> GetFieldValueAsync<T>(int ordinal, CancellationToken cancellationToken)
        {
            return source.GetFieldValueAsync<T>(ordinal, cancellationToken);
        }

        /// <inheritdoc />
        public override Type GetProviderSpecificFieldType(int ordinal)
        {
            return source.GetProviderSpecificFieldType(ordinal);
        }

        /// <inheritdoc />
        public override object GetProviderSpecificValue(int ordinal)
        {
            return source.GetProviderSpecificValue(ordinal);
        }

        /// <inheritdoc />
        public override int GetProviderSpecificValues(object[] values)
        {
            return source.GetProviderSpecificValues(values);
        }

        /// <inheritdoc />
        public override DataTable GetSchemaTable()
        {
            return source.GetSchemaTable();
        }

        /// <inheritdoc />
        public override Stream GetStream(int ordinal)
        {
            return source.GetStream(ordinal);
        }

        /// <inheritdoc />
        public override TextReader GetTextReader(int ordinal)
        {
            return source.GetTextReader(ordinal);
        }

        /// <inheritdoc />
        public override Task<bool> IsDBNullAsync(int ordinal, CancellationToken cancellationToken)
        {
            return source.IsDBNullAsync(ordinal, cancellationToken);
        }

        /// <inheritdoc />
        public override Task<bool> ReadAsync(CancellationToken cancellationToken)
        {
            return source.ReadAsync(cancellationToken);
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly")]
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1816:CallGCSuppressFinalizeCorrectly")]
        public new void Dispose()
        {
            source.Dispose();
        }

        public new IDataReader GetData(int ordinal)
        {
            return source.GetData(ordinal);
        }
    }
}
user2397863
источник
Пока это кажется лучшим ответом. Сначала я попробовал вариант атрибута, так как он казался менее перспективным, но мои модульные тесты завершились неудачно из-за насмешки, поскольку привязка события конструктора, похоже, не знает о сопоставлениях таблиц, которые происходят в событии OnModelCreating. Этот получил мой голос!
Сенатор
1
Почему ты слежка Disposeи GetData?
user247702 04
2
Этот код, вероятно, должен указывать на @IvanStoev: stackoverflow.com/a/40349051/90287
Рами А.
К сожалению, это не удается, если вы сопоставляете пространственные данные,
Крис,
@ user247702 да затенение Dispose ошибочно, переопределить Dispose (bool)
user2397863
12

Для EF Core на GitHub есть отличное обсуждение этой темы: https://github.com/dotnet/efcore/issues/4711

Решение (кредит Кристофера Хоуза ), которое приведет к обработке всех дат при их сохранении / извлечении из базы данных в формате UTC, состоит в том, чтобы добавить следующее к OnModelCreatingметоду вашего DbContextкласса:

var dateTimeConverter = new ValueConverter<DateTime, DateTime>(
    v => v.ToUniversalTime(),
    v => DateTime.SpecifyKind(v, DateTimeKind.Utc));

var nullableDateTimeConverter = new ValueConverter<DateTime?, DateTime?>(
    v => v.HasValue ? v.Value.ToUniversalTime() : v,
    v => v.HasValue ? DateTime.SpecifyKind(v.Value, DateTimeKind.Utc) : v);

foreach (var entityType in builder.Model.GetEntityTypes())
{
    if (entityType.IsQueryType)
    {
        continue;
    }

    foreach (var property in entityType.GetProperties())
    {
        if (property.ClrType == typeof(DateTime))
        {
            property.SetValueConverter(dateTimeConverter);
        }
        else if (property.ClrType == typeof(DateTime?))
        {
            property.SetValueConverter(nullableDateTimeConverter);
        }
    }
}

Также проверьте эту ссылку, если вы хотите исключить некоторые свойства некоторых объектов из обработки как UTC.

Хонза Кальфус
источник
Однозначно лучшее решение для меня! Благодарность
Бен Моррис
1
@MarkRedman Я не думаю, что это имеет смысл, потому что, если у вас есть законный вариант использования DateTimeOffset, вы также хотите сохранить информацию о часовом поясе. См. Docs.microsoft.com/en-us/dotnet/standard/datetime/… или stackoverflow.com/a/14268167/3979621, чтобы узнать, когда выбирать между DateTime и DateTimeOffset.
Хонза Калфус,
2
IsQueryTypeпохоже, был заменен IsKeyLess: github.com/dotnet/efcore/commit/…
Марк Тилеманс,
Зачем IsQueryType(или IsKeyLessсейчас) нужна проверка?
Петр Перак
9

Я считаю, что нашел решение, которое не требует специальной проверки UTC или манипуляций с DateTime.

В основном вам нужно изменить объекты EF, чтобы использовать тип данных DateTimeOffset (НЕ DateTime). Это сохранит часовой пояс со значением даты в базе данных (в моем случае - SQL Server 2015).

Когда EF Core запрашивает данные из БД, он также получает информацию о часовом поясе. Когда вы передаете эти данные в веб-приложение (Angular2 в моем случае), дата автоматически преобразуется в местный часовой пояс браузера, чего я и ожидал.

И когда он передается обратно на мой сервер, он снова автоматически конвертируется в UTC, как и ожидалось.

Мутоно
источник
8
DateTimeOffset не сохраняет часовой пояс, вопреки распространенному мнению. Он хранит смещение от UTC, которое представляет значение. Смещение не может быть отображено в обратном порядке, чтобы определить фактический часовой пояс, из которого было создано смещение, что делает тип данных почти бесполезным.
Suncat2000
2
Нет, но его можно использовать для правильного хранения DateTime: medium.com/@ojb500/in-praise-of-datetimeoffset-e0711f991cba
Carl
1
Только UTC не нуждается в местоположении, потому что оно везде одинаково. Если вы используете что-то еще, кроме UTC, вам также нужно местоположение, иначе информация о времени бесполезна, также при использовании datetimeoffset.
Хорицу
1
DATETIMEOFFSET будет делать то, что хотел исходный плакат: сохранять дату и время в формате UTC без необходимости выполнять какое-либо (явное) преобразование. @Carl DATETIME, DATETIME2 и DATETIMEOFFSET все правильно хранят значение даты и времени. Помимо дополнительного хранения смещения от UTC, DATETIMEOFFSET практически не имеет никаких преимуществ. То, что вы используете в своей базе данных, - это ваш звонок. Я просто хотел подчеркнуть, что он не хранит часовой пояс, как ошибочно думают многие.
Suncat2000
1
@ Suncat2000 Преимущество в том, что вы можете отправить эту дату, как есть, из вашего API в ваш клиентский браузер. Когда клиентский браузер открывает эту дату, он знает, какое смещение от UCT, и поэтому может преобразовать его в дату по умолчанию в системе, в которой клиент ее просматривает. Таким образом, преобразование часового пояса вашего сервера в часовой пояс браузера происходит без необходимости написания кода для этого разработчика.
Moutono 05
6

Я исследую это прямо сейчас, и большинство из этих ответов не совсем хороши. Насколько я могу судить, невозможно сказать EF6, что даты, исходящие из базы данных, находятся в формате UTC. Если это так, то самый простой способ убедиться, что свойства DateTime вашей модели находятся в формате UTC, - это проверить и преобразовать в установщике.

Вот какой-то псевдокод, похожий на С #, который описывает алгоритм

public DateTime MyUtcDateTime 
{    
    get 
    {        
        return _myUtcDateTime;        
    }
    set
    {   
        if(value.Kind == DateTimeKind.Utc)      
            _myUtcDateTime = value;            
        else if (value.Kind == DateTimeKind.Local)         
            _myUtcDateTime = value.ToUniversalTime();
        else 
            _myUtcDateTime = DateTime.SpecifyKind(value, DateTimeKind.Utc);        
    }    
}

Первые две ветви очевидны. В последнем есть секрет соуса.

Когда EF6 создает модель из данных, загруженных из базы данных, DateTimes DateTimeKind.Unspecified. Если вы знаете, что все ваши даты - это UTC в базе данных, то последняя ветка отлично подойдет вам.

DateTime.Nowвсегда DateTimeKind.Local, поэтому приведенный выше алгоритм отлично работает для дат, созданных в коде. Большую часть времени.

Однако вы должны быть осторожны, поскольку есть и другие способы DateTimeKind.Unspecifiedпроникнуть в ваш код. Например, вы можете десериализовать свои модели из данных JSON, и ваш десериализатор по умолчанию настроен на этот тип. Вы должны остерегаться локализованных дат, отмеченныхDateTimeKind.Unspecified доступ к этому сеттеру от кого-либо, кроме EF.

staa99
источник
6
Как я узнал после нескольких лет борьбы с этой проблемой, если вы назначаете или выбираете поля DateTime в другие структуры, например объект передачи данных, EF игнорирует методы получения и установки. В этих случаях вам все равно придется изменить Kind на DateTimeKind.Utcпосле получения результатов. Пример: from o in myContext.Records select new DTO() { BrokenTimestamp = o.BbTimestamp };устанавливает для всех Kind значение DateTimeKind.Unspecified.
Suncat2000
1
Я использую DateTimeOffset с Entity Framework некоторое время, и если вы укажете свои объекты EF с типом данных DateTimeOffset, тогда все ваши запросы EF будут возвращать даты со смещением от UTC, точно так же, как они сохранены в БД. Поэтому, если вы изменили тип данных на DateTimeOffset вместо DateTime, вам не понадобится вышеуказанный обходной путь.
Moutono
Это хорошо знать! Спасибо @Moutono
Согласно комментарию @ Suncat2000, это просто не работает, и его следует удалить
Бен Моррис,
5

Невозможно указать DataTimeKind в Entity Framework. Вы можете решить преобразовать значения даты и времени в utc перед сохранением в db и всегда предполагать, что данные получены из db как UTC. Но объекты DateTime, материализованные во время запроса, всегда будут «Unspecified». Вы также можете оценить, используя объект DateTimeOffset вместо DateTime.

Виджай
источник
5

Еще один год, другое решение! Это для EF Core.

У меня много DATETIME2(7)столбцов, которые соответствуютDateTime формате UTC и всегда хранятся в нем. Я не хочу сохранять смещение, потому что, если мой код правильный, смещение всегда будет нулевым.

Между тем у меня есть другие столбцы, в которых хранятся базовые значения даты и времени с неизвестным смещением (предоставленные пользователями), поэтому они просто хранятся / отображаются «как есть» и не сравниваются ни с чем.

Поэтому мне нужно решение, которое я могу применить к определенным столбцам.

Определите метод расширения UsesUtc:

private static DateTime FromCodeToData(DateTime fromCode, string name)
    => fromCode.Kind == DateTimeKind.Utc ? fromCode : throw new InvalidOperationException($"Column {name} only accepts UTC date-time values");

private static DateTime FromDataToCode(DateTime fromData) 
    => fromData.Kind == DateTimeKind.Unspecified ? DateTime.SpecifyKind(fromData, DateTimeKind.Utc) : fromData.ToUniversalTime();

public static PropertyBuilder<DateTime?> UsesUtc(this PropertyBuilder<DateTime?> property)
{
    var name = property.Metadata.Name;
    return property.HasConversion<DateTime?>(
        fromCode => fromCode != null ? FromCodeToData(fromCode.Value, name) : default,
        fromData => fromData != null ? FromDataToCode(fromData.Value) : default
    );
}

public static PropertyBuilder<DateTime> UsesUtc(this PropertyBuilder<DateTime> property)
{
    var name = property.Metadata.Name;
    return property.HasConversion(fromCode => FromCodeToData(fromCode, name), fromData => FromDataToCode(fromData));
}

Затем это можно использовать для свойств в настройке модели:

modelBuilder.Entity<CustomerProcessingJob>().Property(x => x.Started).UsesUtc();

У него есть небольшое преимущество перед атрибутами в том, что вы можете применить его только к свойствам правильного типа.

Обратите внимание, что он предполагает, что значения из БД находятся в формате UTC, но просто неверно Kind . Поэтому он контролирует значения, которые вы пытаетесь сохранить в БД, генерируя описательное исключение, если они не являются UTC.

Дэниел Эрвикер
источник
1
Это отличное решение, которое должно быть выше, особенно сейчас, когда большинство новых разработок будет использовать Core или .NET 5. Бонусные воображаемые точки для политики применения UTC - если бы больше людей сохраняли свои даты UTC до фактического отображения пользователя, у нас почти не будет ошибок даты / времени.
oflahero
4

Если вы внимательно следите за тем, чтобы правильно передавать даты в формате UTC при установке значений, и все, о чем вы заботитесь, это убедиться, что DateTimeKind установлен правильно, когда объекты извлекаются из базы данных, см. Мой ответ здесь: https://stackoverflow.com/ а / 9386364/279590

michael.aird
источник
1

Для тех, кому нужно достичь решения @MattJohnson с .net framework 4, как я, с ограничением синтаксиса / метода отражения, потребуется небольшая модификация, как указано ниже:

     foreach (var property in properties)
        {     

            DateTimeKindAttribute attr  = (DateTimeKindAttribute) Attribute.GetCustomAttribute(property, typeof(DateTimeKindAttribute));

            if (attr == null)
                continue;

            var dt = property.PropertyType == typeof(DateTime?)
                ? (DateTime?)property.GetValue(entity,null)
                : (DateTime)property.GetValue(entity, null);

            if (dt == null)
                continue;

            //If the value is not null set the appropriate DateTimeKind;
            property.SetValue(entity, DateTime.SpecifyKind(dt.Value, attr.Kind) ,null);
        }  
Sxc
источник
1

Решение Мэтта Джонсона-Пинта работает, но если все ваши DateTimes должны быть в формате UTC, создание атрибута было бы слишком обходным. Вот как я это упростил:

public class MyContext : DbContext
{
    public DbSet<Foo> Foos { get; set; }

    public MyContext()
    {
        ((IObjectContextAdapter)this).ObjectContext.ObjectMaterialized +=
            (sender, e) => SetDateTimesToUtc(e.Entity);
    }

    private static void SetDateTimesToUtc(object entity)
    {
        if (entity == null)
        {
            return;
        }

        var properties = entity.GetType().GetProperties();
        foreach (var property in properties)
        {
            if (property.PropertyType == typeof(DateTime))
            {
                property.SetValue(entity, DateTime.SpecifyKind((DateTime)property.GetValue(entity), DateTimeKind.Utc));
            }
            else if (property.PropertyType == typeof(DateTime?))
            {
                var value = (DateTime?)property.GetValue(entity);
                if (value.HasValue)
                {
                    property.SetValue(entity, DateTime.SpecifyKind(value.Value, DateTimeKind.Utc));
                }
            }
        }
    }
}
Mielipuoli
источник
0

Другой подход - создать интерфейс со свойствами datetime, реализовать их в частичных классах сущностей. Затем используйте событие SavingChanges, чтобы проверить, относится ли объект к типу интерфейса, установите для этих значений datetime все, что хотите. Фактически, если они создаются / изменяются по типу дат, вы можете использовать это событие для их заполнения.

AD.Net
источник
неплохая идея, но классы не будут использоваться в анонимных выборках.
Джон Лорд
0

В моем случае у меня была только одна таблица с датами UTC. Вот что я сделал:

public partial class MyEntity
{
    protected override void OnPropertyChanged(string property)
    {
        base.OnPropertyChanged(property);            

        // ensure that values coming from database are set as UTC
        // watch out for property name changes!
        switch (property)
        {
            case "TransferDeadlineUTC":
                if (TransferDeadlineUTC.Kind == DateTimeKind.Unspecified)
                    TransferDeadlineUTC = DateTime.SpecifyKind(TransferDeadlineUTC, DateTimeKind.Utc);
                break;
            case "ProcessingDeadlineUTC":
                if (ProcessingDeadlineUTC.Kind == DateTimeKind.Unspecified)
                    ProcessingDeadlineUTC = DateTime.SpecifyKind(ProcessingDeadlineUTC, DateTimeKind.Utc);
            default:
                break;
        }
    }
}
Ронни Оверби
источник
0

Решения здесь полезны, но я ожидаю, что многие приходят к этому с проблемой, что они хотят, чтобы все их даты были доступны в локальном часовом поясе, но они хотят, чтобы он был переведен, чтобы постоянная версия сохранялась в формате UTC.

Для реализации этого есть 3 задачи:

  1. Чтение данных как UTC и преобразование в Local
  2. Настройка параметров запроса, например, SELECT * From PRODUCT, где SALEDATE <@ 1
  3. Хранение данных LocalTime в формате UTC

1. Считывание данных как UTC и преобразование в Local

В этом случае вышеупомянутое решение, основанное на работе Ивана Стоева DateTime.Kind установлено в неопределенное время, а не в UTC, при загрузке из базы данных будет делать то, что вам нужно.

2. Настройка параметров запроса.

Подобно решению Ивана для перехватчика, вы можете использовать перехватчик ReaderExecuting. Бонус в том, что это намного проще реализовать, чем ReaderExecuted.

    public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
    {
        foreach (DbParameter dbParameter in command.Parameters)
        {
            if (dbParameter.Value is DateTime dtLocal)
            {
                if (dtLocal.Kind != DateTimeKind.Utc)
                {
                    dbParameter.Value = dtLocal.ToUniversalTime();
                }
            }
        }
        base.ReaderExecuting(command, interceptionContext);
    }

3. Сохранение данных LocalTime в формате UTC.

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

    public override int SaveChanges()
    {
        UpdateCommonProperties();
        UpdateDatesToUtc();
        bool saveFailed;
        do
        {
            saveFailed = false;
            try
            {
                var result = base.SaveChanges();
                return result;
            }
            catch (DbUpdateConcurrencyException ex)
            {
                saveFailed = ConcurrencyExceptionHandler(ex);
            }

        } while (saveFailed);
        return 0;
    }

    private void UpdateDatesToUtc()
    {
        if (!ChangeTracker.HasChanges()) return;

        var modifiedEntries = ChangeTracker.Entries().Where(x => (x.State == EntityState.Added || x.State == EntityState.Modified));

        foreach (var entry in modifiedEntries)
        {
            entry.ModifyTypes<DateTime>(ConvertToUtc);
            entry.ModifyTypes<DateTime?>(ConvertToUtc);
        }
    }

    private static DateTime ConvertToUtc(DateTime dt)
    {
        if (dt.Kind == DateTimeKind.Utc) return dt;
        return dt.ToUniversalTime();
    }

    private static DateTime? ConvertToUtc(DateTime? dt)
    {
        if (dt?.Kind == DateTimeKind.Utc) return dt;
        return dt?.ToUniversalTime();
    }

И расширение (на основе ответа Talon https://stackoverflow.com/a/39974362/618660

public static class TypeReflectionExtension
{
    static Dictionary<Type, PropertyInfo[]> PropertyInfoCache = new Dictionary<Type, PropertyInfo[]>();

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

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

    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)));
        }
    }
}
статлер
источник