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 0d45d719fd..01c7292be9 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; @@ -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,39 @@ 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()) + { + if (Clock == null || !Clock.SupportsMultipleTimezone) + { + return; + } + + 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/AbpEntityFrameworkCoreTestModule.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/AbpEntityFrameworkCoreTestModule.cs index b1b92ab5b2..71522d0216 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/AbpEntityFrameworkCoreTestModule.cs +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/AbpEntityFrameworkCoreTestModule.cs @@ -1,4 +1,5 @@ -using Microsoft.Data.Sqlite; +using System; +using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage; @@ -10,6 +11,7 @@ using Volo.Abp.Modularity; using Volo.Abp.TestApp; using Volo.Abp.TestApp.Domain; using Volo.Abp.TestApp.EntityFrameworkCore; +using Volo.Abp.Timing; namespace Volo.Abp.EntityFrameworkCore { @@ -41,6 +43,8 @@ namespace Volo.Abp.EntityFrameworkCore abpDbContextConfigurationContext.DbContextOptions.UseSqlite(sqliteConnection); }); }); + + Configure(options => options.Kind = DateTimeKind.Utc); } public override void OnPreApplicationInitialization(ApplicationInitializationContext context) @@ -56,6 +60,9 @@ namespace Volo.Abp.EntityFrameworkCore using (var context = new TestMigrationsDbContext(new DbContextOptionsBuilder().UseSqlite(connection).Options)) { context.GetService().CreateTables(); + context.Database.ExecuteSqlRaw( + @"CREATE VIEW View_PersonView AS + SELECT Name, CreationTime, Birthday, LastActive FROM People"); } return connection; 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.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/PersonRepository.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/PersonRepository.cs new file mode 100644 index 0000000000..ab8c1920a7 --- /dev/null +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/PersonRepository.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Volo.Abp.Domain.Repositories.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; +using Volo.Abp.TestApp.Domain; + +namespace Volo.Abp.TestApp.EntityFrameworkCore +{ + public class PersonRepository : EfCoreRepository, IPersonRepository + { + public PersonRepository(IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + public async Task GetViewAsync(string name) + { + return await DbContext.PersonView.Where(x => x.Name == name).FirstOrDefaultAsync(); + } + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs index bfcecdeb65..5e243b9a03 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs @@ -11,6 +11,8 @@ namespace Volo.Abp.TestApp.EntityFrameworkCore public DbSet Cities { get; set; } + public DbSet PersonView { get; set; } + public DbSet DummyEntities { get; set; } public DbSet EntityWithIntPks { get; set; } @@ -29,6 +31,13 @@ namespace Volo.Abp.TestApp.EntityFrameworkCore { b.HasKey(p => new {p.PersonId, p.Number}); }); + + modelBuilder + .Entity(p => + { + p.HasNoKey(); + p.ToView("View_PersonView"); + }); } } } diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/IPersonRepository.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/IPersonRepository.cs new file mode 100644 index 0000000000..9058522346 --- /dev/null +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/IPersonRepository.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories; + +namespace Volo.Abp.TestApp.Domain +{ + public interface IPersonRepository : IBasicRepository + { + Task GetViewAsync(string name); + } +} 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/Domain/PersonView.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/PersonView.cs new file mode 100644 index 0000000000..e77827c263 --- /dev/null +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/PersonView.cs @@ -0,0 +1,17 @@ +using System; +using Volo.Abp.Timing; + +namespace Volo.Abp.TestApp.Domain +{ + public class PersonView + { + public string Name { get; set; } + + public DateTime CreationTime { get; set; } + + public DateTime? Birthday { get; set; } + + [DisableDateTimeNormalization] + public DateTime? LastActive { get; set; } + } +} \ No newline at end of file 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..e36225e53c --- /dev/null +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/AbpDateTimeValueConverter_Tests.cs @@ -0,0 +1,73 @@ +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 IPersonRepository _personRepository; + + protected AbpDateTimeValueConverter_Tests() + { + _personRepository = GetRequiredService(); + } + + [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"); + } + + [Fact] + public async Task DateTime_Kind_Should_Be_Normalized_To_UTC_View_Test() + { + var personName = "bob lee"; + await _personRepository.InsertAsync(new Person(Guid.NewGuid(), personName, 18) + { + Birthday = DateTime.Parse("2020-01-01 00:00:00"), + LastActive = DateTime.Parse("2020-01-01 00:00:00"), + }, true); + + var person = await _personRepository.GetViewAsync(personName); + + 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