diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Microsoft/Extensions/DependencyInjection/AbpEfCoreModelBuilderExtensions.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Microsoft/Extensions/DependencyInjection/AbpEfCoreModelBuilderExtensions.cs index 055c0651cb..4b90d801e6 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Microsoft/Extensions/DependencyInjection/AbpEfCoreModelBuilderExtensions.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Microsoft/Extensions/DependencyInjection/AbpEfCoreModelBuilderExtensions.cs @@ -21,11 +21,11 @@ public static class AbpEfCoreModelBuilderExtensions if (abpEfCoreCurrentDbContext.Context?.DataFilter.IsEnabled() == true) { - // IsDeleted == false + // IsDeleted == false. Use the column's TypeMapping so any ValueConverter is applied to the literal. return new SqlBinaryExpression( ExpressionType.Equal, isDeleted, - new SqlConstantExpression(false, typeof(bool), boolParam.TypeMapping), + new SqlConstantExpression(false, typeof(bool), isDeleted.TypeMapping ?? boolParam.TypeMapping), boolParam.Type, boolParam.TypeMapping); } diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DataFiltering/SoftDelete_DbFunction_Bool_Literal_Tests.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DataFiltering/SoftDelete_DbFunction_Bool_Literal_Tests.cs new file mode 100644 index 0000000000..408920ce3e --- /dev/null +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DataFiltering/SoftDelete_DbFunction_Bool_Literal_Tests.cs @@ -0,0 +1,48 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Volo.Abp.Data; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.TestApp.Domain; +using Volo.Abp.TestApp.EntityFrameworkCore; +using Volo.Abp.TestApp.Testing; +using Xunit; + +namespace Volo.Abp.EntityFrameworkCore.DataFiltering; + +// Regression for the soft-delete DbFunction translator hardcoding a bool false literal +// with the bool TypeMapping instead of routing the literal through the IsDeleted +// property's ValueConverter. EntityWithIntSoftDelete maps false->5, true->9, so the +// buggy SQL "is_deleted = 0" matches no row (returns 0) while the correct SQL +// "is_deleted = 5" matches the not-deleted row (returns 1). +public class SoftDelete_DbFunction_Bool_Literal_Tests : TestAppTestBase +{ + private readonly IRepository _repository; + private readonly IDataFilter _softDeleteFilter; + + public SoftDelete_DbFunction_Bool_Literal_Tests() + { + _repository = GetRequiredService>(); + _softDeleteFilter = GetRequiredService>(); + } + + [Fact] + public async Task SoftDelete_Filter_Should_Route_False_Literal_Through_ValueConverter() + { + await _repository.InsertAsync(new EntityWithIntSoftDelete { Name = "kept" }); + await _repository.InsertAsync(new EntityWithIntSoftDelete { Name = "removed", IsDeleted = true }); + + var visible = await _repository.GetListAsync(); + visible.Count.ShouldBe(1); + visible[0].Name.ShouldBe("kept"); + + using (_softDeleteFilter.Disable()) + { + var all = await _repository.GetListAsync(); + all.Count.ShouldBe(2); + } + } +} diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs index 96f4b1a2e1..3b569d8431 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs @@ -27,6 +27,8 @@ public class TestMigrationsDbContext : AbpDbContext public DbSet Categories { get; set; } + public DbSet EntityWithIntSoftDeletes { get; set; } + public DbSet AppEntityWithNavigations { get; set; } public DbSet AppEntityWithNavigationChildOneToMany { get; set; } @@ -104,6 +106,15 @@ public class TestMigrationsDbContext : AbpDbContext b.HasAbpQueryFilter(e => e.Name.StartsWith("abp")); }); + modelBuilder.Entity(b => + { + b.Property(x => x.IsDeleted) + .HasColumnName(EntityWithIntSoftDelete.IsDeletedColumnName) + .HasConversion( + v => v ? EntityWithIntSoftDelete.DeletedProviderValue : EntityWithIntSoftDelete.NotDeletedProviderValue, + i => i == EntityWithIntSoftDelete.DeletedProviderValue); + }); + modelBuilder.Entity(b => { b.ConfigureByConvention(); 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 2b95877bed..e5de412d6e 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 @@ -33,6 +33,8 @@ public class TestAppDbContext : AbpDbContext, IThirdDbContext, public DbSet Categories { get; set; } + public DbSet EntityWithIntSoftDeletes { get; set; } + public DbSet AppEntityWithNavigations { get; set; } public DbSet AppEntityWithNavigationChildOneToMany { get; set; } @@ -129,6 +131,15 @@ public class TestAppDbContext : AbpDbContext, IThirdDbContext, b.HasAbpQueryFilter(e => e.Name.StartsWith("abp")); }); + modelBuilder.Entity(b => + { + b.Property(x => x.IsDeleted) + .HasColumnName(EntityWithIntSoftDelete.IsDeletedColumnName) + .HasConversion( + v => v ? EntityWithIntSoftDelete.DeletedProviderValue : EntityWithIntSoftDelete.NotDeletedProviderValue, + i => i == EntityWithIntSoftDelete.DeletedProviderValue); + }); + modelBuilder.Entity(b => { b.ConfigureByConvention(); diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/EntityWithIntSoftDelete.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/EntityWithIntSoftDelete.cs new file mode 100644 index 0000000000..a8bcab3331 --- /dev/null +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/EntityWithIntSoftDelete.cs @@ -0,0 +1,19 @@ +using System; +using Volo.Abp.Domain.Entities; + +namespace Volo.Abp.TestApp.Domain; + +// Uses a custom bool<->int converter where false maps to a non-zero value, so the +// soft-delete DbFunction translator (which hardcodes a bool false literal with the +// bool TypeMapping) is observably wrong on every provider: the generated SQL +// compares against 0/FALSE instead of the converter's actual provider value (5). +public class EntityWithIntSoftDelete : AggregateRoot, ISoftDelete +{ + public const string IsDeletedColumnName = "is_deleted"; + public const int NotDeletedProviderValue = 5; + public const int DeletedProviderValue = 9; + + public string Name { get; set; } + + public bool IsDeleted { get; set; } +}