diff --git a/src/Contrib/Data/Orm/EFCore/Masa.Contrib.Data.EFCore.Tests/DbContextTest.cs b/src/Contrib/Data/Orm/EFCore/Masa.Contrib.Data.EFCore.Tests/DbContextTest.cs index 4807739cd..363e92fd6 100644 --- a/src/Contrib/Data/Orm/EFCore/Masa.Contrib.Data.EFCore.Tests/DbContextTest.cs +++ b/src/Contrib/Data/Orm/EFCore/Masa.Contrib.Data.EFCore.Tests/DbContextTest.cs @@ -52,10 +52,7 @@ public void TestDbContext4() [TestMethod] public void TestDbContext5() { - Services.Configure<AppConfigOptions>(options => - { - options.DbConnectionString = MemoryConnectionString; - }); + Services.Configure<AppConfigOptions>(options => { options.DbConnectionString = MemoryConnectionString; }); var dbContext = CreateDbContext<CustomDbContext5>(null); var student = GenerateStudent(); dbContext.Set<Student>().Add(student); @@ -79,10 +76,7 @@ public void TestDbContext6() [TestMethod] public void TestDbContext7() { - Services.Configure<AppConfigOptions>(options => - { - options.DbConnectionString = MemoryConnectionString; - }); + Services.Configure<AppConfigOptions>(options => { options.DbConnectionString = MemoryConnectionString; }); var dbContext = CreateDbContext<CustomDbContext7>(null); var student = GenerateStudent(); dbContext.Set<Student>().Add(student); @@ -128,9 +122,9 @@ private static Student GenerateStudent() private static Student VerifyStudent(DbContext dbContext, Guid id, bool isTracking = false) { - var student = isTracking ? - dbContext.Set<Student>().AsTracking().Include(s => s.Hobbies).FirstOrDefault(s => s.Id == id) : - dbContext.Set<Student>().AsNoTracking().Include(s => s.Hobbies).FirstOrDefault(s => s.Id == id); + var student = isTracking + ? dbContext.Set<Student>().AsTracking().Include(s => s.Hobbies).FirstOrDefault(s => s.Id == id) + : dbContext.Set<Student>().AsNoTracking().Include(s => s.Hobbies).FirstOrDefault(s => s.Id == id); Assert.IsNotNull(student); return student; } @@ -202,6 +196,83 @@ public async Task TestSoftDeleteAsync() } } + [TestMethod] + public async Task TestModifierBySoftDeleteAsync() + { + var creatorUserContext = new CustomUserContext("1"); + Services.Configure<AuditEntityOptions>(options => options.UserIdType = typeof(int)); + Services.AddSingleton<IUserContext>(creatorUserContext); + + IServiceProvider serviceProvider = default!; + var dbContext = await CreateDbContextAsync<CustomDbContext>(dbContextBuilder => + { + dbContextBuilder.UseFilter(); + dbContextBuilder.UseInMemoryDatabase(MemoryConnectionString); + }, sp => serviceProvider = sp); + var goods = new Goods() + { + Name = "masa", + Logs = new List<OperationLog>() + { + new("initialize"), + new("initialize2"), + } + }; + await dbContext.Set<Goods>().AddAsync(goods); + await dbContext.SaveChangesAsync(); + + var goodsCreate = await dbContext.Set<Goods>().Include(g => g.Logs).FirstOrDefaultAsync(g => g.Name == "masa"); + Assert.IsNotNull(goodsCreate); + + var modifierByCrate = goodsCreate.Modifier; + var modificationTimeByCreate = goodsCreate.ModificationTime; + + Assert.AreEqual(2, goodsCreate.Logs.Count); + var log1 = goods.Logs.FirstOrDefault(log => log.Name == "initialize"); + Assert.IsNotNull(log1); + var log2 = goods.Logs.FirstOrDefault(log => log.Name == "initialize2"); + Assert.IsNotNull(log2); + + creatorUserContext.SetUserId("2"); + + var inputModificationTime = DateTime.UtcNow.AddDays(-1); + log1.SetDeleted(true, 3, inputModificationTime); + goods.Logs.Clear(); + dbContext.Set<Goods>().Update(goods); + await dbContext.SaveChangesAsync(); + + var goodsByUpdate = await dbContext.Set<Goods>().IgnoreQueryFilters().Include(g => g.Logs).FirstOrDefaultAsync(); + Assert.IsNotNull(goodsByUpdate); + + Assert.AreEqual(2, goodsByUpdate.Logs.Count); + var log1ByUpdate = goodsByUpdate.Logs.FirstOrDefault(log => log.Name == "initialize"); + Assert.IsNotNull(log1ByUpdate); + + Assert.AreEqual(true, log1ByUpdate.IsDeleted); + Assert.AreEqual(3, log1ByUpdate.Modifier); + Assert.AreEqual(inputModificationTime, log1ByUpdate.ModificationTime); + + var log1ByUpdate2 = goodsByUpdate.Logs.FirstOrDefault(log => log.Name == "initialize2"); + Assert.IsNotNull(log1ByUpdate2); + + Assert.AreEqual(true, log1ByUpdate2.IsDeleted); + Assert.AreEqual(2, log1ByUpdate2.Modifier); + Assert.AreNotEqual(modificationTimeByCreate, log1ByUpdate.ModificationTime); + + var goodsByUpdateAgain = await dbContext.Set<Goods>().AsTracking().IgnoreQueryFilters().Include(g => g.Logs).FirstOrDefaultAsync(); + Assert.IsNotNull(goodsByUpdateAgain); + var modificationTime = DateTime.Parse("2022-01-01 00:00:00"); + goodsByUpdateAgain.SetDeleted(true, 10, modificationTime); + dbContext.Set<Goods>().Remove(goodsByUpdateAgain); + await dbContext.SaveChangesAsync(); + + var goodsByDelete = await dbContext.Set<Goods>().AsNoTracking().IgnoreQueryFilters().FirstOrDefaultAsync(g => g.Name == "masa"); + Assert.IsNotNull(goodsByDelete); + + Assert.AreEqual(10, goodsByDelete.Modifier); + Assert.AreEqual(modificationTime, goodsByDelete.ModificationTime); + } + #endregion #region Test ConnectionString @@ -215,10 +286,8 @@ public async Task TestModifyConnectionString() Services.AddSingleton<IConfiguration>(configuration); IServiceProvider serviceProvider = default!; - var dbContext = await CreateDbContextAsync<CustomDbContext>(optionsBuilder => - { - optionsBuilder.UseSqlite(); - }, sp => serviceProvider = sp); + var dbContext = + await CreateDbContextAsync<CustomDbContext>(optionsBuilder => { optionsBuilder.UseSqlite(); }, sp => serviceProvider = sp); var connectionStringProvider = serviceProvider.GetService<IConnectionStringProvider>(); Assert.IsNotNull(connectionStringProvider); @@ -269,31 +338,20 @@ public async Task TestUseConfigurationAndSpecify() .Build(); Services.AddSingleton<IConfiguration>(configuration); - await CreateDbContextAsync<CustomDbContext>(optionsBuilder => - { - optionsBuilder.UseSqlite(); - }); + await CreateDbContextAsync<CustomDbContext>(optionsBuilder => { optionsBuilder.UseSqlite(); }); await Assert.ThrowsExceptionAsync<ArgumentException>(async () => { - await CreateDbContextAsync<CustomDbContext2>(optionsBuilder => - { - optionsBuilder.UseSqlite(SqliteConnectionString); - }); + await CreateDbContextAsync<CustomDbContext2>(optionsBuilder => { optionsBuilder.UseSqlite(SqliteConnectionString); }); }); } [TestMethod] public async Task TestAddMultiDbContextAsync() { - Services.AddMasaDbContext<CustomDbContext>(dbContextBuilder => - { - dbContextBuilder.UseInMemoryDatabase(MemoryConnectionString); - }); + Services.AddMasaDbContext<CustomDbContext>(dbContextBuilder => { dbContextBuilder.UseInMemoryDatabase(MemoryConnectionString); }); IServiceProvider serviceProvider = default!; - var customDbContext2 = await CreateDbContextAsync<CustomDbContext2>(dbContextBuilder => - { - dbContextBuilder.UseInMemoryDatabase(MemoryConnectionString); - }, sp => serviceProvider = sp); + var customDbContext2 = await CreateDbContextAsync<CustomDbContext2>( + dbContextBuilder => { dbContextBuilder.UseInMemoryDatabase(MemoryConnectionString); }, sp => serviceProvider = sp); var customDbContext = serviceProvider.GetService<CustomDbContext>(); Assert.IsNotNull(customDbContext); @@ -352,8 +410,9 @@ await dbContext.Set<Goods>().AddAsync(new Goods() var creationTimeByCreate = goodsByCreate.CreationTime; var modifierByCreate = goodsByCreate.Modifier; var modificationTimeByCreate = goodsByCreate.ModificationTime; - Assert.AreEqual(expectedCreator, goodsByCreate.Creator); - Assert.AreEqual(expectedCreator, goodsByCreate.Modifier); + + Assert.AreEqual(expectedCreator, creatorByCreate); + Assert.AreEqual(expectedCreator, modifierByCreate); customUserContext.SetUserId(modifier?.ToString()); goodsByCreate.Name = "masa1"; @@ -453,6 +512,65 @@ await dbContext.Set<Goods2>().AddAsync(new Goods2() Assert.AreNotEqual(modificationTimeByUpdate, goodsByUpdate.ModificationTime); } + [DataRow(1, 2, 1, "2023-01-01 00:00:00", "2023-01-01 00:00:00", 3, 3, "2023-01-02 00:00:00", "2023-01-02 00:00:00")] + [DataTestMethod] + public async Task TestAddOrUpdateOrDeleteWhenUserIdIsIntAsyncBySpecifyUserIdAndTime( + int inputCreator, int currentCreator, int expectedCreator, + string inputCreationTimeStr, string expectedCreationTimeStr, + int inputModifier, int expectedModifier, + string inputModificationTimeStr, string expectedModificationTimeStr) + { + DateTime inputCreationTime = DateTime.Parse(inputCreationTimeStr), expectedCreationTime = DateTime.Parse(expectedCreationTimeStr); + DateTime inputModificationTime = DateTime.Parse(inputModificationTimeStr), + expectedModificationTime = DateTime.Parse(expectedModificationTimeStr); + Services.Configure<AuditEntityOptions>(options => options.UserIdType = typeof(int)); + + var customUserContext = new CustomUserContext(currentCreator.ToString()); + Services.AddSingleton<IUserContext>(customUserContext); + + var connectionString = MemoryConnectionString; + var dbContext = await CreateDbContextAsync<CustomDbContext>(dbContextBuilder => + { + dbContextBuilder + .UseInMemoryDatabase(connectionString) + .UseQueryTrackingBehavior(QueryTrackingBehavior.TrackAll) + .UseFilter(); + }); + Assert.IsNotNull(dbContext); + var goods = new Goods("masa", inputCreator, inputCreationTime, inputModifier, inputModificationTime); + await dbContext.Set<Goods>().AddAsync(goods); + await dbContext.SaveChangesAsync(); + + var goodsByCreate = await dbContext.Set<Goods>().FirstOrDefaultAsync(); + Assert.IsNotNull(goodsByCreate); + var creatorByCreate = goodsByCreate.Creator; + var creationTimeByCreate = goodsByCreate.CreationTime; + var modifierByCreate = goodsByCreate.Modifier; + var modificationTimeByCreate = goodsByCreate.ModificationTime; + Assert.AreEqual(expectedCreator, creatorByCreate); + Assert.AreEqual(expectedCreationTime, creationTimeByCreate); + Assert.AreEqual(expectedModifier, modifierByCreate); + Assert.AreEqual(expectedModificationTime, modificationTimeByCreate); + + customUserContext.SetUserId((currentCreator + 5).ToString()); + goodsByCreate.UpdateName("masa1", inputCreator + 2, inputCreationTime.AddDays(1), inputModifier + 3, + inputModificationTime.AddDays(2)); + dbContext.Set<Goods>().Update(goodsByCreate); + await dbContext.SaveChangesAsync(); + + var goodsByUpdate = await dbContext.Set<Goods>().FirstOrDefaultAsync(); + Assert.IsNotNull(goodsByUpdate); + var creatorByUpdate = goodsByUpdate.Creator; + var creationTimeByUpdate = goodsByUpdate.CreationTime; + var modifierByUpdate = goodsByUpdate.Modifier; + var modificationTimeByUpdate = goodsByUpdate.ModificationTime; + Assert.AreEqual(inputCreator + 2, creatorByUpdate); + Assert.AreEqual(inputCreationTime.AddDays(1), creationTimeByUpdate); + Assert.AreEqual(currentCreator + 5, modifierByUpdate); + Assert.AreNotEqual(expectedModificationTime, modificationTimeByUpdate); + Assert.AreNotEqual(inputModificationTime.AddDays(2), modificationTimeByUpdate); + } + #endregion #region Test Model Mapping @@ -460,10 +578,7 @@ await dbContext.Set<Goods2>().AddAsync(new Goods2() [TestMethod] public void TestCustomTableName() { - var dbContext = CreateDbContext<CustomDbContext>(dbContext => - { - dbContext.UseInMemoryDatabase(MemoryConnectionString); - }); + var dbContext = CreateDbContext<CustomDbContext>(dbContext => { dbContext.UseInMemoryDatabase(MemoryConnectionString); }); var entityTableName = dbContext.Model.FindEntityType(typeof(Student))?.GetTableName(); Assert.AreEqual("masa_students", entityTableName); @@ -476,10 +591,7 @@ public void TestCustomTableName() [TestMethod] public void TestQueryTrackingBehaviorByDefault() { - var dbContext = CreateDbContext<CustomDbContext>(dbContext => - { - dbContext.UseInMemoryDatabase(MemoryConnectionString); - }); + var dbContext = CreateDbContext<CustomDbContext>(dbContext => { dbContext.UseInMemoryDatabase(MemoryConnectionString); }); Assert.AreEqual(QueryTrackingBehavior.NoTracking, dbContext.ChangeTracker.QueryTrackingBehavior); } diff --git a/src/Contrib/Data/Orm/EFCore/Masa.Contrib.Data.EFCore.Tests/Entities/Goods.cs b/src/Contrib/Data/Orm/EFCore/Masa.Contrib.Data.EFCore.Tests/Entities/Goods.cs index c25d73e6c..edbfeb80b 100644 --- a/src/Contrib/Data/Orm/EFCore/Masa.Contrib.Data.EFCore.Tests/Entities/Goods.cs +++ b/src/Contrib/Data/Orm/EFCore/Masa.Contrib.Data.EFCore.Tests/Entities/Goods.cs @@ -6,6 +6,43 @@ namespace Masa.Contrib.Data.EFCore.Tests.Entities; public class Goods : FullAggregateRoot<Guid, int> { public string Name { get; set; } + + public List<OperationLog> Logs { get; set; } + + public Goods() + { + } + + public Goods(string name, int creator, DateTime creationTime, int modifier, DateTime modificationTime) : this() + { + Name = name; + Creator = creator; + CreationTime = creationTime; + Modifier = modifier; + ModificationTime = modificationTime; + } + + public void UpdateName(string name, int creator, DateTime creationTime, int modifier, DateTime modificationTime) + { + Name = name; + Creator = creator; + CreationTime = creationTime; + Modifier = modifier; + ModificationTime = modificationTime; + } + + public void UpdateModifier(int modifier, DateTime modificationTime) + { + Modifier = modifier; + ModificationTime = modificationTime; + } + + public void SetDeleted(bool isDeleted, int modifier, DateTime modificationTime) + { + IsDeleted = isDeleted; + Modifier = modifier; + ModificationTime = modificationTime; + } } public class Goods2 : FullAggregateRoot<Guid, int?> diff --git a/src/Contrib/Data/Orm/EFCore/Masa.Contrib.Data.EFCore.Tests/Entities/OperationLog.cs b/src/Contrib/Data/Orm/EFCore/Masa.Contrib.Data.EFCore.Tests/Entities/OperationLog.cs new file mode 100644 index 000000000..a58db1276 --- /dev/null +++ b/src/Contrib/Data/Orm/EFCore/Masa.Contrib.Data.EFCore.Tests/Entities/OperationLog.cs @@ -0,0 +1,29 @@ +// Copyright (c) MASA Stack All rights reserved. +// Licensed under the MIT License. See LICENSE.txt in the project root for license information. + +namespace Masa.Contrib.Data.EFCore.Tests.Entities; + +public class OperationLog : FullEntity<Guid, int> +{ + public string Name { get; set; } + + public Guid GoodsId { get; private set; } = default!; + + public Goods Goods { get; private set; } = default!; + + public OperationLog() + { + } + + public OperationLog(string name) : this() + { + Name = name; + } + + public void SetDeleted(bool isDeleted, int modifier, DateTime modificationTime) + { + IsDeleted = isDeleted; + Modifier = modifier; + ModificationTime = modificationTime; + } +} diff --git a/src/Contrib/Data/Orm/EFCore/Masa.Contrib.Data.EFCore.Tests/Infrastructure/EntityConfigurations/GoodsEntityTypeConfiguration.cs b/src/Contrib/Data/Orm/EFCore/Masa.Contrib.Data.EFCore.Tests/Infrastructure/EntityConfigurations/GoodsEntityTypeConfiguration.cs index 1a130cd8f..b01357719 100644 --- a/src/Contrib/Data/Orm/EFCore/Masa.Contrib.Data.EFCore.Tests/Infrastructure/EntityConfigurations/GoodsEntityTypeConfiguration.cs +++ b/src/Contrib/Data/Orm/EFCore/Masa.Contrib.Data.EFCore.Tests/Infrastructure/EntityConfigurations/GoodsEntityTypeConfiguration.cs @@ -10,6 +10,10 @@ public class GoodsEntityTypeConfiguration : IEntityTypeConfiguration<Goods> public void Configure(EntityTypeBuilder<Goods> builder) { builder.Property(s => s.Id).IsRequired(); + + builder + .HasMany(u => u.Logs) + .WithOne(ur => ur.Goods).HasForeignKey(ur => ur.GoodsId); } } diff --git a/src/Contrib/Data/Orm/EFCore/Masa.Contrib.Data.EFCore/DefaultMasaDbContext.cs b/src/Contrib/Data/Orm/EFCore/Masa.Contrib.Data.EFCore/DefaultMasaDbContext.cs index 3b9c51abd..64df82c4a 100644 --- a/src/Contrib/Data/Orm/EFCore/Masa.Contrib.Data.EFCore/DefaultMasaDbContext.cs +++ b/src/Contrib/Data/Orm/EFCore/Masa.Contrib.Data.EFCore/DefaultMasaDbContext.cs @@ -84,7 +84,10 @@ internal void TryInitializeMasaDbContextOptions(MasaDbContextOptions? options) logger ??= MasaApp.GetService<ILogger<MasaDbContext>>(); logger?.LogDebug(ex, "Error generating data context"); - throw new InvalidOperationException("No database provider has been configured for this DbContext. A provider can be configured by overriding the 'MasaDbContext.OnConfiguring' method or by using 'AddMasaDbContext' on the application service provider. If 'AddMasaDbContext' is used, then also ensure that your DbContext type accepts a 'MasaDbContextOptions<TContext>' object in its constructor and passes it to the base constructor for DbContext."); + + if (ex.Message.Contains("overriding the 'DbContext.OnConfiguring'")) + throw new InvalidOperationException("No database provider has been configured for this DbContext. A provider can be configured by overriding the 'MasaDbContext.OnConfiguring' method or by using 'AddMasaDbContext' on the application service provider. If 'AddMasaDbContext' is used, then also ensure that your DbContext type accepts a 'MasaDbContextOptions<TContext>' object in its constructor and passes it to the base constructor for DbContext.", ex); + throw; } } @@ -165,7 +168,9 @@ protected virtual void ConfigureGlobalFilters<TEntity>(ModelBuilder modelBuilder { Expression<Func<TEntity, bool>> envFilter = entity => !IsEnvironmentFilterEnabled || EF.Property<string>(entity, nameof(IMultiEnvironment.Environment)) - .Equals(EnvironmentContext != null ? EnvironmentContext.CurrentEnvironment : default); + .Equals(EnvironmentContext != null + ? EnvironmentContext.CurrentEnvironment + : default); expression = envFilter.And(expression != null, expression); } @@ -182,8 +187,11 @@ protected virtual void ConfigureGlobalFilters<TEntity>(ModelBuilder modelBuilder { string defaultTenantId = Guid.Empty.ToString(); Expression<Func<TEntity, bool>> tenantFilter = entity => !IsTenantFilterEnabled || - (EF.Property<Guid>(entity, nameof(IMultiTenant<Guid>.TenantId)).ToString()) - .Equals(TenantContext.CurrentTenant != null ? TenantContext.CurrentTenant.Id : defaultTenantId); + (EF.Property<Guid>(entity, nameof(IMultiTenant<Guid>.TenantId)) + .ToString()) + .Equals(TenantContext.CurrentTenant != null + ? TenantContext.CurrentTenant.Id + : defaultTenantId); expression = tenantFilter.And(expression != null, expression); } @@ -317,8 +325,12 @@ public DefaultMasaDbContext(MasaDbContextOptions options) : base(options) { string defaultTenantId = default(TMultiTenantId)?.ToString() ?? string.Empty; Expression<Func<TEntity, bool>> tenantFilter = entity => !IsTenantFilterEnabled || - (EF.Property<TMultiTenantId>(entity, nameof(IMultiTenant<TMultiTenantId>.TenantId)).ToString() ?? string.Empty) - .Equals(TenantContext.CurrentTenant != null ? TenantContext.CurrentTenant.Id : defaultTenantId); + (EF.Property<TMultiTenantId>(entity, + nameof(IMultiTenant<TMultiTenantId>.TenantId)).ToString() ?? + string.Empty) + .Equals(TenantContext.CurrentTenant != null + ? TenantContext.CurrentTenant.Id + : defaultTenantId); expression = tenantFilter.And(expression != null, expression); } diff --git a/src/Contrib/Data/Orm/EFCore/Masa.Contrib.Data.EFCore/Filters/SaveChangeFilter.cs b/src/Contrib/Data/Orm/EFCore/Masa.Contrib.Data.EFCore/Filters/SaveChangeFilter.cs index 53accbdd4..7cb733077 100644 --- a/src/Contrib/Data/Orm/EFCore/Masa.Contrib.Data.EFCore/Filters/SaveChangeFilter.cs +++ b/src/Contrib/Data/Orm/EFCore/Masa.Contrib.Data.EFCore/Filters/SaveChangeFilter.cs @@ -15,6 +15,7 @@ public SaveChangeFilter(IUserContext? userContext = null) { _userIdType = typeof(TUserId); _userContext = userContext; + Masa.Contrib.Data.EFCore.TypeExtensions.TypeAndDefaultValues.TryAdd(_userIdType, type => Activator.CreateInstance(type)?.ToString()); } public void OnExecuting(ChangeTracker changeTracker) @@ -23,33 +24,66 @@ public void OnExecuting(ChangeTracker changeTracker) var userId = GetUserId(_userContext?.UserId); + var defaultUserId = Masa.Contrib.Data.EFCore.TypeExtensions.TypeAndDefaultValues[_userIdType]; + var defaultDateTime = Masa.Contrib.Data.EFCore.TypeExtensions.TypeAndDefaultValues[typeof(DateTime)]; + foreach (var entity in changeTracker.Entries() .Where(entry => entry.State == EntityState.Added || entry.State == EntityState.Modified)) { - AuditEntityHandler(entity, userId); + AuditEntityHandler(entity, userId, defaultUserId, defaultDateTime); } } - private static void AuditEntityHandler(EntityEntry entity, object? userId) + private static void AuditEntityHandler(EntityEntry entity, object? userId, string? defaultUserId, string? defaultDateTime) { if (entity.Entity.GetType().IsImplementerOfGeneric(typeof(IAuditEntity<>))) { if (entity.State == EntityState.Added) + { + AuditEntityHandlerByAdded(entity, userId, defaultUserId, defaultDateTime); + } + else { if (userId != null) - entity.CurrentValues[nameof(IAuditEntity<TUserId>.Creator)] = userId; + entity.CurrentValues[nameof(IAuditEntity<TUserId>.Modifier)] = userId; - entity.CurrentValues[nameof(IAuditEntity<TUserId>.CreationTime)] = + entity.CurrentValues[nameof(IAuditEntity<TUserId>.ModificationTime)] = DateTime.UtcNow; //The current time to change to localization after waiting for localization } - if (userId != null) + } + } + + private static void AuditEntityHandlerByAdded(EntityEntry entity, object? userId, string? defaultUserId, string? defaultDateTime) + { + if (userId != null) + { + if (IsDefault(entity.CurrentValues[nameof(IAuditEntity<TUserId>.Creator)], defaultUserId)) + { + entity.CurrentValues[nameof(IAuditEntity<TUserId>.Creator)] = userId; + } + + if (IsDefault(entity.CurrentValues[nameof(IAuditEntity<TUserId>.Modifier)], defaultUserId)) + { entity.CurrentValues[nameof(IAuditEntity<TUserId>.Modifier)] = userId; + } + } + + if (IsDefault(entity.CurrentValues[nameof(IAuditEntity<TUserId>.CreationTime)], defaultDateTime)) + { + entity.CurrentValues[nameof(IAuditEntity<TUserId>.CreationTime)] = + DateTime.UtcNow; //The current time to change to localization after waiting for localization + } - entity.CurrentValues[nameof(IAuditEntity<TUserId>.ModificationTime)] = + if (IsDefault(entity.CurrentValues[nameof(IAuditEntity<TUserId>.ModificationTime)], defaultDateTime)) + { + entity.CurrentValues[nameof(IAuditEntity<TUserId>.ModificationTime)] ??= DateTime.UtcNow; //The current time to change to localization after waiting for localization } } + private static bool IsDefault(object? value, string? defaultValue) + => value == null || value.ToString() == defaultValue; + private object? GetUserId(string? userId) { if (userId == null) diff --git a/src/Contrib/Data/Orm/EFCore/Masa.Contrib.Data.EFCore/Filters/SoftDeleteSaveChangesFilter.cs b/src/Contrib/Data/Orm/EFCore/Masa.Contrib.Data.EFCore/Filters/SoftDeleteSaveChangesFilter.cs index b860a1509..e7a7f7caf 100644 --- a/src/Contrib/Data/Orm/EFCore/Masa.Contrib.Data.EFCore/Filters/SoftDeleteSaveChangesFilter.cs +++ b/src/Contrib/Data/Orm/EFCore/Masa.Contrib.Data.EFCore/Filters/SoftDeleteSaveChangesFilter.cs @@ -39,11 +39,16 @@ public void OnExecuting(ChangeTracker changeTracker) { var navigationEntries = entity.Navigations .Where(navigationEntry => navigationEntry.Metadata is not ISkipNavigation && - !((IReadOnlyNavigation)navigationEntry.Metadata).IsOnDependent && navigationEntry.CurrentValue != null && - entries.All(e => e.Entity != navigationEntry.CurrentValue)); + !((IReadOnlyNavigation)navigationEntry.Metadata).IsOnDependent && + navigationEntry.CurrentValue != null && + entries.All(e => e.Entity != navigationEntry.CurrentValue)); HandleNavigationEntry(navigationEntries); entity.State = EntityState.Modified; + + if (!bool.TryParse(entity.CurrentValues[nameof(ISoftDelete.IsDeleted)]?.ToString(), out bool isDeleted) || isDeleted) + continue; + entity.CurrentValues[nameof(ISoftDelete.IsDeleted)] = true; if (entity.Entity.GetType().IsImplementerOfGeneric(typeof(IAuditEntity<>))) @@ -83,12 +88,16 @@ private void HandleDependent(object dependentEntry) var entityEntry = _context.Entry(dependentEntry); entityEntry.State = EntityState.Modified; - if (entityEntry.Entity is ISoftDelete) + if (entityEntry.Entity is ISoftDelete && + bool.TryParse(entityEntry.CurrentValues[nameof(ISoftDelete.IsDeleted)]?.ToString(), out bool isDeleted) && !isDeleted) + { entityEntry.CurrentValues[nameof(ISoftDelete.IsDeleted)] = true; + } var navigationEntries = entityEntry.Navigations .Where(navigationEntry => navigationEntry.Metadata is not ISkipNavigation && - !((IReadOnlyNavigation)navigationEntry.Metadata).IsOnDependent && navigationEntry.CurrentValue != null); + !((IReadOnlyNavigation)navigationEntry.Metadata).IsOnDependent && + navigationEntry.CurrentValue != null); HandleNavigationEntry(navigationEntries); } diff --git a/src/Contrib/Data/Orm/EFCore/Masa.Contrib.Data.EFCore/Internal/TypeExtensions.cs b/src/Contrib/Data/Orm/EFCore/Masa.Contrib.Data.EFCore/Internal/TypeExtensions.cs index 86eeb39c9..247f25c99 100644 --- a/src/Contrib/Data/Orm/EFCore/Masa.Contrib.Data.EFCore/Internal/TypeExtensions.cs +++ b/src/Contrib/Data/Orm/EFCore/Masa.Contrib.Data.EFCore/Internal/TypeExtensions.cs @@ -13,4 +13,6 @@ internal static class TypeExtensions public static bool IsGenericInterfaceAssignableFrom(this Type genericType, Type type) => type.IsConcrete() && type.GetInterfaces().Any(t => t.GetTypeInfo().IsGenericType && t.GetGenericTypeDefinition() == genericType); + + public static readonly MemoryCache<Type, string?> TypeAndDefaultValues = new(); } diff --git a/src/Contrib/Data/Orm/EFCore/Masa.Contrib.Data.EFCore/_Imports.cs b/src/Contrib/Data/Orm/EFCore/Masa.Contrib.Data.EFCore/_Imports.cs index 1333c9ca5..646c40baa 100644 --- a/src/Contrib/Data/Orm/EFCore/Masa.Contrib.Data.EFCore/_Imports.cs +++ b/src/Contrib/Data/Orm/EFCore/Masa.Contrib.Data.EFCore/_Imports.cs @@ -11,6 +11,7 @@ global using Masa.BuildingBlocks.Ddd.Domain.Repositories; global using Masa.BuildingBlocks.Isolation; global using Masa.Contrib.Data.EFCore; +global using Masa.Utils.Caching.Memory; global using Masa.Utils.Models; global using Microsoft.EntityFrameworkCore; global using Microsoft.EntityFrameworkCore.ChangeTracking;