Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/EFCore.Relational/EFCore.Relational.baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -7352,10 +7352,10 @@
"Member": "virtual Microsoft.EntityFrameworkCore.Query.JsonQueryExpression BindStructuralProperty(Microsoft.EntityFrameworkCore.Metadata.IPropertyBase structuralProperty);"
},
{
"Member": "virtual Microsoft.EntityFrameworkCore.Metadata.IRelationalJsonElement? FindJsonElement(Microsoft.EntityFrameworkCore.Metadata.IPropertyBase propertyBase);"
"Member": "override bool Equals(object? obj);"
},
{
"Member": "override bool Equals(object? obj);"
"Member": "virtual Microsoft.EntityFrameworkCore.Metadata.IRelationalJsonElement? FindJsonElement(Microsoft.EntityFrameworkCore.Metadata.IPropertyBase propertyBase);"
},
{
"Member": "override int GetHashCode();"
Expand Down Expand Up @@ -17425,6 +17425,9 @@
{
"Member": "static string? GetContainerColumnName(this Microsoft.EntityFrameworkCore.Metadata.IReadOnlyTypeBase typeBase);"
},
{
"Member": "static string? GetContainerColumnName(this Microsoft.EntityFrameworkCore.Metadata.IReadOnlyTypeBase typeBase, in Microsoft.EntityFrameworkCore.Metadata.StoreObjectIdentifier storeObject);"
},
{
"Member": "static Microsoft.EntityFrameworkCore.Metadata.ConfigurationSource? GetContainerColumnNameConfigurationSource(this Microsoft.EntityFrameworkCore.Metadata.IConventionTypeBase typeBase);"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,10 @@ public static string GetDefaultColumnName(this IReadOnlyProperty property)
}
else if (StoreObjectIdentifier.Create(property.DeclaringType, currentStoreObject.StoreObjectType) == currentStoreObject
|| property.DeclaringType.GetMappingFragments(storeObject.StoreObjectType)
.Any(f => f.StoreObject == currentStoreObject))
.Any(f => f.StoreObject == currentStoreObject)
|| (property.IsPrimaryKey()
&& property.DeclaringType.ContainingEntityType.GetDerivedTypes()
.Any(e => StoreObjectIdentifier.Create(e, currentStoreObject.StoreObjectType) == currentStoreObject)))
{
builder = CreateComplexPrefix((IReadOnlyComplexType)property.DeclaringType, storeObject, builder);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,78 @@ public static bool IsMappedToJson(this IReadOnlyTypeBase typeBase)
: ((IReadOnlyComplexType)typeBase).ComplexProperty.DeclaringType.GetContainerColumnName();
}

/// <summary>
/// Gets the container column name to which the type is mapped for a particular table-like store object.
/// </summary>
/// <param name="typeBase">The type to get the container column name for.</param>
/// <param name="storeObject">The identifier of the table-like store object containing the column.</param>
/// <returns>
/// The container column name to which the type is mapped, or <see langword="null" /> if the type is not mapped
/// to a container column in the given store object.
/// </returns>
public static string? GetContainerColumnName(this IReadOnlyTypeBase typeBase, in StoreObjectIdentifier storeObject)
{
var annotation = typeBase.FindAnnotation(RelationalAnnotationNames.ContainerColumnName);
if (annotation != null)
{
var containerColumnName = (string?)annotation.Value;
if (string.IsNullOrEmpty(containerColumnName))
{
return containerColumnName;
}

if (storeObject.StoreObjectType is StoreObjectType.Function or StoreObjectType.SqlQuery)
{
return containerColumnName;
}

var containingEntityType = typeBase.ContainingEntityType;
if (containingEntityType.GetMappingStrategy() == RelationalAnnotationNames.TpcMappingStrategy)
{
var localStoreObject = storeObject;
return StoreObjectIdentifier.Create(containingEntityType, localStoreObject.StoreObjectType) == localStoreObject
|| containingEntityType.GetDerivedTypes().Any(e => StoreObjectIdentifier.Create(e, localStoreObject.StoreObjectType) == localStoreObject)
? containerColumnName
: null;
}
Comment thread
AndriySvyryd marked this conversation as resolved.

// TODO: Support entity splitting with JSON columns. Issue #36172
var declaringStoreObject = StoreObjectIdentifier.Create(typeBase, storeObject.StoreObjectType);
if (declaringStoreObject == null)
{
var tableFound = false;
var queue = new Queue<IReadOnlyEntityType>();
queue.Enqueue(containingEntityType);
while (queue.Count > 0 && !tableFound)
{
foreach (var derivedType in queue.Dequeue().GetDirectlyDerivedTypes())
{
var derivedStoreObject = StoreObjectIdentifier.Create(derivedType, storeObject.StoreObjectType);
if (derivedStoreObject == null)
{
queue.Enqueue(derivedType);
continue;
}

if (derivedStoreObject == storeObject)
{
tableFound = true;
break;
}
}
}

return tableFound ? containerColumnName : null;
}

return declaringStoreObject == storeObject ? containerColumnName : null;
}

return typeBase is IReadOnlyEntityType entityType
? entityType.FindOwnership()?.PrincipalEntityType.GetContainerColumnName(storeObject)
: ((IReadOnlyComplexType)typeBase).ComplexProperty.DeclaringType.GetContainerColumnName(storeObject);
}

