Дата і час UTC


96

Чи можливо, щоб Entity Framework (я зараз використовую Code First Approach з 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");

Відповіді:


144

Ось один із підходів, який ви можете врахувати:

Спочатку визначте наступний атрибут:

[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
Якщо ви не можете зробити це робочим, ви, ймовірно, пропустили одне з цих пристосувань: використання системи; за допомогою System.Collections.Generic; за допомогою System.ComponentModel.DataAnnotations.Schema; за допомогою System.Linq; за допомогою System.Reflection;
Saustrup

7
@Saustrup - Ви знайдете більшість прикладів на SO, опускаючи вставки для стислості, якщо вони безпосередньо не стосуються питання. Але спасибі
Метт Джонсон-Пінт

4
@MattJohnson, не використовуючи заяви @ Saustrup, ви отримуєте деякі непотрібні помилки компіляції, такі як'System.Array' does not contain a definition for 'Where'
Jacob Eggers

7
Як сказав @SilverSideDown, це працює лише з .NET 4.5. Я створив кілька розширень, щоб зробити його сумісним із .NET 4.0 на gist.github.com/munr/3544bd7fab6615290561 . Інша річ, на яку слід звернути увагу, це те, що це не буде працювати з проекціями, а лише повністю завантаженими сутностями.
Мюн

5
Будь-які пропозиції щодо цього з прогнозами?
Jafin

32

Мені дуже подобається підхід Метта Джонсона, але в моїй моделі ВСІ мої учасники 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));
            }
        }
    }
}

1
Це дуже корисне розширення прийнятої відповіді!
Учень

Можливо, мені щось не вистачає, але як це за замовчуванням DateTimeKind.Utc на відміну від DateTimeKind.Unspecified?
Rhonage

1
@Rhonage Вибачте за це. За замовчуванням налаштовано в конструкторі ApplicationDbContext. Я оновив відповідь, щоб включити її.
Bob.at.Indigo.Health

1
@ Bob.at.AIPsychLab Дякую товаришу, зараз набагато зрозуміліше. Намагався розібратися, чи відбувається якесь відображення ваги - але ні, мертве просте!
Rhonage

Це не вдається, якщо модель має DateTImeатрибут без методу (загальнодоступного) встановлення. Запропонувати редагування. Дивіться також stackoverflow.com/a/3762475/2279059
Florian Winter

13

Ця відповідь працює з Entity Framework 6

Прийнята відповідь не працює для проектованого або анонімного об’єкта. Продуктивність також може бути проблемою.

Для досягнення цього нам потрібно використовувати a 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);
        }
    }
}

Поки це здається найкращою відповіддю. Спочатку я спробував варіацію атрибута, оскільки це здавалося менш далекосяжним, але мої модульні тести зазнали б невдачі з глузуванням, оскільки пов’язана подія конструктора, здається, не знає про відображення таблиць, які відбуваються у події OnModelCreating. Цей отримує мій голос!
Сенатор,

1
Чому ти тіниш Disposeі GetData?
user247702

2
Цей код повинен , ймовірно , кредитні @IvanStoev: stackoverflow.com/a/40349051/90287
Рамі А.

На жаль, це не вдається, якщо ви
Кріс,

@ user247702 yea shadowing Dispose is error, override Dispose (bool)
user2397863

9

Я вважаю, що я знайшов рішення, яке не вимагає будь-якої спеціальної перевірки UTC або маніпулювання DateTime.

В основному вам потрібно змінити ваші суб'єкти EF, щоб використовувати тип даних DateTimeOffset (NOT DateTime). Це збереже часовий пояс із значенням дати у базі даних (у моєму випадку SQL Server 2015).

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

І коли він передається назад на мій сервер, він перетворюється на UTC знову автоматично, також як і очікувалося.


7
DateTimeOffset не зберігає часовий пояс, всупереч загальному сприйняттю. Він зберігає зміщення від UTC, яке представляє значення. Зміст не можна відобразити в зворотному порядку, щоб визначити фактичний часовий пояс, з якого був створений зсув, тим самим роблячи тип даних майже марним.
Suncat2000

