Entity Framework DateTime และ UTC


100

เป็นไปได้ไหมที่จะมี 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");

คำตอบ:


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
หากคุณไม่สามารถใช้งานได้แสดงว่าคุณอาจพลาดหนึ่งในการใช้งานเหล่านี้: การใช้ระบบ; ใช้ System.Collections.Generic; ใช้ System.ComponentModel.DataAnnotations.Schema; ใช้ System.Linq; ใช้ System.Reflection;
Saustrup

8
@Saustrup - คุณจะพบตัวอย่างส่วนใหญ่ใน SO จะละเว้นการใช้งานเพื่อความกะทัดรัดเว้นแต่จะเกี่ยวข้องโดยตรงกับคำถาม แต่ขอบคุณ.
Matt Johnson-Pint

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

33

ฉันชอบแนวทางของ Matt Johnson มาก แต่ในแบบจำลองของฉันสมาชิก DateTime ของฉันทุกคนเป็น UTC และฉันไม่ต้องการตกแต่งพวกเขาทั้งหมดด้วยแอตทริบิวต์ ดังนั้นฉันจึงสรุปแนวทางของ Matt เพื่อให้ตัวจัดการเหตุการณ์ใช้ค่า 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.Indigohealth

1
@ Bob.at.AIPsychLab ขอบคุณเพื่อนชัดเจนขึ้นมากตอนนี้ กำลังพยายามคิดว่ามีการสะท้อนน้ำหนักเกิดขึ้นหรือไม่ - แต่ไม่ตายง่ายๆ!
Rhonage

สิ่งนี้จะล้มเหลวหากโมเดลมีDateTImeแอตทริบิวต์โดยไม่มีเมธอด (public) setter แก้ไขที่แนะนำ ดูstackoverflow.com/a/3762475/2279059
Florian Winter

16

สำหรับEF Coreมีการอภิปรายที่ยอดเยี่ยมในหัวข้อนี้ใน GitHub: https://github.com/dotnet/efcore/issues/4711

วิธีแก้ปัญหา (ให้เครดิตกับChristopher Haws ) ที่จะส่งผลในการจัดการวันที่ทั้งหมดเมื่อจัดเก็บเพื่อ / ดึงข้อมูลจากฐานข้อมูลเนื่องจาก 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


ทางออกที่ดีที่สุดสำหรับฉันแน่นอน! ขอบคุณ
Ben Morris

1
@MarkRedman ฉันไม่คิดว่ามันสมเหตุสมผลเพราะถ้าคุณมีกรณีการใช้งานที่ถูกต้องสำหรับ DateTimeOffset คุณต้องการเก็บข้อมูลเกี่ยวกับเขตเวลาไว้ด้วย โปรดดูdocs.microsoft.com/en-us/dotnet/standard/datetime/…หรือstackoverflow.com/a/14268167/3979621สำหรับเวลาที่จะเลือกระหว่าง DateTime และ DateTimeOffset
Honza Kalfus

3
IsQueryTypeดูเหมือนว่าจะถูกแทนที่ด้วยIsKeyLess: github.com/dotnet/efcore/commit/…
Mark Tielemans

เหตุใดจึงจำเป็นต้องตรวจสอบIsQueryType(หรือIsKeyLessตอนนี้)
Piotr Perak

15

คำตอบนี้ใช้ได้กับEntity Framework 6

คำตอบที่ยอมรับใช้ไม่ได้กับวัตถุที่คาดการณ์หรือไม่ระบุชื่อ ประสิทธิภาพอาจเป็นปัญหาได้เช่นกัน

เพื่อให้บรรลุเป้าหมายนี้เราต้องใช้ a DbCommandInterceptorซึ่งเป็นวัตถุที่ EntityFramework จัดเตรียมไว้

สร้าง Interceptor:

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
Rami A.

น่าเสียดายที่สิ่งนี้ล้มเหลวหากคุณกำลังทำแผนที่ข้อมูลเชิงพื้นที่
Chris

@ user247702 ใช่ shadowing Dispose เป็นความผิดพลาดแทนที่ Dispose (bool)
user2397863

10

ฉันเชื่อว่าฉันพบโซลูชันที่ไม่ต้องการการตรวจสอบ 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
Horitsu

