Skip to content

Commit

Permalink
Query: Allow materializing owned entities with no required properties
Browse files Browse the repository at this point in the history
Resolves #28247
  • Loading branch information
smitpatel committed Jul 21, 2022
1 parent bf7c303 commit ed39292
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 6 deletions.
37 changes: 32 additions & 5 deletions src/EFCore.Relational/Query/RelationalEntityShaperExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<MyContext28247>(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<RotRutCase> RotRutCases { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<RotRutCase>(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; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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]");
}
}

0 comments on commit ed39292

Please sign in to comment.