Casper-SC
@Casper-SC
Программист (.NET)

Entity Framework Core и MySQL — почему при чтении из БД и конвертации значения в DateTimeOffset теряются миллисекунды?

SHOW VARIABLES LIKE '%version%';

На Windows 10 установлен MySQL Server 8.
innodb_version - 8.0.11
version - 8.0.11


На Ubuntu 18.04 LTS MySQL Server
innodb_version - 5.7.30
version - 5.7.30-0ubuntu0.18.04.1


Есть такой код:

internal class Const
    {
        internal const string CHARSET = "utf8mb4";
        internal const string COLLATE = "utf8mb4_unicode_ci";
    }


using System;

namespace Domain.Entities.Events
{
    /// <summary>
    /// Событие системы.
    /// </summary>
    public class SystemEvent
    {
        /// <summary>
        /// Уникальный идентификатор события.
        /// </summary>
        public long SystemEventId { get; set; }

        /// <summary>
        /// Метка времени в формате UTC, когда произошло событие.
        /// </summary>
        public DateTimeOffset EventTime { get; set; }
    }
}


using Microsoft.EntityFrameworkCore;
using MySql.Data.EntityFrameworkCore.Extensions;

namespace Infrastructure.Data.Contexts
{
    /// <summary>
    /// Контекст для работы с БД событий.
    /// </summary>
    public class SystemEventContext : DbContextEx
    {
        public DbSet<SystemEvent> Events { get; set; }

        public SystemEventContext(DbContextOptions<SystemEventContext> options)
            : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            const string TIMESTAMP_COLUMN_TYPE = "timestamp(3)";

            builder.Entity<SystemEvent>()
                .ForMySQLHasCharset(Const.CHARSET)
                .ForMySQLHasCollation(Const.COLLATE);
            builder.Entity<SystemEvent>()
                .HasKey(x => new { x.SystemEventId });
            builder.Entity<SystemEvent>()
                .HasIndex(x => new { x.EventTime });
            builder.Entity<SystemEvent>()
                .Property(x => x.EventTime)
                .HasColumnType(TIMESTAMP_COLUMN_TYPE);

            base.OnModelCreating(builder);

            // Это необходимо для MySQL
            // https://github.com/aspnet/EntityFrameworkCore/issues/14051#issuecomment-450469445
            SetBoolToInt32Converter(builder);
        }
    }
}


using System.Data;
using Domain.ValueConversion;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage;

namespace Infrastructure.Data.Contexts
{
    public class DbContextEx : DbContext
    {
        private IDbContextTransaction _currentTransaction;

        public DbContextEx(DbContextOptions options)
            : base(options)
        {
        }
        
        protected static void SetBoolToInt32Converter(ModelBuilder builder)
        {
            foreach (IMutableEntityType entityType in builder.Model.GetEntityTypes())
            {
                foreach (IMutableProperty property in entityType.GetProperties())
                {
                    if (property.ClrType == typeof(bool))
                    {
                        property.SetValueConverter(new BoolToIntConverter());
                    }
                }
            }
        }

        #region Transaction Handling

        public void BeginTransaction(IsolationLevel isolationLevel = IsolationLevel.ReadCommitted)
        {
            if (_currentTransaction != null)
            {
                return;
            }

            if (!Database.IsInMemory())
            {
                _currentTransaction = Database.BeginTransaction(isolationLevel);
            }
        }

        public void CommitTransaction()
        {
            try
            {
                _currentTransaction?.Commit();
            }
            catch
            {
                RollbackTransaction();
                throw;
            }
            finally
            {
                if (_currentTransaction != null)
                {
                    _currentTransaction.Dispose();
                    _currentTransaction = null;
                }
            }
        }

        public void RollbackTransaction()
        {
            try
            {
                _currentTransaction?.Rollback();
            }
            finally
            {
                if (_currentTransaction != null)
                {
                    _currentTransaction.Dispose();
                    _currentTransaction = null;
                }
            }
        }

        #endregion
    }
}
  • Вопрос задан
  • 222 просмотра
Решения вопроса 1
Casper-SC
@Casper-SC Автор вопроса
Программист (.NET)
Проблему удалось решить так:

using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;

namespace Domain.ValueConversion
{
    public static class EntityFrameworkCoreModelBuilderExtensions
    {
        public static void AddDateTimeOffsetConverter(this ModelBuilder builder)
        {
            // SQLite does not support DateTimeOffset
            foreach (var property in builder.Model.GetEntityTypes()
                .SelectMany(t => t.GetProperties())
                .Where(p => p.ClrType == typeof(DateTimeOffset)))
            {
                property.SetValueConverter(
                    new ValueConverter<DateTimeOffset, DateTime>(
                        convertToProviderExpression: dateTimeOffset => dateTimeOffset.UtcDateTime,
                        convertFromProviderExpression: dateTime => new DateTimeOffset(dateTime)
                    ));
            }

            foreach (var property in builder.Model.GetEntityTypes()
                .SelectMany(t => t.GetProperties())
                .Where(p => p.ClrType == typeof(DateTimeOffset?)))
            {
                property.SetValueConverter(
                    new ValueConverter<DateTimeOffset?, DateTime>(
                        convertToProviderExpression: dateTimeOffset => dateTimeOffset.Value.UtcDateTime,
                        convertFromProviderExpression: dateTime => new DateTimeOffset(dateTime)
                    ));
            }
        }

        public static void AddDateTimeUtcKindConverter(this ModelBuilder builder)
        {
            // If you store a DateTime object to the DB with a DateTimeKind of either `Utc` or `Local`,
            // when you read that record back from the DB you'll get a DateTime object whose kind is `Unspecified`.
            // Here is a fix for it!
            var dateTimeConverter = new ValueConverter<DateTime, DateTime>(
                v => v.Kind == DateTimeKind.Utc ? v : v.ToUniversalTime(),
                v => DateTime.SpecifyKind(v, DateTimeKind.Utc));

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

            foreach (var property in builder.Model.GetEntityTypes()
                .SelectMany(t => t.GetProperties()))
            {
                if (property.ClrType == typeof(DateTime))
                {
                    property.SetValueConverter(dateTimeConverter);
                }

                if (property.ClrType == typeof(DateTime?))
                {
                    property.SetValueConverter(nullableDateTimeConverter);
                }
            }
        }
    }
}

Источник, Лицензия

using Microsoft.EntityFrameworkCore;
using MySql.Data.EntityFrameworkCore.Extensions;

namespace Infrastructure.Data.Contexts
{
    /// <summary>
    /// Контекст для работы с БД событий.
    /// </summary>
    public class SystemEventContext : DbContextEx
    {
        public DbSet<SystemEvent> Events { get; set; }

        public SystemEventContext(DbContextOptions<SystemEventContext> options)
            : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            const string TIMESTAMP_COLUMN_TYPE = "timestamp(3)";
           // ...
            base.OnModelCreating(builder);
           // ...

            builder.AddDateTimeOffsetConverter();
            builder.AddDateTimeUtcKindConverter();
        }
    }
}
Ответ написан
Пригласить эксперта
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы