Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
d352388
WIP: Add some tests
JoasE Jan 28, 2026
6490d7b
Copy over files: Use StructuralType where needed in query translation…
JoasE Jan 28, 2026
d9d91d2
Add some more tests
JoasE Jan 28, 2026
003383d
Rewrite some baselines
JoasE Jan 28, 2026
7f7b564
Fix async tests
JoasE Jan 28, 2026
1b4b5cd
WIP: distinct, contains and null
JoasE Jan 29, 2026
bbc3477
Fix test do not explicitly set int ids in base test but use value gen…
JoasE Jan 30, 2026
de6328f
Improve object retrieval for null comparison
JoasE Jan 30, 2026
607a921
Distinct detection
JoasE Jan 30, 2026
0457143
WIP some docs for question
JoasE Jan 31, 2026
5ac80e0
WIP: Match code structure of relational where possible
JoasE Feb 3, 2026
a682368
Merge branch 'main' of https://github.com/dotnet/efcore into feature/…
JoasE Feb 3, 2026
753f31e
Cleanup
JoasE Feb 3, 2026
88c84b6
Rename TypeBase to StructuralType
JoasE Feb 4, 2026
19c1de1
Cleanup and add todo
JoasE Feb 4, 2026
3396b9a
Merge branch 'main' of https://github.com/dotnet/efcore into feature/…
JoasE Feb 9, 2026
a5a8bd4
Implement test
JoasE Feb 9, 2026
3b21620
Remove old unused class
JoasE Feb 9, 2026
7127586
Remove skip on fixed tests
JoasE Feb 9, 2026
82e9b15
Rename test class
JoasE Feb 9, 2026
dd4b3f0
Fix null check structural comparison
JoasE Feb 9, 2026
8126941
Rename field
JoasE Feb 9, 2026
2a96a9b
WIP: Use direct comparison
JoasE Feb 9, 2026
dbd1d17
Use CollectionResultExpression for complex collections
JoasE Feb 10, 2026
ee66c0a
Regenerate test
JoasE Feb 10, 2026
c258b9c
Add test and fix
JoasE Feb 10, 2026
d248623
Add docs
JoasE Feb 10, 2026
b3d68eb
Use property clr type
JoasE Feb 10, 2026
3a19515
Remove bom
JoasE Feb 10, 2026
ddcba7e
Make internal
JoasE Feb 10, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ public static bool TryExtractArray(
projectedStructuralTypeShaper = shaper;
projection = shaper.ValueBufferExpression;
if (projection is ProjectionBindingExpression { ProjectionMember: { } projectionMember }
&& select.GetMappedProjection(projectionMember) is EntityProjectionExpression entityProjection)
&& select.GetMappedProjection(projectionMember) is StructuralTypeProjectionExpression entityProjection)
{
projection = entityProjection.Object;
}
Expand Down
2 changes: 1 addition & 1 deletion src/EFCore.Cosmos/Query/Internal/CosmosAliasManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ protected override Expression VisitExtension(Expression node)
ScalarReferenceExpression reference when aliasRewritingMap.TryGetValue(reference.Name, out var newAlias)
=> new ScalarReferenceExpression(newAlias, reference.Type, reference.TypeMapping),
ObjectReferenceExpression reference when aliasRewritingMap.TryGetValue(reference.Name, out var newAlias)
=> new ObjectReferenceExpression(reference.EntityType, newAlias),
=> new ObjectReferenceExpression(reference.StructuralType, newAlias),

