diff --git a/src/EFCore.Relational/EFCore.Relational.baseline.json b/src/EFCore.Relational/EFCore.Relational.baseline.json index dee6b2f7287..b3d7dc5b073 100644 --- a/src/EFCore.Relational/EFCore.Relational.baseline.json +++ b/src/EFCore.Relational/EFCore.Relational.baseline.json @@ -7322,12 +7322,18 @@ { "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 int GetHashCode();" }, + { + "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.IRelationalJsonElement GetJsonElement(Microsoft.EntityFrameworkCore.Metadata.IPropertyBase propertyBase);" + }, { "Member": "virtual Microsoft.EntityFrameworkCore.Query.JsonQueryExpression MakeNullable();" }, @@ -16519,6 +16525,9 @@ { "Member": "static string JsonCantNavigateToParentEntity(object? jsonEntity, object? parentEntity, object? navigation);" }, + { + "Member": "static string JsonElementMappingNotFound(object? structuralType, object? name, object? columnName);" + }, { "Member": "static string JsonEntityMappedToDifferentColumnThanOwner(object? jsonType, object? containingColumn, object? ownerType, object? ownerContainingColumn);" }, @@ -16570,6 +16579,9 @@ { "Member": "static string JsonProjectingQueryableOperationNoTrackingWithIdentityResolution(object? asNoTrackingWithIdentityResolution);" }, + { + "Member": "static string JsonQueryExpressionWithoutUnderlyingColumn(object? structuralType);" + }, { "Member": "static string JsonRequiredEntityWithNullJson(object? entity);" }, diff --git a/src/EFCore.Relational/Metadata/Internal/RelationalJsonElement.cs b/src/EFCore.Relational/Metadata/Internal/RelationalJsonElement.cs index 5252f4a24c6..f48c2c11dd8 100644 --- a/src/EFCore.Relational/Metadata/Internal/RelationalJsonElement.cs +++ b/src/EFCore.Relational/Metadata/Internal/RelationalJsonElement.cs @@ -85,13 +85,7 @@ public virtual RelationalTypeMapping? StoreTypeMapping public virtual IReadOnlyList PropertyMappings => _propertyMappings; - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - protected virtual RelationalTypeMapping? GetDefaultStoreTypeMapping() + private RelationalTypeMapping? GetDefaultStoreTypeMapping() { if (PropertyMappings.Select(m => m.Property).OfType().FirstOrDefault()?.GetTypeMapping() is RelationalTypeMapping mapping) { diff --git a/src/EFCore.Relational/Metadata/Internal/RelationalTypeBaseExtensions.cs b/src/EFCore.Relational/Metadata/Internal/RelationalTypeBaseExtensions.cs index 36fac663d64..6c1c4e57e46 100644 --- a/src/EFCore.Relational/Metadata/Internal/RelationalTypeBaseExtensions.cs +++ b/src/EFCore.Relational/Metadata/Internal/RelationalTypeBaseExtensions.cs @@ -14,16 +14,120 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Internal; public static class RelationalTypeBaseExtensions { /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// Returns the storage mappings the type queries against, in priority order: default SQL query, default function, + /// view, then table. The first non-empty set wins; an empty enumerable means the type has no + /// real (non-default) storage and queries should fall back to default mappings. /// - public static IEnumerable GetViewOrTableMappings(this ITypeBase typeBase) + /// + /// + /// Only the "default" function/SQL query mappings (those configured via ToFunction/ToSqlQuery + /// on the entity) participate in the priority; additional HasDbFunction-style mappings remain + /// invocation-only and never shadow the entity's view/table mapping for Set<T>() queries. + /// + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + /// + public static IEnumerable GetQueryMappings(this ITypeBase typeBase) { typeBase.Model.EnsureRelationalModel(); - var viewMapping = typeBase.FindRuntimeAnnotationValue(RelationalAnnotationNames.ViewMappings); - var tableMapping = typeBase.FindRuntimeAnnotationValue(RelationalAnnotationNames.TableMappings); - return (IEnumerable?)(viewMapping ?? tableMapping) ?? []; + if (typeBase.FindRuntimeAnnotationValue(RelationalAnnotationNames.SqlQueryMappings) is List sqlQueryMappings + && GetDefaults(sqlQueryMappings, static m => m.IsDefaultSqlQueryMapping) is { } defaultSqlQueryMappings) + { + return defaultSqlQueryMappings; + } + + if (typeBase.FindRuntimeAnnotationValue(RelationalAnnotationNames.FunctionMappings) is List functionMappings + && GetDefaults(functionMappings, static m => m.IsDefaultFunctionMapping) is { } defaultFunctionMappings) + { + return defaultFunctionMappings; + } + + var viewMappings = typeBase.GetViewMappings(); + return viewMappings.Any() ? viewMappings : typeBase.GetTableMappings(); + + static List? GetDefaults(List mappings, Func isDefault) + { + var count = 0; + for (var i = 0; i < mappings.Count; i++) + { + if (isDefault(mappings[i])) + { + count++; + } + } + + if (count == 0) + { + return null; + } + + if (count == mappings.Count) + { + return mappings; + } + + var defaults = new List(count); + for (var i = 0; i < mappings.Count; i++) + { + var mapping = mappings[i]; + if (isDefault(mapping)) + { + defaults.Add(mapping); + } + } + + return defaults; + } + } + + /// + /// Returns the entity type's mappings to the tables actually being projected (as given by ). + /// Used by query translation sites that need to pick the principal split-entity table for an entity reference. + /// + /// + /// + /// The mappings are read straight off the projected tables, so this naturally returns the entity's default + /// mapping for FromSql / table-valued-function queries (whose contains the + /// default table) and its real table/view mapping for ordinary queries, without re-applying the + /// storage-priority logic used by . When is + /// (the projection is unknown) it falls back to all of the entity's query mappings. + /// + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + /// + /// The entity type. + /// The tables being projected from in the containing query, or when unknown. + public static List GetProjectedQueryMappings( + this IEntityType entityType, + IReadOnlyDictionary? tableMap) + { + if (tableMap is null) + { + // The projection isn't known (no table map), so we can't scope to the projected tables; fall back to all of the + // entity's query mappings. + return [.. entityType.GetQueryMappings()]; + } + + var projected = new List(); + foreach (var table in tableMap.Keys) + { + foreach (var mapping in table.EntityTypeMappings) + { + if (mapping.TypeBase == entityType) + { + projected.Add(mapping); + } + } + } + + return projected; } } diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index 52fa9637437..cf701d5b687 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -1209,6 +1209,14 @@ public static string JsonCantNavigateToParentEntity(object? jsonEntity, object? GetString("JsonCantNavigateToParentEntity", nameof(jsonEntity), nameof(parentEntity), nameof(navigation)), jsonEntity, parentEntity, navigation); + /// + /// No JSON element mapping was found for '{structuralType}.{name}' on column '{columnName}'. + /// + public static string JsonElementMappingNotFound(object? structuralType, object? name, object? columnName) + => string.Format( + GetString("JsonElementMappingNotFound", nameof(structuralType), nameof(name), nameof(columnName)), + structuralType, name, columnName); + /// /// The database returned the empty string when a JSON object was expected. /// @@ -1381,6 +1389,14 @@ public static string JsonProjectingQueryableOperationNoTrackingWithIdentityResol public static string JsonPropertyNameShouldBeConfiguredOnNestedNavigation => GetString("JsonPropertyNameShouldBeConfiguredOnNestedNavigation"); + /// + /// The JSON query expression for '{structuralType}' has no underlying column. + /// + public static string JsonQueryExpressionWithoutUnderlyingColumn(object? structuralType) + => string.Format( + GetString("JsonQueryExpressionWithoutUnderlyingColumn", nameof(structuralType)), + structuralType); + /// /// Composing LINQ operators over collections inside JSON documents isn't supported or hasn't been implemented by your EF provider. /// diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index a53d8cd2bd0..d18686297b9 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -574,6 +574,9 @@ Navigation from JSON-mapped entity '{jsonEntity}' to its parent entity '{parentEntity}' using navigation '{navigation}' is not supported. Entities mapped to JSON can only navigate to their children. + + No JSON element mapping was found for '{structuralType}.{name}' on column '{columnName}'. + The database returned the empty string when a JSON object was expected. @@ -643,6 +646,9 @@ The JSON property name should only be configured on nested owned navigations. + + The JSON query expression for '{structuralType}' has no underlying column. + Composing LINQ operators over collections inside JSON documents isn't supported or hasn't been implemented by your EF provider. diff --git a/src/EFCore.Relational/Query/Internal/JsonProjectionInfo.cs b/src/EFCore.Relational/Query/Internal/JsonProjectionInfo.cs index d86d5cec724..bfc2073b9ab 100644 --- a/src/EFCore.Relational/Query/Internal/JsonProjectionInfo.cs +++ b/src/EFCore.Relational/Query/Internal/JsonProjectionInfo.cs @@ -19,10 +19,12 @@ public readonly struct JsonProjectionInfo /// public JsonProjectionInfo( int jsonColumnIndex, - List<(IProperty?, int?, int?)> keyAccessInfo) + List<(IProperty?, int?, int?)> keyAccessInfo, + IColumnBase? jsonColumn = null) { JsonColumnIndex = jsonColumnIndex; KeyAccessInfo = keyAccessInfo; + JsonColumn = jsonColumn; } /// @@ -52,4 +54,16 @@ public JsonProjectionInfo( /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public List<(IProperty? KeyProperty, int? ConstantKeyValue, int? KeyProjectionIndex)> KeyAccessInfo { get; } + + /// + /// The relational-model column containing the JSON document, or null when this projection was built from + /// a synthetic JSON expansion (OPENJSON / json_each) that has no underlying IColumnBase. + /// + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public IColumnBase? JsonColumn { get; } } diff --git a/src/EFCore.Relational/Query/JsonQueryExpression.cs b/src/EFCore.Relational/Query/JsonQueryExpression.cs index b0168a5c1f1..c7407cecf68 100644 --- a/src/EFCore.Relational/Query/JsonQueryExpression.cs +++ b/src/EFCore.Relational/Query/JsonQueryExpression.cs @@ -137,11 +137,13 @@ public virtual SqlExpression BindProperty(IProperty property) return match; } + var element = GetJsonElement(property); + return new JsonScalarExpression( JsonColumn, - [.. Path, new PathSegment(property.GetJsonPropertyName()!)], + [.. Path, new PathSegment(element.PropertyName!)], property.ClrType.UnwrapNullableType(), - property.FindRelationalTypeMapping()!, + element.StoreTypeMapping!, IsNullable || property.IsNullable); } @@ -175,7 +177,7 @@ public virtual JsonQueryExpression BindStructuralProperty(IPropertyBase structur var targetEntityType = navigation.TargetEntityType; var newPath = Path.ToList(); - newPath.Add(new PathSegment(targetEntityType.GetJsonPropertyName()!)); + newPath.Add(new PathSegment(GetJsonElement(navigation).PropertyName!)); var newKeyPropertyMap = new Dictionary(); var targetPrimaryKeyProperties = targetEntityType.FindPrimaryKey()!.Properties.Take(KeyPropertyMap.Count); @@ -199,14 +201,14 @@ public virtual JsonQueryExpression BindStructuralProperty(IPropertyBase structur { if (StructuralType is not IComplexType complexType) { - throw new UnreachableException("Navigation on complex JSON type"); + throw new UnreachableException("Non-root complex property on entity type"); } Check.DebugAssert(KeyPropertyMap is null); var targetComplexType = complexProperty.ComplexType; var newPath = Path.ToList(); - newPath.Add(new PathSegment(targetComplexType.GetJsonPropertyName()!)); + newPath.Add(new PathSegment(GetJsonElement(complexProperty).PropertyName!)); return new JsonQueryExpression( targetComplexType, @@ -219,7 +221,7 @@ public virtual JsonQueryExpression BindStructuralProperty(IPropertyBase structur } default: - throw new UnreachableException(); + throw new UnreachableException("Unexpected structural property type."); } } @@ -290,6 +292,57 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) return Update(jsonColumn, newKeyPropertyMap); } + /// + /// Finds the for the given property/navigation/complex property within the + /// JSON column referenced by this expression by matching 's underlying + /// against . This + /// disambiguates entity-splitting, TPT and TPC scenarios where the same property has multiple JSON element + /// mappings — one per concrete table. + /// may be for shadow keys that have + /// no JSON representation; callers iterating over must handle that case + /// and skip them. + /// + /// The property, navigation or complex property to look up. + /// The JSON element mapping for . + public virtual IRelationalJsonElement GetJsonElement(IPropertyBase propertyBase) + { + var column = JsonColumn.Column + ?? throw new InvalidOperationException( + RelationalStrings.JsonQueryExpressionWithoutUnderlyingColumn(StructuralType.DisplayName())); + return FindJsonElement(propertyBase) + ?? throw new InvalidOperationException( + RelationalStrings.JsonElementMappingNotFound(propertyBase.DeclaringType.DisplayName(), propertyBase.Name, column.Name)); + } + + /// + /// Finds the for the given property/navigation/complex property within the + /// JSON column referenced by this expression by matching 's underlying + /// against , or returns + /// if no such element exists (including when has no underlying + /// , e.g. for synthetic JSON expansions over OPENJSON / json_each, or for + /// iterated properties such as shadow keys that have no JSON representation). + /// + /// The property, navigation or complex property to look up. + /// The JSON element mapping for , or if not found. + public virtual IRelationalJsonElement? FindJsonElement(IPropertyBase propertyBase) + { + var containingColumn = JsonColumn.Column; + if (containingColumn is null) + { + return null; + } + + foreach (var mapping in propertyBase.GetJsonElementMappings()) + { + if (ReferenceEquals(mapping.Element.ContainingColumn, containingColumn)) + { + return mapping.Element; + } + } + + return null; + } + /// /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will /// return this expression. diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.CreateSelect.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.CreateSelect.cs index fadfeac8ca1..7a2b0a1c8f4 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.CreateSelect.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.CreateSelect.cs @@ -114,7 +114,7 @@ SelectExpression CreateRootSelectExpressionCore(IEntityType entityType) _sqlExpressionFactory.Case(caseWhenClauses, elseResult: null)); var projection = new StructuralTypeProjectionExpression( - entityType, propertyMap, complexPropertyMap, nullable: false, discriminatorExpression); + entityType, propertyMap, complexPropertyMap, nullable: false, discriminatorExpression, tableMap); return new SelectExpression(tables, projection, identifier, _sqlAliasManager); } @@ -128,7 +128,7 @@ SelectExpression CreateRootSelectExpressionCore(IEntityType entityType) // (no UNION needed, no discriminator needed) if (concreteEntityTypes is [var singleEntityType]) { - var table = singleEntityType.GetViewOrTableMappings().Single().Table; + var table = singleEntityType.GetQueryMappings().Single().Table; var alias = _sqlAliasManager.GenerateTableAlias(table); var tableExpression = new TableExpression(alias, table); @@ -152,7 +152,7 @@ SelectExpression CreateRootSelectExpressionCore(IEntityType entityType) foreach (var concreteEntityType in concreteEntityTypes) { - var table = concreteEntityType.GetViewOrTableMappings().Single().Table; + var table = concreteEntityType.GetQueryMappings().Single().Table; foreach (var currentType in concreteEntityType.GetAllBaseTypesInclusive()) { @@ -232,7 +232,7 @@ Expression ProcessComplexPropertyTpc( var shaper = GenerateComplexJsonShaper( complexProperty, - containerColumn: null, + containerColumn, projectedColumnName, containerColumn.ProviderClrType, containerColumn.StoreTypeMapping, @@ -272,7 +272,7 @@ Expression ProcessComplexPropertyTpc( var discriminatorValues = new List(concreteEntityTypes.Length); foreach (var concreteEntityType in concreteEntityTypes) { - var table = concreteEntityType.GetViewOrTableMappings().Single().Table; + var table = concreteEntityType.GetQueryMappings().Single().Table; var tableAlias = _sqlAliasManager.GenerateTableAlias(table); var tableExpression = new TableExpression(tableAlias, table); @@ -296,19 +296,22 @@ Expression ProcessComplexPropertyTpc( { IProperty p => table.FindColumn(p), IComplexProperty p => p.ComplexType.GetContainerColumnName() is string columnName ? table.FindColumn(columnName) : null, - _ => throw new UnreachableException() + _ => throw new UnreachableException("Unexpected property type when building TPC union projection.") }; Debug.Assert(column is not null, "Column not found for property " + property.Name); // Note that the projected name may differ from the column name on the entity's concrete table because of // uniquification (i.e. two TPC entities in the same hierarchy have two properties mapped columns with the - // same name) + // same name). + // Each union arm's column references its own concrete table's model column; the outer union column + // (created separately over the union alias) references no single model column and has a null Column. projections.Add( new ProjectionExpression( new ColumnExpression( column.Name, tableAlias, + column, column.ProviderClrType.UnwrapNullableType(), column.StoreTypeMapping, column.IsNullable), @@ -338,9 +341,17 @@ Expression ProcessComplexPropertyTpc( var tpcTablesExpression = new TpcTablesExpression( tpcTableAlias, entityType, subSelectExpressions, discriminatorColumn, discriminatorValues); + // Every concrete TPC table is projected through the union's outer alias. + var tpcTableMap = new Dictionary(concreteEntityTypes.Length); + foreach (var concreteEntityType in concreteEntityTypes) + { + tpcTableMap[concreteEntityType.GetQueryMappings().Single().Table] = tpcTableAlias; + } + return new SelectExpression( [tpcTablesExpression], - new StructuralTypeProjectionExpression(entityType, propertyMap, complexPropertyMap, nullable: false, discriminatorColumn), + new StructuralTypeProjectionExpression( + entityType, propertyMap, complexPropertyMap, nullable: false, discriminatorColumn, tpcTableMap), identifier, _sqlAliasManager); } @@ -357,7 +368,7 @@ Expression ProcessComplexPropertyTpc( entityType, storeFunction, new TableValuedFunctionExpression(alias, (IStoreFunction)storeFunction, [])); } - var mappings = entityType.GetViewOrTableMappings().ToList(); + var mappings = entityType.GetQueryMappings().ToList(); if (mappings is [{ Table: var singleTable }]) { var alias = _sqlAliasManager.GenerateTableAlias(singleTable); @@ -417,22 +428,22 @@ Expression ProcessComplexPropertyTpc( var complexPropertyMap = new Dictionary(); foreach (var complexProperty in entityType.GetComplexProperties()) { - var table = complexProperty.ComplexType.GetViewOrTableMappings().Single().Table; + var table = complexProperty.ComplexType.GetQueryMappings().Single().Table; complexPropertyMap[complexProperty] = ProcessComplexProperty(complexProperty, table, tableMap[table], containerNullable: false); } - var projection = new StructuralTypeProjectionExpression(entityType, propertyMap, complexPropertyMap); + var projection = new StructuralTypeProjectionExpression(entityType, propertyMap, complexPropertyMap, tableMap: tableMap); AddJsonNavigationBindings(entityType, projection, propertyMap, tableMap); return new SelectExpression(tables, projection, identifier, _sqlAliasManager); } default: - throw new UnreachableException(); + throw new UnreachableException("Unexpected mapping strategy."); } static ITableBase GetTableBaseFiltered(IEntityType entityType, Dictionary existingTables) - => entityType.GetViewOrTableMappings().Single(m => !existingTables.ContainsKey(m.Table)).Table; + => entityType.GetQueryMappings().Single(m => !existingTables.ContainsKey(m.Table)).Table; } } @@ -454,7 +465,7 @@ private SelectExpression GenerateSingleTableSelect(IEntityType entityType, ITabl } var tableMap = new Dictionary { [table] = alias }; - var projection = new StructuralTypeProjectionExpression(entityType, propertyMap, complexPropertyMap); + var projection = new StructuralTypeProjectionExpression(entityType, propertyMap, complexPropertyMap, tableMap: tableMap); AddJsonNavigationBindings(entityType, projection, propertyMap, tableMap); var identifier = new List<(ColumnExpression Column, ValueComparer Comparer)>(); @@ -722,7 +733,9 @@ private void AddJsonNavigationBindings( // Find the containing column for the owned JSON entity type, and then the table in the table map that // contains that column. var targetEntityType = ownedJsonNavigation.TargetEntityType; - var containerColumnName = targetEntityType.GetContainerColumnName() ?? throw new UnreachableException(); + var containerColumnName = targetEntityType.GetContainerColumnName() + ?? throw new UnreachableException( + $"JSON-mapped entity type '{targetEntityType.DisplayName()}' without a container column name."); var (containerColumn, tableAlias) = tableMap .Select(kvp => (Column: kvp.Key.FindColumn(containerColumnName), TableAlias: kvp.Value)) .SingleOrDefault(c => c.Column is not null); @@ -739,6 +752,7 @@ private void AddJsonNavigationBindings( var column = new ColumnExpression( containerColumnName, tableAlias, + containerColumn, containerColumnTypeMapping.ClrType, containerColumnTypeMapping, isNullable); @@ -810,13 +824,15 @@ protected virtual SelectExpression CreateSelect( continue; } - // Skip also properties with no JSON name (i.e. shadow keys containing the index in the collection, which don't actually exist + // Skip properties with no JSON name (i.e. shadow keys containing the index in the collection, which don't actually exist // in the JSON document and can't be bound to) - if (property.GetJsonPropertyName() is { } jsonPropertyName) + var element = jsonQueryExpression.GetJsonElement(property); + if (element.PropertyName is { } jsonPropertyName) { propertyExpressions[property] = CreateColumnExpression( - tableExpressionBase, jsonPropertyName, property.ClrType, property.GetRelationalTypeMapping(), - /* jsonQueryExpression.IsNullable || */ property.IsNullable); // TODO: + tableExpressionBase, jsonPropertyName, property.ClrType, + element.StoreTypeMapping!, + /* jsonQueryExpression.IsNullable || */ property.IsNullable); // TODO: Issue #28887 } } @@ -830,9 +846,12 @@ protected virtual SelectExpression CreateSelect( var isNullable = jsonQueryExpression.IsNullable || complexProperty.IsNullable; var containerColumnExpression = new ColumnExpression( - complexType.GetJsonPropertyName() - ?? throw new UnreachableException($"No JSON property name for complex property {complexProperty.Name}"), + jsonQueryExpression.GetJsonElement(complexProperty).PropertyName + ?? throw new UnreachableException($"No JSON property name for complex property {complexProperty.Name}"), tableAlias, + // Nested container projects a sub-document of the same underlying JSON column, so preserve the model column + // for downstream FindJsonElement matching. + jsonColumn.Column, jsonColumn.Type, jsonColumn.TypeMapping, isNullable); @@ -860,7 +879,7 @@ protected virtual SelectExpression CreateSelect( && n.ForeignKey.PrincipalToDependent == n)) { var targetEntityType = ownedJsonNavigation.TargetEntityType; - var jsonNavigationName = ownedJsonNavigation.TargetEntityType.GetJsonPropertyName(); + var jsonNavigationName = jsonQueryExpression.GetJsonElement(ownedJsonNavigation).PropertyName; Check.DebugAssert(jsonNavigationName is not null, "Invalid navigation found on JSON-mapped entity"); var isNullable = jsonQueryExpression.IsNullable || !ownedJsonNavigation.ForeignKey.IsRequiredDependent @@ -868,7 +887,9 @@ protected virtual SelectExpression CreateSelect( // The TableExpressionBase represents a relational expansion of the JSON collection. We now need a ColumnExpression to represent // the specific JSON property (projected as a relational column) which holds the JSON subtree for the target entity. - var column = new ColumnExpression(jsonNavigationName, tableAlias, jsonColumn.Type, jsonColumn.TypeMapping, isNullable); + // Pass the underlying model column through so FindJsonElement matches for properties of the nested entity. + var column = new ColumnExpression( + jsonNavigationName, tableAlias, jsonColumn.Column, jsonColumn.Type, jsonColumn.TypeMapping, isNullable); // need to remap key property map to use target entity key properties var newKeyPropertyMap = new Dictionary(); diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.ExecuteUpdate.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.ExecuteUpdate.cs index 243cd249ed1..42be4915368 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.ExecuteUpdate.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.ExecuteUpdate.cs @@ -468,11 +468,11 @@ void ProcessColumn(ColumnExpression column, IPropertyBase targetProperty) { // Find the container column in the relational model to get its type mapping // Note that we assume exactly one column with the given name mapped to the entity (despite entity splitting). - // See #38060 about improving this. + // See #28520 about improving this. var containerColumnName = complexType.GetContainerColumnName(); if (containerColumnName != null) { - targetColumnModel = complexType.ContainingEntityType.GetTableMappings() + targetColumnModel = complexType.GetTableMappings() .Select(m => m.Table.FindColumn(containerColumnName)) .SingleOrDefault(c => c is not null); } diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs index a6120445123..8d2389cfe81 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs @@ -160,6 +160,8 @@ when entityQueryRootExpression.GetType() == typeof(EntityQueryRootExpression) && entityQueryRootExpression.EntityType.GetSqlQueryMappings().FirstOrDefault(m => m.IsDefaultSqlQueryMapping)?.SqlQuery is { } sqlQuery: { + // TODO: Use the SqlQuery directly instead of the default mapping once hierarchy support is implemented. + // Issue #21660 var table = entityQueryRootExpression.EntityType.GetDefaultMappings().Single().Table; var alias = _sqlAliasManager.GenerateTableAlias(table); diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs index 6f8bac21dd8..1113cfcbc8f 100644 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs @@ -2555,15 +2555,18 @@ internal void ProcessTopLevelComplexJsonProperties( } } - internal ParameterExpression GenerateJsonReader(int jsonColumnIndex, ITypeBase structuralType) + internal ParameterExpression GenerateJsonReader(int jsonColumnIndex, ITypeBase structuralType, IColumnBase? jsonColumn = null) { Check.DebugAssert(structuralType.IsMappedToJson()); - var jsonColumnName = structuralType.GetContainerColumnName()!; - var jsonColumn = structuralType.ContainingEntityType.GetViewOrTableMappings() - .Select(m => m.Table.FindColumn(jsonColumnName)) - .FirstOrDefault(c => c is not null) - ?? throw new UnreachableException($"Could not find JSON container column '{jsonColumnName}' for entity type '{structuralType.DisplayName()}'."); + if (jsonColumn is null) + { + var jsonColumnName = structuralType.GetContainerColumnName()!; + jsonColumn = structuralType.ContainingEntityType.GetQueryMappings() + .Select(m => m.Table.FindColumn(jsonColumnName)) + .FirstOrDefault(c => c is not null) + ?? throw new UnreachableException($"Could not find JSON container column '{jsonColumnName}' for entity type '{structuralType.DisplayName()}'."); + } var jsonColumnTypeMapping = jsonColumn.StoreTypeMapping; @@ -2624,7 +2627,7 @@ internal ParameterExpression GenerateJsonReader(int jsonColumnIndex, ITypeBase s ITypeBase structuralType, bool isCollection) { - var jsonReaderDataVariable = GenerateJsonReader(jsonProjectionInfo.JsonColumnIndex, structuralType); + var jsonReaderDataVariable = GenerateJsonReader(jsonProjectionInfo.JsonColumnIndex, structuralType, jsonProjectionInfo.JsonColumn); // we should have keyAccessInfo for every PK property of the entity, unless we are generating shaper for the collection // in that case the final key property will be synthesized in the shaper code diff --git a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.StructuralEquality.cs b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.StructuralEquality.cs index e203c7cd64f..736e4c3712c 100644 --- a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.StructuralEquality.cs +++ b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.StructuralEquality.cs @@ -159,7 +159,9 @@ bool TryRewriteEntityEquality([NotNullWhen(true)] out SqlExpression? result) if (nullComparedEntityType.GetRootType() == nullComparedEntityType && nullComparedEntityType.GetMappingStrategy() != RelationalAnnotationNames.TpcMappingStrategy) { - var table = nullComparedEntityType.GetViewOrTableMappings().ToList() switch + // Scope to actually-projected tables when known (entity-splitting). + var tableMap = (nonNullEntityReference.Parameter?.ValueBufferExpression as StructuralTypeProjectionExpression)?.TableMap; + var table = nullComparedEntityType.GetProjectedQueryMappings(tableMap) switch { [var singleMapping] => singleMapping.Table, diff --git a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs index 4242f97d5a8..a6ea4c2575d 100644 --- a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs @@ -1291,8 +1291,9 @@ private SqlExpression BindProperty(StructuralTypeReferenceExpression typeReferen return propertyAccess; } - var table = entityType.GetViewOrTableMappings().SingleOrDefault(e => e.IsSplitEntityTypePrincipal ?? true)?.Table - ?? entityType.GetDefaultMappings().Single().Table; + // Scope to actually-projected tables when known (entity-splitting). + var table = entityType.GetProjectedQueryMappings(projection.TableMap) + .Single(e => e.IsSplitEntityTypePrincipal ?? true).Table; if (!table.IsOptional(entityType)) { return propertyAccess; diff --git a/src/EFCore.Relational/Query/RelationalStructuralTypeShaperExpression.cs b/src/EFCore.Relational/Query/RelationalStructuralTypeShaperExpression.cs index b44dfdacfdc..f9257cd97a9 100644 --- a/src/EFCore.Relational/Query/RelationalStructuralTypeShaperExpression.cs +++ b/src/EFCore.Relational/Query/RelationalStructuralTypeShaperExpression.cs @@ -103,8 +103,9 @@ protected override LambdaExpression GenerateMaterializationCondition(ITypeBase t return baseCondition; } - var table = entityType.GetViewOrTableMappings().SingleOrDefault(e => e.IsSplitEntityTypePrincipal ?? true)?.Table - ?? entityType.GetDefaultMappings().Single().Table; + var tableMap = (ValueBufferExpression as StructuralTypeProjectionExpression)?.TableMap; + var table = entityType.GetProjectedQueryMappings(tableMap) + .Single(e => e.IsSplitEntityTypePrincipal ?? true).Table; if (table.IsOptional(entityType)) { // Optional dependent diff --git a/src/EFCore.Relational/Query/SqlExpressions/ColumnExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/ColumnExpression.cs index 4201887b47b..9194c3273e1 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/ColumnExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/ColumnExpression.cs @@ -88,7 +88,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) /// /// A new expression which has property set to true. public virtual ColumnExpression MakeNullable() - => IsNullable ? this : new ColumnExpression(Name, TableAlias, Type, TypeMapping, true); + => IsNullable ? this : new ColumnExpression(Name, TableAlias, Column, Type, TypeMapping, true); /// /// Applies supplied type mapping to this expression. @@ -96,7 +96,7 @@ public virtual ColumnExpression MakeNullable() /// A relational type mapping to apply. /// A new expression which has supplied type mapping. public virtual SqlExpression ApplyTypeMapping(RelationalTypeMapping? typeMapping) - => new ColumnExpression(Name, TableAlias, Type, typeMapping, IsNullable); + => new ColumnExpression(Name, TableAlias, Column, Type, typeMapping, IsNullable); /// public override Expression Quote() diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs index 24a41424f1c..c7432403fa1 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs @@ -360,7 +360,7 @@ private sealed class CloningExpressionVisitor(SqlAliasManager? sqlAliasManager, } case ColumnExpression column when _tableAliasMap.TryGetValue(column.TableAlias, out var newTableAlias): - return new ColumnExpression(column.Name, newTableAlias, column.Type, column.TypeMapping, column.IsNullable); + return new ColumnExpression(column.Name, newTableAlias, column.Column, column.Type, column.TypeMapping, column.IsNullable); default: return base.Visit(expression); diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs index 9f98878e2b7..0c31a2f0453 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs @@ -371,7 +371,8 @@ void ProcessComplexType(StructuralTypeProjectionExpression complexTypeProjection continue; default: - throw new UnreachableException(); + throw new UnreachableException( + "Unexpected complex property binding when processing identifiers for Distinct."); } } } @@ -1286,10 +1287,13 @@ Expression CopyProjectionToOuter(SelectExpression innerSelectExpression, Express keyProjectionIndex != null ? projectionIndexMap[keyProjectionIndex.Value] : null)); } + // The outer projection still references the same underlying JSON column; only the + // index into the outer projection changes, so preserve JsonColumn for the shaper. remappedConstant = Constant( new JsonProjectionInfo( projectionIndexMap[jsonProjectionInfo.JsonColumnIndex], - newKeyAccessInfo)); + newKeyAccessInfo, + jsonProjectionInfo.JsonColumn)); break; } @@ -1317,7 +1321,8 @@ Expression CopyProjectionToOuter(SelectExpression innerSelectExpression, Express newChildrenProjectionInfo.Add( (new JsonProjectionInfo( projectionIndexMap[childProjectionInfo.JsonProjectionInfo.JsonColumnIndex], - newKeyAccessInfo), + newKeyAccessInfo, + childProjectionInfo.JsonProjectionInfo.JsonColumn), childProjectionInfo.Navigation)); } @@ -1356,7 +1361,7 @@ Expression CopyProjectionToOuter(SelectExpression innerSelectExpression, Express StructuralTypeProjectionExpression p => AddStructuralTypeProjection(p), JsonQueryExpression p => AddJsonProjection(p), SqlExpression p => Constant(AddToProjection(p, projectionMember.Last?.Name)), - _ => throw new UnreachableException() + _ => throw new UnreachableException("Unexpected expression type in projection mapping.") }; } @@ -1451,7 +1456,8 @@ void ProcessType(StructuralTypeProjectionExpression typeProjection) break; default: - throw new UnreachableException(); + throw new UnreachableException( + "Unexpected complex property binding when building structural type projection."); } } } @@ -1505,10 +1511,10 @@ ConstantExpression AddJsonProjection(JsonQueryExpression jsonQueryExpression) break; default: - throw new UnreachableException(); + throw new UnreachableException("Unexpected structural type when building JSON projection."); } - return Constant(new JsonProjectionInfo(jsonColumnIndex, keyAccessInfo)); + return Constant(new JsonProjectionInfo(jsonColumnIndex, keyAccessInfo, jsonQueryExpression.JsonColumn.Column)); } static IReadOnlyList GetMappedKeyProperties(IKey key) @@ -2391,7 +2397,8 @@ StructuralTypeProjectionExpression ProcessStructuralType( continue; default: - throw new UnreachableException(); + throw new UnreachableException( + "Unexpected complex property binding in set operation over structural types."); } void ProcessJson(JsonQueryExpression jsonQuery1, JsonQueryExpression jsonQuery2) @@ -2419,7 +2426,9 @@ void ProcessJson(JsonQueryExpression jsonQuery1, JsonQueryExpression jsonQuery2) select1._projection.Add(innerProjection); select2._projection.Add(new ProjectionExpression(jsonScalar2, alias)); - var outerColumn = CreateColumnExpression(innerProjection, setOperationAlias); + // The JsonColumn always represents the containing JSON column, so flow its model column through the set + // operation even though the projection itself is a JsonScalarExpression (which has no model column of its own). + var outerColumn = CreateColumnExpression(innerProjection, setOperationAlias, jsonQuery1.JsonColumn.Column); if (jsonScalar1.IsNullable || jsonScalar2.IsNullable) { outerColumn = outerColumn.MakeNullable(); @@ -2453,7 +2462,8 @@ void ProcessJson(JsonQueryExpression jsonQuery1, JsonQueryExpression jsonQuery2) } var outerProjection = new StructuralTypeProjectionExpression( - type, propertyExpressions, complexPropertyCache, nullable: false, discriminatorExpression); + type, propertyExpressions, complexPropertyCache, nullable: false, discriminatorExpression, + structuralProjection1.TableMap); if (outerIdentifiers.Length > 0 && outerProjection is { StructuralType: IEntityType entityType }) { @@ -2654,7 +2664,7 @@ static IReadOnlyDictionary GetPropertyExpressions( var sourceTableForAnnotations = FindRootTableExpressionForColumn(selectExpression, identifyingColumn); var ownerType = navigation.DeclaringEntityType; var entityType = navigation.TargetEntityType; - var principalMappings = ownerType.GetViewOrTableMappings().Select(e => e.Table); + var principalMappings = ownerType.GetQueryMappings().Select(e => e.Table); var derivedType = ownerType.BaseType != null; var derivedTpt = derivedType && ownerType.GetMappingStrategy() == RelationalAnnotationNames.TptMappingStrategy; var parentNullable = identifyingColumn.IsNullable; @@ -2665,11 +2675,11 @@ static IReadOnlyDictionary GetPropertyExpressions( || derivedType; if (derivedTpt) { - principalMappings = principalMappings.Except(ownerType.BaseType!.GetViewOrTableMappings().Select(e => e.Table)); + principalMappings = principalMappings.Except(ownerType.BaseType!.GetQueryMappings().Select(e => e.Table)); } var principalTables = principalMappings.ToList(); - var dependentTables = entityType.GetViewOrTableMappings().Select(e => e.Table).ToList(); + var dependentTables = entityType.GetQueryMappings().Select(e => e.Table).ToList(); var baseTableIndex = selectExpression._tables.FindIndex(teb => ReferenceEquals(teb.UnwrapJoin(), tableExpressionBase)); var dependentMainTable = dependentTables[0]; var tableMap = new Dictionary(); @@ -3380,7 +3390,7 @@ or ExpressionType.LessThan ExpressionType.GreaterThan => ExpressionType.LessThan, ExpressionType.GreaterThanOrEqual => ExpressionType.LessThanOrEqual, - _ => throw new UnreachableException() + _ => throw new UnreachableException($"Unexpected operator type '{sqlBinaryExpression.OperatorType}'.") }; return new SqlBinaryExpression( @@ -3939,7 +3949,8 @@ StructuralTypeProjectionExpression LiftStructuralProjectionFromSubquery( continue; default: - throw new UnreachableException(); + throw new UnreachableException( + "Unexpected complex property binding when lifting structural projection from subquery."); } } @@ -3952,7 +3963,8 @@ StructuralTypeProjectionExpression LiftStructuralProjectionFromSubquery( } var newEntityProjection = new StructuralTypeProjectionExpression( - projection.StructuralType, propertyExpressions, complexPropertyCache, nullable: false, discriminatorExpression); + projection.StructuralType, propertyExpressions, complexPropertyCache, nullable: false, discriminatorExpression, + projection.TableMap); if (projection.StructuralType is IEntityType entityType2) { @@ -3987,7 +3999,10 @@ JsonQueryExpression LiftJsonQueryFromSubquery(JsonQueryExpression jsonQueryExpre jsonQueryExpression.JsonColumn.TypeMapping, jsonQueryExpression.IsNullable); - var newJsonColumn = subquery.GenerateOuterColumn(subqueryAlias, jsonScalarExpression); + // The JsonColumn always represents the containing JSON column, so flow its model column through the pushdown even + // though the projection itself is a JsonScalarExpression (which has no model column of its own). + var newJsonColumn = subquery.GenerateOuterColumn( + subqueryAlias, jsonScalarExpression, column: jsonQueryExpression.JsonColumn.Column); Dictionary? newKeyPropertyMap = null; @@ -4215,7 +4230,7 @@ private static IEnumerable GetAllComplexPropertiesInHierarchy( IEntityType entityType => entityType.GetAllBaseTypes().Concat(entityType.GetDerivedTypesInclusive()) .SelectMany(t => t.GetDeclaredComplexProperties()), IComplexType complexType => complexType.GetDeclaredComplexProperties(), - _ => throw new UnreachableException() + _ => throw new UnreachableException("Unexpected structural type.") }; private static ColumnExpression CreateColumnExpression( @@ -4238,11 +4253,17 @@ private static ColumnExpression CreateColumnExpression( column.PropertyMappings.First(m => m.Property == property).TypeMapping, nullable || column.IsNullable); - private static ColumnExpression CreateColumnExpression(ProjectionExpression subqueryProjection, string tableAlias) + private static ColumnExpression CreateColumnExpression( + ProjectionExpression subqueryProjection, + string tableAlias, + IColumnBase? column = null) => new( subqueryProjection.Alias, tableAlias, - column: subqueryProjection.Expression is ColumnExpression { Column: { } column } ? column : null, + // The referenced model column is the projected column's own model column, if any. JSON container columns are an + // exception: their JsonScalarExpression projection has no model column of its own, so callers lifting a JSON + // container (e.g. LiftJsonQueryFromSubquery) pass the containing column explicitly to preserve it across the subquery. + column: column ?? (subqueryProjection.Expression as ColumnExpression)?.Column, subqueryProjection.Type, subqueryProjection.Expression.TypeMapping!, nullable: subqueryProjection.Expression switch @@ -4256,13 +4277,14 @@ private static ColumnExpression CreateColumnExpression(ProjectionExpression subq private ColumnExpression GenerateOuterColumn( string tableAlias, SqlExpression projection, - string? columnAlias = null) + string? columnAlias = null, + IColumnBase? column = null) { // TODO: Add check if we can add projection in subquery to generate out column // Subquery having Distinct or GroupBy can block it. var index = AddToProjection(projection, columnAlias); - return CreateColumnExpression(_projection[index], tableAlias); + return CreateColumnExpression(_projection[index], tableAlias, column); } /// diff --git a/src/EFCore.Relational/Query/StructuralTypeProjectionExpression.cs b/src/EFCore.Relational/Query/StructuralTypeProjectionExpression.cs index b098890e5fc..cb5face81f6 100644 --- a/src/EFCore.Relational/Query/StructuralTypeProjectionExpression.cs +++ b/src/EFCore.Relational/Query/StructuralTypeProjectionExpression.cs @@ -32,14 +32,16 @@ public StructuralTypeProjectionExpression( IReadOnlyDictionary propertyExpressionMap, IReadOnlyDictionary complexPropertyMap, bool nullable = false, - SqlExpression? discriminatorExpression = null) + SqlExpression? discriminatorExpression = null, + IReadOnlyDictionary? tableMap = null) : this( type, propertyExpressionMap, ownedNavigationMap: [], complexPropertyMap, nullable, - discriminatorExpression) + discriminatorExpression, + tableMap) { } @@ -49,7 +51,8 @@ private StructuralTypeProjectionExpression( Dictionary ownedNavigationMap, IReadOnlyDictionary complexPropertyMap, bool nullable, - SqlExpression? discriminatorExpression = null) + SqlExpression? discriminatorExpression = null, + IReadOnlyDictionary? tableMap = null) { StructuralType = type; _propertyExpressionMap = propertyExpressionMap; @@ -57,6 +60,7 @@ private StructuralTypeProjectionExpression( _complexPropertyMap = complexPropertyMap; IsNullable = nullable; DiscriminatorExpression = discriminatorExpression; + TableMap = tableMap; } /// @@ -78,6 +82,20 @@ private StructuralTypeProjectionExpression( /// public virtual SqlExpression? DiscriminatorExpression { get; } + /// + /// The tables being projected from, mapping each to its alias in the containing + /// . when the projection wasn't constructed with this + /// information; consumers should fall back to model-wide accessors in that case. + /// + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + [EntityFrameworkInternal] + public virtual IReadOnlyDictionary? TableMap { get; } + /// /// The of the . /// @@ -136,7 +154,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) return changed ? new StructuralTypeProjectionExpression( StructuralType, propertyExpressionMap, ownedNavigationMap, complexPropertyMap, IsNullable, - discriminatorExpression) + discriminatorExpression, TableMap) : this; } @@ -192,7 +210,8 @@ public virtual StructuralTypeProjectionExpression MakeNullable() ownedNavigationMap, complexPropertyMap, nullable: true, - discriminatorExpression); + discriminatorExpression, + TableMap); } /// @@ -260,7 +279,7 @@ public virtual StructuralTypeProjectionExpression UpdateEntityType(IEntityType d return new StructuralTypeProjectionExpression( derivedType, propertyExpressionMap, ownedNavigationMap, complexPropertyMap, IsNullable, - discriminatorExpression); + discriminatorExpression, TableMap); } /// diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs index 8133d06fdea..c339d84561d 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs @@ -203,7 +203,7 @@ private Expression TranslateFullTextTableFunction( { nameof(SqlServerQueryableExtensions.FreeTextTable) => "FREETEXTTABLE", nameof(SqlServerQueryableExtensions.ContainsTable) => "CONTAINSTABLE", - _ => throw new UnreachableException() + _ => throw new UnreachableException($"Unexpected full-text method '{method.Name}'.") }; var (columnsExpression, searchText, languageTerm, topN) = methodCallExpression.Arguments switch @@ -217,7 +217,7 @@ private Expression TranslateFullTextTableFunction( // Use an empty array to signal "*" (all columns) [_, var s, var l, var t] => ((Expression)Expression.NewArrayInit(typeof(ColumnExpression)), s, l, t), - _ => throw new UnreachableException() + _ => throw new UnreachableException("Unexpected argument shape for full-text table function.") }; if (TranslateExpression(searchText) is not { } translatedSearchText @@ -525,29 +525,20 @@ protected override ShapedQueryExpression TransformJsonQueryToTable(JsonQueryExpr // (for owned JSON entities) foreach (var property in structuralType.GetPropertiesInHierarchy()) { - if (property.GetJsonPropertyName() is { } jsonPropertyName) + if (jsonQueryExpression.FindJsonElement(property) is { PropertyName: { } jsonPropertyName } element) { + var typeMapping = element.StoreTypeMapping!; columnInfos.Add( new SqlServerOpenJsonExpression.ColumnInfo { Name = jsonPropertyName, - TypeMapping = property.GetRelationalTypeMapping(), + TypeMapping = typeMapping, Path = [new PathSegment(jsonPropertyName)], - AsJson = property.GetRelationalTypeMapping().ElementTypeMapping is not null + AsJson = typeMapping.ElementTypeMapping is not null }); } } - // Find the container column in the relational model to get its type mapping. - // Note that we assume exactly one column with the given name mapped to the entity (despite entity splitting). - // See #38060 about improving this. - var containerColumnName = structuralType.GetContainerColumnName()!; -#pragma warning disable EF1001 // Internal EF Core API usage. - var containerColumn = structuralType.ContainingEntityType.GetViewOrTableMappings() - .Select(m => m.Table.FindColumn(containerColumnName)) - .First(c => c is not null)!; -#pragma warning restore EF1001 - var nestedJsonPropertyNames = jsonQueryExpression.StructuralType switch { IEntityType entityType @@ -555,12 +546,14 @@ IEntityType entityType .Where(n => n.ForeignKey.IsOwnership && n.TargetEntityType.IsMappedToJson() && n.ForeignKey.PrincipalToDependent == n) - .Select(n => n.TargetEntityType.GetJsonPropertyName() ?? throw new UnreachableException()), + .Select(n => n.TargetEntityType.GetJsonPropertyName() + ?? throw new UnreachableException("JSON-mapped navigation without a JSON property name.")), IComplexType complexType - => complexType.GetComplexProperties().Select(p => p.ComplexType.GetJsonPropertyName() ?? throw new UnreachableException()), + => complexType.GetComplexProperties().Select(p => p.ComplexType.GetJsonPropertyName() + ?? throw new UnreachableException("JSON-mapped complex property without a JSON property name.")), - _ => throw new UnreachableException() + _ => throw new UnreachableException("Unexpected structural type when transforming JSON query to table.") }; foreach (var jsonPropertyName in nestedJsonPropertyNames) @@ -569,7 +562,7 @@ IComplexType complexType new SqlServerOpenJsonExpression.ColumnInfo { Name = jsonPropertyName, - TypeMapping = containerColumn.StoreTypeMapping, + TypeMapping = jsonQueryExpression.JsonColumn.TypeMapping!, Path = [new PathSegment(jsonPropertyName)], AsJson = true }); @@ -1012,7 +1005,7 @@ protected override bool TrySerializeScalarToJson( JsonScalarExpression { TypeMapping.ElementTypeMapping: not null } j => ((ColumnExpression)j.Json, j.Path, false), JsonQueryExpression j => (j.JsonColumn, j.Path, false), - _ => throw new UnreachableException(), + _ => throw new UnreachableException("Unexpected target expression for JSON partial update setter."), }; // SQL Server 2025 introduced the modify method (https://learn.microsoft.com/sql/t-sql/data-types/json-data-type#modify-method), diff --git a/src/EFCore.SqlServer/Update/Internal/SqlServerModificationCommand.cs b/src/EFCore.SqlServer/Update/Internal/SqlServerModificationCommand.cs index 4d84905d621..29bf2a280b8 100644 --- a/src/EFCore.SqlServer/Update/Internal/SqlServerModificationCommand.cs +++ b/src/EFCore.SqlServer/Update/Internal/SqlServerModificationCommand.cs @@ -43,7 +43,7 @@ public SqlServerModificationCommand(in NonTrackedModificationCommandParameters m /// protected override void ProcessSinglePropertyJsonUpdate(ref ColumnModificationParameters parameters) { - // TODO: Move more of this logic to the type mapping. Issue #34432 + // TODO: Move more of this logic to the type mapping. Issue #38036 var property = parameters.Property!; var mapping = property.GetRelationalTypeMapping(); var propertyProviderClrType = (mapping.Converter?.ProviderClrType ?? property.ClrType).UnwrapNullableType(); diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs index cf9810127df..c94ea549927 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs @@ -467,7 +467,7 @@ protected override ShapedQueryExpression TransformJsonQueryToTable(JsonQueryExpr // We're only interested in properties which actually exist in the JSON, filter out uninteresting synthetic keys foreach (var property in structuralType.GetPropertiesInHierarchy()) { - if (property.GetJsonPropertyName() is { } jsonPropertyName) + if (jsonQueryExpression.FindJsonElement(property) is { PropertyName: { } jsonPropertyName } element) { // HACK: currently the only way to project multiple values from a SelectExpression is to simulate a Select out to an anonymous // type; this requires the MethodInfos of the anonymous type properties, from which the projection alias gets taken. @@ -476,9 +476,9 @@ protected override ShapedQueryExpression TransformJsonQueryToTable(JsonQueryExpr propertyJsonScalarExpression[projectionMember] = new JsonScalarExpression( jsonColumn, - [new PathSegment(property.GetJsonPropertyName()!)], + [new PathSegment(jsonPropertyName)], property.ClrType.UnwrapNullableType(), - property.GetRelationalTypeMapping(), + element.StoreTypeMapping!, property.IsNullable); } } @@ -490,7 +490,7 @@ [new PathSegment(property.GetJsonPropertyName()!)], && n.TargetEntityType.IsMappedToJson() && n.ForeignKey.PrincipalToDependent == n)) { - var jsonNavigationName = navigation.TargetEntityType.GetJsonPropertyName(); + var jsonNavigationName = jsonQueryExpression.GetJsonElement(navigation).PropertyName; Check.DebugAssert(jsonNavigationName is not null, "Invalid navigation found on JSON-mapped entity"); var projectionMember = new ProjectionMember().Append(new FakeMemberInfo(jsonNavigationName)); @@ -506,7 +506,7 @@ [new PathSegment(jsonNavigationName)], foreach (var complexProperty in structuralType.GetComplexProperties()) { - var jsonNavigationName = complexProperty.ComplexType.GetJsonPropertyName(); + var jsonNavigationName = jsonQueryExpression.GetJsonElement(complexProperty).PropertyName; Check.DebugAssert(jsonNavigationName is not null, "Invalid complex property found on JSON-mapped structural type"); var projectionMember = new ProjectionMember().Append(new FakeMemberInfo(jsonNavigationName)); diff --git a/test/EFCore.Relational.Specification.Tests/Query/Associations/OwnedJson/OwnedJsonStructuralEqualityRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/Associations/OwnedJson/OwnedJsonStructuralEqualityRelationalTestBase.cs index dc9304d6e15..1cb73773fb0 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/Associations/OwnedJson/OwnedJsonStructuralEqualityRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/Associations/OwnedJson/OwnedJsonStructuralEqualityRelationalTestBase.cs @@ -33,7 +33,7 @@ public override async Task Contains_with_inline() public override async Task Contains_with_parameter() { // No backing field could be found for property 'RootEntity.RequiredRelated#RelatedType.NestedCollection#NestedType.RelatedTypeRootEntityId' and the property does not have a getter. - await Assert.ThrowsAsync(base.Contains_with_parameter); + await Assert.ThrowsAsync(base.Contains_with_parameter); AssertSql(); } @@ -41,7 +41,7 @@ public override async Task Contains_with_parameter() public override async Task Contains_with_operators_composed_on_the_collection() { // No backing field could be found for property 'RootEntity.RequiredRelated#RelatedType.NestedCollection#NestedType.RelatedTypeRootEntityId' and the property does not have a getter. - await Assert.ThrowsAsync(base.Contains_with_operators_composed_on_the_collection); + await Assert.ThrowsAsync(base.Contains_with_operators_composed_on_the_collection); AssertSql(); } @@ -49,7 +49,7 @@ public override async Task Contains_with_operators_composed_on_the_collection() public override async Task Contains_with_nested_and_composed_operators() { // No backing field could be found for property 'RootEntity.RequiredRelated#RelatedType.NestedCollection#NestedType.RelatedTypeRootEntityId' and the property does not have a getter. - await Assert.ThrowsAsync(base.Contains_with_nested_and_composed_operators); + await Assert.ThrowsAsync(base.Contains_with_nested_and_composed_operators); AssertSql(); } diff --git a/test/EFCore.Relational.Specification.Tests/Query/EntitySplittingQueryTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/EntitySplittingQueryTestBase.cs index 494bca257c9..baa3a64b5c2 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/EntitySplittingQueryTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/EntitySplittingQueryTestBase.cs @@ -2255,8 +2255,54 @@ await AssertQuery( entryCount: 5); } + [Theory, MemberData(nameof(IsAsyncData))] + public virtual async Task FromSql_on_split_entity_with_renamed_columns_uses_default_mappings(bool async) + { + await InitializeContextFactoryAsync(mb => + { + mb.Entity().SplitToTable( + "SplitEntityOnePart", + tb => + { + // Configure column names on the split table that differ from the default (logical) column names. + // This makes the table mappings diverge from the default mappings, which is what FromSql must use. + tb.Property(e => e.IntValue3).HasColumnName("CustomIntValue3"); + tb.Property(e => e.StringValue3).HasColumnName("CustomStringValue3"); + tb.Property(e => e.IntValue4); + tb.Property(e => e.StringValue4); + }); + }); + + using var context = CreateContext(); + + // The raw SQL exposes the split-table columns under their default (logical) names. If FromSql incorrectly used + // the table mappings (CustomIntValue3/CustomStringValue3) rather than the default mappings, the composed query + // would reference columns that don't exist in the raw SQL's result. + var sql = NormalizeDelimitersInRawString( + @"SELECT [m].*, [s].[CustomStringValue3] AS [StringValue3], [s].[StringValue4], [s].[CustomIntValue3] AS [IntValue3], [s].[IntValue4] + FROM [EntityOne] AS [m] + INNER JOIN [SplitEntityOnePart] AS [s] ON [m].[Id] = [s].[Id]"); + + var query = context.Set().FromSqlRaw(sql).OrderBy(e => e.Id); + + var actual = async + ? await query.ToListAsync() + : query.ToList(); + + var expected = GetExpectedData().Set().OrderBy(e => e.Id).ToList(); + + Assert.Equal(expected.Count, actual.Count); + for (var i = 0; i < expected.Count; i++) + { + AssertEqual(expected[i], actual[i]); + } + } + #region TestHelpers + protected string NormalizeDelimitersInRawString(string sql) + => ((RelationalTestStore)NonSharedTestStore).NormalizeDelimitersInRawString(sql); + protected async Task AssertQuery( bool async, Func> queryCreator, diff --git a/test/EFCore.Relational.Tests/Metadata/Conventions/Internal/TableValuedDbFunctionConventionTest.cs b/test/EFCore.Relational.Tests/Metadata/Conventions/Internal/TableValuedDbFunctionConventionTest.cs index 1435ef94da2..6fa2a8cb8d1 100644 --- a/test/EFCore.Relational.Tests/Metadata/Conventions/Internal/TableValuedDbFunctionConventionTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/Conventions/Internal/TableValuedDbFunctionConventionTest.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.ComponentModel.DataAnnotations.Schema; -using Microsoft.EntityFrameworkCore.Metadata.Internal; namespace Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal; @@ -23,7 +22,7 @@ public void Does_not_configure_return_entity_as_not_mapped() var entityType = model.FindEntityType(typeof(KeylessEntity)); Assert.Null(entityType.FindPrimaryKey()); - Assert.Equal("KeylessEntity", entityType.GetViewOrTableMappings().Single().Table.Name); + Assert.Equal("KeylessEntity", entityType.GetTableMappings().Single().Table.Name); } [Fact] @@ -41,7 +40,7 @@ public void Finds_existing_entity_type() var entityType = model.FindEntityType(typeof(TestEntity)); Assert.Equal(nameof(TestEntity.Name), entityType.FindPrimaryKey().Properties.Single().Name); - Assert.Equal("TestTable", entityType.GetViewOrTableMappings().Single().Table.Name); + Assert.Equal("TestTable", entityType.GetTableMappings().Single().Table.Name); } [Fact] diff --git a/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs b/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs index 3bfba4c5329..50fe2a2cd3c 100644 --- a/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs @@ -324,7 +324,7 @@ private static void AssertViews(IRelationalModel model, Mapping mapping) { var orderType = model.Model.FindEntityType(typeof(Order))!; var orderMapping = orderType.GetViewMappings().Single(); - Assert.Equal(orderType.GetViewMappings(), orderType.GetViewOrTableMappings()); + Assert.Equal(orderType.GetViewMappings(), orderType.GetQueryMappings()); Assert.Null(orderMapping.IncludesDerivedTypes); Assert.Equal( [nameof(Order.Id), nameof(Order.AlternateId), nameof(Order.CustomerId), nameof(Order.OrderDate)], @@ -3255,6 +3255,66 @@ public void Default_mappings_does_not_share_tableBase() Assert.False(defaultMapping2.Table.Columns.Single().IsNullable); } + [Fact] + public void GetQueryMappings_returns_in_priority_order_sql_query_function_view_table() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity(cb => + { + cb.Ignore(c => c.Customer); + cb.Ignore(c => c.Details); + cb.Ignore(c => c.DateDetails); + cb.Ignore(c => c.Addresses); + cb.HasNoKey(); + }); + + var sqlQueryOnly = (IEntityType)modelBuilder.Model.AddEntityType(typeof(NameSpace1.SameEntityType)); + modelBuilder.Entity(sqlQueryOnly.ClrType).HasNoKey().ToSqlQuery("SELECT 1 AS Id"); + + // Table + view: view wins (table mappings are not returned). + var viewAndTable = modelBuilder.Entity(b => b.HasNoKey().ToView("V").ToTable("T")); + + // Function + view + table: function wins. + modelBuilder.Ignore(); + modelBuilder.Entity(b => + { + b.Ignore(c => c.Orders); + b.HasNoKey(); + b.ToFunction("GetCustomers"); + b.ToView("CustomersView"); + b.ToTable("Customers"); + }); + + // Table-only. + modelBuilder.Entity(b => b.ToTable("Orders")); + + var model = Finalize(modelBuilder); + + // Table-only -> table mappings. + var orderType = model.Model.FindEntityType(typeof(Order)); + var orderMappings = orderType.GetQueryMappings().ToList(); + Assert.Single(orderMappings); + Assert.IsAssignableFrom(orderMappings[0]); + + // SqlQuery-only -> SQL query mappings. + var sqlQueryEntity = model.Model.FindEntityType(typeof(NameSpace1.SameEntityType)); + var sqlQueryMappings = sqlQueryEntity.GetQueryMappings().ToList(); + Assert.Single(sqlQueryMappings); + Assert.IsAssignableFrom(sqlQueryMappings[0]); + + // Table + view -> view mappings win (lower-priority table mappings are not returned). + var viewAndTableEntity = model.Model.FindEntityType(typeof(NameSpace2.SameEntityType)); + var viewAndTableMappings = viewAndTableEntity.GetQueryMappings().ToList(); + Assert.Single(viewAndTableMappings); + Assert.IsAssignableFrom(viewAndTableMappings[0]); + + // Function + view + table -> function mappings win (lower-priority view/table mappings are not returned). + var customerType = model.Model.FindEntityType(typeof(Customer)); + var customerMappings = customerType.GetQueryMappings().ToList(); + Assert.Single(customerMappings); + Assert.IsAssignableFrom(customerMappings[0]); + } + [Fact] public void Container_column_type_is_used_for_complex_property_json_column() {