1
DATETIMEOFFSET จะทำตามที่ผู้โพสต์ต้องการ: เก็บวันที่ - เวลาเป็น UTC โดยไม่ต้องทำการแปลงใด ๆ (อย่างชัดเจน) @Carl DATETIME, DATETIME2 และ DATETIMEOFFSET ทั้งหมดเก็บค่าวันที่ - เวลาอย่างถูกต้อง นอกเหนือจากการจัดเก็บค่าชดเชยเพิ่มเติมจาก UTC แล้ว DATETIMEOFFSET แทบจะไม่มีข้อได้เปรียบเลย สิ่งที่คุณใช้ในฐานข้อมูลของคุณคือการโทรของคุณ ฉันแค่อยากจะขับรถกลับบ้านในจุดที่มันไม่ได้เก็บไทม์โซนอย่างที่หลายคนคิดผิด
Suncat2000

2
@ Suncat2000 ข้อดีคือคุณสามารถส่งวันที่นี้ได้เช่นเดิมจาก API ของคุณไปยังเบราว์เซอร์ไคลเอนต์ของคุณ เมื่อไคลเอนต์เบราว์เซอร์เปิดวันที่นี้จะรู้ว่าออฟเซ็ตมาจาก UCT ดังนั้นจึงสามารถแปลงเป็นวันที่เริ่มต้นบนระบบที่ไคลเอ็นต์กำลังดูอยู่ ดังนั้นการแปลงจากเขตเวลาเซิร์ฟเวอร์ของคุณเป็นเขตเวลาของเบราว์เซอร์จึงเกิดขึ้นโดยที่นักพัฒนาไม่ต้องเขียนโค้ดใด ๆ
Moutono

6

ตอนนี้ฉันกำลังหาข้อมูลอยู่และคำตอบเหล่านี้ส่วนใหญ่ก็ไม่ค่อยดีเท่าไหร่ จากสิ่งที่ฉันเห็นไม่มีทางที่จะบอก EF6 ได้ว่าวันที่ที่ออกจากฐานข้อมูลอยู่ในรูปแบบ UTC หากเป็นเช่นนั้นวิธีที่ง่ายที่สุดในการตรวจสอบให้แน่ใจว่าคุณสมบัติ DateTime ของโมเดลของคุณอยู่ใน UTC คือการตรวจสอบและแปลงใน setter

นี่คือ c # เช่น pseudocode ซึ่งอธิบายอัลกอริทึม

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 และรสชาติของ deserializer ของคุณจะมีค่าเริ่มต้นเป็นประเภทนี้ ขึ้นอยู่กับคุณที่จะป้องกันไม่ให้วันที่ที่แปลเป็นภาษาท้องถิ่นซึ่งทำเครื่องหมายไม่DateTimeKind.Unspecifiedให้ไปถึงผู้ตั้งค่านั้นจากใครก็ได้ยกเว้น EF


6
ดังที่ฉันพบหลังจากต่อสู้กับปัญหานี้มาหลายปีหากคุณกำลังกำหนดหรือเลือกเขตข้อมูล DateTime ในโครงสร้างอื่น ๆ เช่นออบเจ็กต์การถ่ายโอนข้อมูล EF จะละเว้นทั้งเมธอด getter และ setter ในกรณีเหล่านี้คุณยังคงต้องเปลี่ยน Kind DateTimeKind.Utcหลังจากสร้างผลลัพธ์แล้ว ตัวอย่าง: ชุดทุกชนิดเพื่อfrom o in myContext.Records select new DTO() { BrokenTimestamp = o.BbTimestamp }; DateTimeKind.Unspecified
Suncat2000

1
ฉันใช้ DateTimeOffset กับ Entity Framework มาระยะหนึ่งแล้วและหากคุณระบุเอนทิตี EF ของคุณด้วยประเภทข้อมูล DateTimeOffset การสืบค้น EF ทั้งหมดของคุณจะส่งคืนวันที่โดยมีค่าชดเชยจาก UTC เหมือนกับที่บันทึกไว้ในฐานข้อมูล ดังนั้นหากคุณเปลี่ยนชนิดข้อมูลของคุณเป็น DateTimeOffset แทน DateTime คุณก็ไม่จำเป็นต้องใช้วิธีแก้ปัญหาข้างต้น
Moutono

ยินดีที่ได้รู้จัก! ขอบคุณ @Moutono

ตามความคิดเห็นของ @ Suncat2000 สิ่งนี้ไม่ได้ผลและควรถูกลบออก
Ben Morris

6

อีกปีทางออกอื่น! สำหรับ EF Core

