Skip to content
Open
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
12 changes: 12 additions & 0 deletions src/EFCore.Relational/EFCore.Relational.baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -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();"
},
Expand Down Expand Up @@ -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);"
},
Expand Down Expand Up @@ -16570,6 +16579,9 @@
{
"Member": "static string JsonProjectingQueryableOperationNoTrackingWithIdentityResolution(object? asNoTrackingWithIdentityResolution);"
},
{
"Member": "static string JsonQueryExpressionWithoutUnderlyingColumn(object? structuralType);"
},
{
"Member": "static string JsonRequiredEntityWithNullJson(object? entity);"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,7 @@ public virtual RelationalTypeMapping? StoreTypeMapping
public virtual IReadOnlyList<IJsonElementMapping> PropertyMappings
=> _propertyMappings;

/// <summary>
/// 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.
/// </summary>
protected virtual RelationalTypeMapping? GetDefaultStoreTypeMapping()
private RelationalTypeMapping? GetDefaultStoreTypeMapping()
{
if (PropertyMappings.Select(m => m.Property).OfType<IProperty>().FirstOrDefault()?.GetTypeMapping() is RelationalTypeMapping mapping)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,120 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Internal;
public static class RelationalTypeBaseExtensions
{
/// <summary>
/// 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.
/// </summary>
public static IEnumerable<ITableMappingBase> GetViewOrTableMappings(this ITypeBase typeBase)
/// <remarks>
/// <para>
/// Only the "default" function/SQL query mappings (those configured via <c>ToFunction</c>/<c>ToSqlQuery</c>
/// on the entity) participate in the priority; additional <c>HasDbFunction</c>-style mappings remain
/// invocation-only and never shadow the entity's view/table mapping for <c>Set&lt;T&gt;()</c> queries.
/// </para>
/// <para>
/// 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.
/// </para>
/// </remarks>
public static IEnumerable<ITableMappingBase> GetQueryMappings(this ITypeBase typeBase)
{
typeBase.Model.EnsureRelationalModel();
var viewMapping = typeBase.FindRuntimeAnnotationValue(RelationalAnnotationNames.ViewMappings);
var tableMapping = typeBase.FindRuntimeAnnotationValue(RelationalAnnotationNames.TableMappings);
return (IEnumerable<ITableMappingBase>?)(viewMapping ?? tableMapping) ?? [];
if (typeBase.FindRuntimeAnnotationValue(RelationalAnnotationNames.SqlQueryMappings) is List<SqlQueryMapping> sqlQueryMappings
&& GetDefaults(sqlQueryMappings, static m => m.IsDefaultSqlQueryMapping) is { } defaultSqlQueryMappings)
{
return defaultSqlQueryMappings;
}

if (typeBase.FindRuntimeAnnotationValue(RelationalAnnotationNames.FunctionMappings) is List<FunctionMapping> functionMappings
&& GetDefaults(functionMappings, static m => m.IsDefaultFunctionMapping) is { } defaultFunctionMappings)
{
return defaultFunctionMappings;
}

var viewMappings = typeBase.GetViewMappings();
return viewMappings.Any() ? viewMappings : typeBase.GetTableMappings();

static List<T>? GetDefaults<T>(List<T> mappings, Func<T, bool> 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<T>(count);
for (var i = 0; i < mappings.Count; i++)
{
var mapping = mappings[i];
if (isDefault(mapping))
{
defaults.Add(mapping);
}
}

return defaults;
}
}

/// <summary>
/// Returns the entity type's mappings to the tables actually being projected (as given by <paramref name="tableMap" />).
/// Used by query translation sites that need to pick the principal split-entity table for an entity reference.
/// </summary>
/// <remarks>
/// <para>
/// 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 <paramref name="tableMap" /> contains the
/// default table) and its real table/view mapping for ordinary queries, without re-applying the
/// storage-priority logic used by <see cref="GetQueryMappings" />. When <paramref name="tableMap" /> is
/// <see langword="null" /> (the projection is unknown) it falls back to all of the entity's query mappings.
/// </para>
/// <para>
/// 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.
/// </para>
/// </remarks>
/// <param name="entityType">The entity type.</param>
/// <param name="tableMap">The tables being projected from in the containing query, or <see langword="null" /> when unknown.</param>
public static List<ITableMappingBase> GetProjectedQueryMappings(
this IEntityType entityType,
IReadOnlyDictionary<ITableBase, string>? 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<ITableMappingBase>();
foreach (var table in tableMap.Keys)
{
foreach (var mapping in table.EntityTypeMappings)
{
if (mapping.TypeBase == entityType)
{
projected.Add(mapping);
}
}
}

return projected;
}
}
16 changes: 16 additions & 0 deletions src/EFCore.Relational/Properties/RelationalStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions src/EFCore.Relational/Properties/RelationalStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,9 @@
<data name="JsonCantNavigateToParentEntity" xml:space="preserve">
<value>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.</value>
</data>
<data name="JsonElementMappingNotFound" xml:space="preserve">
<value>No JSON element mapping was found for '{structuralType}.{name}' on column '{columnName}'.</value>
</data>
<data name="JsonEmptyString" xml:space="preserve">
<value>The database returned the empty string when a JSON object was expected.</value>
</data>
Expand Down Expand Up @@ -643,6 +646,9 @@
<data name="JsonPropertyNameShouldBeConfiguredOnNestedNavigation" xml:space="preserve">
<value>The JSON property name should only be configured on nested owned navigations.</value>
</data>
<data name="JsonQueryExpressionWithoutUnderlyingColumn" xml:space="preserve">
<value>The JSON query expression for '{structuralType}' has no underlying column.</value>
</data>
<data name="JsonQueryLinqOperatorsNotSupported" xml:space="preserve">
<value>Composing LINQ operators over collections inside JSON documents isn't supported or hasn't been implemented by your EF provider.</value>
</data>
Expand Down
16 changes: 15 additions & 1 deletion src/EFCore.Relational/Query/Internal/JsonProjectionInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ public readonly struct JsonProjectionInfo
/// </summary>
public JsonProjectionInfo(
int jsonColumnIndex,
List<(IProperty?, int?, int?)> keyAccessInfo)
List<(IProperty?, int?, int?)> keyAccessInfo,
IColumnBase? jsonColumn = null)
{
JsonColumnIndex = jsonColumnIndex;
KeyAccessInfo = keyAccessInfo;
JsonColumn = jsonColumn;
}

/// <summary>
Expand Down Expand Up @@ -52,4 +54,16 @@ public JsonProjectionInfo(
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </remarks>
public List<(IProperty? KeyProperty, int? ConstantKeyValue, int? KeyProjectionIndex)> KeyAccessInfo { get; }

/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public IColumnBase? JsonColumn { get; }
}
65 changes: 59 additions & 6 deletions src/EFCore.Relational/Query/JsonQueryExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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<IProperty, ColumnExpression>();
var targetPrimaryKeyProperties = targetEntityType.FindPrimaryKey()!.Properties.Take(KeyPropertyMap.Count);
Expand All @@ -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,
Expand All @@ -219,7 +221,7 @@ public virtual JsonQueryExpression BindStructuralProperty(IPropertyBase structur
}

default:
throw new UnreachableException();
throw new UnreachableException("Unexpected structural property type.");
}
}

