diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs index 3d4b45d75d..9064ddc2ba 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs @@ -18,10 +18,12 @@ using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Entities; using Volo.Abp.Domain.Entities.Events; using Volo.Abp.EntityFrameworkCore.EntityHistory; +using Volo.Abp.EntityFrameworkCore.ValueConverters; using Volo.Abp.Guids; using Volo.Abp.MultiTenancy; using Volo.Abp.Reflection; using Volo.Abp.Threading; +using Volo.Abp.Timing; namespace Volo.Abp.EntityFrameworkCore { @@ -48,6 +50,8 @@ namespace Volo.Abp.EntityFrameworkCore public IAuditingManager AuditingManager { get; set; } + public IClock Clock { get; set; } + public ILogger> Logger { get; set; } private static readonly MethodInfo ConfigureBasePropertiesMethodInfo @@ -57,6 +61,13 @@ namespace Volo.Abp.EntityFrameworkCore BindingFlags.Instance | BindingFlags.NonPublic ); + private static readonly MethodInfo ConfigureValueConverterMethodInfo + = typeof(AbpDbContext) + .GetMethod( + nameof(ConfigureValueConverter), + BindingFlags.Instance | BindingFlags.NonPublic + ); + protected AbpDbContext(DbContextOptions options) : base(options) { @@ -75,6 +86,10 @@ namespace Volo.Abp.EntityFrameworkCore ConfigureBasePropertiesMethodInfo .MakeGenericMethod(entityType.ClrType) .Invoke(this, new object[] { modelBuilder, entityType }); + + ConfigureValueConverterMethodInfo + .MakeGenericMethod(entityType.ClrType) + .Invoke(this, new object[] { modelBuilder, entityType }); } } @@ -488,6 +503,34 @@ namespace Volo.Abp.EntityFrameworkCore } } + protected virtual void ConfigureValueConverter(ModelBuilder modelBuilder, IMutableEntityType mutableEntityType) + where TEntity : class + { + if (mutableEntityType.BaseType == null && + !typeof(TEntity).IsDefined(typeof(DisableDateTimeNormalizationAttribute), true) && + !typeof(TEntity).IsDefined(typeof(OwnedAttribute), true) && + !mutableEntityType.IsOwned()) + { + var dateTimeValueConverter = new AbpDateTimeValueConverter(Clock); + + var dateTimePropertyInfos = typeof(TEntity).GetProperties() + .Where(property => + (property.PropertyType == typeof(DateTime) || + property.PropertyType == typeof(DateTime?)) && + property.CanWrite && + !property.IsDefined(typeof(DisableDateTimeNormalizationAttribute), true) + ).ToList(); + + dateTimePropertyInfos.ForEach(property => + { + modelBuilder + .Entity() + .Property(property.Name) + .HasConversion(dateTimeValueConverter); + }); + } + } + protected virtual bool ShouldFilterEntity(IMutableEntityType entityType) where TEntity : class { if (typeof(IMultiTenant).IsAssignableFrom(typeof(TEntity))) diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueConverters/AbpDateTimeValueConverter.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueConverters/AbpDateTimeValueConverter.cs new file mode 100644 index 0000000000..e3562c4134 --- /dev/null +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueConverters/AbpDateTimeValueConverter.cs @@ -0,0 +1,17 @@ +using System; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Volo.Abp.Timing; + +namespace Volo.Abp.EntityFrameworkCore.ValueConverters +{ + public class AbpDateTimeValueConverter : ValueConverter + { + public AbpDateTimeValueConverter(IClock clock, [CanBeNull] ConverterMappingHints mappingHints = null) + : base( + x => x.HasValue ? clock.Normalize(x.Value) : x, + x => x.HasValue ? clock.Normalize(x.Value) : x, mappingHints) + { + } + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/ValueConverters/AbpDateTimeValueConverter_Tests.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/ValueConverters/AbpDateTimeValueConverter_Tests.cs new file mode 100644 index 0000000000..c2793b3b67 --- /dev/null +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/ValueConverters/AbpDateTimeValueConverter_Tests.cs @@ -0,0 +1,9 @@ +using Volo.Abp.TestApp.Testing; + +namespace Volo.Abp.EntityFrameworkCore.ValueConverters +{ + public class AbpDateTimeValueConverter_Tests : AbpDateTimeValueConverter_Tests + { + + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/Person.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/Person.cs index 3bbec118a2..9075740f11 100644 --- a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/Person.cs +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/Person.cs @@ -2,6 +2,7 @@ using System; using System.Collections.ObjectModel; using Volo.Abp.Domain.Entities.Auditing; using Volo.Abp.MultiTenancy; +using Volo.Abp.Timing; namespace Volo.Abp.TestApp.Domain { @@ -15,6 +16,11 @@ namespace Volo.Abp.TestApp.Domain public virtual int Age { get; set; } + public virtual DateTime? Birthday { get; set; } + + [DisableDateTimeNormalization] + public virtual DateTime? LastActive { get; set; } + public virtual Collection Phones { get; set; } private Person() diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/AbpDateTimeValueConverter_Tests.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/AbpDateTimeValueConverter_Tests.cs new file mode 100644 index 0000000000..cbeef807cb --- /dev/null +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/AbpDateTimeValueConverter_Tests.cs @@ -0,0 +1,53 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Modularity; +using Volo.Abp.TestApp.Domain; +using Volo.Abp.Timing; +using Xunit; + +namespace Volo.Abp.TestApp.Testing +{ + public abstract class AbpDateTimeValueConverter_Tests : TestAppTestBase + where TStartupModule : IAbpModule + { + private readonly IBasicRepository _personRepository; + + protected AbpDateTimeValueConverter_Tests() + { + _personRepository = GetRequiredService>(); + } + + protected override void AfterAddApplication(IServiceCollection services) + { + services.Configure(options => options.Kind = DateTimeKind.Utc); + } + + [Fact] + public async Task DateTime_Kind_Should_Be_Normalized_To_UTC_Test() + { + var personId = Guid.Parse("4125582e-d100-4c27-aa84-e4de85830dca"); + await _personRepository.InsertAsync(new Person(personId, "bob lee", 18) + { + Birthday = DateTime.Parse("2020-01-01 00:00:00"), + LastActive = DateTime.Parse("2020-01-01 00:00:00"), + }, true); + + var person = await _personRepository.GetAsync(personId); + + person.ShouldNotBeNull(); + person.CreationTime.Kind.ShouldBe(DateTimeKind.Utc); + + person.Birthday.ShouldNotBeNull(); + person.Birthday.Value.Kind.ShouldBe(DateTimeKind.Utc); + person.Birthday.Value.ToString("yyy-MM-dd HH:mm:ss").ShouldBe("2020-01-01 00:00:00"); + + //LastActive DisableDateTimeNormalization + person.LastActive.ShouldNotBeNull(); + person.LastActive.Value.Kind.ShouldBe(DateTimeKind.Unspecified); + person.LastActive.Value.ToString("yyy-MM-dd HH:mm:ss").ShouldBe("2020-01-01 00:00:00"); + } + } +} \ No newline at end of file