Skip to content

Commit 0577a3a

Browse files
committed
Json Columns query work
- property/navigation access translation, - materialization (entity, collection, with nesting), - null semantics, + tweaks and fixes to the metadata/update part
1 parent a6b6a2a commit 0577a3a

File tree

49 files changed

+5084
-242
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+5084
-242
lines changed

src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -1758,10 +1758,10 @@ public static void SetJsonPropertyName(this IMutableEntityType entityType, strin
17581758
Check.NullButNotEmpty(name, nameof(name)));
17591759

17601760
/// <summary>
1761-
/// Gets the <see cref="ConfigurationSource" /> for the JSON property name for a given entity Type.
1761+
/// Gets the <see cref="ConfigurationSource" /> for the JSON property name for a given entity type.
17621762
/// </summary>
17631763
/// <param name="entityType">The entity type.</param>
1764-
/// <returns>The <see cref="ConfigurationSource" /> for the JSON property name for a given navigation.</returns>
1764+
/// <returns>The <see cref="ConfigurationSource" /> for the JSON property name for a given entity type.</returns>
17651765
public static ConfigurationSource? GetJsonPropertyNameConfigurationSource(this IConventionEntityType entityType)
17661766
=> entityType.FindAnnotation(RelationalAnnotationNames.JsonPropertyName)?.GetConfigurationSource();
17671767

src/EFCore.Relational/Metadata/Internal/RelationalKeyExtensions.cs

+5
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,11 @@ public static bool AreCompatible(
124124
return null;
125125
}
126126