Expand Down Expand Up @@ -290,6 +292,57 @@ protected override Expression VisitChildren(ExpressionVisitor visitor)
return Update(jsonColumn, newKeyPropertyMap);
}

/// <summary>
/// Finds the <see cref="IRelationalJsonElement" /> for the given property/navigation/complex property within the
/// JSON column referenced by this expression by matching <see cref="JsonColumn" />'s underlying
/// <see cref="ColumnExpression.Column" /> against <see cref="IRelationalJsonElement.ContainingColumn" />. This
/// disambiguates entity-splitting, TPT and TPC scenarios where the same property has multiple JSON element
/// mappings — one per concrete table.
/// <see cref="IRelationalJsonElement.PropertyName" /> may be <see langword="null" /> for shadow keys that have
/// no JSON representation; callers iterating over <see cref="ITypeBase.GetProperties" /> must handle that case
/// and skip them.
/// </summary>
/// <param name="propertyBase">The property, navigation or complex property to look up.</param>
/// <returns>The JSON element mapping for <paramref name="propertyBase" />.</returns>
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));
}

/// <summary>
/// Finds the <see cref="IRelationalJsonElement" /> for the given property/navigation/complex property within the
/// JSON column referenced by this expression by matching <see cref="JsonColumn" />'s underlying
/// <see cref="ColumnExpression.Column" /> against <see cref="IRelationalJsonElement.ContainingColumn" />, or returns
/// <see langword="null" /> if no such element exists (including when <see cref="JsonColumn" /> has no underlying
/// <see cref="ColumnExpression.Column" />, e.g. for synthetic JSON expansions over OPENJSON / json_each, or for
/// iterated properties such as shadow keys that have no JSON representation).
/// </summary>
/// <param name="propertyBase">The property, navigation or complex property to look up.</param>
/// <returns>The JSON element mapping for <paramref name="propertyBase" />, or <see langword="null" /> if not found.</returns>
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;
}
Comment thread
AndriySvyryd marked this conversation as resolved.
Comment thread
AndriySvyryd marked this conversation as resolved.

/// <summary>
/// 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.
Expand Down
Loading
Loading