diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs index 73756688170..3c761592e23 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs @@ -22,6 +22,7 @@ public class CosmosQueryableMethodTranslatingExpressionVisitor : QueryableMethod private readonly CosmosSqlTranslatingExpressionVisitor _sqlTranslator; private readonly CosmosProjectionBindingExpressionVisitor _projectionBindingExpressionVisitor; private readonly bool _subquery; + private ReadItemInfo? _readItemExpression; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -91,56 +92,85 @@ protected CosmosQueryableMethodTranslatingExpressionVisitor( [return: NotNullIfNotNull(nameof(expression))] public override Expression? Visit(Expression? expression) { - if (expression is MethodCallExpression + if (_queryCompilationContext.QueryTrackingBehavior == QueryTrackingBehavior.TrackAll // Issue #33893 + && expression is MethodCallExpression { Method: { Name: nameof(Queryable.FirstOrDefault), IsGenericMethod: true }, - Arguments: - [ - MethodCallExpression - { - Method: { Name: nameof(Queryable.Where), IsGenericMethod: true }, - Arguments: - [ - EntityQueryRootExpression { EntityType: var entityType }, - UnaryExpression { Operand: LambdaExpression lambdaExpression, NodeType: ExpressionType.Quote } - ] - } whereMethodCall - ] - } firstOrDefaultMethodCall - && firstOrDefaultMethodCall.Method.GetGenericMethodDefinition() == QueryableMethods.FirstOrDefaultWithoutPredicate - && whereMethodCall.Method.GetGenericMethodDefinition() == QueryableMethods.Where) + Arguments: [MethodCallExpression innerMethodCall] + }) { - var queryProperties = new List(); - var parameterNames = new List(); - - if (ExtractPartitionKeyFromPredicate(entityType, lambdaExpression.Body, queryProperties, parameterNames)) + var clrType = innerMethodCall.Type.TryGetSequenceType() ?? typeof(object); + if (innerMethodCall is + { + Method: { Name: nameof(Queryable.Select), IsGenericMethod: true }, + Arguments: + [ + MethodCallExpression innerInnerMethodCall, + UnaryExpression { NodeType: ExpressionType.Quote } unaryExpression + ] + }) { - var entityTypePrimaryKeyProperties = entityType.FindPrimaryKey()!.Properties; - var idProperty = entityType.GetProperties() - .First(p => p.GetJsonPropertyName() == StoreKeyConvention.IdPropertyJsonName); - var partitionKeyProperties = entityType.GetPartitionKeyProperties(); - - if (entityTypePrimaryKeyProperties.SequenceEqual(queryProperties) - && (!partitionKeyProperties.Any() - || partitionKeyProperties.All(p => entityTypePrimaryKeyProperties.Contains(p))) - && (idProperty.GetValueGeneratorFactory() != null - || entityTypePrimaryKeyProperties.Contains(idProperty))) + if (unaryExpression.Operand is LambdaExpression) { - var propertyParameterList = queryProperties.Zip( - parameterNames, - (property, parameter) => (property, parameter)) - .ToDictionary(tuple => tuple.property, tuple => tuple.parameter); + innerMethodCall = innerInnerMethodCall; + } + } - var readItemExpression = new ReadItemExpression(entityType, propertyParameterList); + if (innerMethodCall is + { + Method: { Name: nameof(Queryable.Where), IsGenericMethod: true }, + Arguments: + [ + EntityQueryRootExpression { EntityType: var entityType }, + UnaryExpression { Operand: LambdaExpression lambdaExpression, NodeType: ExpressionType.Quote } + ] + }) + { + var queryProperties = new List(); + var parameterNames = new List(); + + if (ExtractPartitionKeyFromPredicate(entityType, lambdaExpression.Body, queryProperties, parameterNames)) + { + var entityTypePrimaryKeyProperties = entityType.FindPrimaryKey()!.Properties; + var idProperty = entityType.GetProperties() + .First(p => p.GetJsonPropertyName() == StoreKeyConvention.IdPropertyJsonName); + var partitionKeyProperties = entityType.GetPartitionKeyProperties(); + + if (entityTypePrimaryKeyProperties.SequenceEqual(queryProperties) + && (!partitionKeyProperties.Any() + || partitionKeyProperties.All(p => entityTypePrimaryKeyProperties.Contains(p))) + // This should ideally only be looking for properties with the `IdValueGeneratorFactory` generator. since + // this is how the `id` property will be generated from other key values. + && ((idProperty.GetValueGeneratorFactory() != null + // If we can't create an instance, then we might not be able to construct the resource id. + && CanCreateEmptyInstance(entityType)) + || entityTypePrimaryKeyProperties.Contains(idProperty))) + { + var propertyParameterList = queryProperties.Zip( + parameterNames, + (property, parameter) => (property, parameter)) + .ToDictionary(tuple => tuple.property, tuple => tuple.parameter); - return CreateShapedQueryExpression(entityType, readItemExpression) - .UpdateResultCardinality(ResultCardinality.SingleOrDefault); + _readItemExpression = new ReadItemInfo(entityType, propertyParameterList, clrType); + } } } } return base.Visit(expression); + static bool CanCreateEmptyInstance(IEntityType entityType) + { + var binding = entityType.ServiceOnlyConstructorBinding; + if (binding == null) + { + _ = entityType.ConstructorBinding; + binding = entityType.ServiceOnlyConstructorBinding; + } + + return binding != null; + } + static bool ExtractPartitionKeyFromPredicate( IEntityType entityType, Expression joinCondition, @@ -256,7 +286,11 @@ protected override QueryableMethodTranslatingExpressionVisitor CreateSubqueryVis /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override ShapedQueryExpression CreateShapedQueryExpression(IEntityType entityType) - => CreateShapedQueryExpression(entityType, _sqlExpressionFactory.Select(entityType)); + => CreateShapedQueryExpression( + entityType, + _readItemExpression == null + ? _sqlExpressionFactory.Select(entityType) + : _sqlExpressionFactory.ReadItem(entityType, _readItemExpression)); private ShapedQueryExpression CreateShapedQueryExpression(IEntityType entityType, Expression queryExpression) { diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingReadItemExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingReadItemExpressionVisitor.cs deleted file mode 100644 index 74d658c4de7..00000000000 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingReadItemExpressionVisitor.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#nullable disable - -namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; - -public partial class CosmosShapedQueryCompilingExpressionVisitor -{ - private sealed class CosmosProjectionBindingRemovingReadItemExpressionVisitor : CosmosProjectionBindingRemovingExpressionVisitorBase - { - private readonly ReadItemExpression _readItemExpression; - - public CosmosProjectionBindingRemovingReadItemExpressionVisitor( - ReadItemExpression readItemExpression, - ParameterExpression jObjectParameter, - bool trackQueryResults) - : base(jObjectParameter, trackQueryResults) - { - _readItemExpression = readItemExpression; - } - - protected override ProjectionExpression GetProjection(ProjectionBindingExpression _) - => _readItemExpression.ProjectionExpression; - } -} diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.ReadItemQueryingEnumerable.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.ReadItemQueryingEnumerable.cs index 420dc595df8..c8c7130dc87 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.ReadItemQueryingEnumerable.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.ReadItemQueryingEnumerable.cs @@ -23,7 +23,7 @@ private sealed class ReadItemQueryingEnumerable : IEnumerable, IAsyncEnume { private readonly CosmosQueryContext _cosmosQueryContext; private readonly string _cosmosContainer; - private readonly ReadItemExpression _readItemExpression; + private readonly ReadItemInfo _readItemInfo; private readonly Func _shaper; private readonly Type _contextType; private readonly IDiagnosticsLogger _queryLogger; @@ -33,7 +33,7 @@ private sealed class ReadItemQueryingEnumerable : IEnumerable, IAsyncEnume public ReadItemQueryingEnumerable( CosmosQueryContext cosmosQueryContext, string cosmosContainer, - ReadItemExpression readItemExpression, + ReadItemInfo readItemInfo, Func shaper, Type contextType, bool standAloneStateManager, @@ -41,7 +41,7 @@ public ReadItemQueryingEnumerable( { _cosmosQueryContext = cosmosQueryContext; _cosmosContainer = cosmosContainer; - _readItemExpression = readItemExpression; + _readItemInfo = readItemInfo; _shaper = shaper; _contextType = contextType; _queryLogger = _cosmosQueryContext.QueryLogger; @@ -67,7 +67,7 @@ public string ToQueryString() private bool TryGetPartitionKey(out PartitionKey partitionKeyValue) { - var properties = _readItemExpression.EntityType.GetPartitionKeyProperties(); + var properties = _readItemInfo.EntityType.GetPartitionKeyProperties(); if (!properties.Any()) { partitionKeyValue = PartitionKey.None; @@ -95,7 +95,7 @@ private bool TryGetPartitionKey(out PartitionKey partitionKeyValue) private bool TryGetResourceId(out string resourceId) { - var idProperty = _readItemExpression.EntityType.GetProperties() + var idProperty = _readItemInfo.EntityType.GetProperties() .FirstOrDefault(p => p.GetJsonPropertyName() == StoreKeyConvention.IdPropertyJsonName); if (TryGetParameterValue(idProperty, out var value)) @@ -124,7 +124,7 @@ private bool TryGetResourceId(out string resourceId) private bool TryGetParameterValue(IProperty property, out object value) { value = null; - return _readItemExpression.PropertyParameters.TryGetValue(property, out var parameterName) + return _readItemInfo.PropertyParameters.TryGetValue(property, out var parameterName) && _cosmosQueryContext.ParameterValues.TryGetValue(parameterName, out value); } @@ -139,39 +139,36 @@ private static string GetString(IProperty property, object value) private bool TryGenerateIdFromKeys(IProperty idProperty, out object value) { - var entityEntry = Activator.CreateInstance(_readItemExpression.EntityType.ClrType); - #pragma warning disable EF1001 // Internal EF Core API usage. + // The idea here is that if a `IdValueGeneratorFactory` has been configured to generate an `id` value from the + // values of other properties, then we need an entity instance to use with the value generator. + var entityInstance = _readItemInfo.EntityType.GetOrCreateEmptyMaterializer(_cosmosQueryContext.EntityMaterializerSource) + (new MaterializationContext(ValueBuffer.Empty, _cosmosQueryContext.Context)); + var internalEntityEntry = new InternalEntityEntry( - _cosmosQueryContext.Context.GetDependencies().StateManager, _readItemExpression.EntityType, entityEntry); -#pragma warning restore EF1001 // Internal EF Core API usage. + _cosmosQueryContext.Context.GetDependencies().StateManager, _readItemInfo.EntityType, entityInstance); - foreach (var keyProperty in _readItemExpression.EntityType.FindPrimaryKey().Properties) + foreach (var keyProperty in _readItemInfo.EntityType.FindPrimaryKey().Properties) { - var property = _readItemExpression.EntityType.FindProperty(keyProperty.Name); + var property = _readItemInfo.EntityType.FindProperty(keyProperty.Name); if (TryGetParameterValue(property, out var parameterValue)) { -#pragma warning disable EF1001 // Internal EF Core API usage. internalEntityEntry[property] = parameterValue; -#pragma warning restore EF1001 // Internal EF Core API usage. } } -#pragma warning disable EF1001 // Internal EF Core API usage. internalEntityEntry.SetEntityState(EntityState.Added); - value = internalEntityEntry[idProperty]; - internalEntityEntry.SetEntityState(EntityState.Detached); -#pragma warning restore EF1001 // Internal EF Core API usage. return value != null; +#pragma warning restore EF1001 // Internal EF Core API usage. } - private sealed class Enumerator : IEnumerator, IAsyncEnumerator { private readonly CosmosQueryContext _cosmosQueryContext; + private readonly ReadItemInfo _readItemInfo; private readonly string _cosmosContainer; private readonly Func _shaper; private readonly Type _contextType; @@ -188,6 +185,7 @@ private sealed class Enumerator : IEnumerator, IAsyncEnumerator public Enumerator(ReadItemQueryingEnumerable readItemEnumerable, CancellationToken cancellationToken = default) { _cosmosQueryContext = readItemEnumerable._cosmosQueryContext; + _readItemInfo = readItemEnumerable._readItemInfo; _cosmosContainer = readItemEnumerable._cosmosContainer; _shaper = readItemEnumerable._shaper; _contextType = readItemEnumerable._contextType; diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.cs index 93634431db9..23a17d2d437 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.cs @@ -59,45 +59,36 @@ protected override Expression VisitShapedQuery(ShapedQueryExpression shapedQuery QueryCompilationContext.QueryContextParameter, jObjectParameter); - return New( - typeof(QueryingEnumerable<>).MakeGenericType(shaperLambda.ReturnType).GetConstructors()[0], - Convert( - QueryCompilationContext.QueryContextParameter, - typeof(CosmosQueryContext)), - Constant(sqlExpressionFactory), - Constant(querySqlGeneratorFactory), - Constant(selectExpression), - Constant(shaperLambda.Compile()), - Constant(_contextType), - Constant(cosmosQueryCompilationContext.CosmosContainer), - Constant(_partitionKeyValueFromExtension, typeof(PartitionKey)), - Constant( - QueryCompilationContext.QueryTrackingBehavior == QueryTrackingBehavior.NoTrackingWithIdentityResolution), - Constant(_threadSafetyChecksEnabled)); + var cosmosQueryContextConstant = Convert(QueryCompilationContext.QueryContextParameter, typeof(CosmosQueryContext)); + var shaperConstant = Constant(shaperLambda.Compile()); + var contextTypeConstant = Constant(_contextType); + var containerConstant = Constant(cosmosQueryCompilationContext.CosmosContainer); + var threadSafetyConstant = Constant(_threadSafetyChecksEnabled); + var standAloneStateManagerConstant = Constant( + QueryCompilationContext.QueryTrackingBehavior == QueryTrackingBehavior.NoTrackingWithIdentityResolution); - case ReadItemExpression readItemExpression: - shaperBody = new CosmosProjectionBindingRemovingReadItemExpressionVisitor( - readItemExpression, jObjectParameter, - QueryCompilationContext.QueryTrackingBehavior == QueryTrackingBehavior.TrackAll) - .Visit(shaperBody); - - var shaperReadItemLambda = Lambda( - shaperBody, - QueryCompilationContext.QueryContextParameter, - jObjectParameter); - - return New( - typeof(ReadItemQueryingEnumerable<>).MakeGenericType(shaperReadItemLambda.ReturnType).GetConstructors()[0], - Convert( - QueryCompilationContext.QueryContextParameter, - typeof(CosmosQueryContext)), - Constant(cosmosQueryCompilationContext.CosmosContainer), - Constant(readItemExpression), - Constant(shaperReadItemLambda.Compile()), - Constant(_contextType), - Constant( - QueryCompilationContext.QueryTrackingBehavior == QueryTrackingBehavior.NoTrackingWithIdentityResolution), - Constant(_threadSafetyChecksEnabled)); + return selectExpression.ReadItemInfo != null + ? New( + typeof(ReadItemQueryingEnumerable<>).MakeGenericType(selectExpression.ReadItemInfo.Type).GetConstructors()[0], + cosmosQueryContextConstant, + containerConstant, + Constant(selectExpression.ReadItemInfo), + shaperConstant, + contextTypeConstant, + standAloneStateManagerConstant, + threadSafetyConstant) + : New( + typeof(QueryingEnumerable<>).MakeGenericType(shaperLambda.ReturnType).GetConstructors()[0], + cosmosQueryContextConstant, + Constant(sqlExpressionFactory), + Constant(querySqlGeneratorFactory), + Constant(selectExpression), + shaperConstant, + contextTypeConstant, + containerConstant, + Constant(_partitionKeyValueFromExtension, typeof(PartitionKey)), + standAloneStateManagerConstant, + threadSafetyConstant); default: throw new NotSupportedException(CoreStrings.UnhandledExpressionNode(shapedQueryExpression.QueryExpression)); diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosValueConverterCompensatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosValueConverterCompensatingExpressionVisitor.cs index e58f20a8f82..9cbdde344fb 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosValueConverterCompensatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosValueConverterCompensatingExpressionVisitor.cs @@ -24,7 +24,6 @@ protected override Expression VisitExtension(Expression extensionExpression) => extensionExpression switch { ShapedQueryExpression shapedQueryExpression => VisitShapedQueryExpression(shapedQueryExpression), - ReadItemExpression readItemExpression => readItemExpression, SelectExpression selectExpression => VisitSelect(selectExpression), SqlConditionalExpression sqlConditionalExpression => VisitSqlConditional(sqlConditionalExpression), _ => base.VisitExtension(extensionExpression) diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/ReadItemExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/ReadItemInfo.cs similarity index 64% rename from src/EFCore.Cosmos/Query/Internal/Expressions/ReadItemExpression.cs rename to src/EFCore.Cosmos/Query/Internal/Expressions/ReadItemInfo.cs index 1989781ce1f..f6e8178fdd4 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/ReadItemExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/ReadItemInfo.cs @@ -10,35 +10,15 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; /// 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. /// -public class ReadItemExpression : Expression +public class ReadItemInfo { - private const string RootAlias = "c"; - /// /// 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. /// - public override Type Type - => typeof(object); - - /// - /// 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. - /// - public override ExpressionType NodeType - => ExpressionType.Extension; - - /// - /// 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. - /// - public virtual ProjectionExpression ProjectionExpression { get; } + public virtual Type Type { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -62,18 +42,13 @@ public override ExpressionType NodeType /// 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. /// - public ReadItemExpression( + public ReadItemInfo( IEntityType entityType, - IDictionary propertyParameters) + IDictionary propertyParameters, + Type type) { - ProjectionExpression = new ProjectionExpression( - new EntityProjectionExpression( - entityType, - new ObjectReferenceExpression(entityType, RootAlias)), - RootAlias); - + Type = type; EntityType = entityType; - PropertyParameters = propertyParameters; } } diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs index 3f759ce9cc9..fdf006147c0 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs @@ -25,6 +25,18 @@ public class SelectExpression : Expression, IPrintableExpression private readonly List<(Expression ValueExpression, IProperty Property)> _partitionKeyValues = new(); + /// + /// 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. + /// + public SelectExpression(IEntityType entityType, ReadItemInfo readItemInfo) + : this(entityType) + { + ReadItemInfo = readItemInfo; + } + /// /// 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 @@ -69,6 +81,14 @@ public SelectExpression( _orderings = orderings; } + /// + /// 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. + /// + public virtual ReadItemInfo? ReadItemInfo { get; } + /// /// 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 diff --git a/src/EFCore.Cosmos/Query/Internal/ISqlExpressionFactory.cs b/src/EFCore.Cosmos/Query/Internal/ISqlExpressionFactory.cs index c277d45246c..3ef9969b804 100644 --- a/src/EFCore.Cosmos/Query/Internal/ISqlExpressionFactory.cs +++ b/src/EFCore.Cosmos/Query/Internal/ISqlExpressionFactory.cs @@ -312,4 +312,12 @@ SqlConditionalExpression Condition( /// doing so can result in application failures when updating to a new Entity Framework Core release. /// SelectExpression Select(IEntityType entityType, string sql, Expression argument); + + /// + /// 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. + /// + SelectExpression ReadItem(IEntityType entityType, ReadItemInfo argument); } diff --git a/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs b/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs index 7ba033dddb3..6ebb83cd545 100644 --- a/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs +++ b/src/EFCore.Cosmos/Query/Internal/SqlExpressionFactory.cs @@ -631,6 +631,20 @@ public virtual SelectExpression Select(IEntityType entityType) return selectExpression; } + /// + /// 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. + /// + public virtual SelectExpression ReadItem(IEntityType entityType, ReadItemInfo readItemInfo) + { + var selectExpression = new SelectExpression(entityType, readItemInfo); + AddDiscriminator(selectExpression, entityType); + + return selectExpression; + } + /// /// 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 diff --git a/src/EFCore.Cosmos/Query/Internal/SqlExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/SqlExpressionVisitor.cs index e867950b8f9..98c96d8860d 100644 --- a/src/EFCore.Cosmos/Query/Internal/SqlExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/SqlExpressionVisitor.cs @@ -22,7 +22,6 @@ protected override Expression VisitExtension(Expression extensionExpression) { ShapedQueryExpression shapedQueryExpression => shapedQueryExpression.UpdateQueryExpression(Visit(shapedQueryExpression.QueryExpression)), - ReadItemExpression readItemExpression => readItemExpression, SelectExpression selectExpression => VisitSelect(selectExpression), ProjectionExpression projectionExpression => VisitProjection(projectionExpression), EntityProjectionExpression entityProjectionExpression => VisitEntityProjection(entityProjectionExpression), diff --git a/src/EFCore/Query/QueryContext.cs b/src/EFCore/Query/QueryContext.cs index 08bad5a6903..e3a3ef09b81 100644 --- a/src/EFCore/Query/QueryContext.cs +++ b/src/EFCore/Query/QueryContext.cs @@ -51,6 +51,12 @@ protected QueryContext(QueryContextDependencies dependencies) /// protected virtual QueryContextDependencies Dependencies { get; } + /// + /// The , which can be used to create stand-alone entity instances. + /// + public virtual IEntityMaterializerSource EntityMaterializerSource + => Dependencies.EntityMaterializerSource; + /// /// Sets the navigation for given entity as loaded. /// diff --git a/src/EFCore/Query/QueryContextDependencies.cs b/src/EFCore/Query/QueryContextDependencies.cs index 1978342f43f..3623094a885 100644 --- a/src/EFCore/Query/QueryContextDependencies.cs +++ b/src/EFCore/Query/QueryContextDependencies.cs @@ -53,6 +53,7 @@ public QueryContextDependencies( IExecutionStrategy executionStrategy, IConcurrencyDetector concurrencyDetector, IExceptionDetector exceptionDetector, + IEntityMaterializerSource entityMaterializerSource, IDiagnosticsLogger commandLogger, IDiagnosticsLogger queryLogger) { @@ -60,6 +61,7 @@ public QueryContextDependencies( ExecutionStrategy = executionStrategy; ConcurrencyDetector = concurrencyDetector; ExceptionDetector = exceptionDetector; + EntityMaterializerSource = entityMaterializerSource; CommandLogger = commandLogger; QueryLogger = queryLogger; } @@ -94,6 +96,11 @@ public IStateManager StateManager /// public IExceptionDetector ExceptionDetector { get; init; } + /// + /// The , which can be used to create stand-alone entity instances. + /// + public IEntityMaterializerSource EntityMaterializerSource { get; } + /// /// The command logger. /// diff --git a/test/EFCore.Cosmos.FunctionalTests/FindCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/FindCosmosTest.cs index 3897e3be08d..bf0e875145a 100644 --- a/test/EFCore.Cosmos.FunctionalTests/FindCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/FindCosmosTest.cs @@ -72,6 +72,407 @@ public override void Find_shadow_key_from_store() public override void Returns_null_for_shadow_key_not_in_store() => CosmosTestHelpers.Instance.NoSyncTest(() => base.Returns_null_for_shadow_key_not_in_store()); + public override void Find_int_key_tracked() + { + base.Find_int_key_tracked(); + + AssertSql(); + } + + public override void Find_nullable_int_key_tracked() + { + base.Find_nullable_int_key_tracked(); + + AssertSql(); + } + + public override void Find_string_key_tracked() + { + base.Find_string_key_tracked(); + + AssertSql(); + } + + public override void Find_composite_key_tracked() + { + base.Find_composite_key_tracked(); + + AssertSql(); + } + + public override void Find_base_type_tracked() + { + base.Find_base_type_tracked(); + + AssertSql(); + } + + public override void Find_derived_type_tracked() + { + base.Find_derived_type_tracked(); + + AssertSql(); + } + + public override void Find_derived_type_using_base_set_tracked() + { + base.Find_derived_type_using_base_set_tracked(); + + AssertSql(); + } + + public override void Find_shadow_key_tracked() + { + base.Find_shadow_key_tracked(); + + AssertSql(); + } + + public override void Returns_null_for_null_key_values_array() + { + base.Returns_null_for_null_key_values_array(); + + AssertSql(); + } + + public override void Returns_null_for_null_key() + { + base.Returns_null_for_null_key(); + + AssertSql(); + } + + public override void Returns_null_for_null_nullable_key() + { + base.Returns_null_for_null_nullable_key(); + + AssertSql(); + } + + public override void Returns_null_for_null_in_composite_key() + { + base.Returns_null_for_null_in_composite_key(); + + AssertSql(); + } + + public override void Throws_for_multiple_values_passed_for_simple_key() + { + base.Throws_for_multiple_values_passed_for_simple_key(); + + AssertSql(); + } + + public override void Throws_for_wrong_number_of_values_for_composite_key() + { + base.Throws_for_wrong_number_of_values_for_composite_key(); + + AssertSql(); + } + + public override void Throws_for_bad_type_for_simple_key() + { + base.Throws_for_bad_type_for_simple_key(); + + AssertSql(); + } + + public override void Throws_for_bad_type_for_composite_key() + { + base.Throws_for_bad_type_for_composite_key(); + + AssertSql(); + } + + public override void Throws_for_bad_entity_type() + { + base.Throws_for_bad_entity_type(); + + AssertSql(); + } + + public override void Throws_for_bad_entity_type_with_different_namespace() + { + base.Throws_for_bad_entity_type_with_different_namespace(); + + AssertSql(); + } + + public override async Task Find_int_key_tracked_async(CancellationType cancellationType) + { + await base.Find_int_key_tracked_async(cancellationType); + + AssertSql(); + } + + public override async Task Find_int_key_from_store_async(CancellationType cancellationType) + { + await base.Find_int_key_from_store_async(cancellationType); + + AssertSql( + """ +ReadItem(None, IntKey|77) +"""); + } + + public override async Task Returns_null_for_int_key_not_in_store_async(CancellationType cancellationType) + { + await base.Returns_null_for_int_key_not_in_store_async(cancellationType); + + AssertSql( + """ +ReadItem(None, IntKey|99) +"""); + } + + public override async Task Find_nullable_int_key_tracked_async(CancellationType cancellationType) + { + await base.Find_nullable_int_key_tracked_async(cancellationType); + + AssertSql(); + } + + public override async Task Find_nullable_int_key_from_store_async(CancellationType cancellationType) + { + await base.Find_nullable_int_key_from_store_async(cancellationType); + + AssertSql( + """ +ReadItem(None, NullableIntKey|77) +"""); + } + + public override async Task Returns_null_for_nullable_int_key_not_in_store_async(CancellationType cancellationType) + { + await base.Returns_null_for_nullable_int_key_not_in_store_async(cancellationType); + + AssertSql( + """ +ReadItem(None, NullableIntKey|99) +"""); + } + + public override async Task Find_string_key_tracked_async(CancellationType cancellationType) + { + await base.Find_string_key_tracked_async(cancellationType); + + AssertSql(); + } + + public override async Task Find_string_key_from_store_async(CancellationType cancellationType) + { + await base.Find_string_key_from_store_async(cancellationType); + + AssertSql( + """ +ReadItem(None, StringKey|Cat) +"""); + } + + public override async Task Returns_null_for_string_key_not_in_store_async(CancellationType cancellationType) + { + await base.Returns_null_for_string_key_not_in_store_async(cancellationType); + + AssertSql( + """ +ReadItem(None, StringKey|Fox) +"""); + } + + public override async Task Find_composite_key_tracked_async(CancellationType cancellationType) + { + await base.Find_composite_key_tracked_async(cancellationType); + + AssertSql(); + } + + public override async Task Find_composite_key_from_store_async(CancellationType cancellationType) + { + await base.Find_composite_key_from_store_async(cancellationType); + + AssertSql( + """ +ReadItem(None, CompositeKey|77|Dog) +"""); + } + + public override async Task Returns_null_for_composite_key_not_in_store_async(CancellationType cancellationType) + { + await base.Returns_null_for_composite_key_not_in_store_async(cancellationType); + + AssertSql( + """ +ReadItem(None, CompositeKey|77|Fox) +"""); + } + + public override async Task Find_base_type_tracked_async(CancellationType cancellationType) + { + await base.Find_base_type_tracked_async(cancellationType); + + AssertSql(); + } + + public override async Task Find_base_type_from_store_async(CancellationType cancellationType) + { + await base.Find_base_type_from_store_async(cancellationType); + + AssertSql( + """ +ReadItem(None, BaseType|77) +"""); + } + + public override async Task Returns_null_for_base_type_not_in_store_async(CancellationType cancellationType) + { + await base.Returns_null_for_base_type_not_in_store_async(cancellationType); + + AssertSql( + """ +ReadItem(None, BaseType|99) +"""); + } + + public override async Task Find_derived_type_tracked_async(CancellationType cancellationType) + { + await base.Find_derived_type_tracked_async(cancellationType); + + AssertSql(); + } + + public override async Task Find_derived_type_from_store_async(CancellationType cancellationType) + { + await base.Find_derived_type_from_store_async(cancellationType); + + AssertSql( + """ +ReadItem(None, DerivedType|78) +"""); + } + + public override async Task Returns_null_for_derived_type_not_in_store_async(CancellationType cancellationType) + { + await base.Returns_null_for_derived_type_not_in_store_async(cancellationType); + + AssertSql( + """ +ReadItem(None, DerivedType|99) +"""); + } + + public override async Task Find_base_type_using_derived_set_from_store_async(CancellationType cancellationType) + { + await base.Find_base_type_using_derived_set_from_store_async(cancellationType); + + AssertSql( + """ +ReadItem(None, DerivedType|77) +"""); + } + + public override async Task Find_derived_type_using_base_set_tracked_async(CancellationType cancellationType) + { + await base.Find_derived_type_using_base_set_tracked_async(cancellationType); + + AssertSql(); + } + + public override async Task Find_shadow_key_tracked_async(CancellationType cancellationType) + { + await base.Find_shadow_key_tracked_async(cancellationType); + + AssertSql(); + } + + public override async Task Find_shadow_key_from_store_async(CancellationType cancellationType) + { + await base.Find_shadow_key_from_store_async(cancellationType); + + AssertSql( + """ +ReadItem(None, ShadowKey|77) +"""); + } + + public override async Task Returns_null_for_shadow_key_not_in_store_async(CancellationType cancellationType) + { + await base.Returns_null_for_shadow_key_not_in_store_async(cancellationType); + + AssertSql( + """ +ReadItem(None, ShadowKey|99) +"""); + } + + public override async Task Returns_null_for_null_key_values_array_async(CancellationType cancellationType) + { + await base.Returns_null_for_null_key_values_array_async(cancellationType); + + AssertSql(); + } + + public override async Task Returns_null_for_null_key_async(CancellationType cancellationType) + { + await base.Returns_null_for_null_key_async(cancellationType); + + AssertSql(); + } + + public override async Task Returns_null_for_null_in_composite_key_async(CancellationType cancellationType) + { + await base.Returns_null_for_null_in_composite_key_async(cancellationType); + + AssertSql(); + } + + public override async Task Throws_for_multiple_values_passed_for_simple_key_async(CancellationType cancellationType) + { + await base.Throws_for_multiple_values_passed_for_simple_key_async(cancellationType); + + AssertSql(); + } + + public override async Task Throws_for_wrong_number_of_values_for_composite_key_async(CancellationType cancellationType) + { + await base.Throws_for_wrong_number_of_values_for_composite_key_async(cancellationType); + + AssertSql(); + } + + public override async Task Throws_for_bad_type_for_simple_key_async(CancellationType cancellationType) + { + await base.Throws_for_bad_type_for_simple_key_async(cancellationType); + + AssertSql(); + } + + public override async Task Throws_for_bad_type_for_composite_key_async(CancellationType cancellationType) + { + await base.Throws_for_bad_type_for_composite_key_async(cancellationType); + + AssertSql(); + } + + public override async Task Throws_for_bad_entity_type_async(CancellationType cancellationType) + { + await base.Throws_for_bad_entity_type_async(cancellationType); + + AssertSql(); + } + + public override async Task Throws_for_bad_entity_type_with_different_namespace_async(CancellationType cancellationType) + { + await base.Throws_for_bad_entity_type_with_different_namespace_async(cancellationType); + + AssertSql(); + } + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + protected void ClearLog() + => Fixture.TestSqlLoggerFactory.Clear(); + public class FindCosmosTestSet(FindCosmosFixture fixture) : FindCosmosTest(fixture) { protected override TestFinder Finder { get; } = new FindViaSetFinder(); diff --git a/test/EFCore.Specification.Tests/FindTestBase.cs b/test/EFCore.Specification.Tests/FindTestBase.cs index 982523e9895..c8da019b5ef 100644 --- a/test/EFCore.Specification.Tests/FindTestBase.cs +++ b/test/EFCore.Specification.Tests/FindTestBase.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; #nullable disable @@ -335,7 +336,18 @@ public virtual async Task Find_int_key_tracked_async(CancellationType cancellati public virtual async Task Find_int_key_from_store_async(CancellationType cancellationType) { using var context = CreateContext(); - Assert.Equal("Smokey", (await Finder.FindAsync(cancellationType, context, [77])).Foo); + + var entity = await Finder.FindAsync(cancellationType, context, [77]); + + Assert.Equal("Smokey", entity.Foo); + Assert.Equal(7, entity.OwnedReference.Prop); + Assert.Equal(2, entity.OwnedCollection.Count); + Assert.Contains(71, entity.OwnedCollection.Select(e => e.Prop)); + Assert.Contains(72, entity.OwnedCollection.Select(e => e.Prop)); + Assert.Equal("7", entity.OwnedReference.NestedOwned.Prop); + Assert.Equal(2, entity.OwnedReference.NestedOwnedCollection.Count); + Assert.Contains("71", entity.OwnedReference.NestedOwnedCollection.Select(e => e.Prop)); + Assert.Contains("72", entity.OwnedReference.NestedOwnedCollection.Select(e => e.Prop)); } [ConditionalTheory] @@ -736,6 +748,9 @@ protected class IntKey public int Id { get; set; } public string Foo { get; set; } + + public Owned1 OwnedReference { get; set; } + public List OwnedCollection { get; set; } } protected class NullableIntKey @@ -765,6 +780,21 @@ protected class ShadowKey public string Foo { get; set; } } + [Owned] + protected class Owned1 + { + public int Prop { get; set; } + public Owned2 NestedOwned { get; set; } + public List NestedOwnedCollection { get; set; } + } + + [Owned] + protected class Owned2 + { + [Required] + public string Prop { get; set; } + } + protected DbContext CreateContext() => Fixture.CreateContext(); @@ -788,7 +818,18 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con protected override Task SeedAsync(PoolableDbContext context) { context.AddRange( - new IntKey { Id = 77, Foo = "Smokey" }, + new IntKey + { + Id = 77, + Foo = "Smokey", + OwnedReference = new() + { + Prop = 7, + NestedOwned = new() { Prop = "7" }, + NestedOwnedCollection = new() { new() { Prop = "71" }, new() { Prop = "72" } } + }, + OwnedCollection = new() { new() { Prop = 71 }, new() { Prop = 72 } } + }, new NullableIntKey { Id = 77, Foo = "Smokey" }, new StringKey { Id = "Cat", Foo = "Alice" }, new CompositeKey @@ -847,7 +888,7 @@ public override ValueTask FindAsync( }; } - public class FindViaContextFinder : TestFinder + public class FindViaNonGenericContextFinder : TestFinder { public override TEntity Find(DbContext context, params object[] keyValues) => (TEntity)context.Find(typeof(TEntity), keyValues); @@ -868,7 +909,7 @@ public override async ValueTask FindAsync( }; } - public class FindViaNonGenericContextFinder : TestFinder + public class FindViaContextFinder : TestFinder { public override TEntity Find(DbContext context, params object[] keyValues) => context.Find(keyValues); diff --git a/test/EFCore.SqlServer.FunctionalTests/FindSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/FindSqlServerTest.cs index e03c887696a..5540e94ad32 100644 --- a/test/EFCore.SqlServer.FunctionalTests/FindSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/FindSqlServerTest.cs @@ -43,9 +43,21 @@ public override void Find_int_key_from_store() """ @__p_0='77' -SELECT TOP(1) [i].[Id], [i].[Foo] -FROM [IntKey] AS [i] -WHERE [i].[Id] = @__p_0 +SELECT [i3].[Id], [i3].[Foo], [s].[IntKeyId], [s].[Id], [s].[Prop], [s].[NestedOwned_Prop], [s].[Owned1IntKeyId], [s].[Owned1Id], [s].[Id0], [s].[Prop0], [i3].[OwnedReference_Prop], [i3].[OwnedReference_NestedOwned_Prop], [i2].[Owned1IntKeyId], [i2].[Id], [i2].[Prop] +FROM ( + SELECT TOP(1) [i].[Id], [i].[Foo], [i].[OwnedReference_Prop], [i].[OwnedReference_NestedOwned_Prop] + FROM [IntKey] AS [i] + WHERE [i].[Id] = @__p_0 +) AS [i3] +LEFT JOIN ( + SELECT [i0].[IntKeyId], [i0].[Id], [i0].[Prop], [i0].[NestedOwned_Prop], [i1].[Owned1IntKeyId], [i1].[Owned1Id], [i1].[Id] AS [Id0], [i1].[Prop] AS [Prop0] + FROM [IntKey_OwnedCollection] AS [i0] + LEFT JOIN [IntKey_OwnedCollection_NestedOwnedCollection] AS [i1] ON [i0].[IntKeyId] = [i1].[Owned1IntKeyId] AND [i0].[Id] = [i1].[Owned1Id] +) AS [s] ON [i3].[Id] = [s].[IntKeyId] +LEFT JOIN [IntKey_NestedOwnedCollection] AS [i2] ON CASE + WHEN [i3].[OwnedReference_Prop] IS NOT NULL THEN [i3].[Id] +END = [i2].[Owned1IntKeyId] +ORDER BY [i3].[Id], [s].[IntKeyId], [s].[Id], [s].[Owned1IntKeyId], [s].[Owned1Id], [s].[Id0], [i2].[Owned1IntKeyId] """); } @@ -57,9 +69,21 @@ public override void Returns_null_for_int_key_not_in_store() """ @__p_0='99' -SELECT TOP(1) [i].[Id], [i].[Foo] -FROM [IntKey] AS [i] -WHERE [i].[Id] = @__p_0 +SELECT [i3].[Id], [i3].[Foo], [s].[IntKeyId], [s].[Id], [s].[Prop], [s].[NestedOwned_Prop], [s].[Owned1IntKeyId], [s].[Owned1Id], [s].[Id0], [s].[Prop0], [i3].[OwnedReference_Prop], [i3].[OwnedReference_NestedOwned_Prop], [i2].[Owned1IntKeyId], [i2].[Id], [i2].[Prop] +FROM ( + SELECT TOP(1) [i].[Id], [i].[Foo], [i].[OwnedReference_Prop], [i].[OwnedReference_NestedOwned_Prop] + FROM [IntKey] AS [i] + WHERE [i].[Id] = @__p_0 +) AS [i3] +LEFT JOIN ( + SELECT [i0].[IntKeyId], [i0].[Id], [i0].[Prop], [i0].[NestedOwned_Prop], [i1].[Owned1IntKeyId], [i1].[Owned1Id], [i1].[Id] AS [Id0], [i1].[Prop] AS [Prop0] + FROM [IntKey_OwnedCollection] AS [i0] + LEFT JOIN [IntKey_OwnedCollection_NestedOwnedCollection] AS [i1] ON [i0].[IntKeyId] = [i1].[Owned1IntKeyId] AND [i0].[Id] = [i1].[Owned1Id] +) AS [s] ON [i3].[Id] = [s].[IntKeyId] +LEFT JOIN [IntKey_NestedOwnedCollection] AS [i2] ON CASE + WHEN [i3].[OwnedReference_Prop] IS NOT NULL THEN [i3].[Id] +END = [i2].[Owned1IntKeyId] +ORDER BY [i3].[Id], [s].[IntKeyId], [s].[Id], [s].[Owned1IntKeyId], [s].[Owned1Id], [s].[Id0], [i2].[Owned1IntKeyId] """); } @@ -72,29 +96,29 @@ public override void Find_nullable_int_key_tracked() public override void Find_nullable_int_key_from_store() { - base.Find_int_key_from_store(); + base.Find_nullable_int_key_from_store(); AssertSql( """ -@__p_0='77' +@__p_0='77' (Nullable = true) -SELECT TOP(1) [i].[Id], [i].[Foo] -FROM [IntKey] AS [i] -WHERE [i].[Id] = @__p_0 +SELECT TOP(1) [n].[Id], [n].[Foo] +FROM [NullableIntKey] AS [n] +WHERE [n].[Id] = @__p_0 """); } public override void Returns_null_for_nullable_int_key_not_in_store() { - base.Returns_null_for_int_key_not_in_store(); + base.Returns_null_for_nullable_int_key_not_in_store(); AssertSql( """ -@__p_0='99' +@__p_0='99' (Nullable = true) -SELECT TOP(1) [i].[Id], [i].[Foo] -FROM [IntKey] AS [i] -WHERE [i].[Id] = @__p_0 +SELECT TOP(1) [n].[Id], [n].[Foo] +FROM [NullableIntKey] AS [n] +WHERE [n].[Id] = @__p_0 """); } diff --git a/test/EFCore.Sqlite.FunctionalTests/FindSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/FindSqliteTest.cs index 5bdcfb7ac78..c44ae69657b 100644 --- a/test/EFCore.Sqlite.FunctionalTests/FindSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/FindSqliteTest.cs @@ -31,5 +31,31 @@ public class FindSqliteFixture : FindFixtureBase { protected override ITestStoreFactory TestStoreFactory => SqliteTestStoreFactory.Instance; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + modelBuilder.Entity( + b => + { + // This configuration for SQLite prevents attempts to use the default composite key config, which doesn't work + // on SQLite. See #26708 + b.OwnsOne( + e => e.OwnedReference, b => + { + b.OwnsOne(e => e.NestedOwned); + b.OwnsMany(e => e.NestedOwnedCollection).ToTable("NestedOwnedCollection").HasKey(e => e.Prop); + }); + + b.OwnsMany( + e => e.OwnedCollection, b => + { + b.ToTable("OwnedCollection").HasKey(e => e.Prop); + b.OwnsOne(e => e.NestedOwned); + b.OwnsMany(e => e.NestedOwnedCollection).ToTable("OwnedNestedOwnedCollection").HasKey(e => e.Prop); + }); + }); + } } }