Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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 thread
AndriySvyryd marked this conversation as resolved.
Outdated
}
}
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; }
}
40 changes: 36 additions & 4 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 Down Expand Up @@ -206,7 +208,7 @@ public virtual JsonQueryExpression BindStructuralProperty(IPropertyBase structur

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 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()));

Comment thread
AndriySvyryd marked this conversation as resolved.
Outdated
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 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