/// <summary>
/// Sets the name of the container column to which the type is mapped.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ public virtual void ProcessModelFinalizing(
{
var pk = entityType.FindPrimaryKey();
if (pk != null
&& pk.Properties.All(p => p.DeclaringType is IConventionEntityType)
&& !entityType.FindDeclaredForeignKeys(pk.Properties)
.Any(fk => fk.PrincipalKey.IsPrimaryKey()
&& fk.PrincipalEntityType.IsAssignableFrom(entityType)
Expand Down
4 changes: 2 additions & 2 deletions src/EFCore.Relational/Metadata/Internal/RelationalModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,7 @@ private static void CreateTableMapping(
IsSplitEntityTypePrincipal = isSplitEntityTypePrincipal
};

var containerColumnName = mappedType.GetContainerColumnName();
var containerColumnName = mappedType.GetContainerColumnName(mappedTable);
var containerColumnType = mappedType.GetContainerColumnType();
if (!string.IsNullOrEmpty(containerColumnName))
{
Expand Down Expand Up @@ -1028,7 +1028,7 @@ private static void CreateViewMapping(
IsSplitEntityTypePrincipal = isSplitEntityTypePrincipal
};

var containerColumnName = mappedType.GetContainerColumnName();
var containerColumnName = mappedType.GetContainerColumnName(mappedView);
var containerColumnType = mappedType.GetContainerColumnType();
if (!string.IsNullOrEmpty(containerColumnName))
{
Expand Down
208 changes: 208 additions & 0 deletions test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3381,6 +3381,183 @@ public void Complex_property_json_column_is_nullable_in_TPH_hierarchy()
Assert.IsType<JsonColumn>(jsonColumn);
}

[Fact]
public void Complex_property_json_column_is_not_duplicated_in_TPT_child_tables()
{
var modelBuilder = CreateConventionModelBuilder();

modelBuilder.Entity<TptBaseEntityWithComplexProperty>()
.UseTptMappingStrategy()
.ComplexProperty(e => e.ComplexProperty, b => b.ToJson());
modelBuilder.Entity<TptDerivedEntityWithoutComplexProperty>();

var model = modelBuilder.FinalizeModel();
var relationalModel = model.GetRelationalModel();

var baseTable = relationalModel.Tables.Single(t => t.Name == nameof(TptBaseEntityWithComplexProperty));
var childTable = relationalModel.Tables.Single(t => t.Name == nameof(TptDerivedEntityWithoutComplexProperty));

// The JSON column for the base complex property must appear only in the base table
Assert.Contains(baseTable.Columns, c => c.Name == nameof(TptBaseEntityWithComplexProperty.ComplexProperty));
Assert.DoesNotContain(childTable.Columns, c => c.Name == nameof(TptBaseEntityWithComplexProperty.ComplexProperty));
}

[Fact]
public void Complex_property_columns_are_not_duplicated_in_TPT_child_tables()
{
var modelBuilder = CreateConventionModelBuilder();

modelBuilder.Entity<TptBaseEntityWithComplexProperty>()
.UseTptMappingStrategy()
.ComplexProperty(e => e.ComplexProperty);
modelBuilder.Entity<TptDerivedEntityWithoutComplexProperty>();

var model = modelBuilder.FinalizeModel();
var relationalModel = model.GetRelationalModel();

var baseTable = relationalModel.Tables.Single(t => t.Name == nameof(TptBaseEntityWithComplexProperty));
var childTable = relationalModel.Tables.Single(t => t.Name == nameof(TptDerivedEntityWithoutComplexProperty));

// Non-JSON complex property columns appear only on the base table.
var valueColumnName = nameof(TptBaseEntityWithComplexProperty.ComplexProperty) + "_" + nameof(ComplexData.Value);
var numberColumnName = nameof(TptBaseEntityWithComplexProperty.ComplexProperty) + "_" + nameof(ComplexData.Number);
Assert.Contains(baseTable.Columns, c => c.Name == valueColumnName);
Assert.Contains(baseTable.Columns, c => c.Name == numberColumnName);
Assert.DoesNotContain(childTable.Columns, c => c.Name == valueColumnName);
Assert.DoesNotContain(childTable.Columns, c => c.Name == numberColumnName);
}

[Fact]
public void Complex_property_json_column_is_created_in_every_TPC_table()
{
var modelBuilder = CreateConventionModelBuilder();

modelBuilder.Entity<TpcBaseEntityWithComplexProperty>(b =>
{
b.UseTpcMappingStrategy();
b.ComplexProperty(e => e.ComplexProperty, cb => cb.ToJson());
});
modelBuilder.Entity<TpcDerivedEntityWithoutComplexProperty>();

var model = modelBuilder.FinalizeModel();
var relationalModel = model.GetRelationalModel();

var baseTable = relationalModel.Tables.Single(t => t.Name == nameof(TpcBaseEntityWithComplexProperty));
var derivedTable = relationalModel.Tables.Single(t => t.Name == nameof(TpcDerivedEntityWithoutComplexProperty));

// In TPC the JSON container column appears on every concrete table.
Assert.Contains(baseTable.Columns, c => c.Name == nameof(TpcBaseEntityWithComplexProperty.ComplexProperty));
Assert.Contains(derivedTable.Columns, c => c.Name == nameof(TpcBaseEntityWithComplexProperty.ComplexProperty));
}

[Fact]
public void GetContainerColumnName_with_StoreObjectIdentifier_resolves_per_table()
{
var modelBuilder = CreateConventionModelBuilder();

modelBuilder.Entity<TptBaseEntityWithComplexProperty>()
.UseTptMappingStrategy()
.ComplexProperty(e => e.ComplexProperty, cb => cb.ToJson());
modelBuilder.Entity<TptDerivedEntityWithoutComplexProperty>();

var model = modelBuilder.FinalizeModel();
var baseEntity = model.FindEntityType(typeof(TptBaseEntityWithComplexProperty))!;
var complexProperty = baseEntity.FindComplexProperty(nameof(TptBaseEntityWithComplexProperty.ComplexProperty))!;
var complexType = complexProperty.ComplexType;

var baseTable = StoreObjectIdentifier.Table(nameof(TptBaseEntityWithComplexProperty));
var childTable = StoreObjectIdentifier.Table(nameof(TptDerivedEntityWithoutComplexProperty));
var unrelatedTable = StoreObjectIdentifier.Table("SomeOtherTable");

Assert.Equal(nameof(TptBaseEntityWithComplexProperty.ComplexProperty), complexType.GetContainerColumnName(baseTable));
Assert.Null(complexType.GetContainerColumnName(childTable));
Assert.Null(complexType.GetContainerColumnName(unrelatedTable));
}

[Fact]
public void GetContainerColumnName_with_StoreObjectIdentifier_returns_column_for_every_TPC_table()
{
var modelBuilder = CreateConventionModelBuilder();

modelBuilder.Entity<TpcBaseEntityWithComplexProperty>(b =>
{
b.UseTpcMappingStrategy();
b.ComplexProperty(e => e.ComplexProperty, cb => cb.ToJson());
});
modelBuilder.Entity<TpcDerivedEntityWithoutComplexProperty>();

var model = modelBuilder.FinalizeModel();
var baseEntity = model.FindEntityType(typeof(TpcBaseEntityWithComplexProperty))!;
var complexProperty = baseEntity.FindComplexProperty(nameof(TpcBaseEntityWithComplexProperty.ComplexProperty))!;
var complexType = complexProperty.ComplexType;

var baseTable = StoreObjectIdentifier.Table(nameof(TpcBaseEntityWithComplexProperty));
var derivedTable = StoreObjectIdentifier.Table(nameof(TpcDerivedEntityWithoutComplexProperty));
var unrelatedTable = StoreObjectIdentifier.Table("SomeOtherTable");

Assert.Equal(nameof(TpcBaseEntityWithComplexProperty.ComplexProperty), complexType.GetContainerColumnName(baseTable));
Assert.Equal(nameof(TpcBaseEntityWithComplexProperty.ComplexProperty), complexType.GetContainerColumnName(derivedTable));
Comment thread
AndriySvyryd marked this conversation as resolved.
Assert.Null(complexType.GetContainerColumnName(unrelatedTable));
}

[Fact]
public void Complex_type_with_PK_property_creates_column_mapping_in_TPT_child_table()
{
var modelBuilder = CreateConventionModelBuilder();

modelBuilder.Entity<TptBaseWithComplexTypePK>(b =>
{
b.UseTptMappingStrategy();
b.ComplexProperty(e => e.Key);
b.HasKey(e => e.Key.Id);
b.Property(e => e.Key.Id).ValueGeneratedNever();
});
modelBuilder.Entity<TptDerivedWithComplexTypePK>();

var model = modelBuilder.FinalizeModel();
var relationalModel = model.GetRelationalModel();

var baseTable = relationalModel.Tables.Single(t => t.Name == nameof(TptBaseWithComplexTypePK));
var childTable = relationalModel.Tables.Single(t => t.Name == nameof(TptDerivedWithComplexTypePK));

// The PK column for the complex-typed key must appear in both the base table and the child table.
Assert.Contains(baseTable.Columns, c => c.Name == "Key_Id");
Assert.Contains(childTable.Columns, c => c.Name == "Key_Id");

// Non-key properties of the derived entity stay on the child table.
Assert.Contains(childTable.Columns, c => c.Name == nameof(TptDerivedWithComplexTypePK.Extra));
Assert.DoesNotContain(baseTable.Columns, c => c.Name == nameof(TptDerivedWithComplexTypePK.Extra));
}

[Fact]
public void Complex_property_json_column_is_not_duplicated_in_TPT_child_views()
{
var modelBuilder = CreateConventionModelBuilder();

modelBuilder.Entity<TptBaseEntityWithComplexProperty>(b =>
{
b.UseTptMappingStrategy();
b.ToTable(nameof(TptBaseEntityWithComplexProperty));
b.ToView(nameof(TptBaseEntityWithComplexProperty) + "View");
b.ComplexProperty(e => e.ComplexProperty, cb => cb.ToJson());
});
modelBuilder.Entity<TptDerivedEntityWithoutComplexProperty>(b =>
{
b.ToTable(nameof(TptDerivedEntityWithoutComplexProperty));
b.ToView(nameof(TptDerivedEntityWithoutComplexProperty) + "View");
});

var model = modelBuilder.FinalizeModel();
var relationalModel = model.GetRelationalModel();

var baseView = relationalModel.Views.Single(v => v.Name == nameof(TptBaseEntityWithComplexProperty) + "View");
var childView = relationalModel.Views.Single(v => v.Name == nameof(TptDerivedEntityWithoutComplexProperty) + "View");

// The JSON column for the base complex property must appear only in the base view.
Assert.Contains(baseView.Columns, c => c.Name == nameof(TptBaseEntityWithComplexProperty.ComplexProperty));
Assert.DoesNotContain(childView.Columns, c => c.Name == nameof(TptBaseEntityWithComplexProperty.ComplexProperty));
}

[Fact]
public void Json_element_tree_is_built_for_owned_entity_json_columns()
{
Expand Down Expand Up @@ -3994,6 +4171,37 @@ private class TphEntityWithComplexProperty : TphBaseEntity
public ComplexData ComplexProperty { get; set; }
}

private abstract class TptBaseEntityWithComplexProperty
{
public int Id { get; set; }
public ComplexData ComplexProperty { get; set; }
}

private class TptDerivedEntityWithoutComplexProperty : TptBaseEntityWithComplexProperty;

private class TpcBaseEntityWithComplexProperty
{
public int Id { get; set; }
public ComplexData ComplexProperty { get; set; }
}

private class TpcDerivedEntityWithoutComplexProperty : TpcBaseEntityWithComplexProperty;

private abstract class TptBaseWithComplexTypePK
{
public TptComplexKey Key { get; set; } = null!;
}

private class TptComplexKey
{
public int Id { get; set; }
}

private class TptDerivedWithComplexTypePK : TptBaseWithComplexTypePK
{
public int Extra { get; set; }
}

private class EntityWithJsonOwnedWithCollection
{
public int Id { get; set; }
Expand Down
Loading