Entity Framework DateTime và UTC


96

Có thể có Entity Framework (hiện tại tôi đang sử dụng Code First Approach với CTP5) lưu trữ tất cả các giá trị DateTime dưới dạng UTC trong cơ sở dữ liệu không?

Hoặc có thể có một cách nào đó để chỉ định nó trong ánh xạ, ví dụ trong cách này cho cột 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");

Câu trả lời:


144

Đây là một cách tiếp cận bạn có thể xem xét:

Trước tiên, hãy xác định thuộc tính sau:

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

Bây giờ kết nối thuộc tính đó với ngữ cảnh EF của bạn:

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

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

Bây giờ trên bất kỳ DateTimehoặc DateTime?thuộc tính nào, bạn có thể áp dụng thuộc tính này:

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

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

Với điều này, bất cứ khi nào Entity Framework tải một thực thể từ cơ sở dữ liệu, nó sẽ đặt thực DateTimeKindthể mà bạn chỉ định, chẳng hạn như UTC.

Lưu ý rằng điều này không có tác dụng gì khi lưu. Bạn vẫn phải chuyển đổi giá trị sang UTC một cách chính xác trước khi cố gắng lưu nó. Nhưng nó cho phép bạn đặt loại khi truy xuất, cho phép nó được xếp tuần tự thành UTC hoặc chuyển đổi sang các múi giờ khác với TimeZoneInfo.


7
Nếu bạn không thể làm cho điều này hoạt động, có thể bạn đang thiếu một trong những cách sử dụng sau: using System; sử dụng System.Collections.Generic; sử dụng System.ComponentModel.DataAnnotations.Schema; sử dụng System.Linq; sử dụng System.Reflection;
Saustrup

7
@Saustrup - Bạn sẽ thấy hầu hết các ví dụ trên SO sẽ bỏ qua cách sử dụng cho ngắn gọn, trừ khi chúng liên quan trực tiếp đến câu hỏi. Nhưng cảm ơn.
Matt Johnson-Pint

4
@MattJohnson mà không sử dụng câu lệnh của @ Saustrup, bạn sẽ gặp một số lỗi biên dịch không hữu ích như'System.Array' does not contain a definition for 'Where'
Jacob Eggers

7
Như @SilverSideDown đã nói, điều này chỉ hoạt động với .NET 4.5. Tôi đã tạo một số tiện ích mở rộng để làm cho nó tương thích với .NET 4.0 tại gist.github.com/munr/3544bd7fab6615290561 . Một điều khác cần lưu ý là điều này sẽ không hoạt động với các phép chiếu, chỉ các thực thể được tải đầy đủ.
Mun

5
Bất kỳ đề xuất nào về việc thực hiện điều này với các dự báo?
Jafin

32

Tôi thực sự thích cách tiếp cận của Matt Johnson, nhưng trong mô hình của tôi, TẤT CẢ các thành viên DateTime của tôi đều là UTC và tôi không muốn phải trang trí tất cả họ bằng một thuộc tính. Vì vậy, tôi đã tổng quát hóa cách tiếp cận của Matt để cho phép trình xử lý sự kiện áp dụng giá trị Loại mặc định trừ khi một thành viên được trang trí rõ ràng bằng thuộc tính.

Hàm tạo cho lớp ApplicationDbContext bao gồm mã này:

/// <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 trông như thế này:

/// <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
Đây là phần mở rộng rất hữu ích cho câu trả lời được chấp nhận!
Người học

Có lẽ tôi đang thiếu một cái gì đó, nhưng làm thế nào điều này mặc định thành DateTimeKind.Utc trái ngược với DateTimeKind.Unspecified?
Rhonage

1
@Rhonage Xin lỗi về điều đó. Mặc định được thiết lập trong phương thức khởi tạo ApplicationDbContext. Tôi đã cập nhật câu trả lời để bao gồm điều đó.
Bob.at.Indigo.Health

1
@ Bob.at.AIPsychLab Cảm ơn người bạn đời, bây giờ rõ ràng hơn nhiều. Đang cố gắng tìm hiểu xem có sự phản chiếu trọng lượng nào đang xảy ra hay không - nhưng không, đơn giản là chết!
Rhonage

Điều này không thành công nếu một mô hình có DateTImethuộc tính không có phương thức setter (công khai). Chỉnh sửa được đề xuất. Xem thêm stackoverflow.com/a/3762475/2279059
Florian Winter

