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
9 changes: 9 additions & 0 deletions src/EFCore.Relational/EFCore.Relational.baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -7328,6 +7328,9 @@
{
"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 +16522,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 +16576,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,108 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Internal;
public static class RelationalTypeBaseExtensions
{
/// <summary>
/// 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>
/// <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();
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 query mappings scoped to the tables actually being projected (when known via
/// <paramref name="tableMap" />); falls back to all query mappings when no projected mapping qualifies. Used by
/// query translation sites that need to pick the principal split-entity table for an entity reference.
/// </summary>
/// <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>
/// <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.
/// </summary>
public static IEnumerable<ITableMappingBase> GetViewOrTableMappings(this ITypeBase typeBase)
/// </remarks>
public static List<ITableMappingBase> GetProjectedQueryMappings(
this IEntityType entityType,
IReadOnlyDictionary<ITableBase, string>? tableMap)
{
typeBase.Model.EnsureRelationalModel();
var viewMapping = typeBase.FindRuntimeAnnotationValue(RelationalAnnotationNames.ViewMappings);
var tableMapping = typeBase.FindRuntimeAnnotationValue(RelationalAnnotationNames.TableMappings);
return (IEnumerable<ITableMappingBase>?)(viewMapping ?? tableMapping) ?? [];
var allMappings = entityType.GetQueryMappings();
if (tableMap is null)
{
return [.. allMappings];
}

var projected = new List<ITableMappingBase>();
foreach (var mapping in allMappings)
{
if (tableMap.ContainsKey(mapping.Table))
{
projected.Add(mapping);
}
}

return projected.Count > 0 ? projected : [.. allMappings];
Comment on lines +104 to +119
}
}
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; }
}
44 changes: 38 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,36 @@ 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(propertyBase.DeclaringType.DisplayName()));

foreach (var mapping in propertyBase.GetJsonElementMappings())
{
if (ReferenceEquals(mapping.Element.ContainingColumn, column))
{
return mapping.Element;
}
}

throw new InvalidOperationException(
RelationalStrings.JsonElementMappingNotFound(propertyBase.DeclaringType.DisplayName(), propertyBase.Name, column.Name));
}
Comment on lines +309 to +323

/// <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