_ => base.VisitExtension(node)
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ public virtual Expression Translate(SelectExpression selectExpression, Expressio
var translation = _sqlTranslator.Translate(expression);
if (translation == null)
{
_selectExpression.IndicateClientProjection();
return base.Visit(expression);
}

Expand Down Expand Up @@ -214,7 +215,7 @@ protected override Expression VisitExtension(Expression extensionExpression)

if (_clientEval)
{
var entityProjection = (EntityProjectionExpression)projection;
var entityProjection = (StructuralTypeProjectionExpression)projection;

return entityShaperExpression.Update(
new ProjectionBindingExpression(
Expand Down Expand Up @@ -306,11 +307,11 @@ protected override Expression VisitMember(MemberExpression memberExpression)
var innerEntityProjection = shaperExpression.ValueBufferExpression switch
{
ProjectionBindingExpression innerProjectionBindingExpression
=> (EntityProjectionExpression)_selectExpression.Projection[innerProjectionBindingExpression.Index!.Value].Expression,
=> (StructuralTypeProjectionExpression)_selectExpression.Projection[innerProjectionBindingExpression.Index!.Value].Expression,

// Unwrap EntityProjectionExpression when the root entity is not projected
UnaryExpression unaryExpression
=> (EntityProjectionExpression)((UnaryExpression)unaryExpression.Operand).Operand,
=> (StructuralTypeProjectionExpression)((UnaryExpression)unaryExpression.Operand).Operand,

_ => throw new InvalidOperationException(CoreStrings.TranslationFailed(memberExpression.Print()))
};
Expand All @@ -326,7 +327,7 @@ UnaryExpression unaryExpression

switch (navigationProjection)
{
case EntityProjectionExpression entityProjection:
case StructuralTypeProjectionExpression entityProjection:
return new StructuralTypeShaperExpression(
navigation.TargetEntityType,
Expression.Convert(Expression.Convert(entityProjection, typeof(object)), typeof(ValueBuffer)),
Expand Down Expand Up @@ -527,14 +528,14 @@ when _collectionShaperMapping.TryGetValue(parameterExpression, out var collectio

var innerEntityProjection = shaperExpression.ValueBufferExpression switch
{
EntityProjectionExpression entityProjection
StructuralTypeProjectionExpression entityProjection
=> entityProjection,

ProjectionBindingExpression innerProjectionBindingExpression
=> (EntityProjectionExpression)_selectExpression.Projection[innerProjectionBindingExpression.Index!.Value].Expression,
=> (StructuralTypeProjectionExpression)_selectExpression.Projection[innerProjectionBindingExpression.Index!.Value].Expression,

UnaryExpression unaryExpression
=> (EntityProjectionExpression)((UnaryExpression)unaryExpression.Operand).Operand,
=> (StructuralTypeProjectionExpression)((UnaryExpression)unaryExpression.Operand).Operand,

_ => throw new InvalidOperationException(CoreStrings.TranslationFailed(methodCallExpression.Print()))
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public virtual CosmosSqlQuery GetSqlQuery(
/// 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 override Expression VisitEntityProjection(EntityProjectionExpression entityProjectionExpression)
protected override Expression VisitEntityProjection(StructuralTypeProjectionExpression entityProjectionExpression)
{
Visit(entityProjectionExpression.Object);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ protected override Expression VisitExtension(Expression extensionExpression)
var alias = _aliasManager.GenerateSourceAlias(fromSql);
var selectExpression = new SelectExpression(
new SourceExpression(fromSql, alias),
new EntityProjectionExpression(new ObjectReferenceExpression(entityType, alias), entityType));
new StructuralTypeProjectionExpression(new ObjectReferenceExpression(entityType, alias), entityType));
return CreateShapedQueryExpression(entityType, selectExpression) ?? QueryCompilationContext.NotTranslatedExpression;

default:
Expand Down Expand Up @@ -300,7 +300,7 @@ protected override QueryableMethodTranslatingExpressionVisitor CreateSubqueryVis
var alias = _aliasManager.GenerateSourceAlias("c");
var selectExpression = new SelectExpression(
new SourceExpression(new ObjectReferenceExpression(entityType, "root"), alias),
new EntityProjectionExpression(new ObjectReferenceExpression(entityType, alias), entityType));
new StructuralTypeProjectionExpression(new ObjectReferenceExpression(entityType, alias), entityType));

// Add discriminator predicate
var concreteEntityTypes = entityType.GetConcreteDerivedTypesInclusive().ToList();
Expand All @@ -323,7 +323,7 @@ protected override QueryableMethodTranslatingExpressionVisitor CreateSubqueryVis
"Missing discriminator property in hierarchy");
if (discriminatorProperty is not null)
{
var discriminatorColumn = ((EntityProjectionExpression)selectExpression.GetMappedProjection(new ProjectionMember()))
var discriminatorColumn = ((StructuralTypeProjectionExpression)selectExpression.GetMappedProjection(new ProjectionMember()))
.BindProperty(discriminatorProperty, clientEval: false);

var success = TryApplyPredicate(
Expand All @@ -340,9 +340,9 @@ protected override QueryableMethodTranslatingExpressionVisitor CreateSubqueryVis
return CreateShapedQueryExpression(entityType, selectExpression);
}

private ShapedQueryExpression? CreateShapedQueryExpression(IEntityType entityType, SelectExpression queryExpression)
private ShapedQueryExpression? CreateShapedQueryExpression(ITypeBase structuralType, SelectExpression queryExpression)
{
if (!entityType.IsOwned())
if (structuralType is IEntityType entityType && !entityType.IsOwned())
{
var existingEntityType = _queryCompilationContext.RootEntityType;
if (existingEntityType is not null && existingEntityType != entityType)
Expand All @@ -358,7 +358,7 @@ protected override QueryableMethodTranslatingExpressionVisitor CreateSubqueryVis
return new ShapedQueryExpression(
queryExpression,
new StructuralTypeShaperExpression(
entityType,
structuralType,
new ProjectionBindingExpression(queryExpression, new ProjectionMember(), typeof(ValueBuffer)),
nullable: false));
}
Expand Down Expand Up @@ -532,6 +532,14 @@ protected override ShapedQueryExpression TranslateCast(ShapedQueryExpression sou
return null;
}

// We can not apply distinct because SQL DISTINCT operates on the full
// structural type, but the shaper extracts only a subset of that data.
// Cosmos: Projecting out nested documents retrieves the entire document #34067
if (select.UsesClientProjection)
{
return null;
}

select.ApplyDistinct();

return source;
Expand Down Expand Up @@ -607,7 +615,7 @@ protected override ShapedQueryExpression TranslateCast(ShapedQueryExpression sou

var translatedSelect =
new SelectExpression(
new EntityProjectionExpression(translation, (IEntityType)projectedStructuralTypeShaper.StructuralType));
new StructuralTypeProjectionExpression(translation, projectedStructuralTypeShaper.StructuralType));
return source.Update(
translatedSelect,
new StructuralTypeShaperExpression(
Expand Down Expand Up @@ -896,7 +904,7 @@ protected override ShapedQueryExpression TranslateCast(ShapedQueryExpression sou
var projectionMember = projectionBindingExpression.ProjectionMember;
Check.DebugAssert(new ProjectionMember().Equals(projectionMember), "Invalid ProjectionMember when processing OfType");

var entityProjectionExpression = (EntityProjectionExpression)select.GetMappedProjection(projectionMember);
var entityProjectionExpression = (StructuralTypeProjectionExpression)select.GetMappedProjection(projectionMember);
select.ReplaceProjectionMapping(
new Dictionary<ProjectionMember, Expression>
{
Expand Down Expand Up @@ -1131,9 +1139,9 @@ protected override ShapedQueryExpression TranslateSelect(ShapedQueryExpression s
var translatedSelect = SelectExpression.CreateForCollection(
slice,
alias,
new EntityProjectionExpression(
new ObjectReferenceExpression((IEntityType)projectedStructuralTypeShaper.StructuralType, alias),
(IEntityType)projectedStructuralTypeShaper.StructuralType));
new StructuralTypeProjectionExpression(
new ObjectReferenceExpression(projectedStructuralTypeShaper.StructuralType, alias),
projectedStructuralTypeShaper.StructuralType));
return source.Update(
translatedSelect,
new StructuralTypeShaperExpression(
Expand Down Expand Up @@ -1270,9 +1278,9 @@ protected override ShapedQueryExpression TranslateSelect(ShapedQueryExpression s
var translatedSelect = SelectExpression.CreateForCollection(
slice,
alias,
new EntityProjectionExpression(
new ObjectReferenceExpression((IEntityType)projectedStructuralTypeShaper.StructuralType, alias),
(IEntityType)projectedStructuralTypeShaper.StructuralType));
new StructuralTypeProjectionExpression(
new ObjectReferenceExpression(projectedStructuralTypeShaper.StructuralType, alias),
projectedStructuralTypeShaper.StructuralType));
return source.Update(
translatedSelect,
new StructuralTypeShaperExpression(
Expand Down Expand Up @@ -1380,17 +1388,30 @@ protected override ShapedQueryExpression TranslateSelect(ShapedQueryExpression s
{
case StructuralTypeShaperExpression shaper when property is INavigation { IsCollection: true }:
{
var targetEntityType = (IEntityType)shaper.StructuralType;
var projection = new EntityProjectionExpression(
new ObjectReferenceExpression(targetEntityType, sourceAlias), targetEntityType);
var targetStructuralType = shaper.StructuralType;
var projection = new StructuralTypeProjectionExpression(
new ObjectReferenceExpression(targetStructuralType, sourceAlias), targetStructuralType);
var select = SelectExpression.CreateForCollection(
shaper.ValueBufferExpression,
sourceAlias,
projection);
return CreateShapedQueryExpression(targetEntityType, select);
return CreateShapedQueryExpression(targetStructuralType, select);
}

// TODO: Collection of complex type (#31253)
case CollectionResultExpression collectionResult:
{
Debug.Assert(collectionResult.Parameter != null, "CollectionResultExpression can't be bound to member without parameter.");

var shaper = collectionResult.Parameter;
var targetStructuralType = shaper.StructuralType;
var projection = new StructuralTypeProjectionExpression(
new ObjectReferenceExpression(targetStructuralType, sourceAlias), targetStructuralType);
var select = SelectExpression.CreateForCollection(
shaper.ValueBufferExpression,
sourceAlias,
projection);
return CreateShapedQueryExpression(targetStructuralType, select);
}

// Note that non-collection navigations/complex types are handled in CosmosSqlTranslatingExpressionVisitor
// (no collection -> no queryable operators)
Expand Down Expand Up @@ -1666,7 +1687,7 @@ private bool TryPushdownIntoSubquery(SelectExpression select)
var translation = new ObjectFunctionExpression(functionName, [array1, array2], arrayType);
var alias = _aliasManager.GenerateSourceAlias(translation);
var select = SelectExpression.CreateForCollection(
translation, alias, new ObjectReferenceExpression((IEntityType)structuralType1, alias));
translation, alias, new ObjectReferenceExpression(structuralType1, alias));
return CreateShapedQueryExpression(select, structuralType1.ClrType);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections;
using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal;
using Microsoft.EntityFrameworkCore.Update.Internal;
using Newtonsoft.Json.Linq;

namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal;

/// <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>
public static class CosmosSerializationUtilities
{
/// <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>
public static readonly MethodInfo SerializeObjectToComplexPropertyMethod
= typeof(CosmosSerializationUtilities).GetMethod(nameof(SerializeObjectToComplexProperty)) ?? throw new UnreachableException();

/// <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>
public static JToken SerializeObjectToComplexProperty(IComplexType type, object? value, bool collection) // #34567
{
if (value == null)
{
return JValue.CreateNull();
}

if (collection)
{
var array = new JArray();
foreach (var element in (IEnumerable)value)
{
array.Add(SerializeObjectToComplexProperty(type, element, false));
}
return array;
}

var obj = new JObject();
foreach (var property in type.GetProperties())
{
var jsonPropertyName = property.GetJsonPropertyName();

var propertyValue = property.GetGetter().GetClrValue(value);
#pragma warning disable EF1001 // Internal EF Core API usage.
var providerValue = property.ConvertToProviderValue(propertyValue);
#pragma warning restore EF1001 // Internal EF Core API usage.
if (providerValue is null)
{
if (!property.IsNullable)
{
throw new InvalidOperationException(CoreStrings.PropertyConceptualNull(property.Name, type.DisplayName()));
}

obj[jsonPropertyName] = null;
}
else
{
obj[jsonPropertyName] = JToken.FromObject(providerValue, CosmosClientWrapper.Serializer);
}
}

foreach (var complexProperty in type.GetComplexProperties())
{
var jsonPropertyName = complexProperty.Name;
var propertyValue = complexProperty.GetGetter().GetClrValue(value);
if (propertyValue is null)
{
if (!complexProperty.IsNullable)
{
throw new InvalidOperationException(CoreStrings.PropertyConceptualNull(complexProperty.Name, type.DisplayName()));
}

obj[jsonPropertyName] = null;
}
else
{
obj[jsonPropertyName] = SerializeObjectToComplexProperty(complexProperty.ComplexType, propertyValue, complexProperty.IsCollection);
}
}

return obj;
}
}
Loading
Loading