diff --git a/src/EFCore.Relational/Query/RelationalEntityShaperExpression.cs b/src/EFCore.Relational/Query/RelationalEntityShaperExpression.cs index 4c5c4d475a0..41f068a0c48 100644 --- a/src/EFCore.Relational/Query/RelationalEntityShaperExpression.cs +++ b/src/EFCore.Relational/Query/RelationalEntityShaperExpression.cs @@ -99,16 +99,43 @@ protected override LambdaExpression GenerateMaterializationCondition(IEntityType if (table.IsOptional(entityType)) { // Optional dependent + var body = baseCondition.Body; var valueBufferParameter = baseCondition.Parameters[0]; - var condition = entityType.GetNonPrincipalSharedNonPkProperties(table) - .Where(e => !e.IsNullable) - .Select( + Expression? condition = null; + var requiredNonPkProperties = entityType.GetProperties().Where(p => !p.IsNullable && !p.IsPrimaryKey()).ToList(); + if (requiredNonPkProperties.Count > 0) + { + condition = requiredNonPkProperties + .Select( + p => NotEqual( + valueBufferParameter.CreateValueBufferReadValueExpression(typeof(object), p.GetIndex(), p), + Constant(null))) + .Aggregate((a, b) => AndAlso(a, b)); + } + + var allNonPrincipalSharedNonPkProperties = entityType.GetNonPrincipalSharedNonPkProperties(table); + // We don't need condition for nullable property if there exist at least one required property which is non shared. + if (allNonPrincipalSharedNonPkProperties.Any() + && allNonPrincipalSharedNonPkProperties.All(p => p.IsNullable)) + { + var atLeastOneNonNullValueInNullablePropertyCondition = allNonPrincipalSharedNonPkProperties + .Select( p => NotEqual( valueBufferParameter.CreateValueBufferReadValueExpression(typeof(object), p.GetIndex(), p), Constant(null))) - .Aggregate((a, b) => AndAlso(a, b)); + .Aggregate((a, b) => OrElse(a, b)); + + condition = condition == null + ? atLeastOneNonNullValueInNullablePropertyCondition + : AndAlso(condition, atLeastOneNonNullValueInNullablePropertyCondition); + } + + if (condition != null) + { + body = Condition(condition, body, Default(typeof(IEntityType))); + } - return Lambda(Condition(condition, baseCondition.Body, Default(typeof(IEntityType))), valueBufferParameter); + return Lambda(body, valueBufferParameter); } return baseCondition; diff --git a/test/EFCore.Relational.Specification.Tests/Query/OwnedEntityQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/OwnedEntityQueryRelationalTestBase.cs index 88728379702..ba4435abfc4 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/OwnedEntityQueryRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/OwnedEntityQueryRelationalTestBase.cs @@ -227,4 +227,12 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) }); } } + + protected override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + { + return base.AddOptions(builder).ConfigureWarnings( + c => c + .Log(RelationalEventId.OptionalDependentWithoutIdentifyingPropertyWarning) + .Log(RelationalEventId.OptionalDependentWithAllNullPropertiesWarning)); + } } diff --git a/test/EFCore.Specification.Tests/Query/OwnedEntityQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/OwnedEntityQueryTestBase.cs index 966c06835d2..47095868bce 100644 --- a/test/EFCore.Specification.Tests/Query/OwnedEntityQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/OwnedEntityQueryTestBase.cs @@ -231,7 +231,8 @@ public virtual async Task Projecting_correlated_collection_property_for_owned_en var query = context.Warehouses.Select( x => new WarehouseModel { - WarehouseCode = x.WarehouseCode, DestinationCountryCodes = x.DestinationCountries.Select(c => c.CountryCode).ToArray() + WarehouseCode = x.WarehouseCode, + DestinationCountryCodes = x.DestinationCountries.Select(c => c.CountryCode).ToArray() }).AsNoTracking(); var result = async @@ -401,4 +402,95 @@ protected class IntermediateOwnedEntity public CustomerData CustomerData { get; set; } public SupplierData SupplierData { get; set; } } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Owned_entity_with_all_null_properties_materializes_when_not_containing_another_owned_entity(bool async) + { + var contextFactory = await InitializeAsync(seed: c => c.Seed()); + + using var context = contextFactory.CreateContext(); + var query = context.RotRutCases.OrderBy(e => e.Buyer); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + Assert.Collection(result, + t => + { + Assert.Equal("Buyer1", t.Buyer); + Assert.NotNull(t.Rot); + Assert.Equal(1, t.Rot.ServiceType); + Assert.Equal("1", t.Rot.ApartmentNo); + Assert.NotNull(t.Rut); + Assert.Equal(1, t.Rut.Value); + }, + t => + { + Assert.Equal("Buyer2", t.Buyer); + // Cannot verify owned entities here since they differ between relational/in-memory + }); + } + + protected class MyContext28247 : DbContext + { + public MyContext28247(DbContextOptions options) + : base(options) + { + } + + public DbSet RotRutCases { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(b => + { + b.ToTable("RotRutCases"); + + b.OwnsOne(e => e.Rot); + b.OwnsOne(e => e.Rut); + }); + } + + public void Seed() + { + Add( + new RotRutCase + { + Buyer = "Buyer1", + Rot = new Rot { ServiceType = 1, ApartmentNo = "1" }, + Rut = new Rut { Value = 1 } + }); + + Add( + new RotRutCase + { + Buyer = "Buyer2", + Rot = new Rot { ServiceType = null, ApartmentNo = null }, + Rut = new Rut { Value = null } + }); + + SaveChanges(); + } + } + + public class RotRutCase + { + public int Id { get; set; } + public string Buyer { get; set; } + public Rot Rot { get; set; } + public Rut Rut { get; set; } + } + + public class Rot + { + public int? ServiceType { get; set; } + public string ApartmentNo { get; set; } + } + + public class Rut + { + public int? Value { get; set; } + } } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/OwnedEntityQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/OwnedEntityQuerySqlServerTest.cs index ebd1df420bc..593a2ed35c4 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/OwnedEntityQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/OwnedEntityQuerySqlServerTest.cs @@ -138,4 +138,14 @@ ORDER BY [o].[Id] LEFT JOIN [IM_SupplierData] AS [i1] ON [t].[OwnerId] = [i1].[IntermediateOwnedEntityOwnerId] ORDER BY [t].[Id]"); } + + public override async Task Owned_entity_with_all_null_properties_materializes_when_not_containing_another_owned_entity(bool async) + { + await base.Owned_entity_with_all_null_properties_materializes_when_not_containing_another_owned_entity(async); + + AssertSql( + @"SELECT [r].[Id], [r].[Buyer], [r].[Rot_ApartmentNo], [r].[Rot_ServiceType], [r].[Rut_Value] +FROM [RotRutCases] AS [r] +ORDER BY [r].[Buyer]"); + } }