ฉันมีDATETIME2(7)คอลัมน์มากมายที่จับDateTimeคู่และเก็บ UTC ไว้เสมอ ฉันไม่ต้องการจัดเก็บออฟเซ็ตเพราะถ้ารหัสของฉันถูกต้องออฟเซ็ตจะเป็นศูนย์เสมอ

ในขณะเดียวกันฉันมี colums อื่น ๆ ที่เก็บค่าวันที่ - เวลาพื้นฐานของออฟเซ็ตที่ไม่รู้จัก (ให้โดยผู้ใช้) ดังนั้นพวกเขาจะถูกจัดเก็บ / แสดง "ตามสภาพ" เท่านั้นและไม่ได้เปรียบเทียบกับสิ่งใดเลย

ดังนั้นฉันจึงต้องการโซลูชันที่สามารถนำไปใช้กับคอลัมน์เฉพาะได้

กำหนดวิธีการขยาย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


2
นี่เป็นทางออกที่ยอดเยี่ยมที่ควรจะสูงขึ้นโดยเฉพาะตอนนี้การพัฒนาใหม่ ๆ ส่วนใหญ่จะใช้ Core หรือ. NET 5 คะแนนจินตภาพเป็นโบนัสสำหรับนโยบายการบังคับใช้ UTC - หากผู้คนจำนวนมากเก็บวันที่ UTC ไว้จนถึงหน้าจอผู้ใช้จริง เราแทบจะไม่มีจุดบกพร่องของวันที่ / เวลาเลย
oflahero

5

ไม่มีวิธีระบุ DataTimeKind ใน Entity Framework คุณอาจตัดสินใจแปลงค่าวันเวลาเป็น utc ก่อนจัดเก็บเป็น db และถือว่าข้อมูลที่ดึงมาจาก db เป็น UTC เสมอ แต่อ็อบเจ็กต์ DateTime ที่ถูกทำให้เป็นคู่ระหว่างการสืบค้นจะเป็น "ไม่ระบุ" เสมอ คุณยังสามารถประเมินโดยใช้วัตถุ DateTimeOffset แทน DateTime


4

หากคุณระมัดระวังในการส่งวันที่ UTC อย่างถูกต้องเมื่อคุณตั้งค่าและสิ่งที่คุณสนใจคือตรวจสอบให้แน่ใจว่า DateTimeKind ได้รับการตั้งค่าอย่างถูกต้องเมื่อเรียกข้อมูลเอนทิตีจากฐานข้อมูลดูคำตอบของฉันที่นี่: https://stackoverflow.com/ ก / 9386364/279590


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

โซลูชันของ Matt Johnson-Pint ใช้งานได้ แต่ถ้า 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));
                }
            }
        }
    }
}

0

อีกแนวทางหนึ่งคือการสร้างอินเทอร์เฟซที่มีคุณสมบัติวันที่และเวลานำไปใช้กับคลาสเอนทิตีบางส่วน จากนั้นใช้เหตุการณ์ SavingChanges เพื่อตรวจสอบว่าวัตถุเป็นประเภทอินเทอร์เฟซหรือไม่ให้ตั้งค่าวันที่และเวลาเหล่านั้นเป็นสิ่งที่คุณต้องการ ในความเป็นจริงถ้าสิ่งเหล่านี้ถูกสร้างขึ้น / แก้ไขในประเภทวันที่คุณสามารถใช้เหตุการณ์นั้นเพื่อเติมข้อมูลได้


ไม่ใช่ความคิดที่ไม่ดี แต่จะไม่มีการใช้คลาสในการเลือกแบบไม่ระบุตัวตน
John Lord

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 * จาก PRODUCT โดยที่ SALEDATE <@ 1
  3. การจัดเก็บข้อมูลซึ่งเป็น LocalTime เป็น UTC

1. การอ่านข้อมูลเป็น UTC และแปลงเป็น Local

ในกรณีนี้วิธีแก้ปัญหาข้างต้นตามการทำงานของ Ivan Stoev DateTime.Kind ตั้งค่าเป็นไม่ระบุไม่ใช่ UTC เมื่อโหลดจากฐานข้อมูลจะทำสิ่งที่คุณต้องการ

2. การปรับพารามิเตอร์การสืบค้น

เช่นเดียวกับโซลูชันของ Ivan สำหรับ interceptor คุณสามารถใช้เครื่องดักฟัง 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)));
        }
    }
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.