2
Ні, але його можна використовувати для правильного збереження DateTime: medium.com/@ojb500/in-praise-of-datetimeoffset-e0711f991cba
Карл,

1
Лише UTC не потребує місцезнаходження, оскільки воно скрізь однакове. Якщо ви використовуєте щось інше, ніж UTC, вам також потрібно місцезнаходження, інакше інформація про час марна, також при використанні datetimeoffset.
Хоріцу

@ Suncat2000 Це, безумовно, найрозумніший спосіб зберігати крапку в часі. Усі інші типи дати та часу також не дають вам часового поясу.
Джон

1
DATETIMEOFFSET зробить те, що хотів оригінальний плакат: збереже дату-час як UTC без необхідності виконувати будь-яке (явне) перетворення. @Carl DATETIME, DATETIME2 і DATETIMEOFFSET правильно зберігають значення дати та часу. Окрім додаткового зберігання зсуву від UTC, DATETIMEOFFSET майже не має переваг взагалі. Що ви використовуєте у своїй базі даних - це ваш дзвінок. Я просто хотів загнати додому, що він не зберігає часовий пояс, як багато людей помилково думають.
Suncat2000

5

Немає можливості вказати DataTimeKind в Entity Framework. Ви можете вирішити перетворити значення дати в utc перед збереженням у db і завжди вважати дані, отримані з db, як UTC. Але об'єкти DateTime, матералізовані під час запиту, завжди будуть "Не визначені". Ви також можете оцінити, використовуючи об'єкт DateTimeOffset замість DateTime.


5

Я зараз це досліджую, і більшість цих відповідей не дуже великі. З того, що я бачу, неможливо сказати EF6, що дати, що виходять з бази даних, мають формат UTC. Якщо це так, найпростішим способом переконатися, що властивості DateTime вашої моделі знаходяться в UTC, було б перевірити та перетворити в сеттері.

Ось псевдокод, подібний до c #, який описує алгоритм

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.


6
Як я з’ясував після кількох років боротьби з цією проблемою, якщо ви призначаєте або вибираєте поля DateTime в інші структури, наприклад об’єкт передачі даних, EF ігнорує як getter, так і setter методи. У цих випадках вам все одно доведеться змінити 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

Відповідно до коментаря @ Suncat2000, це просто не працює, і його слід видалити
Бен Морріс,

5

Для EF Core є велика дискусія на цю тему на GitHub: https://github.com/dotnet/efcore/isissue/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.


Однозначно найкраще рішення для мене! Дякую
Бен Морріс

Це працює з DateTimeOffset?
Марк Редман

1
@MarkRedman Я не думаю, що це має сенс, тому що якщо у вас є законний випадок використання DateTimeOffset, ви хочете зберегти інформацію про часовий пояс. Див docs.microsoft.com/en-us/dotnet/standard/datetime / ... або stackoverflow.com/a/14268167/3979621 коли вибирати між DateTime і DateTimeOffset.
Хонза Калфус

IsQueryTypeздається, замінено на IsKeyLess: github.com/dotnet/efcore/commit/…
Марк Тілеманс

4

Якщо ви обережно належним чином пропускаєте дати UTC, коли ви встановлюєте значення, і все, що вам важливо, - переконатися, що DateTimeKind встановлений належним чином при отриманні об'єктів із бази даних, дивіться мою відповідь тут: https://stackoverflow.com/ а / 9386364/279590


3

Ще рік, ще одне рішення! Це для 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

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);
        }  

1

Рішення Метта Джонсона-Пінта працює, але якщо всі ваші дати, як передбачається, є 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));
                }
            }
        }
    }
}

0

Іншим підходом було б створення інтерфейсу з властивостями datetime, реалізація їх на часткових класах сутності. А потім використовуйте подію SavingChanges, щоб перевірити, чи об’єкт має тип інтерфейсу, встановіть ці значення дати та часу на те, що ви хочете. Насправді, якщо вони створені / модифіковані на дату, ви можете використовувати цю подію для їх заповнення.


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;
        }
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.