From 2e079da60b16f4d8c19478b4149bb9a1c9fe759e Mon Sep 17 00:00:00 2001 From: Arthur Vickers Date: Thu, 6 Jun 2024 11:34:52 +0100 Subject: [PATCH 1/3] Support ReadItem for no-tracking queries Part of #20693 Part of #33893 There is a lot left to do here, but I'm making a break here to get reviews before it goes too far. Major changes here are: - Discover and record properties used to form the JSON `id` in one place. - Use this to generate ate `id` values without tracking an instance. (Makes no-tracking work, needed for Reload.) - Be better at detecting only detecting patterns we can later translate. Next up: be better at detecting non-Find query patterns that we can translate. --- ...mosCSharpRuntimeAnnotationCodeGenerator.cs | 1 + .../CosmosServiceCollectionExtensions.cs | 2 + .../CosmosRuntimeModelConvention.cs | 14 +- .../Internal/CosmosConventionSetBuilder.cs | 11 +- .../Metadata/Conventions/JsonIdConvention.cs | 99 ++ .../Conventions/StoreKeyConvention.cs | 3 +- .../Internal/CosmosAnnotationNames.cs | 8 + .../IRuntimeJsonIdDefinitionFactory.cs | 21 + .../Metadata/Internal/JsonIdDefinition.cs | 32 + .../Internal/RuntimeJsonIdDefinition.cs | 130 ++ .../RuntimeJsonIdDefinitionFactory.cs | 22 + ...yableMethodTranslatingExpressionVisitor.cs | 47 +- ...ssionVisitor.ReadItemQueryingEnumerable.cs | 79 +- .../Storage/Internal/CosmosDatabaseCreator.cs | 4 +- .../Internal/IdValueGenerator.cs | 90 +- ...roviderConventionSetBuilderDependencies.cs | 2 +- .../EndToEndCosmosTest.cs | 7 +- .../ReadItemTest.cs | 1277 +++++++++++++++++ .../CustomRuntimeJsonIdDefinition.cs | 16 + .../CustomRuntimeJsonIdDefinitionFactory.cs | 12 + 20 files changed, 1705 insertions(+), 172 deletions(-) create mode 100644 src/EFCore.Cosmos/Metadata/Conventions/JsonIdConvention.cs create mode 100644 src/EFCore.Cosmos/Metadata/Internal/IRuntimeJsonIdDefinitionFactory.cs create mode 100644 src/EFCore.Cosmos/Metadata/Internal/JsonIdDefinition.cs create mode 100644 src/EFCore.Cosmos/Metadata/Internal/RuntimeJsonIdDefinition.cs create mode 100644 src/EFCore.Cosmos/Metadata/Internal/RuntimeJsonIdDefinitionFactory.cs create mode 100644 test/EFCore.Cosmos.FunctionalTests/ReadItemTest.cs create mode 100644 test/EFCore.Cosmos.FunctionalTests/TestUtilities/CustomRuntimeJsonIdDefinition.cs create mode 100644 test/EFCore.Cosmos.FunctionalTests/TestUtilities/CustomRuntimeJsonIdDefinitionFactory.cs diff --git a/src/EFCore.Cosmos/Design/Internal/CosmosCSharpRuntimeAnnotationCodeGenerator.cs b/src/EFCore.Cosmos/Design/Internal/CosmosCSharpRuntimeAnnotationCodeGenerator.cs index fbea3aeaada..a3feae2f0c8 100644 --- a/src/EFCore.Cosmos/Design/Internal/CosmosCSharpRuntimeAnnotationCodeGenerator.cs +++ b/src/EFCore.Cosmos/Design/Internal/CosmosCSharpRuntimeAnnotationCodeGenerator.cs @@ -48,6 +48,7 @@ public override void Generate(IEntityType entityType, CSharpRuntimeAnnotationCod annotations.Remove(CosmosAnnotationNames.AnalyticalStoreTimeToLive); annotations.Remove(CosmosAnnotationNames.DefaultTimeToLive); annotations.Remove(CosmosAnnotationNames.Throughput); + annotations.Remove(CosmosAnnotationNames.JsonIdDefinition); } base.Generate(entityType, parameters); diff --git a/src/EFCore.Cosmos/Extensions/CosmosServiceCollectionExtensions.cs b/src/EFCore.Cosmos/Extensions/CosmosServiceCollectionExtensions.cs index 8a93cf5cb1f..b78a0c6ca7b 100644 --- a/src/EFCore.Cosmos/Extensions/CosmosServiceCollectionExtensions.cs +++ b/src/EFCore.Cosmos/Extensions/CosmosServiceCollectionExtensions.cs @@ -5,6 +5,7 @@ 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; @@ -115,6 +116,7 @@ public static IServiceCollection AddEntityFrameworkCosmos(this IServiceCollectio .TryAddSingleton() .TryAddSingleton() .TryAddSingleton() + .TryAddSingleton() .TryAddScoped() .TryAddScoped() .TryAddScoped() diff --git a/src/EFCore.Cosmos/Metadata/Conventions/CosmosRuntimeModelConvention.cs b/src/EFCore.Cosmos/Metadata/Conventions/CosmosRuntimeModelConvention.cs index 717145097dc..0bc99973a37 100644 --- a/src/EFCore.Cosmos/Metadata/Conventions/CosmosRuntimeModelConvention.cs +++ b/src/EFCore.Cosmos/Metadata/Conventions/CosmosRuntimeModelConvention.cs @@ -15,14 +15,19 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; /// public class CosmosRuntimeModelConvention : RuntimeModelConvention { + private readonly IRuntimeJsonIdDefinitionFactory _runtimeJsonIdDefinitionFactory; + /// /// Creates a new instance of . /// /// Parameter object containing dependencies for this convention. + /// A factory for creating instance. public CosmosRuntimeModelConvention( - ProviderConventionSetBuilderDependencies dependencies) + ProviderConventionSetBuilderDependencies dependencies, + IRuntimeJsonIdDefinitionFactory runtimeJsonIdDefinitionFactory) : base(dependencies) { + _runtimeJsonIdDefinitionFactory = runtimeJsonIdDefinitionFactory; } /// @@ -67,5 +72,12 @@ protected override void ProcessEntityTypeAnnotations( annotations.Remove(CosmosAnnotationNames.DefaultTimeToLive); annotations.Remove(CosmosAnnotationNames.Throughput); } + + if (annotations.TryGetAndRemove(CosmosAnnotationNames.JsonIdDefinition, out JsonIdDefinition jsonId)) + { + runtimeEntityType.AddRuntimeAnnotation( + CosmosAnnotationNames.JsonIdDefinition, + _runtimeJsonIdDefinitionFactory.Create(runtimeEntityType, jsonId)); + } } } diff --git a/src/EFCore.Cosmos/Metadata/Conventions/Internal/CosmosConventionSetBuilder.cs b/src/EFCore.Cosmos/Metadata/Conventions/Internal/CosmosConventionSetBuilder.cs index 46e8eeda1a6..e460eabe21a 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; /// @@ -11,6 +13,8 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Metadata.Conventions.Internal; /// public class CosmosConventionSetBuilder : ProviderConventionSetBuilder { + private readonly IRuntimeJsonIdDefinitionFactory _runtimeJsonIdDefinitionFactory; + /// /// 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 @@ -18,9 +22,11 @@ public class CosmosConventionSetBuilder : ProviderConventionSetBuilder /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public CosmosConventionSetBuilder( - ProviderConventionSetBuilderDependencies dependencies) + ProviderConventionSetBuilderDependencies dependencies, + IRuntimeJsonIdDefinitionFactory runtimeJsonIdDefinitionFactory) : base(dependencies) { + _runtimeJsonIdDefinitionFactory = runtimeJsonIdDefinitionFactory; } /// @@ -36,6 +42,7 @@ public override ConventionSet CreateConventionSet() conventionSet.Add(new ContextContainerConvention(Dependencies)); conventionSet.Add(new ETagPropertyConvention()); conventionSet.Add(new StoreKeyConvention(Dependencies)); + conventionSet.Add(new JsonIdConvention(Dependencies)); conventionSet.Replace(new CosmosValueGenerationConvention(Dependencies)); conventionSet.Replace(new CosmosKeyDiscoveryConvention(Dependencies)); @@ -43,7 +50,7 @@ public override ConventionSet CreateConventionSet() conventionSet.Replace(new CosmosRelationshipDiscoveryConvention(Dependencies)); conventionSet.Replace(new CosmosDiscriminatorConvention(Dependencies)); conventionSet.Replace(new CosmosManyToManyJoinEntityTypeConvention(Dependencies)); - conventionSet.Replace(new CosmosRuntimeModelConvention(Dependencies)); + conventionSet.Replace(new CosmosRuntimeModelConvention(Dependencies, _runtimeJsonIdDefinitionFactory)); return conventionSet; } diff --git a/src/EFCore.Cosmos/Metadata/Conventions/JsonIdConvention.cs b/src/EFCore.Cosmos/Metadata/Conventions/JsonIdConvention.cs new file mode 100644 index 00000000000..eb1c68e9229 --- /dev/null +++ b/src/EFCore.Cosmos/Metadata/Conventions/JsonIdConvention.cs @@ -0,0 +1,99 @@ +// 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; + +/// +/// A convention that builds an for each top-level entity type. This is used +/// to build JSON `id` property values from combinations of other property values. +/// +/// +/// See Model building conventions, and +/// Accessing Azure Cosmos DB with EF Core for more information and examples. +/// +public class JsonIdConvention : IModelFinalizingConvention +{ + /// + /// Creates a new instance of . + /// + /// Parameter object containing dependencies for this convention. + public JsonIdConvention(ProviderConventionSetBuilderDependencies dependencies) + { + Dependencies = dependencies; + } + + /// + /// Dependencies for this service. + /// + protected virtual ProviderConventionSetBuilderDependencies Dependencies { get; } + + /// + public virtual void ProcessModelFinalizing( + IConventionModelBuilder modelBuilder, + IConventionContext context) + { + foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()) + { + var primaryKey = entityType.FindPrimaryKey(); + if (entityType.IsOwned() || primaryKey == null) + { + entityType.RemoveAnnotation(CosmosAnnotationNames.JsonIdDefinition); + continue; + } + + // 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) + { + entityType.RemoveAnnotation(CosmosAnnotationNames.JsonIdDefinition); + continue; + } + 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); + } + } + + entityType.SetAnnotation(CosmosAnnotationNames.JsonIdDefinition, new JsonIdDefinition(properties)); + } + } +} 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..e9cf959d70d 100644 --- a/src/EFCore.Cosmos/Metadata/Internal/CosmosAnnotationNames.cs +++ b/src/EFCore.Cosmos/Metadata/Internal/CosmosAnnotationNames.cs @@ -74,4 +74,12 @@ 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"; } diff --git a/src/EFCore.Cosmos/Metadata/Internal/IRuntimeJsonIdDefinitionFactory.cs b/src/EFCore.Cosmos/Metadata/Internal/IRuntimeJsonIdDefinitionFactory.cs new file mode 100644 index 00000000000..585a980ee56 --- /dev/null +++ b/src/EFCore.Cosmos/Metadata/Internal/IRuntimeJsonIdDefinitionFactory.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 IRuntimeJsonIdDefinitionFactory +{ + /// + /// 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. + /// + RuntimeJsonIdDefinition Create(RuntimeEntityType entityType, JsonIdDefinition jsonIdDefinition); +} diff --git a/src/EFCore.Cosmos/Metadata/Internal/JsonIdDefinition.cs b/src/EFCore.Cosmos/Metadata/Internal/JsonIdDefinition.cs new file mode 100644 index 00000000000..e857f751c82 --- /dev/null +++ b/src/EFCore.Cosmos/Metadata/Internal/JsonIdDefinition.cs @@ -0,0 +1,32 @@ +// 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 readonly struct 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 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 JsonIdDefinition(IReadOnlyList properties) + { + Properties = properties; + } +} diff --git a/src/EFCore.Cosmos/Metadata/Internal/RuntimeJsonIdDefinition.cs b/src/EFCore.Cosmos/Metadata/Internal/RuntimeJsonIdDefinition.cs new file mode 100644 index 00000000000..4c300d84aa8 --- /dev/null +++ b/src/EFCore.Cosmos/Metadata/Internal/RuntimeJsonIdDefinition.cs @@ -0,0 +1,130 @@ +// 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 RuntimeJsonIdDefinition +{ + /// + /// 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 RuntimeJsonIdDefinition(RuntimeEntityType entityType, JsonIdDefinition jsonIdDefinition) + { + var properties = new List(jsonIdDefinition.Properties.Count); + foreach (var conventionProperty in jsonIdDefinition.Properties) + { + properties.Add(entityType.FindProperty(conventionProperty.Name)!); + } + + 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/RuntimeJsonIdDefinitionFactory.cs b/src/EFCore.Cosmos/Metadata/Internal/RuntimeJsonIdDefinitionFactory.cs new file mode 100644 index 00000000000..20d09b68300 --- /dev/null +++ b/src/EFCore.Cosmos/Metadata/Internal/RuntimeJsonIdDefinitionFactory.cs @@ -0,0 +1,22 @@ +// 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 RuntimeJsonIdDefinitionFactory : IRuntimeJsonIdDefinitionFactory +{ + /// + /// 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 RuntimeJsonIdDefinition Create(RuntimeEntityType entityType, JsonIdDefinition jsonIdDefinition) + => new(entityType, jsonIdDefinition); +} diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs index c42ae691446..4280f960ecc 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.FindRuntimeAnnotation(CosmosAnnotationNames.JsonIdDefinition) != 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..8ae8f3a9bac 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,40 @@ private bool TryGetPartitionKey(out PartitionKey partitionKeyValue) private bool TryGetResourceId(out string resourceId) { - var idProperty = _readItemInfo.EntityType.GetProperties() - .FirstOrDefault(p => p.GetJsonPropertyName() == StoreKeyConvention.IdPropertyJsonName); - - if (TryGetParameterValue(idProperty, out var value)) + var entityType = _readItemInfo.EntityType; + var jsonIdDefinition = (RuntimeJsonIdDefinition)entityType.FindRuntimeAnnotation(CosmosAnnotationNames.JsonIdDefinition)?.Value; + if (jsonIdDefinition == null) { - resourceId = GetString(idProperty, value); + resourceId = null; + return false; + } - if (string.IsNullOrEmpty(resourceId)) + var values = new List(jsonIdDefinition.Properties.Count); + foreach (var property in jsonIdDefinition.Properties) + { + 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 +138,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 +157,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..45bbb9cd413 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,7 @@ 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); - } + => ((RuntimeJsonIdDefinition)entry + .Metadata + .FindRuntimeAnnotation(CosmosAnnotationNames.JsonIdDefinition)!.Value!).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..43ff0b9c246 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..5d5b5b0a17e --- /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 CustomRuntimeJsonIdDefinition(RuntimeEntityType entityType, JsonIdDefinition jsonIdDefinition) + : RuntimeJsonIdDefinition(entityType, jsonIdDefinition) +{ + 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..011fe9b7d60 --- /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 CustomRuntimeJsonIdDefinitionFactory : RuntimeJsonIdDefinitionFactory +{ + public override RuntimeJsonIdDefinition Create(RuntimeEntityType entityType, JsonIdDefinition jsonIdDefinition) + => new CustomRuntimeJsonIdDefinition(entityType, jsonIdDefinition); +} From df601f34a496ad02e5c4ea0c042423fa703d3389 Mon Sep 17 00:00:00 2001 From: Arthur Vickers Date: Fri, 14 Jun 2024 17:59:54 +0100 Subject: [PATCH 2/3] Updated version, based on Andriy's comment. --- ...mosCSharpRuntimeAnnotationCodeGenerator.cs | 5 +- .../CosmosServiceCollectionExtensions.cs | 8 +- .../Internal/CosmosModelRuntimeInitializer.cs | 51 +++++++ ...smosModelRuntimeInitializerDependencies.cs | 43 ++++++ .../CosmosRuntimeModelConvention.cs | 15 +- .../Internal/CosmosConventionSetBuilder.cs | 10 +- .../Metadata/Conventions/JsonIdConvention.cs | 99 ------------- .../Internal/CosmosAnnotationNames.cs | 8 ++ .../Internal/CosmosEntityTypeExtensions.cs | 14 ++ .../Metadata/Internal/IJsonIdDefinition.cs | 37 +++++ ...Factory.cs => IJsonIdDefinitionFactory.cs} | 4 +- .../Metadata/Internal/JsonIdDefinition.cs | 100 +++++++++++++- .../Internal/JsonIdDefinitionFactory.cs | 82 +++++++++++ .../Internal/RuntimeJsonIdDefinition.cs | 130 ------------------ .../RuntimeJsonIdDefinitionFactory.cs | 22 --- ...yableMethodTranslatingExpressionVisitor.cs | 2 +- ...ssionVisitor.ReadItemQueryingEnumerable.cs | 9 +- .../Internal/IdValueGenerator.cs | 4 +- .../EndToEndCosmosTest.cs | 4 +- .../CustomRuntimeJsonIdDefinition.cs | 4 +- .../CustomRuntimeJsonIdDefinitionFactory.cs | 6 +- 21 files changed, 357 insertions(+), 300 deletions(-) create mode 100644 src/EFCore.Cosmos/Infrastructure/Internal/CosmosModelRuntimeInitializer.cs create mode 100644 src/EFCore.Cosmos/Infrastructure/Internal/CosmosModelRuntimeInitializerDependencies.cs delete mode 100644 src/EFCore.Cosmos/Metadata/Conventions/JsonIdConvention.cs create mode 100644 src/EFCore.Cosmos/Metadata/Internal/IJsonIdDefinition.cs rename src/EFCore.Cosmos/Metadata/Internal/{IRuntimeJsonIdDefinitionFactory.cs => IJsonIdDefinitionFactory.cs} (88%) create mode 100644 src/EFCore.Cosmos/Metadata/Internal/JsonIdDefinitionFactory.cs delete mode 100644 src/EFCore.Cosmos/Metadata/Internal/RuntimeJsonIdDefinition.cs delete mode 100644 src/EFCore.Cosmos/Metadata/Internal/RuntimeJsonIdDefinitionFactory.cs diff --git a/src/EFCore.Cosmos/Design/Internal/CosmosCSharpRuntimeAnnotationCodeGenerator.cs b/src/EFCore.Cosmos/Design/Internal/CosmosCSharpRuntimeAnnotationCodeGenerator.cs index a3feae2f0c8..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); } @@ -48,7 +52,6 @@ public override void Generate(IEntityType entityType, CSharpRuntimeAnnotationCod annotations.Remove(CosmosAnnotationNames.AnalyticalStoreTimeToLive); annotations.Remove(CosmosAnnotationNames.DefaultTimeToLive); annotations.Remove(CosmosAnnotationNames.Throughput); - annotations.Remove(CosmosAnnotationNames.JsonIdDefinition); } base.Generate(entityType, parameters); diff --git a/src/EFCore.Cosmos/Extensions/CosmosServiceCollectionExtensions.cs b/src/EFCore.Cosmos/Extensions/CosmosServiceCollectionExtensions.cs index b78a0c6ca7b..6169e54ca44 100644 --- a/src/EFCore.Cosmos/Extensions/CosmosServiceCollectionExtensions.cs +++ b/src/EFCore.Cosmos/Extensions/CosmosServiceCollectionExtensions.cs @@ -9,6 +9,7 @@ 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; @@ -100,6 +101,7 @@ public static IServiceCollection AddEntityFrameworkCosmos(this IServiceCollectio .TryAdd() .TryAdd() .TryAdd() + .TryAdd() .TryAdd() .TryAdd() .TryAdd() @@ -116,12 +118,12 @@ public static IServiceCollection AddEntityFrameworkCosmos(this IServiceCollectio .TryAddSingleton() .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..e2bd35dfd11 --- /dev/null +++ b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosModelRuntimeInitializer.cs @@ -0,0 +1,51 @@ +// 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); + 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 0bc99973a37..0f37e0a9cf0 100644 --- a/src/EFCore.Cosmos/Metadata/Conventions/CosmosRuntimeModelConvention.cs +++ b/src/EFCore.Cosmos/Metadata/Conventions/CosmosRuntimeModelConvention.cs @@ -15,19 +15,13 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; /// public class CosmosRuntimeModelConvention : RuntimeModelConvention { - private readonly IRuntimeJsonIdDefinitionFactory _runtimeJsonIdDefinitionFactory; - /// /// Creates a new instance of . /// /// Parameter object containing dependencies for this convention. - /// A factory for creating instance. - public CosmosRuntimeModelConvention( - ProviderConventionSetBuilderDependencies dependencies, - IRuntimeJsonIdDefinitionFactory runtimeJsonIdDefinitionFactory) + public CosmosRuntimeModelConvention(ProviderConventionSetBuilderDependencies dependencies) : base(dependencies) { - _runtimeJsonIdDefinitionFactory = runtimeJsonIdDefinitionFactory; } /// @@ -72,12 +66,5 @@ protected override void ProcessEntityTypeAnnotations( annotations.Remove(CosmosAnnotationNames.DefaultTimeToLive); annotations.Remove(CosmosAnnotationNames.Throughput); } - - if (annotations.TryGetAndRemove(CosmosAnnotationNames.JsonIdDefinition, out JsonIdDefinition jsonId)) - { - runtimeEntityType.AddRuntimeAnnotation( - CosmosAnnotationNames.JsonIdDefinition, - _runtimeJsonIdDefinitionFactory.Create(runtimeEntityType, jsonId)); - } } } diff --git a/src/EFCore.Cosmos/Metadata/Conventions/Internal/CosmosConventionSetBuilder.cs b/src/EFCore.Cosmos/Metadata/Conventions/Internal/CosmosConventionSetBuilder.cs index e460eabe21a..16043d00a05 100644 --- a/src/EFCore.Cosmos/Metadata/Conventions/Internal/CosmosConventionSetBuilder.cs +++ b/src/EFCore.Cosmos/Metadata/Conventions/Internal/CosmosConventionSetBuilder.cs @@ -13,20 +13,15 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Metadata.Conventions.Internal; /// public class CosmosConventionSetBuilder : ProviderConventionSetBuilder { - private readonly IRuntimeJsonIdDefinitionFactory _runtimeJsonIdDefinitionFactory; - /// /// 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 CosmosConventionSetBuilder( - ProviderConventionSetBuilderDependencies dependencies, - IRuntimeJsonIdDefinitionFactory runtimeJsonIdDefinitionFactory) + public CosmosConventionSetBuilder(ProviderConventionSetBuilderDependencies dependencies) : base(dependencies) { - _runtimeJsonIdDefinitionFactory = runtimeJsonIdDefinitionFactory; } /// @@ -42,7 +37,6 @@ public override ConventionSet CreateConventionSet() conventionSet.Add(new ContextContainerConvention(Dependencies)); conventionSet.Add(new ETagPropertyConvention()); conventionSet.Add(new StoreKeyConvention(Dependencies)); - conventionSet.Add(new JsonIdConvention(Dependencies)); conventionSet.Replace(new CosmosValueGenerationConvention(Dependencies)); conventionSet.Replace(new CosmosKeyDiscoveryConvention(Dependencies)); @@ -50,7 +44,7 @@ public override ConventionSet CreateConventionSet() conventionSet.Replace(new CosmosRelationshipDiscoveryConvention(Dependencies)); conventionSet.Replace(new CosmosDiscriminatorConvention(Dependencies)); conventionSet.Replace(new CosmosManyToManyJoinEntityTypeConvention(Dependencies)); - conventionSet.Replace(new CosmosRuntimeModelConvention(Dependencies, _runtimeJsonIdDefinitionFactory)); + conventionSet.Replace(new CosmosRuntimeModelConvention(Dependencies)); return conventionSet; } diff --git a/src/EFCore.Cosmos/Metadata/Conventions/JsonIdConvention.cs b/src/EFCore.Cosmos/Metadata/Conventions/JsonIdConvention.cs deleted file mode 100644 index eb1c68e9229..00000000000 --- a/src/EFCore.Cosmos/Metadata/Conventions/JsonIdConvention.cs +++ /dev/null @@ -1,99 +0,0 @@ -// 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; - -/// -/// A convention that builds an for each top-level entity type. This is used -/// to build JSON `id` property values from combinations of other property values. -/// -/// -/// See Model building conventions, and -/// Accessing Azure Cosmos DB with EF Core for more information and examples. -/// -public class JsonIdConvention : IModelFinalizingConvention -{ - /// - /// Creates a new instance of . - /// - /// Parameter object containing dependencies for this convention. - public JsonIdConvention(ProviderConventionSetBuilderDependencies dependencies) - { - Dependencies = dependencies; - } - - /// - /// Dependencies for this service. - /// - protected virtual ProviderConventionSetBuilderDependencies Dependencies { get; } - - /// - public virtual void ProcessModelFinalizing( - IConventionModelBuilder modelBuilder, - IConventionContext context) - { - foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()) - { - var primaryKey = entityType.FindPrimaryKey(); - if (entityType.IsOwned() || primaryKey == null) - { - entityType.RemoveAnnotation(CosmosAnnotationNames.JsonIdDefinition); - continue; - } - - // 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) - { - entityType.RemoveAnnotation(CosmosAnnotationNames.JsonIdDefinition); - continue; - } - 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); - } - } - - entityType.SetAnnotation(CosmosAnnotationNames.JsonIdDefinition, new JsonIdDefinition(properties)); - } - } -} diff --git a/src/EFCore.Cosmos/Metadata/Internal/CosmosAnnotationNames.cs b/src/EFCore.Cosmos/Metadata/Internal/CosmosAnnotationNames.cs index e9cf959d70d..e9eab69bbb5 100644 --- a/src/EFCore.Cosmos/Metadata/Internal/CosmosAnnotationNames.cs +++ b/src/EFCore.Cosmos/Metadata/Internal/CosmosAnnotationNames.cs @@ -82,4 +82,12 @@ public static class CosmosAnnotationNames /// 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..8acf03ef0da 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,16 @@ 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 => + // new JsonIdDefinitionFactory().Create(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/IRuntimeJsonIdDefinitionFactory.cs b/src/EFCore.Cosmos/Metadata/Internal/IJsonIdDefinitionFactory.cs similarity index 88% rename from src/EFCore.Cosmos/Metadata/Internal/IRuntimeJsonIdDefinitionFactory.cs rename to src/EFCore.Cosmos/Metadata/Internal/IJsonIdDefinitionFactory.cs index 585a980ee56..8436e89a8e0 100644 --- a/src/EFCore.Cosmos/Metadata/Internal/IRuntimeJsonIdDefinitionFactory.cs +++ b/src/EFCore.Cosmos/Metadata/Internal/IJsonIdDefinitionFactory.cs @@ -9,7 +9,7 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Metadata.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 interface IRuntimeJsonIdDefinitionFactory +public interface IJsonIdDefinitionFactory { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -17,5 +17,5 @@ public interface IRuntimeJsonIdDefinitionFactory /// 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. /// - RuntimeJsonIdDefinition Create(RuntimeEntityType entityType, JsonIdDefinition jsonIdDefinition); + IJsonIdDefinition? Create(IEntityType entityType); } diff --git a/src/EFCore.Cosmos/Metadata/Internal/JsonIdDefinition.cs b/src/EFCore.Cosmos/Metadata/Internal/JsonIdDefinition.cs index e857f751c82..c94d957fcc7 100644 --- a/src/EFCore.Cosmos/Metadata/Internal/JsonIdDefinition.cs +++ b/src/EFCore.Cosmos/Metadata/Internal/JsonIdDefinition.cs @@ -1,6 +1,9 @@ // 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; /// @@ -9,7 +12,7 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Metadata.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 readonly struct JsonIdDefinition +public class JsonIdDefinition : IJsonIdDefinition { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -17,7 +20,18 @@ public readonly struct JsonIdDefinition /// 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 IReadOnlyList Properties { get; } + 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 @@ -25,8 +39,86 @@ public readonly struct JsonIdDefinition /// 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) + 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) { - Properties = properties; + 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/Metadata/Internal/RuntimeJsonIdDefinition.cs b/src/EFCore.Cosmos/Metadata/Internal/RuntimeJsonIdDefinition.cs deleted file mode 100644 index 4c300d84aa8..00000000000 --- a/src/EFCore.Cosmos/Metadata/Internal/RuntimeJsonIdDefinition.cs +++ /dev/null @@ -1,130 +0,0 @@ -// 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 RuntimeJsonIdDefinition -{ - /// - /// 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 RuntimeJsonIdDefinition(RuntimeEntityType entityType, JsonIdDefinition jsonIdDefinition) - { - var properties = new List(jsonIdDefinition.Properties.Count); - foreach (var conventionProperty in jsonIdDefinition.Properties) - { - properties.Add(entityType.FindProperty(conventionProperty.Name)!); - } - - 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/RuntimeJsonIdDefinitionFactory.cs b/src/EFCore.Cosmos/Metadata/Internal/RuntimeJsonIdDefinitionFactory.cs deleted file mode 100644 index 20d09b68300..00000000000 --- a/src/EFCore.Cosmos/Metadata/Internal/RuntimeJsonIdDefinitionFactory.cs +++ /dev/null @@ -1,22 +0,0 @@ -// 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 RuntimeJsonIdDefinitionFactory : IRuntimeJsonIdDefinitionFactory -{ - /// - /// 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 RuntimeJsonIdDefinition Create(RuntimeEntityType entityType, JsonIdDefinition jsonIdDefinition) - => new(entityType, jsonIdDefinition); -} diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs index 4280f960ecc..5f017ec2e84 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs @@ -157,7 +157,7 @@ protected CosmosQueryableMethodTranslatingExpressionVisitor( if (entityTypePrimaryKeyProperties.SequenceEqual(queryProperties) && (!partitionKeyProperties.Any() || partitionKeyProperties.All(p => entityTypePrimaryKeyProperties.Contains(p))) - && entityType.FindRuntimeAnnotation(CosmosAnnotationNames.JsonIdDefinition) != null) + && entityType.GetJsonIdDefinition() != null) { var propertyParameterList = queryProperties.Zip( parameterNames, diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.ReadItemQueryingEnumerable.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.ReadItemQueryingEnumerable.cs index 8ae8f3a9bac..ffe797e53c8 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.ReadItemQueryingEnumerable.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.ReadItemQueryingEnumerable.cs @@ -96,12 +96,9 @@ private bool TryGetPartitionKey(out PartitionKey partitionKeyValue) private bool TryGetResourceId(out string resourceId) { var entityType = _readItemInfo.EntityType; - var jsonIdDefinition = (RuntimeJsonIdDefinition)entityType.FindRuntimeAnnotation(CosmosAnnotationNames.JsonIdDefinition)?.Value; - if (jsonIdDefinition == null) - { - resourceId = null; - return false; - } + var jsonIdDefinition = entityType.GetJsonIdDefinition(); + Check.DebugAssert(jsonIdDefinition != null, + "Should not be using this enumerable if not using ReadItem, which needs an id definition."); var values = new List(jsonIdDefinition.Properties.Count); foreach (var property in jsonIdDefinition.Properties) diff --git a/src/EFCore.Cosmos/ValueGeneration/Internal/IdValueGenerator.cs b/src/EFCore.Cosmos/ValueGeneration/Internal/IdValueGenerator.cs index 45bbb9cd413..af1077fb736 100644 --- a/src/EFCore.Cosmos/ValueGeneration/Internal/IdValueGenerator.cs +++ b/src/EFCore.Cosmos/ValueGeneration/Internal/IdValueGenerator.cs @@ -38,7 +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) - => ((RuntimeJsonIdDefinition)entry - .Metadata - .FindRuntimeAnnotation(CosmosAnnotationNames.JsonIdDefinition)!.Value!).GenerateIdString(entry); + => entry.Metadata.GetJsonIdDefinition()!.GenerateIdString(entry); } diff --git a/test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs index 43ff0b9c246..7ef100ca47a 100644 --- a/test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/EndToEndCosmosTest.cs @@ -1167,7 +1167,7 @@ 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)), - addServices: s => s.AddSingleton()); + addServices: s => s.AddSingleton()); const int pk1 = 1; const int pk2 = 2; @@ -1235,7 +1235,7 @@ 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)), - addServices: s => s.AddSingleton()); + addServices: s => s.AddSingleton()); const int pk1 = 1; const int pk2 = 2; diff --git a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CustomRuntimeJsonIdDefinition.cs b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CustomRuntimeJsonIdDefinition.cs index 5d5b5b0a17e..af82dc44289 100644 --- a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CustomRuntimeJsonIdDefinition.cs +++ b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CustomRuntimeJsonIdDefinition.cs @@ -5,8 +5,8 @@ namespace Microsoft.EntityFrameworkCore.TestUtilities; -public class CustomRuntimeJsonIdDefinition(RuntimeEntityType entityType, JsonIdDefinition jsonIdDefinition) - : RuntimeJsonIdDefinition(entityType, jsonIdDefinition) +public class CustomJsonIdDefinition(IReadOnlyList properties) + : JsonIdDefinition(properties) { public override string GenerateIdString(IEnumerable values) { diff --git a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CustomRuntimeJsonIdDefinitionFactory.cs b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CustomRuntimeJsonIdDefinitionFactory.cs index 011fe9b7d60..cab262e21af 100644 --- a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CustomRuntimeJsonIdDefinitionFactory.cs +++ b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CustomRuntimeJsonIdDefinitionFactory.cs @@ -5,8 +5,8 @@ namespace Microsoft.EntityFrameworkCore.TestUtilities; -public class CustomRuntimeJsonIdDefinitionFactory : RuntimeJsonIdDefinitionFactory +public class CustomJsonIdDefinitionFactory : JsonIdDefinitionFactory { - public override RuntimeJsonIdDefinition Create(RuntimeEntityType entityType, JsonIdDefinition jsonIdDefinition) - => new CustomRuntimeJsonIdDefinition(entityType, jsonIdDefinition); + public override IJsonIdDefinition? Create(IEntityType entityType) + => new CustomJsonIdDefinition(base.Create(entityType)!.Properties); } From ba5990525230622542ae1fddb71d1048e610304b Mon Sep 17 00:00:00 2001 From: Arthur Vickers Date: Sat, 15 Jun 2024 13:06:12 +0100 Subject: [PATCH 3/3] Code review updates. --- .../Internal/CosmosModelRuntimeInitializer.cs | 6 +++++- .../Metadata/Internal/CosmosEntityTypeExtensions.cs | 1 - 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosModelRuntimeInitializer.cs b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosModelRuntimeInitializer.cs index e2bd35dfd11..40ebe958f43 100644 --- a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosModelRuntimeInitializer.cs +++ b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosModelRuntimeInitializer.cs @@ -46,6 +46,10 @@ public CosmosModelRuntimeInitializer( protected override void InitializeModel(IModel model, bool designTime, bool prevalidation) { base.InitializeModel(model, designTime, prevalidation); - model.SetRuntimeAnnotation(CosmosAnnotationNames.ModelDependencies, CosmosDependencies); + + if (prevalidation || !designTime) + { + model.SetRuntimeAnnotation(CosmosAnnotationNames.ModelDependencies, CosmosDependencies); + } } } diff --git a/src/EFCore.Cosmos/Metadata/Internal/CosmosEntityTypeExtensions.cs b/src/EFCore.Cosmos/Metadata/Internal/CosmosEntityTypeExtensions.cs index 8acf03ef0da..181f6aeee15 100644 --- a/src/EFCore.Cosmos/Metadata/Internal/CosmosEntityTypeExtensions.cs +++ b/src/EFCore.Cosmos/Metadata/Internal/CosmosEntityTypeExtensions.cs @@ -31,7 +31,6 @@ public static bool IsDocumentRoot(this IReadOnlyEntityType entityType) public static IJsonIdDefinition? GetJsonIdDefinition(this IEntityType entityType) => entityType.GetOrAddRuntimeAnnotationValue(CosmosAnnotationNames.JsonIdDefinition, static e => - // new JsonIdDefinitionFactory().Create(e!), ((CosmosModelRuntimeInitializerDependencies)e!.Model.FindRuntimeAnnotationValue( CosmosAnnotationNames.ModelDependencies)!).JsonIdDefinitionFactory.Create(e), entityType);