13

Câu trả lời này hoạt động với Entity Framework 6

Câu trả lời được chấp nhận không hoạt động cho đối tượng Dự kiến ​​hoặc Ẩn danh. Hiệu suất cũng có thể là một vấn đề.

Để đạt được điều này, chúng ta cần sử dụng a DbCommandInterceptor, một đối tượng do EntityFramework cung cấp.

Tạo 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 là DbDataReader, được chúng tôi thay thế bằng

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

Đăng ký thiết bị đánh chặn trong DbConfiguration

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

Cuối cùng, đăng ký cấu hình cho DbContext

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

Đó là nó. Chúc mừng.

Để đơn giản, đây là toàn bộ việc triển khai 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);
        }
    }
}

Cho đến nay đây có vẻ là câu trả lời tốt nhất. Tôi đã thử biến thể thuộc tính trước vì nó có vẻ ít tiếp cận hơn nhưng các bài kiểm tra đơn vị của tôi sẽ không thành công với chế độ chế nhạo vì liên kết sự kiện phương thức khởi tạo dường như không biết về ánh xạ bảng xảy ra trong sự kiện OnModelCreating. Cái này nhận được phiếu bầu của tôi!
The Senator

1
Tại sao bạn lại bóng DisposeGetData?
user247702

2
Mã này có thể sẽ ghi có @IvanStoev: stackoverflow.com/a/40349051/90287
Rami A.

Rất tiếc, điều này không thành công nếu bạn đang lập bản đồ dữ liệu Không gian
Chris

@ user247702 yea shadowing Dispose là nhầm lẫn, ghi đè Dispose (bool)
user2397863 30/09/19

9

Tôi tin rằng tôi đã tìm thấy một giải pháp không yêu cầu bất kỳ kiểm tra UTC tùy chỉnh hoặc thao tác DateTime nào.

Về cơ bản, bạn cần thay đổi các thực thể EF của mình để sử dụng kiểu dữ liệu DateTimeOffset (NOT DateTime). Điều này sẽ lưu trữ múi giờ với giá trị ngày trong cơ sở dữ liệu (trong trường hợp của tôi là SQL Server 2015).

Khi EF Core yêu cầu dữ liệu từ DB, nó cũng sẽ nhận được thông tin múi giờ. Khi bạn chuyển dữ liệu này đến một ứng dụng web (trong trường hợp của tôi là Angular2), ngày sẽ tự động được chuyển đổi thành múi giờ địa phương của trình duyệt, đó là điều tôi mong đợi.

Và khi nó được chuyển trở lại máy chủ của tôi, nó sẽ tự động được chuyển đổi thành UTC một lần nữa, cũng như mong đợi.


7
DateTimeOffset không lưu trữ múi giờ, trái với nhận thức thông thường. Nó lưu trữ phần bù từ UTC mà giá trị đại diện. Sự chênh lệch không thể được ánh xạ ngược lại để xác định múi giờ thực sự mà khoảng chênh lệch được tạo từ đó, do đó làm cho kiểu dữ liệu gần như vô dụng.
Suncat2000

2
Không, nhưng nó có thể được sử dụng để lưu trữ một DateTime một cách chính xác: medium.com/@ojb500/in-praise-of-datetimeoffset-e0711f991cba
Carl

1
Chỉ UTC là không cần vị trí, vì nó ở mọi nơi như nhau. Nếu bạn sử dụng thứ gì đó khác ngoài giờ UTC, bạn cũng cần vị trí, nếu không thì thông tin về thời gian cũng vô dụng, cũng như sử dụng datetimeoffset.
Horitsu

@ Suncat2000 Cho đến nay, đó là cách hợp lý nhất để lưu trữ một thời điểm. Tất cả các loại ngày / giờ khác cũng không cung cấp cho bạn múi giờ.
John

1
DATETIMEOFFSET sẽ làm những gì mà người đăng ban đầu muốn: lưu trữ ngày-giờ dưới dạng UTC mà không cần phải thực hiện bất kỳ chuyển đổi nào (rõ ràng). @Carl DATETIME, DATETIME2 và DATETIMEOFFSET đều lưu trữ chính xác giá trị ngày-giờ. Ngoài việc lưu trữ bổ sung một phần bù từ UTC, DATETIMEOFFSET hầu như không có lợi thế nào. Những gì bạn sử dụng trong cơ sở dữ liệu của mình là cuộc gọi của bạn. Tôi chỉ muốn lái xe về nhà ở điểm nó không lưu trữ múi giờ như nhiều người lầm tưởng.
Suncat2000