127+
if (key.DeclaringEntityType.IsMappedToJson())
128+
{
129+
return null;
130+
}
131+
127132
string? name;
128133
if (key.IsPrimaryKey())
129134
{

src/EFCore.Relational/Metadata/Internal/RelationalModel.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -468,7 +468,7 @@ private static void CreateTableMapping(
468468
var jsonColumnTypeMapping = relationalTypeMappingSource.FindMapping(typeof(JsonElement))!;
469469
var jsonColumn = new JsonColumn(containerColumnName, jsonColumnTypeMapping.StoreType, table, jsonColumnTypeMapping.ProviderValueComparer);
470470
table.Columns.Add(containerColumnName, jsonColumn);
471-
jsonColumn.IsNullable = !ownership.IsRequired || !ownership.IsUnique;
471+
jsonColumn.IsNullable = !ownership.IsRequiredDependent || !ownership.IsUnique;
472472

473473
if (ownership.PrincipalEntityType.BaseType != null)
474474
{

src/EFCore.Relational/Properties/RelationalStrings.Designer.cs

+30
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/EFCore.Relational/Properties/RelationalStrings.resx

+12
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,9 @@
481481
<data name="InvalidPropertyInSetProperty" xml:space="preserve">
482482
<value>The following lambda argument to 'SetProperty' does not represent a valid property to be set: '{propertyExpression}'.</value>
483483
</data>
484+
<data name="JsonCantNavigateToParentEntity" xml:space="preserve">
485+
<value>Can't navigate from JSON-mapped entity '{jsonEntity}' to its parent entity '{parentEntity}' using navigation '{navigation}'. Entities mapped to JSON can only navigate to their children.</value>
486+
</data>
484487
<data name="JsonEntityMappedToDifferentViewThanOwner" xml:space="preserve">
485488
<value>Entity '{jsonType}' is mapped to JSON and also to a view '{viewName}', but its owner '{ownerType}' is mapped to a different view '{ownerViewName}'. Every entity mapped to JSON must also map to the same view as its owner.</value>
486489
</data>
@@ -517,9 +520,18 @@
517520
<data name="JsonEntityWithTableSplittingIsNotSupported" xml:space="preserve">
518521
<value>Table splitting is not supported for entities containing entities mapped to JSON.</value>
519522
</data>
523+
<data name="JsonErrorExtractingJsonProperty" xml:space="preserve">
524+
<value>An error occurred while reading a JSON value for property '{entityType}.{propertyName}'. See the inner exception for more information.</value>
525+
</data>
526+
<data name="JsonNodeMustBeHandledByProviderSpecificVisitor" xml:space="preserve">
527+
<value>This node should be handled by provider-specific sql generator.</value>
528+
</data>
520529
<data name="JsonPropertyNameShouldBeConfiguredOnNestedNavigation" xml:space="preserve">
521530
<value>The JSON property name should only be configured on nested owned navigations.</value>
522531
</data>
532+
<data name="JsonRequiredEntityWithNullJson" xml:space="preserve">
533+
<value>Entity {entity} is required but the JSON element containing it is null.</value>
534+
</data>
523535
<data name="KeylessMappingStrategy" xml:space="preserve">
524536
<value>The mapping strategy '{mappingStrategy}' used for '{entityType}' is not supported for keyless entity types. See https://go.microsoft.com/fwlink/?linkid=2130430 for more information.</value>
525537
</data>

src/EFCore.Relational/Query/EntityProjectionExpression.cs

+94-4
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ namespace Microsoft.EntityFrameworkCore.Query;
1717
public class EntityProjectionExpression : Expression
1818
{
1919
private readonly IReadOnlyDictionary<IProperty, ColumnExpression> _propertyExpressionMap;
20-
private readonly Dictionary<INavigation, EntityShaperExpression> _ownedNavigationMap = new();
20+
private readonly Dictionary<INavigation, EntityShaperExpression> _ownedNavigationMap;
2121

2222
/// <summary>
2323
/// Creates a new instance of the <see cref="EntityProjectionExpression" /> class.
@@ -29,9 +29,23 @@ public EntityProjectionExpression(
2929
IEntityType entityType,
3030
IReadOnlyDictionary<IProperty, ColumnExpression> propertyExpressionMap,
3131
SqlExpression? discriminatorExpression = null)
32+
: this(
33+
entityType,
34+
propertyExpressionMap,
35+
new Dictionary<INavigation, EntityShaperExpression>(),
36+
discriminatorExpression)
37+
{
38+
}
39+
40+
private EntityProjectionExpression(
41+
IEntityType entityType,
42+
IReadOnlyDictionary<IProperty, ColumnExpression> propertyExpressionMap,
43+
Dictionary<INavigation, EntityShaperExpression> ownedNavigationMap,
44+
SqlExpression? discriminatorExpression = null)
3245
{
3346
EntityType = entityType;
3447
_propertyExpressionMap = propertyExpressionMap;
48+
_ownedNavigationMap = ownedNavigationMap;
3549
DiscriminatorExpression = discriminatorExpression;
3650
}
3751

@@ -69,8 +83,16 @@ protected override Expression VisitChildren(ExpressionVisitor visitor)
6983
var discriminatorExpression = (SqlExpression?)visitor.Visit(DiscriminatorExpression);
7084
changed |= discriminatorExpression != DiscriminatorExpression;
7185

86+
var ownedNavigationMap = new Dictionary<INavigation, EntityShaperExpression>();
87+
foreach (var (navigation, entityShaperExpression) in _ownedNavigationMap)
88+
{
89+
var newExpression = (EntityShaperExpression)visitor.Visit(entityShaperExpression);
90+
changed |= newExpression != entityShaperExpression;
91+
ownedNavigationMap[navigation] = newExpression;
92+
}
93+
7294
return changed
73-
? new EntityProjectionExpression(EntityType, propertyExpressionMap, discriminatorExpression)
95+
? new EntityProjectionExpression(EntityType, propertyExpressionMap, ownedNavigationMap, discriminatorExpression)
7496
: this;
7597
}
7698

@@ -92,7 +114,65 @@ public virtual EntityProjectionExpression MakeNullable()
92114
// if discriminator is column then we need to make it nullable
93115
discriminatorExpression = ce.MakeNullable();
94116
}
95-
return new EntityProjectionExpression(EntityType, propertyExpressionMap, discriminatorExpression);
117+
118+
var primaryKeyProperties = GetMappedKeyProperties(EntityType.FindPrimaryKey()!);
119+
var ownedNavigationMap = new Dictionary<INavigation, EntityShaperExpression>();
120+
foreach (var (navigation, shaper) in _ownedNavigationMap)
121+
{
122+
if (shaper.EntityType.IsMappedToJson())
123+
{
124+
// even if shaper is nullable, we need to make sure key property map contains nullable keys,
125+
// if json entity itself is optional, the shaper would be null, but the PK of the owner entity would be non-nullable intially
126+
Debug.Assert(primaryKeyProperties != null, "Json entity type can't be keyless");
127+
128+
var jsonQueryExpression = (JsonQueryExpression)shaper.ValueBufferExpression;
129+
var ownedPrimaryKeyProperties = GetMappedKeyProperties(shaper.EntityType.FindPrimaryKey()!)!;
130+
var nullableKeyPropertyMap = new Dictionary<IProperty, ColumnExpression>();
131+
for (var i = 0; i < primaryKeyProperties.Count; i++)
132+
{
133+
nullableKeyPropertyMap[ownedPrimaryKeyProperties[i]] = propertyExpressionMap[primaryKeyProperties[i]];
134+
}
135+
136+
// reuse key columns from owner (that we just made nullable), so that the references are the same
137+
var newJsonQueryExpression = jsonQueryExpression.MakeNullable(nullableKeyPropertyMap);
138+
var newShaper = shaper.Update(newJsonQueryExpression).MakeNullable();
139+
ownedNavigationMap[navigation] = newShaper;
140+
}
141+
}
142+
143+
return new EntityProjectionExpression(
144+
EntityType,
145+
propertyExpressionMap,
146+
ownedNavigationMap,
147+
discriminatorExpression);
148+
149+
static IReadOnlyList<IProperty>? GetMappedKeyProperties(IKey? key)
150+
{
151+
if (key == null)
152+
{
153+
return null;
154+
}
155+
156+
if (!key.DeclaringEntityType.IsMappedToJson())
157+
{
158+
return key.Properties;
159+
}
160+
161+
// TODO: fix this once we enable json entity being owned by another owned non-json entity (issue #28441)
162+
163+
// for json collections we need to filter out the ordinal key as it's not mapped to any column
164+
// there could be multiple of these in deeply nested structures,
165+
// so we traverse to the outermost owner to see how many mapped keys there are
166+
var currentEntity = key.DeclaringEntityType;
167+
while (currentEntity.IsMappedToJson())
168+
{
169+
currentEntity = currentEntity.FindOwnership()!.PrincipalEntityType;
170+
}
171+
172+
var count = currentEntity.FindPrimaryKey()!.Properties.Count;
173+
174+
return key.Properties.Take(count).ToList();
175+
}
96176
}
97177

98178
/// <summary>
@@ -119,6 +199,16 @@ public virtual EntityProjectionExpression UpdateEntityType(IEntityType derivedTy
119199
}
120200
}
121201

202+
var ownedNavigationMap = new Dictionary<INavigation, EntityShaperExpression>();
203+
foreach (var (navigation, entityShaperExpression) in _ownedNavigationMap)
204+
{
205+
if (derivedType.IsAssignableFrom(navigation.DeclaringEntityType)
206+
|| navigation.DeclaringEntityType.IsAssignableFrom(derivedType))
207+
{
208+
ownedNavigationMap[navigation] = entityShaperExpression;
209+
}
210+
}
211+
122212
var discriminatorExpression = DiscriminatorExpression;
123213
if (DiscriminatorExpression is CaseExpression caseExpression)
124214
{
@@ -130,7 +220,7 @@ public virtual EntityProjectionExpression UpdateEntityType(IEntityType derivedTy
130220
discriminatorExpression = caseExpression.Update(operand: null, whenClauses, elseResult: null);
131221
}
132222

133-
return new EntityProjectionExpression(derivedType, propertyExpressionMap, discriminatorExpression);
223+
return new EntityProjectionExpression(derivedType, propertyExpressionMap, ownedNavigationMap, discriminatorExpression);
134224
}
135225

136226
/// <summary>

0 commit comments

Comments
 (0)