diff --git a/src/EFCore.Cosmos/Design/Internal/CosmosCSharpRuntimeAnnotationCodeGenerator.cs b/src/EFCore.Cosmos/Design/Internal/CosmosCSharpRuntimeAnnotationCodeGenerator.cs index fbea3aeaada..18ad77fa0bb 100644 --- a/src/EFCore.Cosmos/Design/Internal/CosmosCSharpRuntimeAnnotationCodeGenerator.cs +++ b/src/EFCore.Cosmos/Design/Internal/CosmosCSharpRuntimeAnnotationCodeGenerator.cs @@ -35,6 +35,10 @@ public override void Generate(IModel model, CSharpRuntimeAnnotationCodeGenerator { annotations.Remove(CosmosAnnotationNames.Throughput); } + else + { + annotations.Remove(CosmosAnnotationNames.ModelDependencies); + } base.Generate(model, parameters); } diff --git a/src/EFCore.Cosmos/Extensions/CosmosServiceCollectionExtensions.cs b/src/EFCore.Cosmos/Extensions/CosmosServiceCollectionExtensions.cs index 8a93cf5cb1f..6169e54ca44 100644 --- a/src/EFCore.Cosmos/Extensions/CosmosServiceCollectionExtensions.cs +++ b/src/EFCore.Cosmos/Extensions/CosmosServiceCollectionExtensions.cs @@ -5,9 +5,11 @@ using Microsoft.EntityFrameworkCore.Cosmos.Diagnostics.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Infrastructure.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Conventions.Internal; +using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; using Microsoft.EntityFrameworkCore.Cosmos.ValueGeneration.Internal; +using Microsoft.EntityFrameworkCore.Infrastructure.Internal; // ReSharper disable once CheckNamespace namespace Microsoft.Extensions.DependencyInjection; @@ -99,6 +101,7 @@ public static IServiceCollection AddEntityFrameworkCosmos(this IServiceCollectio .TryAdd() .TryAdd() .TryAdd() + .TryAdd() .TryAdd() .TryAdd() .TryAdd() @@ -115,11 +118,12 @@ public static IServiceCollection AddEntityFrameworkCosmos(this IServiceCollectio .TryAddSingleton() .TryAddSingleton() .TryAddSingleton() + .TryAddSingleton() + .TryAddSingleton() .TryAddScoped() .TryAddScoped() .TryAddScoped() - .TryAddScoped() - ); + .TryAddScoped()); builder.TryAddCoreServices(); diff --git a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosModelRuntimeInitializer.cs b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosModelRuntimeInitializer.cs new file mode 100644 index 00000000000..40ebe958f43 --- /dev/null +++ b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosModelRuntimeInitializer.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// ReSharper disable once CheckNamespace + +using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; + +namespace Microsoft.EntityFrameworkCore.Infrastructure.Internal; + +/// +/// 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 class CosmosModelRuntimeInitializer : ModelRuntimeInitializer +{ + /// + /// 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 CosmosModelRuntimeInitializer( + ModelRuntimeInitializerDependencies dependencies, + CosmosModelRuntimeInitializerDependencies cosmosDependencies) + : base(dependencies) + { + CosmosDependencies = cosmosDependencies; + } + + /// + /// 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. + /// + protected virtual CosmosModelRuntimeInitializerDependencies CosmosDependencies { 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 + /// 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. + /// + protected override void InitializeModel(IModel model, bool designTime, bool prevalidation) + { + base.InitializeModel(model, designTime, prevalidation); + + if (prevalidation || !designTime) + { + model.SetRuntimeAnnotation(CosmosAnnotationNames.ModelDependencies, CosmosDependencies); + } + } +} diff --git a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosModelRuntimeInitializerDependencies.cs b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosModelRuntimeInitializerDependencies.cs new file mode 100644 index 00000000000..c82e63a9056 --- /dev/null +++ b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosModelRuntimeInitializerDependencies.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; + +namespace Microsoft.EntityFrameworkCore.Infrastructure.Internal; + +/// +/// 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 sealed record CosmosModelRuntimeInitializerDependencies +{ + /// + /// 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. + /// + /// + /// Do not call this constructor directly from either provider or application code as it may change + /// as new dependencies are added. Instead, use this type in your constructor so that an instance + /// will be created and injected automatically by the dependency injection container. To create + /// an instance with some dependent services replaced, first resolve the object from the dependency + /// injection container, then replace selected services using the C# 'with' operator. Do not call + /// the constructor at any point in this process. + /// + [EntityFrameworkInternal] + public CosmosModelRuntimeInitializerDependencies(IJsonIdDefinitionFactory jsonIdDefinitionFactory) + { + JsonIdDefinitionFactory = jsonIdDefinitionFactory; + } + + /// + /// 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 IJsonIdDefinitionFactory JsonIdDefinitionFactory { get; } +} diff --git a/src/EFCore.Cosmos/Metadata/Conventions/CosmosRuntimeModelConvention.cs b/src/EFCore.Cosmos/Metadata/Conventions/CosmosRuntimeModelConvention.cs index 717145097dc..0f37e0a9cf0 100644 --- a/src/EFCore.Cosmos/Metadata/Conventions/CosmosRuntimeModelConvention.cs +++ b/src/EFCore.Cosmos/Metadata/Conventions/CosmosRuntimeModelConvention.cs @@ -19,8 +19,7 @@ public class CosmosRuntimeModelConvention : RuntimeModelConvention /// Creates a new instance of . /// /// Parameter object containing dependencies for this convention. - public CosmosRuntimeModelConvention( - ProviderConventionSetBuilderDependencies dependencies) + public CosmosRuntimeModelConvention(ProviderConventionSetBuilderDependencies dependencies) : base(dependencies) { } diff --git a/src/EFCore.Cosmos/Metadata/Conventions/Internal/CosmosConventionSetBuilder.cs b/src/EFCore.Cosmos/Metadata/Conventions/Internal/CosmosConventionSetBuilder.cs index 46e8eeda1a6..16043d00a05 100644 --- a/src/EFCore.Cosmos/Metadata/Conventions/Internal/CosmosConventionSetBuilder.cs +++ b/src/EFCore.Cosmos/Metadata/Conventions/Internal/CosmosConventionSetBuilder.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; + namespace Microsoft.EntityFrameworkCore.Cosmos.Metadata.Conventions.Internal; /// @@ -17,8 +19,7 @@ public class CosmosConventionSetBuilder : ProviderConventionSetBuilder /// 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 CosmosConventionSetBuilder( - ProviderConventionSetBuilderDependencies dependencies) + public CosmosConventionSetBuilder(ProviderConventionSetBuilderDependencies dependencies) : base(dependencies) { } diff --git a/src/EFCore.Cosmos/Metadata/Conventions/StoreKeyConvention.cs b/src/EFCore.Cosmos/Metadata/Conventions/StoreKeyConvention.cs index 7aa0413bb61..43c86da1b76 100644 --- a/src/EFCore.Cosmos/Metadata/Conventions/StoreKeyConvention.cs +++ b/src/EFCore.Cosmos/Metadata/Conventions/StoreKeyConvention.cs @@ -88,7 +88,8 @@ private static void ProcessIdProperty(IConventionEntityTypeBuilder entityTypeBui if (idProperty != null) { - if (idProperty.ClrType == typeof(string)) + var converter = idProperty.GetValueConverter(); + if ((converter == null ? idProperty.ClrType : converter.ProviderClrType) == typeof(string)) { if (idProperty.IsPrimaryKey()) { diff --git a/src/EFCore.Cosmos/Metadata/Internal/CosmosAnnotationNames.cs b/src/EFCore.Cosmos/Metadata/Internal/CosmosAnnotationNames.cs index 6c0020aeb62..e9eab69bbb5 100644 --- a/src/EFCore.Cosmos/Metadata/Internal/CosmosAnnotationNames.cs +++ b/src/EFCore.Cosmos/Metadata/Internal/CosmosAnnotationNames.cs @@ -74,4 +74,20 @@ public static class CosmosAnnotationNames /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public const string Throughput = Prefix + "Throughput"; + + /// + /// 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 const string JsonIdDefinition = Prefix + "JsonIdDefinition"; + + /// + /// 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 const string ModelDependencies = Prefix + "ModelDependencies"; } diff --git a/src/EFCore.Cosmos/Metadata/Internal/CosmosEntityTypeExtensions.cs b/src/EFCore.Cosmos/Metadata/Internal/CosmosEntityTypeExtensions.cs index 45f84c93ef0..181f6aeee15 100644 --- a/src/EFCore.Cosmos/Metadata/Internal/CosmosEntityTypeExtensions.cs +++ b/src/EFCore.Cosmos/Metadata/Internal/CosmosEntityTypeExtensions.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.EntityFrameworkCore.Infrastructure.Internal; + namespace Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; /// @@ -21,4 +23,15 @@ public static bool IsDocumentRoot(this IReadOnlyEntityType entityType) => entityType.BaseType?.IsDocumentRoot() ?? (entityType.FindOwnership() == null || entityType[CosmosAnnotationNames.ContainerName] != null); + + /// + /// Returns the JSON `id` definition, or if there is none. + /// + /// The entity type. + public static IJsonIdDefinition? GetJsonIdDefinition(this IEntityType entityType) + => entityType.GetOrAddRuntimeAnnotationValue(CosmosAnnotationNames.JsonIdDefinition, + static e => + ((CosmosModelRuntimeInitializerDependencies)e!.Model.FindRuntimeAnnotationValue( + CosmosAnnotationNames.ModelDependencies)!).JsonIdDefinitionFactory.Create(e), + entityType); } diff --git a/src/EFCore.Cosmos/Metadata/Internal/IJsonIdDefinition.cs b/src/EFCore.Cosmos/Metadata/Internal/IJsonIdDefinition.cs new file mode 100644 index 00000000000..32078af8539 --- /dev/null +++ b/src/EFCore.Cosmos/Metadata/Internal/IJsonIdDefinition.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; + +/// +/// 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 interface IJsonIdDefinition +{ + /// + /// 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. + /// + IReadOnlyList Properties { 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 + /// 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. + /// + string GenerateIdString(EntityEntry entry); + + /// + /// 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. + /// + string GenerateIdString(IEnumerable values); +} diff --git a/src/EFCore.Cosmos/Metadata/Internal/IJsonIdDefinitionFactory.cs b/src/EFCore.Cosmos/Metadata/Internal/IJsonIdDefinitionFactory.cs new file mode 100644 index 00000000000..8436e89a8e0 --- /dev/null +++ b/src/EFCore.Cosmos/Metadata/Internal/IJsonIdDefinitionFactory.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; + +/// +/// 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 interface IJsonIdDefinitionFactory +{ + /// + /// 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. + /// + IJsonIdDefinition? Create(IEntityType entityType); +} diff --git a/src/EFCore.Cosmos/Metadata/Internal/JsonIdDefinition.cs b/src/EFCore.Cosmos/Metadata/Internal/JsonIdDefinition.cs new file mode 100644 index 00000000000..c94d957fcc7 --- /dev/null +++ b/src/EFCore.Cosmos/Metadata/Internal/JsonIdDefinition.cs @@ -0,0 +1,124 @@ +// 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 System.Text; + +namespace Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; + +/// +/// 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 class JsonIdDefinition : IJsonIdDefinition +{ + /// + /// 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 JsonIdDefinition(IReadOnlyList properties) + { + Properties = properties; + } + + /// + /// 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 IReadOnlyList Properties { 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 + /// 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 string GenerateIdString(EntityEntry entry) + => GenerateIdString(Properties.Select(p => entry.Property(p).CurrentValue)); + + /// + /// 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 string GenerateIdString(IEnumerable values) + { + var builder = new StringBuilder(); + var i = 0; + foreach (var value in values) + { + var property = Properties[i++]; + var converter = property.GetTypeMapping().Converter; + AppendString(builder, converter == null ? value : converter.ConvertToProvider(value)); + builder.Append('|'); + } + + builder.Remove(builder.Length - 1, 1); + + return builder.ToString(); + } + + /// + /// 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. + /// + protected virtual void AppendString(StringBuilder builder, object? propertyValue) + { + switch (propertyValue) + { + case string stringValue: + AppendEscape(builder, stringValue); + return; + case IEnumerable enumerable: + foreach (var item in enumerable) + { + AppendEscape(builder, item.ToString()!); + builder.Append('|'); + } + + return; + case DateTime dateTime: + AppendEscape(builder, dateTime.ToString("O")); + return; + default: + if (propertyValue == null) + { + builder.Append("null"); + } + else + { + AppendEscape(builder, propertyValue.ToString()!); + } + + return; + } + } + + /// + /// 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. + /// + protected virtual StringBuilder AppendEscape(StringBuilder builder, string stringValue) + { + var startingIndex = builder.Length; + return builder.Append(stringValue) + // We need this to avoid collisions with the value separator + .Replace("|", "^|", startingIndex, builder.Length - startingIndex) + // These are invalid characters, see https://docs.microsoft.com/dotnet/api/microsoft.azure.documents.resource.id + .Replace("/", "^2F", startingIndex, builder.Length - startingIndex) + .Replace("\\", "^5C", startingIndex, builder.Length - startingIndex) + .Replace("?", "^3F", startingIndex, builder.Length - startingIndex) + .Replace("#", "^23", startingIndex, builder.Length - startingIndex); + } +} diff --git a/src/EFCore.Cosmos/Metadata/Internal/JsonIdDefinitionFactory.cs b/src/EFCore.Cosmos/Metadata/Internal/JsonIdDefinitionFactory.cs new file mode 100644 index 00000000000..c404f9d5648 --- /dev/null +++ b/src/EFCore.Cosmos/Metadata/Internal/JsonIdDefinitionFactory.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; + +/// +/// 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 class JsonIdDefinitionFactory : IJsonIdDefinitionFactory +{ + /// + /// 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 IJsonIdDefinition? Create(IEntityType entityType) + { + var primaryKey = entityType.FindPrimaryKey(); + if (entityType.IsOwned() || primaryKey == null) + { + return null; + } + + // Remove properties that are also partition keys, since Cosmos handles those separately, and so they should not be in `id`. + var partitionKeyNames = entityType.GetPartitionKeyPropertyNames(); + var primaryKeyProperties = new List(primaryKey.Properties.Count); + foreach (var property in primaryKey.Properties) + { + if (!partitionKeyNames.Contains(property.Name)) + { + primaryKeyProperties.Add(property); + } + } + + var idProperty = entityType.GetProperties() + .FirstOrDefault(p => p.GetJsonPropertyName() == StoreKeyConvention.IdPropertyJsonName); + + var properties = new List(); + + // If the property mapped to the JSON id is simply the primary key, or is the primary key without partition keys, then use + // it directly. + if ((primaryKeyProperties.Count == 1 + && primaryKeyProperties[0] == idProperty) + || (primaryKey.Properties.Count == 1 + && primaryKey.Properties[0] == idProperty)) + { + properties.Add(idProperty); + } + + // Otherwise, if the property mapped to the JSON id doesn't have a generator, then we can't use ReadItem. + else if (idProperty != null && idProperty.GetValueGeneratorFactory() == null) + { + return null; + } + else + { + var discriminator = entityType.GetDiscriminatorValue(); + + // If the discriminator is not part of the primary key already, then add it to the Cosmos `id`. + if (discriminator != null) + { + var discriminatorProperty = entityType.FindDiscriminatorProperty(); + if (!primaryKey.Properties.Contains(discriminatorProperty)) + { + properties.Add(discriminatorProperty!); + } + } + + // Next add all primary key properties, except for those that are also partition keys, which were removed above. + foreach (var property in primaryKeyProperties) + { + properties.Add(property); + } + } + + return new JsonIdDefinition(properties); + } +} diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs index c42ae691446..5f017ec2e84 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore.Cosmos.Internal; +using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; @@ -92,8 +93,7 @@ protected CosmosQueryableMethodTranslatingExpressionVisitor( [return: NotNullIfNotNull(nameof(expression))] public override Expression? Visit(Expression? expression) { - if (_queryCompilationContext.QueryTrackingBehavior == QueryTrackingBehavior.TrackAll // Issue #33893 - && expression is MethodCallExpression + if (expression is MethodCallExpression { Method: { Name: nameof(Queryable.FirstOrDefault), IsGenericMethod: true }, Arguments: [MethodCallExpression innerMethodCall] @@ -110,7 +110,27 @@ protected CosmosQueryableMethodTranslatingExpressionVisitor( ] }) { - if (unaryExpression.Operand is LambdaExpression) + // Strip out Include and Convert expressions until we get to the parameter, or not. + var processing = unaryExpression.Operand; + while (true) + { + switch (processing) + { + case UnaryExpression { NodeType: ExpressionType.Quote or ExpressionType.Convert } q: + processing = q.Operand; + continue; + case LambdaExpression l: + processing = l.Body; + continue; + case IncludeExpression i: + processing = i.EntityExpression; + continue; + } + break; + } + + // If we are left with the ParameterExpression, then it's safe to use ReadItem. + if (processing is ParameterExpression) { innerMethodCall = innerInnerMethodCall; } @@ -132,19 +152,12 @@ protected CosmosQueryableMethodTranslatingExpressionVisitor( 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))) + && entityType.GetJsonIdDefinition() != null) { var propertyParameterList = queryProperties.Zip( parameterNames, @@ -159,18 +172,6 @@ protected CosmosQueryableMethodTranslatingExpressionVisitor( 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, diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.ReadItemQueryingEnumerable.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.ReadItemQueryingEnumerable.cs index be3267336af..ffe797e53c8 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.ReadItemQueryingEnumerable.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.ReadItemQueryingEnumerable.cs @@ -4,8 +4,8 @@ #nullable disable using System.Collections; -using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Internal; +using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; using Microsoft.EntityFrameworkCore.Internal; using Newtonsoft.Json.Linq; @@ -95,30 +95,37 @@ private bool TryGetPartitionKey(out PartitionKey partitionKeyValue) private bool TryGetResourceId(out string resourceId) { - var idProperty = _readItemInfo.EntityType.GetProperties() - .FirstOrDefault(p => p.GetJsonPropertyName() == StoreKeyConvention.IdPropertyJsonName); + var entityType = _readItemInfo.EntityType; + var jsonIdDefinition = entityType.GetJsonIdDefinition(); + Check.DebugAssert(jsonIdDefinition != null, + "Should not be using this enumerable if not using ReadItem, which needs an id definition."); - if (TryGetParameterValue(idProperty, out var value)) + var values = new List(jsonIdDefinition.Properties.Count); + foreach (var property in jsonIdDefinition.Properties) { - resourceId = GetString(idProperty, value); - - if (string.IsNullOrEmpty(resourceId)) + if (!TryGetParameterValue(property, out var value)) { - throw new InvalidOperationException(CosmosStrings.InvalidResourceId); + var discriminatorProperty = entityType.FindDiscriminatorProperty(); + if (discriminatorProperty == property) + { + value = entityType.GetDiscriminatorValue(); + } + else + { + Check.DebugFail("Parameters should cover all properties or we should not be using ReadItem."); + } } - return true; + values.Add(value); } - if (TryGenerateIdFromKeys(idProperty, out var generatedValue)) + resourceId = jsonIdDefinition.GenerateIdString(values); + if (string.IsNullOrEmpty(resourceId)) { - resourceId = GetString(idProperty, generatedValue); - - return true; + throw new InvalidOperationException(CosmosStrings.InvalidResourceId); } - resourceId = null; - return false; + return true; } private bool TryGetParameterValue(IProperty property, out object value) @@ -128,47 +135,9 @@ private bool TryGetParameterValue(IProperty property, out object value) && _cosmosQueryContext.ParameterValues.TryGetValue(parameterName, out value); } - private static string GetString(IProperty property, object value) - { - var converter = property.GetTypeMapping().Converter; - - return converter is null - ? (string)value - : (string)converter.ConvertToProvider(value); - } - - private bool TryGenerateIdFromKeys(IProperty idProperty, out object value) - { -#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, _readItemInfo.EntityType, entityInstance); - - foreach (var keyProperty in _readItemInfo.EntityType.FindPrimaryKey().Properties) - { - var property = _readItemInfo.EntityType.FindProperty(keyProperty.Name); - - if (TryGetParameterValue(property, out var parameterValue)) - { - internalEntityEntry[property] = parameterValue; - } - } - - internalEntityEntry.SetEntityState(EntityState.Added); - value = internalEntityEntry[idProperty]; - internalEntityEntry.SetEntityState(EntityState.Detached); - - 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; @@ -185,7 +154,6 @@ 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/Storage/Internal/CosmosDatabaseCreator.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs index 7b6382ce10e..e46852477e3 100644 --- a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs +++ b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs @@ -166,8 +166,8 @@ private IUpdateAdapter AddSeedData() { foreach (var targetSeed in entityType.GetSeedData()) { - updateAdapter.Model.FindEntityType(entityType.Name); - var entry = updateAdapter.CreateEntry(targetSeed, entityType); + var runtimeEntityType = updateAdapter.Model.FindEntityType(entityType.Name)!; + var entry = updateAdapter.CreateEntry(targetSeed, runtimeEntityType); entry.EntityState = EntityState.Added; } } diff --git a/src/EFCore.Cosmos/ValueGeneration/Internal/IdValueGenerator.cs b/src/EFCore.Cosmos/ValueGeneration/Internal/IdValueGenerator.cs index 4b7725d55d2..af1077fb736 100644 --- a/src/EFCore.Cosmos/ValueGeneration/Internal/IdValueGenerator.cs +++ b/src/EFCore.Cosmos/ValueGeneration/Internal/IdValueGenerator.cs @@ -1,8 +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.Collections; -using System.Text; +using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; namespace Microsoft.EntityFrameworkCore.Cosmos.ValueGeneration.Internal; @@ -39,88 +38,5 @@ public override bool GeneratesStableValues /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override object NextValue(EntityEntry entry) - { - var builder = new StringBuilder(); - var entityType = entry.Metadata; - - var primaryKey = entityType.FindPrimaryKey()!; - var discriminator = entityType.GetDiscriminatorValue(); - if (discriminator != null - && !primaryKey.Properties.Contains(entityType.FindDiscriminatorProperty())) - { - AppendString(builder, discriminator); - builder.Append('|'); - } - - var partitionKeyNames = entityType.GetPartitionKeyPropertyNames(); - foreach (var property in primaryKey.Properties) - { - if (partitionKeyNames.Contains(property.Name) - && primaryKey.Properties.Count > 1) - { - continue; - } - - var value = entry.Property(property).CurrentValue; - - var converter = property.GetTypeMapping().Converter; - if (converter != null) - { - value = converter.ConvertToProvider(value); - } - - AppendString(builder, value); - - builder.Append('|'); - } - - builder.Remove(builder.Length - 1, 1); - - return builder.ToString(); - } - - private static void AppendString(StringBuilder builder, object? propertyValue) - { - switch (propertyValue) - { - case string stringValue: - AppendEscape(builder, stringValue); - return; - case IEnumerable enumerable: - foreach (var item in enumerable) - { - AppendEscape(builder, item.ToString()!); - builder.Append('|'); - } - - return; - case DateTime dateTime: - AppendEscape(builder, dateTime.ToString("O")); - return; - default: - if (propertyValue == null) - { - builder.Append("null"); - } - else - { - AppendEscape(builder, propertyValue.ToString()!); - } - - return; - } - } - - private static StringBuilder AppendEscape(StringBuilder builder, string stringValue) - { - var startingIndex = builder.Length; - return builder.Append(stringValue) - // We need this to avoid collisions with the value separator - .Replace("|", "^|", startingIndex, builder.Length - startingIndex) - // These are invalid characters, see https://docs.microsoft.com/dotnet/api/microsoft.azure.documents.resource.id - .Replace("/", "^2F", startingIndex, builder.Length - startingIndex) - .Replace("\\", "^5C", startingIndex, builder.Length - startingIndex) - .Replace("?", "^3F", startingIndex, builder.Length - startingIndex) - .Replace("#", "^23", startingIndex, builder.Length - startingIndex); - } + => entry.Metadata.GetJsonIdDefinition()!.GenerateIdString(entry); } diff --git a/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilderDependencies.cs b/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilderDependencies.cs index 4772de906b2..bf436a5944f 100644 --- a/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilderDependencies.cs +++ b/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilderDependencies.cs @@ -112,7 +112,7 @@ public ProviderConventionSetBuilderDependencies( public IDbSetFinder SetFinder { get; init; } /// - /// The current context instance. + /// The type of the current context instance. /// public Type ContextType => _currentContext.Context.GetType(); diff --git a/test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs index 99e997bc69f..7ef100ca47a 100644 --- a/test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs @@ -4,6 +4,7 @@ using System.Collections.Immutable; using Microsoft.Azure.Cosmos; using Microsoft.EntityFrameworkCore.Cosmos.Internal; +using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; using Newtonsoft.Json.Linq; // ReSharper disable UnusedMember.Local @@ -1165,7 +1166,8 @@ public async Task Can_read_with_find_with_partition_key_and_value_generator_asyn { var contextFactory = await InitializeAsync( shouldLogCategory: _ => true, - onConfiguring: o => o.ConfigureWarnings(w => w.Log(CosmosEventId.SyncNotSupported))); + onConfiguring: o => o.ConfigureWarnings(w => w.Log(CosmosEventId.SyncNotSupported)), + addServices: s => s.AddSingleton()); const int pk1 = 1; const int pk2 = 2; @@ -1232,7 +1234,8 @@ public async Task Can_read_with_find_with_partition_key_and_value_generator() { var contextFactory = await InitializeAsync( shouldLogCategory: _ => true, - onConfiguring: o => o.ConfigureWarnings(w => w.Log(CosmosEventId.SyncNotSupported))); + onConfiguring: o => o.ConfigureWarnings(w => w.Log(CosmosEventId.SyncNotSupported)), + addServices: s => s.AddSingleton()); const int pk1 = 1; const int pk2 = 2; diff --git a/test/EFCore.Cosmos.FunctionalTests/ReadItemTest.cs b/test/EFCore.Cosmos.FunctionalTests/ReadItemTest.cs new file mode 100644 index 00000000000..d4f4e682f38 --- /dev/null +++ b/test/EFCore.Cosmos.FunctionalTests/ReadItemTest.cs @@ -0,0 +1,1277 @@ +// 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.Schema; + +namespace Microsoft.EntityFrameworkCore; + +public class ReadItemTest : IClassFixture +{ + public ReadItemTest(ReadItemFixture fixture) + { + Fixture = fixture; + fixture.TestSqlLoggerFactory.Clear(); + } + + protected ReadItemFixture Fixture { get; } + + [ConditionalTheory] // Issue #20693 + [InlineData(QueryTrackingBehavior.TrackAll)] + [InlineData(QueryTrackingBehavior.NoTracking)] + [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual async Task FirstOrDefault_int_key_constant_is_translated_to_ReadItem(QueryTrackingBehavior trackingBehavior) + { + using var context = CreateContext(); + var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior).FirstOrDefaultAsync(e => e.Id == 77); + + AssertSql( + """ +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "IntKey") AND (c["Id"] = 77)) +OFFSET 0 LIMIT 1 +"""); + + ValidateIntKeyValues(entity!); + } + + [ConditionalTheory] // Issue #20693 + [InlineData(QueryTrackingBehavior.TrackAll)] + [InlineData(QueryTrackingBehavior.NoTracking)] + [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual async Task FirstOrDefault_int_key_variable_is_translated_to_ReadItem(QueryTrackingBehavior trackingBehavior) + { + using var context = CreateContext(); + + var val = 77; + var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior).FirstOrDefaultAsync(e => e.Id == val); + + AssertSql( + """ +@__val_0='77' + +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "IntKey") AND (c["Id"] = @__val_0)) +OFFSET 0 LIMIT 1 +"""); + + ValidateIntKeyValues(entity!); + } + + [ConditionalTheory] // Issue #20693 + [InlineData(QueryTrackingBehavior.TrackAll)] + [InlineData(QueryTrackingBehavior.NoTracking)] + [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual async Task FirstOrDefault_int_key_constant_value_first_is_translated_to_ReadItem(QueryTrackingBehavior trackingBehavior) + { + using var context = CreateContext(); + var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior).FirstOrDefaultAsync(e => 77 == e.Id); + + AssertSql( + """ +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "IntKey") AND (77 = c["Id"])) +OFFSET 0 LIMIT 1 +"""); + + ValidateIntKeyValues(entity!); + } + + [ConditionalTheory] // Issue #20693 + [InlineData(QueryTrackingBehavior.TrackAll)] + [InlineData(QueryTrackingBehavior.NoTracking)] + [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual async Task FirstOrDefault_int_key_variable_value_first_is_translated_to_ReadItem(QueryTrackingBehavior trackingBehavior) + { + using var context = CreateContext(); + + var val = 77; + var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior).FirstOrDefaultAsync(e => val == e.Id); + + AssertSql( + """ +@__val_0='77' + +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "IntKey") AND (@__val_0 = c["Id"])) +OFFSET 0 LIMIT 1 +"""); + + ValidateIntKeyValues(entity!); + } + + [ConditionalTheory] // Issue #20693 + [InlineData(QueryTrackingBehavior.TrackAll)] + [InlineData(QueryTrackingBehavior.NoTracking)] + [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual async Task First_int_key_constant_is_translated_to_ReadItem(QueryTrackingBehavior trackingBehavior) + { + using var context = CreateContext(); + var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior).FirstAsync(e => e.Id == 77); + + AssertSql( + """ +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "IntKey") AND (c["Id"] = 77)) +OFFSET 0 LIMIT 1 +"""); + + AssertSql( + ); + } + + [ConditionalTheory] // Issue #20693 + [InlineData(QueryTrackingBehavior.TrackAll)] + [InlineData(QueryTrackingBehavior.NoTracking)] + [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual async Task First_int_key_variable_is_translated_to_ReadItem(QueryTrackingBehavior trackingBehavior) + { + using var context = CreateContext(); + + var val = 77; + var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior).FirstAsync(e => e.Id == val); + + AssertSql( + """ +@__val_0='77' + +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "IntKey") AND (c["Id"] = @__val_0)) +OFFSET 0 LIMIT 1 +"""); + + ValidateIntKeyValues(entity); + } + + [ConditionalTheory] // Issue #20693 + [InlineData(QueryTrackingBehavior.TrackAll)] + [InlineData(QueryTrackingBehavior.NoTracking)] + [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual async Task First_int_key_constant_value_first_is_translated_to_ReadItem(QueryTrackingBehavior trackingBehavior) + { + using var context = CreateContext(); + var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior).FirstAsync(e => 77 == e.Id); + + AssertSql( + """ +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "IntKey") AND (77 = c["Id"])) +OFFSET 0 LIMIT 1 +"""); + } + + [ConditionalTheory] // Issue #20693 + [InlineData(QueryTrackingBehavior.TrackAll)] + [InlineData(QueryTrackingBehavior.NoTracking)] + [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual async Task First_int_key_variable_value_first_is_translated_to_ReadItem(QueryTrackingBehavior trackingBehavior) + { + using var context = CreateContext(); + + var val = 77; + var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior).FirstAsync(e => val == e.Id); + + AssertSql( + """ +@__val_0='77' + +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "IntKey") AND (@__val_0 = c["Id"])) +OFFSET 0 LIMIT 1 +"""); + } + + [ConditionalTheory] // Issue #20693 + [InlineData(QueryTrackingBehavior.TrackAll)] + [InlineData(QueryTrackingBehavior.NoTracking)] + [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual async Task SingleOrDefault_int_key_constant_is_translated_to_ReadItem(QueryTrackingBehavior trackingBehavior) + { + using var context = CreateContext(); + var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior).SingleOrDefaultAsync(e => e.Id == 77); + + AssertSql( + """ +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "IntKey") AND (c["Id"] = 77)) +OFFSET 0 LIMIT 2 +"""); + + AssertSql( + ); + } + + [ConditionalTheory] // Issue #20693 + [InlineData(QueryTrackingBehavior.TrackAll)] + [InlineData(QueryTrackingBehavior.NoTracking)] + [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual async Task SingleOrDefault_int_key_variable_is_translated_to_ReadItem(QueryTrackingBehavior trackingBehavior) + { + using var context = CreateContext(); + + var val = 77; + var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior).SingleOrDefaultAsync(e => e.Id == val); + + AssertSql( + """ +@__val_0='77' + +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "IntKey") AND (c["Id"] = @__val_0)) +OFFSET 0 LIMIT 2 +"""); + + ValidateIntKeyValues(entity!); + } + + [ConditionalTheory] // Issue #20693 + [InlineData(QueryTrackingBehavior.TrackAll)] + [InlineData(QueryTrackingBehavior.NoTracking)] + [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual async Task SingleOrDefault_int_key_constant_value_first_is_translated_to_ReadItem(QueryTrackingBehavior trackingBehavior) + { + using var context = CreateContext(); + var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior).SingleOrDefaultAsync(e => 77 == e.Id); + + AssertSql( + """ +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "IntKey") AND (77 = c["Id"])) +OFFSET 0 LIMIT 2 +"""); + + ValidateIntKeyValues(entity!); + } + + [ConditionalTheory] // Issue #20693 + [InlineData(QueryTrackingBehavior.TrackAll)] + [InlineData(QueryTrackingBehavior.NoTracking)] + [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual async Task SingleOrDefault_int_key_variable_value_first_is_translated_to_ReadItem(QueryTrackingBehavior trackingBehavior) + { + using var context = CreateContext(); + + var val = 77; + var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior).SingleOrDefaultAsync(e => val == e.Id); + + AssertSql( + """ +@__val_0='77' + +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "IntKey") AND (@__val_0 = c["Id"])) +OFFSET 0 LIMIT 2 +"""); + + ValidateIntKeyValues(entity!); + } + + [ConditionalTheory] // Issue #20693 + [InlineData(QueryTrackingBehavior.TrackAll)] + [InlineData(QueryTrackingBehavior.NoTracking)] + [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual async Task Single_int_key_constant_is_translated_to_ReadItem(QueryTrackingBehavior trackingBehavior) + { + using var context = CreateContext(); + var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior).SingleAsync(e => e.Id == 77); + + AssertSql( + """ +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "IntKey") AND (c["Id"] = 77)) +OFFSET 0 LIMIT 2 +"""); + + ValidateIntKeyValues(entity); + } + + [ConditionalTheory] // Issue #20693 + [InlineData(QueryTrackingBehavior.TrackAll)] + [InlineData(QueryTrackingBehavior.NoTracking)] + [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual async Task Single_int_key_variable_is_translated_to_ReadItem(QueryTrackingBehavior trackingBehavior) + { + using var context = CreateContext(); + + var val = 77; + var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior).SingleAsync(e => e.Id == val); + + AssertSql( + """ +@__val_0='77' + +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "IntKey") AND (c["Id"] = @__val_0)) +OFFSET 0 LIMIT 2 +"""); + } + + [ConditionalTheory] // Issue #20693 + [InlineData(QueryTrackingBehavior.TrackAll)] + [InlineData(QueryTrackingBehavior.NoTracking)] + [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual async Task Single_int_key_constant_value_first_is_translated_to_ReadItem(QueryTrackingBehavior trackingBehavior) + { + using var context = CreateContext(); + var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior).SingleAsync(e => 77 == e.Id); + + AssertSql( + """ +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "IntKey") AND (77 = c["Id"])) +OFFSET 0 LIMIT 2 +"""); + + ValidateIntKeyValues(entity); + } + + [ConditionalTheory] // Issue #20693 + [InlineData(QueryTrackingBehavior.TrackAll)] + [InlineData(QueryTrackingBehavior.NoTracking)] + [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual async Task Single_int_key_variable_value_first_is_translated_to_ReadItem(QueryTrackingBehavior trackingBehavior) + { + using var context = CreateContext(); + + var val = 77; + var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior).SingleAsync(e => val == e.Id); + + AssertSql( + """ +@__val_0='77' + +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "IntKey") AND (@__val_0 = c["Id"])) +OFFSET 0 LIMIT 2 +"""); + + ValidateIntKeyValues(entity); + } + + [ConditionalTheory] // Issue #20693 + [InlineData(QueryTrackingBehavior.TrackAll)] + [InlineData(QueryTrackingBehavior.NoTracking)] + [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual async Task FirstOrDefault_int_key_constant_with_EF_Property_is_translated_to_ReadItem( + QueryTrackingBehavior trackingBehavior) + { + using var context = CreateContext(); + var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior) + .FirstOrDefaultAsync(e => EF.Property(e, nameof(IntKey.Id)) == 77); + + AssertSql( + """ +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "IntKey") AND (c["Id"] = 77)) +OFFSET 0 LIMIT 1 +"""); + + ValidateIntKeyValues(entity!); + } + + [ConditionalTheory] + [InlineData(QueryTrackingBehavior.TrackAll)] + [InlineData(QueryTrackingBehavior.NoTracking)] + [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual async Task FirstOrDefault_int_key_variable_with_EF_Property_is_translated_to_ReadItem( + QueryTrackingBehavior trackingBehavior) + { + using var context = CreateContext(); + + var val = 77; + var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior) + .FirstOrDefaultAsync(e => EF.Property(e, nameof(IntKey.Id)) == val); + + AssertSql( + """ +ReadItem(None, IntKey|77) +"""); + + ValidateIntKeyValues(entity!); + } + + [ConditionalTheory] // Issue #20693 + [InlineData(QueryTrackingBehavior.TrackAll)] + [InlineData(QueryTrackingBehavior.NoTracking)] + [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual async Task FirstOrDefault_int_key_constant_value_first_with_EF_Property_is_translated_to_ReadItem( + QueryTrackingBehavior trackingBehavior) + { + using var context = CreateContext(); + var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior) + .FirstOrDefaultAsync(e => 77 == EF.Property(e, nameof(IntKey.Id))); + + AssertSql( + """ +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "IntKey") AND (77 = c["Id"])) +OFFSET 0 LIMIT 1 +"""); + + ValidateIntKeyValues(entity!); + } + + [ConditionalTheory] // Issue #20693 + [InlineData(QueryTrackingBehavior.TrackAll)] + [InlineData(QueryTrackingBehavior.NoTracking)] + [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual async Task FirstOrDefault_int_key_variable_value_first_with_EF_Property_is_translated_to_ReadItem( + QueryTrackingBehavior trackingBehavior) + { + using var context = CreateContext(); + + var val = 77; + var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior) + .FirstOrDefaultAsync(e => val == EF.Property(e, nameof(IntKey.Id))); + + AssertSql( + """ +@__val_0='77' + +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "IntKey") AND (@__val_0 = c["Id"])) +OFFSET 0 LIMIT 1 +"""); + + ValidateIntKeyValues(entity!); + } + + [ConditionalTheory] // Issue #20693 + [InlineData(QueryTrackingBehavior.TrackAll)] + [InlineData(QueryTrackingBehavior.NoTracking)] + [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual async Task First_int_key_constant_with_EF_Property_is_translated_to_ReadItem(QueryTrackingBehavior trackingBehavior) + { + using var context = CreateContext(); + var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior) + .FirstAsync(e => EF.Property(e, nameof(IntKey.Id)) == 77); + + AssertSql( + """ +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "IntKey") AND (c["Id"] = 77)) +OFFSET 0 LIMIT 1 +"""); + + AssertSql( + ); + } + + [ConditionalTheory] // Issue #20693 + [InlineData(QueryTrackingBehavior.TrackAll)] + [InlineData(QueryTrackingBehavior.NoTracking)] + [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual async Task First_int_key_variable_with_EF_Property_is_translated_to_ReadItem(QueryTrackingBehavior trackingBehavior) + { + using var context = CreateContext(); + + var val = 77; + var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior) + .FirstAsync(e => EF.Property(e, nameof(IntKey.Id)) == val); + + AssertSql( + """ +@__val_0='77' + +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "IntKey") AND (c["Id"] = @__val_0)) +OFFSET 0 LIMIT 1 +"""); + + ValidateIntKeyValues(entity); + } + + [ConditionalTheory] // Issue #20693 + [InlineData(QueryTrackingBehavior.TrackAll)] + [InlineData(QueryTrackingBehavior.NoTracking)] + [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual async Task First_int_key_constant_value_first_with_EF_Property_is_translated_to_ReadItem( + QueryTrackingBehavior trackingBehavior) + { + using var context = CreateContext(); + var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior) + .FirstAsync(e => 77 == EF.Property(e, nameof(IntKey.Id))); + + AssertSql( + """ +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "IntKey") AND (77 = c["Id"])) +OFFSET 0 LIMIT 1 +"""); + } + + [ConditionalTheory] // Issue #20693 + [InlineData(QueryTrackingBehavior.TrackAll)] + [InlineData(QueryTrackingBehavior.NoTracking)] + [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual async Task First_int_key_variable_value_first_with_EF_Property_is_translated_to_ReadItem( + QueryTrackingBehavior trackingBehavior) + { + using var context = CreateContext(); + + var val = 77; + var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior) + .FirstAsync(e => val == EF.Property(e, nameof(IntKey.Id))); + + AssertSql( + """ +@__val_0='77' + +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "IntKey") AND (@__val_0 = c["Id"])) +OFFSET 0 LIMIT 1 +"""); + } + + [ConditionalTheory] // Issue #20693 + [InlineData(QueryTrackingBehavior.TrackAll)] + [InlineData(QueryTrackingBehavior.NoTracking)] + [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual async Task SingleOrDefault_int_key_constant_with_EF_Property_is_translated_to_ReadItem( + QueryTrackingBehavior trackingBehavior) + { + using var context = CreateContext(); + var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior) + .SingleOrDefaultAsync(e => EF.Property(e, nameof(IntKey.Id)) == 77); + + AssertSql( + """ +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "IntKey") AND (c["Id"] = 77)) +OFFSET 0 LIMIT 2 +"""); + + AssertSql( + ); + } + + [ConditionalTheory] // Issue #20693 + [InlineData(QueryTrackingBehavior.TrackAll)] + [InlineData(QueryTrackingBehavior.NoTracking)] + [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual async Task SingleOrDefault_int_key_variable_with_EF_Property_is_translated_to_ReadItem( + QueryTrackingBehavior trackingBehavior) + { + using var context = CreateContext(); + + var val = 77; + var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior) + .SingleOrDefaultAsync(e => EF.Property(e, nameof(IntKey.Id)) == val); + + AssertSql( + """ +@__val_0='77' + +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "IntKey") AND (c["Id"] = @__val_0)) +OFFSET 0 LIMIT 2 +"""); + + ValidateIntKeyValues(entity!); + } + + [ConditionalTheory] // Issue #20693 + [InlineData(QueryTrackingBehavior.TrackAll)] + [InlineData(QueryTrackingBehavior.NoTracking)] + [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual async Task SingleOrDefault_int_key_constant_value_first_with_EF_Property_is_translated_to_ReadItem( + QueryTrackingBehavior trackingBehavior) + { + using var context = CreateContext(); + var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior) + .SingleOrDefaultAsync(e => 77 == EF.Property(e, nameof(IntKey.Id))); + + AssertSql( + """ +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "IntKey") AND (77 = c["Id"])) +OFFSET 0 LIMIT 2 +"""); + + ValidateIntKeyValues(entity!); + } + + [ConditionalTheory] // Issue #20693 + [InlineData(QueryTrackingBehavior.TrackAll)] + [InlineData(QueryTrackingBehavior.NoTracking)] + [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual async Task SingleOrDefault_int_key_variable_value_first_with_EF_Property_is_translated_to_ReadItem( + QueryTrackingBehavior trackingBehavior) + { + using var context = CreateContext(); + + var val = 77; + var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior) + .SingleOrDefaultAsync(e => val == EF.Property(e, nameof(IntKey.Id))); + + AssertSql( + """ +@__val_0='77' + +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "IntKey") AND (@__val_0 = c["Id"])) +OFFSET 0 LIMIT 2 +"""); + + ValidateIntKeyValues(entity!); + } + + [ConditionalTheory] // Issue #20693 + [InlineData(QueryTrackingBehavior.TrackAll)] + [InlineData(QueryTrackingBehavior.NoTracking)] + [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual async Task Single_int_key_constant_with_EF_Property_is_translated_to_ReadItem(QueryTrackingBehavior trackingBehavior) + { + using var context = CreateContext(); + var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior) + .SingleAsync(e => EF.Property(e, nameof(IntKey.Id)) == 77); + + AssertSql( + """ +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "IntKey") AND (c["Id"] = 77)) +OFFSET 0 LIMIT 2 +"""); + + ValidateIntKeyValues(entity); + } + + [ConditionalTheory] // Issue #20693 + [InlineData(QueryTrackingBehavior.TrackAll)] + [InlineData(QueryTrackingBehavior.NoTracking)] + [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual async Task Single_int_key_variable_with_EF_Property_is_translated_to_ReadItem(QueryTrackingBehavior trackingBehavior) + { + using var context = CreateContext(); + + var val = 77; + var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior) + .SingleAsync(e => EF.Property(e, nameof(IntKey.Id)) == val); + + AssertSql( + """ +@__val_0='77' + +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "IntKey") AND (c["Id"] = @__val_0)) +OFFSET 0 LIMIT 2 +"""); + } + + [ConditionalTheory] // Issue #20693 + [InlineData(QueryTrackingBehavior.TrackAll)] + [InlineData(QueryTrackingBehavior.NoTracking)] + [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual async Task Single_int_key_constant_value_first_with_EF_Property_is_translated_to_ReadItem( + QueryTrackingBehavior trackingBehavior) + { + using var context = CreateContext(); + var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior) + .SingleAsync(e => 77 == EF.Property(e, nameof(IntKey.Id))); + + AssertSql( + """ +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "IntKey") AND (77 = c["Id"])) +OFFSET 0 LIMIT 2 +"""); + + ValidateIntKeyValues(entity); + } + + [ConditionalTheory] // Issue #20693 + [InlineData(QueryTrackingBehavior.TrackAll)] + [InlineData(QueryTrackingBehavior.NoTracking)] + [InlineData(QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual async Task Single_int_key_variable_value_first_with_EF_Property_is_translated_to_ReadItem( + QueryTrackingBehavior trackingBehavior) + { + using var context = CreateContext(); + + var val = 77; + var entity = await ApplyTrackingBehavior(context.Set(), trackingBehavior) + .SingleAsync(e => val == EF.Property(e, nameof(IntKey.Id))); + + AssertSql( + """ +@__val_0='77' + +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "IntKey") AND (@__val_0 = c["Id"])) +OFFSET 0 LIMIT 2 +"""); + + ValidateIntKeyValues(entity); + } + + private static void ValidateIntKeyValues(IntKey entity) + { + 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] + // [InlineData((int)CancellationType.Right)] + // [InlineData((int)CancellationType.Wrong)] + // [InlineData((int)CancellationType.None)] + // public virtual async Task Returns_null_for_int_key_not_in_store_async(CancellationType cancellationType) + // { + // using var context = CreateContext(); + // Assert.Null(await Finder.FindAsync(cancellationType, context, [99])); + // } + // + // [ConditionalTheory] + // [InlineData((int)CancellationType.Right)] + // [InlineData((int)CancellationType.Wrong)] + // [InlineData((int)CancellationType.None)] + // public virtual async Task Find_nullable_int_key_tracked_async(CancellationType cancellationType) + // { + // using var context = CreateContext(); + // var entity = context.Attach( + // new NullableIntKey { Id = 88 }).Entity; + // + // Assert.Same(entity, await Finder.FindAsync(cancellationType, context, [88])); + // } + // + // [ConditionalTheory] + // [InlineData((int)CancellationType.Right)] + // [InlineData((int)CancellationType.Wrong)] + // [InlineData((int)CancellationType.None)] + // public virtual async Task Find_nullable_int_key_from_store_async(CancellationType cancellationType) + // { + // using var context = CreateContext(); + // Assert.Equal("Smokey", (await Finder.FindAsync(cancellationType, context, [77])).Foo); + // } + // + // [ConditionalTheory] + // [InlineData((int)CancellationType.Right)] + // [InlineData((int)CancellationType.Wrong)] + // [InlineData((int)CancellationType.None)] + // public virtual async Task Returns_null_for_nullable_int_key_not_in_store_async(CancellationType cancellationType) + // { + // using var context = CreateContext(); + // Assert.Null(await Finder.FindAsync(cancellationType, context, [99])); + // } + // + // [ConditionalTheory] + // [InlineData((int)CancellationType.Right)] + // [InlineData((int)CancellationType.Wrong)] + // [InlineData((int)CancellationType.None)] + // public virtual async Task Find_string_key_tracked_async(CancellationType cancellationType) + // { + // using var context = CreateContext(); + // var entity = context.Attach( + // new StringKey { Id = "Rabbit" }).Entity; + // + // Assert.Same(entity, await Finder.FindAsync(cancellationType, context, ["Rabbit"])); + // } + // + // [ConditionalTheory] + // [InlineData((int)CancellationType.Right)] + // [InlineData((int)CancellationType.Wrong)] + // [InlineData((int)CancellationType.None)] + // public virtual async Task Find_string_key_from_store_async(CancellationType cancellationType) + // { + // using var context = CreateContext(); + // Assert.Equal("Alice", (await Finder.FindAsync(cancellationType, context, ["Cat"])).Foo); + // } + // + // [ConditionalTheory] + // [InlineData((int)CancellationType.Right)] + // [InlineData((int)CancellationType.Wrong)] + // [InlineData((int)CancellationType.None)] + // public virtual async Task Returns_null_for_string_key_not_in_store_async(CancellationType cancellationType) + // { + // using var context = CreateContext(); + // Assert.Null(await Finder.FindAsync(cancellationType, context, ["Fox"])); + // } + // + // [ConditionalTheory] + // [InlineData((int)CancellationType.Right)] + // [InlineData((int)CancellationType.Wrong)] + // [InlineData((int)CancellationType.None)] + // public virtual async Task Find_composite_key_tracked_async(CancellationType cancellationType) + // { + // using var context = CreateContext(); + // var entity = context.Attach( + // new CompositeKey { Id1 = 88, Id2 = "Rabbit" }).Entity; + // + // Assert.Same(entity, await Finder.FindAsync(cancellationType, context, [88, "Rabbit"])); + // } + // + // [ConditionalTheory] + // [InlineData((int)CancellationType.Right)] + // [InlineData((int)CancellationType.Wrong)] + // [InlineData((int)CancellationType.None)] + // public virtual async Task Find_composite_key_from_store_async(CancellationType cancellationType) + // { + // using var context = CreateContext(); + // Assert.Equal("Olive", (await Finder.FindAsync(cancellationType, context, [77, "Dog"])).Foo); + // } + // + // [ConditionalTheory] + // [InlineData((int)CancellationType.Right)] + // [InlineData((int)CancellationType.Wrong)] + // [InlineData((int)CancellationType.None)] + // public virtual async Task Returns_null_for_composite_key_not_in_store_async(CancellationType cancellationType) + // { + // using var context = CreateContext(); + // Assert.Null(await Finder.FindAsync(cancellationType, context, [77, "Fox"])); + // } + // + // [ConditionalTheory] + // [InlineData((int)CancellationType.Right)] + // [InlineData((int)CancellationType.Wrong)] + // [InlineData((int)CancellationType.None)] + // public virtual async Task Find_base_type_tracked_async(CancellationType cancellationType) + // { + // using var context = CreateContext(); + // var entity = context.Attach( + // new BaseType { Id = 88 }).Entity; + // + // Assert.Same(entity, await Finder.FindAsync(cancellationType, context, [88])); + // } + // + // [ConditionalTheory] + // [InlineData((int)CancellationType.Right)] + // [InlineData((int)CancellationType.Wrong)] + // [InlineData((int)CancellationType.None)] + // public virtual async Task Find_base_type_from_store_async(CancellationType cancellationType) + // { + // using var context = CreateContext(); + // Assert.Equal("Baxter", (await Finder.FindAsync(cancellationType, context, [77])).Foo); + // } + // + // [ConditionalTheory] + // [InlineData((int)CancellationType.Right)] + // [InlineData((int)CancellationType.Wrong)] + // [InlineData((int)CancellationType.None)] + // public virtual async Task Returns_null_for_base_type_not_in_store_async(CancellationType cancellationType) + // { + // using var context = CreateContext(); + // Assert.Null(await Finder.FindAsync(cancellationType, context, [99])); + // } + // + // [ConditionalTheory] + // [InlineData((int)CancellationType.Right)] + // [InlineData((int)CancellationType.Wrong)] + // [InlineData((int)CancellationType.None)] + // public virtual async Task Find_derived_type_tracked_async(CancellationType cancellationType) + // { + // using var context = CreateContext(); + // var entity = context.Attach( + // new DerivedType { Id = 88 }).Entity; + // + // Assert.Same(entity, await Finder.FindAsync(cancellationType, context, [88])); + // } + // + // [ConditionalTheory] + // [InlineData((int)CancellationType.Right)] + // [InlineData((int)CancellationType.Wrong)] + // [InlineData((int)CancellationType.None)] + // public virtual async Task Find_derived_type_from_store_async(CancellationType cancellationType) + // { + // using var context = CreateContext(); + // var derivedType = await Finder.FindAsync(cancellationType, context, [78]); + // Assert.Equal("Strawberry", derivedType.Foo); + // Assert.Equal("Cheesecake", derivedType.Boo); + // } + // + // [ConditionalTheory] + // [InlineData((int)CancellationType.Right)] + // [InlineData((int)CancellationType.Wrong)] + // [InlineData((int)CancellationType.None)] + // public virtual async Task Returns_null_for_derived_type_not_in_store_async(CancellationType cancellationType) + // { + // using var context = CreateContext(); + // Assert.Null(await Finder.FindAsync(cancellationType, context, [99])); + // } + // + // [ConditionalTheory] + // [InlineData((int)CancellationType.Right)] + // [InlineData((int)CancellationType.Wrong)] + // [InlineData((int)CancellationType.None)] + // public virtual async Task Find_base_type_using_derived_set_tracked_async(CancellationType cancellationType) + // { + // using var context = CreateContext(); + // context.Attach( + // new BaseType { Id = 88 }); + // + // Assert.Null(await Finder.FindAsync(cancellationType, context, [88])); + // } + // + // [ConditionalTheory] + // [InlineData((int)CancellationType.Right)] + // [InlineData((int)CancellationType.Wrong)] + // [InlineData((int)CancellationType.None)] + // public virtual async Task Find_base_type_using_derived_set_from_store_async(CancellationType cancellationType) + // { + // using var context = CreateContext(); + // Assert.Null(await Finder.FindAsync(cancellationType, context, [77])); + // } + // + // [ConditionalTheory] + // [InlineData((int)CancellationType.Right)] + // [InlineData((int)CancellationType.Wrong)] + // [InlineData((int)CancellationType.None)] + // public virtual async Task Find_derived_type_using_base_set_tracked_async(CancellationType cancellationType) + // { + // using var context = CreateContext(); + // var entity = context.Attach( + // new DerivedType { Id = 88 }).Entity; + // + // Assert.Same(entity, await Finder.FindAsync(cancellationType, context, [88])); + // } + // + // [ConditionalTheory] + // [InlineData((int)CancellationType.Right)] + // [InlineData((int)CancellationType.Wrong)] + // [InlineData((int)CancellationType.None)] + // public virtual async Task Find_derived_using_base_set_type_from_store_async(CancellationType cancellationType) + // { + // using var context = CreateContext(); + // var derivedType = await Finder.FindAsync(cancellationType, context, [78]); + // Assert.Equal("Strawberry", derivedType.Foo); + // Assert.Equal("Cheesecake", ((DerivedType)derivedType).Boo); + // } + // + // [ConditionalTheory] + // [InlineData((int)CancellationType.Right)] + // [InlineData((int)CancellationType.Wrong)] + // [InlineData((int)CancellationType.None)] + // public virtual async Task Find_shadow_key_tracked_async(CancellationType cancellationType) + // { + // using var context = CreateContext(); + // var entry = context.Entry(new ShadowKey()); + // entry.Property("Id").CurrentValue = 88; + // entry.State = EntityState.Unchanged; + // + // Assert.Same(entry.Entity, await Finder.FindAsync(cancellationType, context, [88])); + // } + // + // [ConditionalTheory] + // [InlineData((int)CancellationType.Right)] + // [InlineData((int)CancellationType.Wrong)] + // [InlineData((int)CancellationType.None)] + // public virtual async Task Find_shadow_key_from_store_async(CancellationType cancellationType) + // { + // using var context = CreateContext(); + // Assert.Equal("Clippy", (await Finder.FindAsync(cancellationType, context, [77])).Foo); + // } + // + // [ConditionalTheory] + // [InlineData((int)CancellationType.Right)] + // [InlineData((int)CancellationType.Wrong)] + // [InlineData((int)CancellationType.None)] + // public virtual async Task Returns_null_for_shadow_key_not_in_store_async(CancellationType cancellationType) + // { + // using var context = CreateContext(); + // Assert.Null(await Finder.FindAsync(cancellationType, context, [99])); + // } + // + // [ConditionalTheory] + // [InlineData((int)CancellationType.Right)] + // [InlineData((int)CancellationType.Wrong)] + // [InlineData((int)CancellationType.None)] + // public virtual async Task Returns_null_for_null_key_values_array_async(CancellationType cancellationType) + // { + // using var context = CreateContext(); + // Assert.Null(await Finder.FindAsync(cancellationType, context, null)); + // } + // + // [ConditionalTheory] + // [InlineData((int)CancellationType.Right)] + // [InlineData((int)CancellationType.Wrong)] + // [InlineData((int)CancellationType.None)] + // public virtual async Task Returns_null_for_null_key_async(CancellationType cancellationType) + // { + // using var context = CreateContext(); + // Assert.Null(await Finder.FindAsync(cancellationType, context, [null])); + // } + // + // [ConditionalTheory] + // [InlineData((int)CancellationType.Right)] + // [InlineData((int)CancellationType.Wrong)] + // [InlineData((int)CancellationType.None)] + // public virtual async Task Returns_null_for_null_in_composite_key_async(CancellationType cancellationType) + // { + // using var context = CreateContext(); + // Assert.Null(await Finder.FindAsync(cancellationType, context, [77, null])); + // } + // + // [ConditionalTheory] + // [InlineData((int)CancellationType.Right)] + // [InlineData((int)CancellationType.Wrong)] + // [InlineData((int)CancellationType.None)] + // public virtual async Task Throws_for_multiple_values_passed_for_simple_key_async(CancellationType cancellationType) + // { + // using var context = CreateContext(); + // Assert.Equal( + // CoreStrings.FindNotCompositeKey("IntKey", cancellationType == CancellationType.Wrong ? 3 : 2), + // (await Assert.ThrowsAsync( + // () => Finder.FindAsync(cancellationType, context, [77, 88]).AsTask())).Message); + // } + // + // [ConditionalTheory] + // [InlineData((int)CancellationType.Right)] + // [InlineData((int)CancellationType.Wrong)] + // [InlineData((int)CancellationType.None)] + // public virtual async Task Throws_for_wrong_number_of_values_for_composite_key_async(CancellationType cancellationType) + // { + // using var context = CreateContext(); + // Assert.Equal( + // cancellationType == CancellationType.Wrong + // ? CoreStrings.FindValueTypeMismatch(1, "CompositeKey", "CancellationToken", "string") + // : CoreStrings.FindValueCountMismatch("CompositeKey", 2, 1), + // (await Assert.ThrowsAsync( + // () => Finder.FindAsync(cancellationType, context, [77]).AsTask())).Message); + // } + // + // [ConditionalTheory] + // [InlineData((int)CancellationType.Right)] + // [InlineData((int)CancellationType.Wrong)] + // [InlineData((int)CancellationType.None)] + // public virtual async Task Throws_for_bad_type_for_simple_key_async(CancellationType cancellationType) + // { + // using var context = CreateContext(); + // Assert.Equal( + // CoreStrings.FindValueTypeMismatch(0, "IntKey", "string", "int"), + // (await Assert.ThrowsAsync( + // () => Finder.FindAsync(cancellationType, context, ["77"]).AsTask())).Message); + // } + // + // [ConditionalTheory] + // [InlineData((int)CancellationType.Right)] + // [InlineData((int)CancellationType.Wrong)] + // [InlineData((int)CancellationType.None)] + // public virtual async Task Throws_for_bad_type_for_composite_key_async(CancellationType cancellationType) + // { + // using var context = CreateContext(); + // Assert.Equal( + // CoreStrings.FindValueTypeMismatch(1, "CompositeKey", "int", "string"), + // (await Assert.ThrowsAsync( + // () => Finder.FindAsync(cancellationType, context, [77, 78]).AsTask())).Message); + // } + // + // [ConditionalTheory] + // [InlineData((int)CancellationType.Right)] + // [InlineData((int)CancellationType.Wrong)] + // [InlineData((int)CancellationType.None)] + // public virtual async Task Throws_for_bad_entity_type_async(CancellationType cancellationType) + // { + // using var context = CreateContext(); + // Assert.Equal( + // CoreStrings.InvalidSetType(nameof(Random)), + // (await Assert.ThrowsAsync( + // () => Finder.FindAsync(cancellationType, context, [77]).AsTask())).Message); + // } + // + // [ConditionalTheory] + // [InlineData((int)CancellationType.Right)] + // [InlineData((int)CancellationType.Wrong)] + // [InlineData((int)CancellationType.None)] + // public virtual async Task Throws_for_bad_entity_type_with_different_namespace_async(CancellationType cancellationType) + // { + // using var context = CreateContext(); + // + // Assert.Equal( + // CoreStrings.InvalidSetSameTypeWithDifferentNamespace( + // typeof(DifferentNamespace.ShadowKey).DisplayName(), typeof(ShadowKey).DisplayName()), + // (await Assert.ThrowsAsync( + // () => Finder.FindAsync(cancellationType, context, [77]).AsTask())) + // .Message); + // } + + public enum CancellationType + { + Right, + Wrong, + None + } + + protected class BaseType + { + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int Id { get; set; } + + public string? Foo { get; set; } + } + + protected class DerivedType : BaseType + { + public string? Boo { get; set; } + } + + protected class IntKey + { + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int Id { get; set; } + + public string? Foo { get; set; } + + public Owned1 OwnedReference { get; set; } = null!; + public List OwnedCollection { get; set; } = null!; + } + + protected class NullableIntKey + { + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int? Id { get; set; } + + public string? Foo { get; set; } + } + + protected class StringKey + { + public string Id { get; set; } = null!; + + public string? Foo { get; set; } + } + + protected class CompositeKey + { + public int Id1 { get; set; } + public string Id2 { get; set; } = null!; + public string? Foo { get; set; } + } + + protected class ShadowKey + { + public string? Foo { get; set; } + } + + [Owned] + protected class Owned1 + { + public int Prop { get; set; } + public Owned2 NestedOwned { get; set; } = null!; + public List NestedOwnedCollection { get; set; } = null!; + } + + [Owned] + protected class Owned2 + { + public string Prop { get; set; } = null!; + } + + protected DbContext CreateContext() + => Fixture.CreateContext(); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + private static IQueryable ApplyTrackingBehavior(IQueryable query, QueryTrackingBehavior trackingBehavior) + { + query = trackingBehavior switch + { + QueryTrackingBehavior.TrackAll => query, + QueryTrackingBehavior.NoTracking => query.AsNoTracking(), + QueryTrackingBehavior.NoTrackingWithIdentityResolution => query.AsNoTrackingWithIdentityResolution(), + _ => throw new ArgumentOutOfRangeException(nameof(trackingBehavior), trackingBehavior, null) + }; + return query; + } + + public class ReadItemFixture : SharedStoreFixtureBase + { + protected override string StoreName + => "ReadItemTest"; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + modelBuilder.Entity(); + modelBuilder.Entity(); + modelBuilder.Entity(); + modelBuilder.Entity().HasKey( + e => new { e.Id1, e.Id2 }); + modelBuilder.Entity(); + modelBuilder.Entity(); + modelBuilder.Entity().Property(typeof(int), "Id").ValueGeneratedNever(); + } + + protected override Task SeedAsync(PoolableDbContext context) + { + context.AddRange( + 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 + { + Id1 = 77, + Id2 = "Dog", + Foo = "Olive" + }, + new BaseType { Id = 77, Foo = "Baxter" }, + new DerivedType + { + Id = 78, + Foo = "Strawberry", + Boo = "Cheesecake" + }); + + var entry = context.Entry( + new ShadowKey { Foo = "Clippy" }); + entry.Property("Id").CurrentValue = 77; + entry.State = EntityState.Added; + + return context.SaveChangesAsync(); + } + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + => base.AddOptions(builder).ConfigureWarnings(w => w.Ignore(CosmosEventId.NoPartitionKeyDefined)); + + protected override ITestStoreFactory TestStoreFactory + => CosmosTestStoreFactory.Instance; + } +} diff --git a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CustomRuntimeJsonIdDefinition.cs b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CustomRuntimeJsonIdDefinition.cs new file mode 100644 index 00000000000..af82dc44289 --- /dev/null +++ b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CustomRuntimeJsonIdDefinition.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; + +namespace Microsoft.EntityFrameworkCore.TestUtilities; + +public class CustomJsonIdDefinition(IReadOnlyList properties) + : JsonIdDefinition(properties) +{ + public override string GenerateIdString(IEnumerable values) + { + var id = base.GenerateIdString(values); + return id.Replace('|', '-'); + } +} diff --git a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CustomRuntimeJsonIdDefinitionFactory.cs b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CustomRuntimeJsonIdDefinitionFactory.cs new file mode 100644 index 00000000000..cab262e21af --- /dev/null +++ b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CustomRuntimeJsonIdDefinitionFactory.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; + +namespace Microsoft.EntityFrameworkCore.TestUtilities; + +public class CustomJsonIdDefinitionFactory : JsonIdDefinitionFactory +{ + public override IJsonIdDefinition? Create(IEntityType entityType) + => new CustomJsonIdDefinition(base.Create(entityType)!.Properties); +}