5

Không có cách nào để chỉ định DataTimeKind trong Khung thực thể. Bạn có thể quyết định chuyển đổi các giá trị ngày giờ thành utc trước khi lưu trữ thành db và luôn giả sử dữ liệu được lấy lại từ db dưới dạng UTC. Nhưng các đối tượng DateTime được vật chất hóa trong quá trình truy vấn sẽ luôn là "Không xác định". Bạn cũng có thể đánh giá bằng cách sử dụng đối tượng DateTimeOffset thay vì DateTime.


5

Tôi đang nghiên cứu vấn đề này ngay bây giờ và hầu hết các câu trả lời này không chính xác tuyệt vời. Từ những gì tôi có thể thấy, không có cách nào để nói với EF6 rằng ngày tháng ra khỏi cơ sở dữ liệu ở định dạng UTC. Nếu đúng như vậy, cách đơn giản nhất để đảm bảo thuộc tính DateTime của mô hình của bạn ở UTC là xác minh và chuyển đổi trong bộ định thời.

Đây là một số mã giả giống c # mô tả thuật toán

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

Hai nhánh đầu tiên là hiển nhiên. Cuối cùng giữ nước sốt bí mật.

Khi EF6 tạo một mô hình từ dữ liệu được tải từ cơ sở dữ liệu, thì DateTimes DateTimeKind.Unspecified. Nếu bạn biết tất cả các ngày của bạn đều là UTC trong db, thì nhánh cuối cùng sẽ phù hợp với bạn.

DateTime.Nowluôn luôn DateTimeKind.Local, vì vậy thuật toán trên hoạt động tốt cho các ngày được tạo trong mã. Hầu hết thời gian.

Tuy nhiên, bạn phải thận trọng vì có nhiều cách khác DateTimeKind.Unspecifiedcó thể xâm nhập vào mã của bạn. Ví dụ: bạn có thể deserialize các mô hình của mình từ dữ liệu JSON và hương vị deserializer của bạn được mặc định thành loại này. Bạn tùy thuộc vào việc đề phòng các ngày đã bản địa hóa được đánh dấu DateTimeKind.Unspecifiedtừ bất kỳ ai khác ngoài EF.


6
Như tôi đã phát hiện ra sau vài năm vật lộn với vấn đề này, nếu bạn đang gán hoặc chọn các trường DateTime vào các cấu trúc khác, ví dụ như một đối tượng truyền dữ liệu, EF sẽ bỏ qua cả phương thức getter và setter. Trong những trường hợp này, bạn vẫn phải thay đổi Loại thành DateTimeKind.Utcsau khi kết quả của bạn được tạo. Ví dụ: from o in myContext.Records select new DTO() { BrokenTimestamp = o.BbTimestamp };đặt tất cả Loại thành DateTimeKind.Unspecified.
Suncat2000,

1
Tôi đã sử dụng DateTimeOffset với Entity Framework được một thời gian và nếu bạn chỉ định các thực thể EF của mình với kiểu dữ liệu là DateTimeOffset, thì tất cả các truy vấn EF của bạn sẽ trả về ngày tháng với độ lệch từ UTC, giống như nó được lưu trong DB. Vì vậy, nếu bạn đã thay đổi kiểu dữ liệu của mình thành DateTimeOffset thay vì DateTime, bạn sẽ không cần giải pháp trên.
Moutono

Thật tuyệt khi biết! Cảm ơn @Moutono

Theo nhận xét của @ Suncat2000, điều này chỉ đơn giản là không hoạt động và nên bị xóa
Ben Morris

5

Đối với EF Core , có một cuộc thảo luận tuyệt vời về chủ đề này trên GitHub: https://github.com/dotnet/efcore/issues/4711

Một giải pháp (tín dụng cho Christopher Haws ) sẽ dẫn đến việc xử lý tất cả các ngày khi lưu trữ chúng vào / truy xuất chúng từ cơ sở dữ liệu dưới dạng UTC là thêm phần sau vào OnModelCreatingphương thức của DbContextlớp của bạn :

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

Ngoài ra, hãy kiểm tra liên kết này nếu bạn muốn loại trừ một số thuộc tính của một số thực thể khỏi bị coi là UTC.


Chắc chắn là giải pháp tốt nhất cho tôi! Cảm ơn
Ben Morris

Điều này có hoạt động với DateTimeOffset không?
Mark Redman

1
@MarkRedman Tôi không nghĩ điều đó có lý, vì nếu bạn có trường hợp sử dụng hợp pháp cho DateTimeOffset, bạn cũng muốn giữ thông tin về múi giờ. Xem docs.microsoft.com/en-us/dotnet/standard/datetime/… hoặc stackoverflow.com/a/14268167/3979621 để biết thời điểm chọn giữa DateTime và DateTimeOffset.
Honza Kalfus

IsQueryTypedường như đã được thay thế bằng IsKeyLess: github.com/dotnet/efcore/commit/…
Mark Tielemans

4

Nếu bạn cẩn thận chuyển đúng ngày UTC khi bạn đặt giá trị và tất cả những gì bạn quan tâm là đảm bảo DateTimeKind được đặt đúng cách khi các thực thể được truy xuất từ ​​cơ sở dữ liệu, hãy xem câu trả lời của tôi tại đây: https://stackoverflow.com/ a / 9386364/279590


3

Một năm nữa, một giải pháp khác! Đây là dành cho EF Core.

Tôi có rất nhiều DATETIME2(7)cột ánh xạ tới DateTimevà luôn lưu trữ UTC. Tôi không muốn lưu trữ phần bù vì nếu mã của tôi đúng thì phần bù sẽ luôn bằng 0.

Trong khi đó, tôi có các cột khác lưu trữ các giá trị ngày-giờ cơ bản không xác định (do người dùng cung cấp), vì vậy chúng chỉ được lưu trữ / hiển thị "nguyên trạng", và không được so sánh với bất kỳ thứ gì.

Do đó, tôi cần một giải pháp mà tôi có thể áp dụng cho các cột cụ thể.

Xác định phương thức mở rộng 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));
}

Điều này sau đó có thể được sử dụng trên các thuộc tính trong thiết lập mô hình:

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

Nó có một lợi thế nhỏ so với các thuộc tính là bạn chỉ có thể áp dụng nó cho các thuộc tính đúng loại.

Lưu ý rằng nó giả định các giá trị từ DB là ở UTC nhưng chỉ có sai Kind. Do đó, nó phân tích các giá trị mà bạn cố gắng lưu trữ trong DB, đưa ra một ngoại lệ mô tả nếu chúng không phải là UTC.


1
Đây là một giải pháp tuyệt vời nên được nâng cao hơn nữa, đặc biệt là hiện tại hầu hết các phát triển mới sẽ sử dụng Core hoặc .NET 5. Thưởng điểm tưởng tượng cho chính sách thực thi UTC - nếu nhiều người giữ nguyên ngày UTC của họ cho đến màn hình người dùng thực tế, chúng tôi hầu như không có bất kỳ lỗi ngày / giờ nào.
oflahero

1

Đối với những người cần đạt được giải pháp @MattJohnson với .net framework 4 như tôi, với giới hạn về cú pháp / phương thức phản ánh, nó yêu cầu sửa đổi một chút như được liệt kê bên dưới:

     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

Giải pháp của Matt Johnson-Pint hoạt động, nhưng nếu tất cả DateTimes của bạn được cho là UTC, thì việc tạo một thuộc tính sẽ quá mạch lạc. Đây là cách tôi đơn giản hóa nó:

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

Một cách tiếp cận khác là tạo một giao diện với các thuộc tính datetime, triển khai chúng trên các lớp thực thể một phần. Và sau đó sử dụng sự kiện SavingChanges để kiểm tra xem đối tượng có thuộc loại giao diện hay không, đặt các giá trị datetime đó thành bất kỳ thứ gì bạn muốn. Trên thực tế, nếu đây là loại ngày được tạo / sửa đổi, bạn có thể sử dụng sự kiện đó để điền chúng.


0

Trong trường hợp của tôi, tôi chỉ có một bảng với lịch ngày UTC. Đây là những gì tôi đã làm:

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;
        }
    }
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.