From b357c75f6b5eb462f1adafd473f911065acdd97f Mon Sep 17 00:00:00 2001 From: Arthur Vickers Date: Mon, 22 Jul 2024 08:17:57 +0100 Subject: [PATCH] Generate suppressible error when entity saved to Cosmos without PK value set (#34250) * Generate suppressible error when entity saved to Cosmos without PK value set Fixes #34180 We throw if there is no property in the primary key that is not generated and does not have a value set, excluding partition key properties. * Review updates --- .../Diagnostics/CosmosEventId.cs | 21 + .../Internal/CosmosLoggerExtensions.cs | 27 + .../Internal/CosmosLoggingDefinitions.cs | 8 + .../Properties/CosmosStrings.Designer.cs | 25 + .../Properties/CosmosStrings.resx | 4 + .../Storage/Internal/CosmosDatabaseWrapper.cs | 64 ++ src/EFCore/Update/IUpdateEntry.cs | 7 + .../DefaultKeyValuesTest.cs | 648 ++++++++++++++++++ .../TestUtilities/CosmosTestStore.cs | 3 + .../Diagnostics/CosmosEventIdTest.cs | 4 +- .../MaterializationInterceptionTestBase.cs | 2 +- test/EFCore.Tests/ExceptionTest.cs | 3 + 12 files changed, 814 insertions(+), 2 deletions(-) create mode 100644 test/EFCore.Cosmos.FunctionalTests/DefaultKeyValuesTest.cs diff --git a/src/EFCore.Cosmos/Diagnostics/CosmosEventId.cs b/src/EFCore.Cosmos/Diagnostics/CosmosEventId.cs index 6de27fd0a63..dfbc3cceb71 100644 --- a/src/EFCore.Cosmos/Diagnostics/CosmosEventId.cs +++ b/src/EFCore.Cosmos/Diagnostics/CosmosEventId.cs @@ -36,6 +36,9 @@ private enum Id ExecutedReplaceItem, ExecutedDeleteItem, + // Update events + PrimaryKeyValueNotSet = CoreEventId.ProviderBaseId + 200, + // Model validation events NoPartitionKeyDefined = CoreEventId.ProviderBaseId + 600, @@ -170,4 +173,22 @@ private static EventId MakeValidationId(Id id) /// /// public static readonly EventId NoPartitionKeyDefined = MakeValidationId(Id.NoPartitionKeyDefined); + + private static EventId MakeUpdateId(Id id) + => new((int)id, DbLoggerCategory.Update.Name + "." + id); + + /// + /// A property is not configured to generate values and has the CLR default or sentinel value while saving a new entity + /// to the database. The Azure Cosmos DB database provider for EF Core does not generate key values by default. This means key + /// values must be explicitly set before saving new entities. See https://aka.ms/ef-cosmos-keys for more information. + /// + /// + /// + /// This event is in the category. + /// + /// + /// This event uses the payload when used with a . + /// + /// + public static readonly EventId PrimaryKeyValueNotSet = MakeUpdateId(Id.PrimaryKeyValueNotSet); } diff --git a/src/EFCore.Cosmos/Diagnostics/Internal/CosmosLoggerExtensions.cs b/src/EFCore.Cosmos/Diagnostics/Internal/CosmosLoggerExtensions.cs index 1689ca7b98a..1c6d629221d 100644 --- a/src/EFCore.Cosmos/Diagnostics/Internal/CosmosLoggerExtensions.cs +++ b/src/EFCore.Cosmos/Diagnostics/Internal/CosmosLoggerExtensions.cs @@ -487,6 +487,33 @@ public static void NoPartitionKeyDefined( } } + /// + /// 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 static void PrimaryKeyValueNotSet( + this IDiagnosticsLogger diagnostics, + IProperty property) + { + var definition = CosmosResources.LogPrimaryKeyValueNotSet(diagnostics); + + if (diagnostics.ShouldLog(definition)) + { + definition.Log(diagnostics, property.DeclaringType.DisplayName(), property.Name); + } + + if (diagnostics.NeedsEventData(definition, out var diagnosticSourceEnabled, out var simpleLogEnabled)) + { + var eventData = new PropertyEventData( + definition, + (d, p) => ((EventDefinition)d).GenerateMessage(((PropertyEventData)p).Property.DeclaringType.DisplayName(), ((PropertyEventData)p).Property.Name), + property); + diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled); + } + } + private static string FormatParameters(IReadOnlyList<(string Name, object? Value)> parameters, bool shouldLogParameterValues) => FormatParameters(parameters.Select(p => new SqlParameter(p.Name, p.Value)).ToList(), shouldLogParameterValues); diff --git a/src/EFCore.Cosmos/Diagnostics/Internal/CosmosLoggingDefinitions.cs b/src/EFCore.Cosmos/Diagnostics/Internal/CosmosLoggingDefinitions.cs index a4d504358d8..5e44a175bf6 100644 --- a/src/EFCore.Cosmos/Diagnostics/Internal/CosmosLoggingDefinitions.cs +++ b/src/EFCore.Cosmos/Diagnostics/Internal/CosmosLoggingDefinitions.cs @@ -82,4 +82,12 @@ public class CosmosLoggingDefinitions : LoggingDefinitions /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public EventDefinitionBase? LogNoPartitionKeyDefined; + + /// + /// 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 EventDefinitionBase? LogPrimaryKeyValueNotSet; } diff --git a/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs b/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs index de52cff2d77..39658f31475 100644 --- a/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs +++ b/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs @@ -684,6 +684,31 @@ public static EventDefinition LogNoPartitionKeyDefined(IDiagnosticsLogge return (EventDefinition)definition; } + /// + /// The key property '{entityType}.{property}' is not configured to generate values and has the CLR default or sentinel value while saving a new entity to the database. The Azure Cosmos DB database provider for EF Core does not generate key values by default. This means key values must be explicitly set before saving new entities. See https://aka.ms/ef-cosmos-keys for more information. + /// + public static EventDefinition LogPrimaryKeyValueNotSet(IDiagnosticsLogger logger) + { + var definition = ((Diagnostics.Internal.CosmosLoggingDefinitions)logger.Definitions).LogPrimaryKeyValueNotSet; + if (definition == null) + { + definition = NonCapturingLazyInitializer.EnsureInitialized( + ref ((Diagnostics.Internal.CosmosLoggingDefinitions)logger.Definitions).LogPrimaryKeyValueNotSet, + logger, + static logger => new EventDefinition( + logger.Options, + CosmosEventId.PrimaryKeyValueNotSet, + LogLevel.Warning, + "CosmosEventId.PrimaryKeyValueNotSet", + level => LoggerMessage.Define( + level, + CosmosEventId.PrimaryKeyValueNotSet, + _resourceManager.GetString("LogPrimaryKeyValueNotSet")!))); + } + + return (EventDefinition)definition; + } + /// /// Azure Cosmos DB does not support synchronous I/O. Make sure to use and correctly await only async methods when using Entity Framework Core to access Azure Cosmos DB. See https://aka.ms/ef-cosmos-nosync for more information. /// diff --git a/src/EFCore.Cosmos/Properties/CosmosStrings.resx b/src/EFCore.Cosmos/Properties/CosmosStrings.resx index e4ad7e2a6ab..4f864f42a63 100644 --- a/src/EFCore.Cosmos/Properties/CosmosStrings.resx +++ b/src/EFCore.Cosmos/Properties/CosmosStrings.resx @@ -209,6 +209,10 @@ No partition key has been configured for entity type '{entityType}'. It is highly recommended that an appropriate partition key be defined. See https://aka.ms/efdocs-cosmos-partition-keys for more information. Warning CosmosEventId.NoPartitionKeyDefined string + + The key property '{entityType}.{property}' is not configured to generate values and has the CLR default or sentinel value while saving a new entity to the database. The Azure Cosmos DB database provider for EF Core does not generate key values by default. This means key values must be explicitly set before saving new entities. See https://aka.ms/ef-cosmos-keys for more information. + Warning CosmosEventId.PrimaryKeyValueNotSet string string + Azure Cosmos DB does not support synchronous I/O. Make sure to use and correctly await only async methods when using Entity Framework Core to access Azure Cosmos DB. See https://aka.ms/ef-cosmos-nosync for more information. Error CosmosEventId.SyncNotSupported diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs index 87632517098..4eed39b426f 100644 --- a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs +++ b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs @@ -3,6 +3,7 @@ using System.Net; using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; +using Microsoft.EntityFrameworkCore.Cosmos.Diagnostics.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Update.Internal; @@ -283,6 +284,69 @@ private Task SaveAsync(IUpdateEntry entry, CancellationToken cancellationT switch (state) { case EntityState.Added: + var primaryKey = entityType.FindPrimaryKey(); + if (primaryKey != null) + { + // The code below checks for primary key properties that are not configured for value generation but have not + // had a non-sentinel (effectively, non-CLR default) value set. For composite keys, we only check if at least + // one property has value generation or a value set, since it is normal to have non-value generated parts of composite + // keys where one part is the CLR default. However, on Cosmos, we exclude the partition key properties from this + // check to ensure that, even if partition key properties have been set, at least one other primary key property is + // also set. + var partitionPropertyNeedsValue = true; + var propertyNeedsValue = true; + var allPkPropertiesAreFk = true; + IProperty? firstNonPartitionKeyProperty = null; + + var partitionKeyProperties = entityType.GetPartitionKeyProperties(); + foreach (var property in primaryKey.Properties) + { + if (property.IsForeignKey()) + { + // FK properties conceptually get their value from the associated principal key, which can be handled + // automatically by the update pipeline in some cases, so exclude from this check. + continue; + } + + allPkPropertiesAreFk = false; + + var isPartitionKeyProperty = partitionKeyProperties.Contains(property); + if (!isPartitionKeyProperty) + { + firstNonPartitionKeyProperty = property; + } + + if (property.ValueGenerated != ValueGenerated.Never + || entry.HasExplicitValue(property)) + { + if (!isPartitionKeyProperty) + { + propertyNeedsValue = false; + break; + } + + partitionPropertyNeedsValue = false; + } + } + + if (!allPkPropertiesAreFk) + { + if (firstNonPartitionKeyProperty != null + && propertyNeedsValue) + { + // There were non-partition key properties, so only throw if it is one of these that is not set, + // ignoring partition key properties. + Dependencies.Logger.PrimaryKeyValueNotSet(firstNonPartitionKeyProperty!); + } + else if (firstNonPartitionKeyProperty == null + && partitionPropertyNeedsValue) + { + // There were no non-partition key properties in the primary key, so in this case check if any of these is not set. + Dependencies.Logger.PrimaryKeyValueNotSet(primaryKey.Properties[0]); + } + } + } + var newDocument = documentSource.GetCurrentDocument(entry); if (newDocument != null) { diff --git a/src/EFCore/Update/IUpdateEntry.cs b/src/EFCore/Update/IUpdateEntry.cs index 379d51afeb7..1c4ebe04024 100644 --- a/src/EFCore/Update/IUpdateEntry.cs +++ b/src/EFCore/Update/IUpdateEntry.cs @@ -66,6 +66,13 @@ public interface IUpdateEntry /// if the property has a temporary value, otherwise . bool HasTemporaryValue(IProperty property); + /// + /// Gets a value indicating if the specified property has an explicit value set. + /// + /// The property to be checked. + /// if the property has an explicitly set value, otherwise . + bool HasExplicitValue(IProperty property); + /// /// Gets a value indicating if the specified property has a store-generated value that has not yet been saved to the entity. /// diff --git a/test/EFCore.Cosmos.FunctionalTests/DefaultKeyValuesTest.cs b/test/EFCore.Cosmos.FunctionalTests/DefaultKeyValuesTest.cs new file mode 100644 index 00000000000..cafa4b3771b --- /dev/null +++ b/test/EFCore.Cosmos.FunctionalTests/DefaultKeyValuesTest.cs @@ -0,0 +1,648 @@ +// 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.Diagnostics.Internal; +using Microsoft.EntityFrameworkCore.Cosmos.Internal; + +namespace Microsoft.EntityFrameworkCore; + +public class DefaultKeyValuesTest(DefaultKeyValuesTest.CosmosDefaultKeyValuesTestFixture fixture) + : IClassFixture +{ + protected CosmosDefaultKeyValuesTestFixture Fixture { get; } = fixture; + + [ConditionalFact] + public async Task Single_key_value_with_single_partition_key_must_have_key_set() + { + using var context = CreateContext(); + + context.Add(new SingleKeySinglePartitionKey()); + await AssertKeyValueNotSet(context, nameof(SingleKeySinglePartitionKey), nameof(SingleKeySinglePartitionKey.Id)); + + context.Add(new SingleKeySinglePartitionKey { PartitionKey = 1 }); + await AssertKeyValueNotSet(context, nameof(SingleKeySinglePartitionKey), nameof(SingleKeySinglePartitionKey.Id)); + + context.Add(new SingleKeySinglePartitionKey { Id = 1 }); + await AssertSaves(context); + + context.Add(new SingleKeySinglePartitionKey { Id = 1, PartitionKey = 1 }); + await AssertSaves(context); + } + + [ConditionalFact] + public async Task Composite_key_value_with_single_partition_key_must_have_key_set() + { + using var context = CreateContext(); + + context.Add(new CompositeKeySinglePartitionKey()); + await AssertKeyValueNotSet(context, nameof(CompositeKeySinglePartitionKey), nameof(CompositeKeySinglePartitionKey.Id3)); + + context.Add(new CompositeKeySinglePartitionKey { PartitionKey = 1 }); + await AssertKeyValueNotSet(context, nameof(CompositeKeySinglePartitionKey), nameof(CompositeKeySinglePartitionKey.Id3)); + + context.Add(new CompositeKeySinglePartitionKey { Id1 = 1 }); + await AssertSaves(context); + + context.Add(new CompositeKeySinglePartitionKey { Id2 = Guid.NewGuid() }); + await AssertSaves(context); + + context.Add(new CompositeKeySinglePartitionKey { Id3 = true }); + await AssertSaves(context); + + context.Add(new CompositeKeySinglePartitionKey { Id1 = 1, Id2 = Guid.NewGuid() }); + await AssertSaves(context); + + context.Add(new CompositeKeySinglePartitionKey { Id1 = 1, Id3 = true }); + await AssertSaves(context); + + context.Add(new CompositeKeySinglePartitionKey { Id2 = Guid.NewGuid(), Id3 = true }); + await AssertSaves(context); + + context.Add(new CompositeKeySinglePartitionKey { Id1 = 1, Id2 = Guid.NewGuid(), Id3 = true }); + await AssertSaves(context); + + context.Add(new CompositeKeySinglePartitionKey { Id1 = 1, PartitionKey = 1 }); + await AssertSaves(context); + + context.Add(new CompositeKeySinglePartitionKey { Id2 = Guid.NewGuid(), PartitionKey = 1 }); + await AssertSaves(context); + + context.Add(new CompositeKeySinglePartitionKey { Id3 = true, PartitionKey = 1 }); + await AssertSaves(context); + + context.Add(new CompositeKeySinglePartitionKey { Id1 = 1, Id2 = Guid.NewGuid(), PartitionKey = 1 }); + await AssertSaves(context); + + context.Add(new CompositeKeySinglePartitionKey { Id1 = 1, Id3 = true, PartitionKey = 1 }); + await AssertSaves(context); + + context.Add(new CompositeKeySinglePartitionKey { Id2 = Guid.NewGuid(), Id3 = true, PartitionKey = 1 }); + await AssertSaves(context); + + context.Add(new CompositeKeySinglePartitionKey { Id1 = 1, Id2 = Guid.NewGuid(), Id3 = true, PartitionKey = 1 }); + await AssertSaves(context); + } + + [ConditionalFact] + public async Task Single_key_value_with_composite_partition_key_must_have_key_set() + { + using var context = CreateContext(); + + context.Add(new SingleKeyCompositePartitionKey()); + await AssertKeyValueNotSet(context, nameof(SingleKeyCompositePartitionKey), nameof(SingleKeyCompositePartitionKey.Id)); + + context.Add(new SingleKeyCompositePartitionKey { PartitionKey1 = 1 }); + await AssertKeyValueNotSet(context, nameof(SingleKeyCompositePartitionKey), nameof(SingleKeyCompositePartitionKey.Id)); + + context.Add(new SingleKeyCompositePartitionKey { PartitionKey2 = Guid.NewGuid() }); + await AssertKeyValueNotSet(context, nameof(SingleKeyCompositePartitionKey), nameof(SingleKeyCompositePartitionKey.Id)); + + context.Add(new SingleKeyCompositePartitionKey { PartitionKey3 = true }); + await AssertKeyValueNotSet(context, nameof(SingleKeyCompositePartitionKey), nameof(SingleKeyCompositePartitionKey.Id)); + + context.Add(new SingleKeyCompositePartitionKey { PartitionKey1 = 1, PartitionKey2 = Guid.NewGuid(), PartitionKey3 = true }); + await AssertKeyValueNotSet(context, nameof(SingleKeyCompositePartitionKey), nameof(SingleKeyCompositePartitionKey.Id)); + + context.Add(new SingleKeyCompositePartitionKey { Id = 1 }); + await AssertSaves(context); + + context.Add(new SingleKeyCompositePartitionKey { Id = 1, PartitionKey1 = 1, PartitionKey2 = Guid.NewGuid(), PartitionKey3 = true }); + await AssertSaves(context); + + context.Add(new SingleKeyCompositePartitionKey { Id = 1, PartitionKey2 = Guid.NewGuid(), PartitionKey3 = true }); + await AssertSaves(context); + + context.Add(new SingleKeyCompositePartitionKey { Id = 1, PartitionKey2 = Guid.NewGuid() }); + await AssertSaves(context); + } + + [ConditionalFact] + public async Task Composite_key_value_with_composite_partition_key_must_have_key_set() + { + using var context = CreateContext(); + + context.Add(new CompositeKeyCompositePartitionKey()); + await AssertKeyValueNotSet(context, nameof(CompositeKeyCompositePartitionKey), nameof(CompositeKeyCompositePartitionKey.Id3)); + + context.Add(new CompositeKeyCompositePartitionKey { PartitionKey1 = 1 }); + await AssertKeyValueNotSet(context, nameof(CompositeKeyCompositePartitionKey), nameof(CompositeKeyCompositePartitionKey.Id3)); + + context.Add(new CompositeKeyCompositePartitionKey { PartitionKey2 = Guid.NewGuid() }); + await AssertKeyValueNotSet(context, nameof(CompositeKeyCompositePartitionKey), nameof(CompositeKeyCompositePartitionKey.Id3)); + + context.Add(new CompositeKeyCompositePartitionKey { PartitionKey3 = true }); + await AssertKeyValueNotSet(context, nameof(CompositeKeyCompositePartitionKey), nameof(CompositeKeyCompositePartitionKey.Id3)); + + context.Add(new CompositeKeyCompositePartitionKey { PartitionKey1 = 1, PartitionKey2 = Guid.NewGuid(), PartitionKey3 = true }); + await AssertKeyValueNotSet(context, nameof(CompositeKeyCompositePartitionKey), nameof(CompositeKeyCompositePartitionKey.Id3)); + + context.Add(new CompositeKeyCompositePartitionKey { Id1 = 1 }); + await AssertSaves(context); + + context.Add(new CompositeKeyCompositePartitionKey + { + Id1 = 1, PartitionKey1 = 1, PartitionKey2 = Guid.NewGuid(), PartitionKey3 = true + }); + await AssertSaves(context); + + context.Add(new CompositeKeyCompositePartitionKey { Id1 = 1, PartitionKey2 = Guid.NewGuid(), PartitionKey3 = true }); + await AssertSaves(context); + + context.Add(new CompositeKeyCompositePartitionKey { Id1 = 1, PartitionKey2 = Guid.NewGuid() }); + await AssertSaves(context); + + context.Add(new CompositeKeyCompositePartitionKey { Id2 = Guid.NewGuid() }); + await AssertSaves(context); + + context.Add(new CompositeKeyCompositePartitionKey + { + Id2 = Guid.NewGuid(), PartitionKey1 = 1, PartitionKey2 = Guid.NewGuid(), PartitionKey3 = true + }); + await AssertSaves(context); + + context.Add(new CompositeKeyCompositePartitionKey { Id2 = Guid.NewGuid(), PartitionKey2 = Guid.NewGuid(), PartitionKey3 = true }); + await AssertSaves(context); + + context.Add(new CompositeKeyCompositePartitionKey { Id2 = Guid.NewGuid(), PartitionKey2 = Guid.NewGuid() }); + await AssertSaves(context); + + context.Add(new CompositeKeyCompositePartitionKey { Id3 = true }); + await AssertSaves(context); + + context.Add(new CompositeKeyCompositePartitionKey + { + Id3 = true, PartitionKey1 = 1, PartitionKey2 = Guid.NewGuid(), PartitionKey3 = true + }); + await AssertSaves(context); + + context.Add(new CompositeKeyCompositePartitionKey { Id3 = true, PartitionKey2 = Guid.NewGuid(), PartitionKey3 = true }); + await AssertSaves(context); + + context.Add(new CompositeKeyCompositePartitionKey { Id3 = true, PartitionKey2 = Guid.NewGuid() }); + await AssertSaves(context); + } + + [ConditionalFact] + public async Task Single_same_key_and_partition_key_must_have_key_set() + { + using var context = CreateContext(); + + context.Add(new SingleSameKeyAndPartitionKey()); + await AssertKeyValueNotSet(context, nameof(SingleSameKeyAndPartitionKey), nameof(SingleSameKeyAndPartitionKey.Id)); + + context.Add(new SingleSameKeyAndPartitionKey { Id = 1 }); + await AssertSaves(context); + } + + [ConditionalFact] + public async Task Composite_same_key_and_partition_key_must_have_key_set() + { + using var context = CreateContext(); + + context.Add(new CompositeSameKeyAndPartitionKey()); + await AssertKeyValueNotSet(context, nameof(CompositeSameKeyAndPartitionKey), nameof(CompositeSameKeyAndPartitionKey.Key1)); + + context.Add(new CompositeSameKeyAndPartitionKey { Key1 = 1 }); + await AssertSaves(context); + + context.Add(new CompositeSameKeyAndPartitionKey { Key2 = Guid.NewGuid() }); + await AssertSaves(context); + + context.Add(new CompositeSameKeyAndPartitionKey { Key3 = true }); + await AssertSaves(context); + + context.Add(new CompositeSameKeyAndPartitionKey { Key1 = 1, Key2 = Guid.NewGuid(), Key3 = true }); + await AssertSaves(context); + } + + [ConditionalFact] + public async Task Single_key_value_with_single_partition_key_can_use_generated_value() + { + using var context = CreateContext(); + + context.Add(new SingleGeneratedKeySinglePartitionKey()); + await AssertSaves(context); + + context.Add(new SingleGeneratedKeySinglePartitionKey { PartitionKey = 1 }); + await AssertSaves(context); + + context.Add(new SingleGeneratedKeySinglePartitionKey { Id = Guid.NewGuid() }); + await AssertSaves(context); + + context.Add(new SingleGeneratedKeySinglePartitionKey { Id = Guid.NewGuid(), PartitionKey = 1 }); + await AssertSaves(context); + } + + [ConditionalFact] + public async Task Composite_key_value_with_single_partition_key_can_use_generated_value() + { + using var context = CreateContext(); + + context.Add(new CompositeGeneratedKeySinglePartitionKey()); + await AssertSaves(context); + + context.Add(new CompositeGeneratedKeySinglePartitionKey { PartitionKey = 1 }); + await AssertSaves(context); + + context.Add(new CompositeGeneratedKeySinglePartitionKey { Id1 = 1 }); + await AssertSaves(context); + + context.Add(new CompositeGeneratedKeySinglePartitionKey { Id2 = Guid.NewGuid() }); + await AssertSaves(context); + + context.Add(new CompositeGeneratedKeySinglePartitionKey { Id3 = true }); + await AssertSaves(context); + + context.Add(new CompositeGeneratedKeySinglePartitionKey { Id1 = 1, Id2 = Guid.NewGuid() }); + await AssertSaves(context); + + context.Add(new CompositeGeneratedKeySinglePartitionKey { Id1 = 1, Id3 = true }); + await AssertSaves(context); + + context.Add(new CompositeGeneratedKeySinglePartitionKey { Id2 = Guid.NewGuid(), Id3 = true }); + await AssertSaves(context); + + context.Add(new CompositeGeneratedKeySinglePartitionKey { Id1 = 1, Id2 = Guid.NewGuid(), Id3 = true }); + await AssertSaves(context); + + context.Add(new CompositeGeneratedKeySinglePartitionKey { Id1 = 1, PartitionKey = 1 }); + await AssertSaves(context); + + context.Add(new CompositeGeneratedKeySinglePartitionKey { Id2 = Guid.NewGuid(), PartitionKey = 1 }); + await AssertSaves(context); + + context.Add(new CompositeGeneratedKeySinglePartitionKey { Id3 = true, PartitionKey = 1 }); + await AssertSaves(context); + + context.Add(new CompositeGeneratedKeySinglePartitionKey { Id1 = 1, Id2 = Guid.NewGuid(), PartitionKey = 1 }); + await AssertSaves(context); + + context.Add(new CompositeGeneratedKeySinglePartitionKey { Id1 = 1, Id3 = true, PartitionKey = 1 }); + await AssertSaves(context); + + context.Add(new CompositeGeneratedKeySinglePartitionKey { Id2 = Guid.NewGuid(), Id3 = true, PartitionKey = 1 }); + await AssertSaves(context); + + context.Add(new CompositeGeneratedKeySinglePartitionKey { Id1 = 1, Id2 = Guid.NewGuid(), Id3 = true, PartitionKey = 1 }); + await AssertSaves(context); + } + + [ConditionalFact] + public async Task Single_key_value_with_composite_partition_key_can_use_generated_value() + { + using var context = CreateContext(); + + context.Add(new SingleGeneratedKeyCompositePartitionKey()); + await AssertSaves(context); + + context.Add(new SingleGeneratedKeyCompositePartitionKey { PartitionKey1 = 1 }); + await AssertSaves(context); + + context.Add(new SingleGeneratedKeyCompositePartitionKey { PartitionKey2 = Guid.NewGuid() }); + await AssertSaves(context); + + context.Add(new SingleGeneratedKeyCompositePartitionKey { PartitionKey3 = true }); + await AssertSaves(context); + + context.Add(new SingleGeneratedKeyCompositePartitionKey { PartitionKey1 = 1, PartitionKey2 = Guid.NewGuid(), PartitionKey3 = true }); + await AssertSaves(context); + + context.Add(new SingleGeneratedKeyCompositePartitionKey { Id = Guid.NewGuid() }); + await AssertSaves(context); + + context.Add(new SingleGeneratedKeyCompositePartitionKey { Id = Guid.NewGuid(), PartitionKey1 = 1, PartitionKey2 = Guid.NewGuid(), PartitionKey3 = true }); + await AssertSaves(context); + + context.Add(new SingleGeneratedKeyCompositePartitionKey { Id = Guid.NewGuid(), PartitionKey2 = Guid.NewGuid(), PartitionKey3 = true }); + await AssertSaves(context); + + context.Add(new SingleGeneratedKeyCompositePartitionKey { Id = Guid.NewGuid(), PartitionKey2 = Guid.NewGuid() }); + await AssertSaves(context); + } + + [ConditionalFact] + public async Task Composite_key_value_with_composite_partition_key_can_use_generated_value() + { + using var context = CreateContext(); + + context.Add(new CompositeGeneratedKeyCompositePartitionKey()); + await AssertSaves(context); + + context.Add(new CompositeGeneratedKeyCompositePartitionKey { PartitionKey1 = 1 }); + await AssertSaves(context); + + context.Add(new CompositeGeneratedKeyCompositePartitionKey { PartitionKey2 = Guid.NewGuid() }); + await AssertSaves(context); + + context.Add(new CompositeGeneratedKeyCompositePartitionKey { PartitionKey3 = true }); + await AssertSaves(context); + + context.Add(new CompositeGeneratedKeyCompositePartitionKey { PartitionKey1 = 1, PartitionKey2 = Guid.NewGuid(), PartitionKey3 = true }); + await AssertSaves(context); + + context.Add(new CompositeGeneratedKeyCompositePartitionKey { Id1 = 1 }); + await AssertSaves(context); + + context.Add(new CompositeGeneratedKeyCompositePartitionKey + { + Id1 = 1, PartitionKey1 = 1, PartitionKey2 = Guid.NewGuid(), PartitionKey3 = true + }); + await AssertSaves(context); + + context.Add(new CompositeGeneratedKeyCompositePartitionKey { Id1 = 1, PartitionKey2 = Guid.NewGuid(), PartitionKey3 = true }); + await AssertSaves(context); + + context.Add(new CompositeGeneratedKeyCompositePartitionKey { Id1 = 1, PartitionKey2 = Guid.NewGuid() }); + await AssertSaves(context); + + context.Add(new CompositeGeneratedKeyCompositePartitionKey { Id2 = Guid.NewGuid() }); + await AssertSaves(context); + + context.Add(new CompositeGeneratedKeyCompositePartitionKey + { + Id2 = Guid.NewGuid(), PartitionKey1 = 1, PartitionKey2 = Guid.NewGuid(), PartitionKey3 = true + }); + await AssertSaves(context); + + context.Add(new CompositeGeneratedKeyCompositePartitionKey { Id2 = Guid.NewGuid(), PartitionKey2 = Guid.NewGuid(), PartitionKey3 = true }); + await AssertSaves(context); + + context.Add(new CompositeGeneratedKeyCompositePartitionKey { Id2 = Guid.NewGuid(), PartitionKey2 = Guid.NewGuid() }); + await AssertSaves(context); + + context.Add(new CompositeGeneratedKeyCompositePartitionKey { Id3 = true }); + await AssertSaves(context); + + context.Add(new CompositeGeneratedKeyCompositePartitionKey + { + Id3 = true, PartitionKey1 = 1, PartitionKey2 = Guid.NewGuid(), PartitionKey3 = true + }); + await AssertSaves(context); + + context.Add(new CompositeGeneratedKeyCompositePartitionKey { Id3 = true, PartitionKey2 = Guid.NewGuid(), PartitionKey3 = true }); + await AssertSaves(context); + + context.Add(new CompositeGeneratedKeyCompositePartitionKey { Id3 = true, PartitionKey2 = Guid.NewGuid() }); + await AssertSaves(context); + } + + [ConditionalFact] + public async Task Single_same_key_and_partition_key_can_use_generated_value() + { + using var context = CreateContext(); + + context.Add(new SingleSameGeneratedKeyAndPartitionKey()); + await AssertSaves(context); + + context.Add(new SingleSameGeneratedKeyAndPartitionKey { Id = Guid.NewGuid() }); + await AssertSaves(context); + } + + [ConditionalFact] + public async Task Composite_same_key_and_partition_key_can_use_generated_value() + { + using var context = CreateContext(); + + context.Add(new CompositeSameGeneratedKeyAndPartitionKey()); + await AssertSaves(context); + + context.Add(new CompositeSameGeneratedKeyAndPartitionKey { Key1 = 1 }); + await AssertSaves(context); + + context.Add(new CompositeSameGeneratedKeyAndPartitionKey { Key2 = Guid.NewGuid() }); + await AssertSaves(context); + + context.Add(new CompositeSameGeneratedKeyAndPartitionKey { Key3 = true }); + await AssertSaves(context); + + context.Add(new CompositeSameGeneratedKeyAndPartitionKey { Key1 = 1, Key2 = Guid.NewGuid(), Key3 = true }); + await AssertSaves(context); + } + + private static async Task AssertSaves(DefaultKeyValuesTestContext context) + { + var entry = context.ChangeTracker.Entries().Single(); + + Assert.Equal(EntityState.Added, entry.State); + await context.SaveChangesAsync(); + Assert.Equal(EntityState.Unchanged, entry.State); + + context.ChangeTracker.Clear(); + } + + private static async Task AssertKeyValueNotSet( + DefaultKeyValuesTestContext context, string entityTypeName, string propertyName) + { + Assert.Equal( + CoreStrings.WarningAsErrorTemplate( + CosmosEventId.PrimaryKeyValueNotSet.ToString(), + CosmosResources.LogPrimaryKeyValueNotSet(new TestLogger()).GenerateMessage( + entityTypeName, propertyName), + "CosmosEventId.PrimaryKeyValueNotSet"), + (await Assert.ThrowsAsync(() => context.SaveChangesAsync())).InnerException!.Message); + + context.ChangeTracker.Clear(); + } + + protected DefaultKeyValuesTestContext CreateContext() + => Fixture.CreateContext(); + + public class CosmosDefaultKeyValuesTestFixture : SharedStoreFixtureBase + { + protected override string StoreName + => nameof(DefaultKeyValuesTest); + + protected override bool UsePooling + => false; + + protected override ITestStoreFactory TestStoreFactory + => CosmosTestStoreFactory.Instance; + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ServiceProvider.GetRequiredService(); + } + + public class DefaultKeyValuesTestContext(DbContextOptions dbContextOptions) : DbContext(dbContextOptions) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(b => + { + b.ToContainer(b.Metadata.ClrType.Name); + b.HasPartitionKey(e => e.PartitionKey); + b.HasKey(e => new { e.Id, e.PartitionKey }); + }); + + modelBuilder.Entity(b => + { + b.ToContainer(b.Metadata.ClrType.Name); + b.HasPartitionKey(e => e.PartitionKey); + b.HasKey(e => new { e.Id1, e.Id2, e.Id3, e.PartitionKey }); + }); + + modelBuilder.Entity(b => + { + b.ToContainer(b.Metadata.ClrType.Name); + b.HasPartitionKey(e => new { e.PartitionKey1, e.PartitionKey2, e.PartitionKey3 }); + b.HasKey(e => new { e.Id, e.PartitionKey1, e.PartitionKey2, e.PartitionKey3 }); + }); + + modelBuilder.Entity(b => + { + b.ToContainer(b.Metadata.ClrType.Name); + b.HasPartitionKey(e => new { e.PartitionKey1, e.PartitionKey2, e.PartitionKey3 }); + b.HasKey(e => new { e.Id1, e.Id2, e.Id3, e.PartitionKey1, e.PartitionKey2, e.PartitionKey3 }); + }); + + modelBuilder.Entity(b => + { + b.ToContainer(b.Metadata.ClrType.Name); + b.HasPartitionKey(e => e.Id); + b.HasKey(e => e.Id); + }); + + modelBuilder.Entity(b => + { + b.ToContainer(b.Metadata.ClrType.Name); + b.HasPartitionKey(e => new { e.Key1, e.Key2, e.Key3 }); + b.HasKey(e => new { e.Key1, e.Key2, e.Key3 }); + }); + + modelBuilder.Entity(b => + { + b.ToContainer(b.Metadata.ClrType.Name); + b.HasPartitionKey(e => e.PartitionKey); + b.HasKey(e => new { e.Id, e.PartitionKey }); + b.Property(e => e.Id).ValueGeneratedOnAdd(); + }); + + modelBuilder.Entity(b => + { + b.ToContainer(b.Metadata.ClrType.Name); + b.HasPartitionKey(e => e.PartitionKey); + b.HasKey(e => new { e.Id1, e.Id2, e.Id3, e.PartitionKey }); + b.Property(e => e.Id2).ValueGeneratedOnAdd(); + }); + + modelBuilder.Entity(b => + { + b.ToContainer(b.Metadata.ClrType.Name); + b.HasPartitionKey(e => new { e.PartitionKey1, e.PartitionKey2, e.PartitionKey3 }); + b.HasKey(e => new { e.Id, e.PartitionKey1, e.PartitionKey2, e.PartitionKey3 }); + b.Property(e => e.Id).ValueGeneratedOnAdd(); + }); + + modelBuilder.Entity(b => + { + b.ToContainer(b.Metadata.ClrType.Name); + b.HasPartitionKey(e => new { e.PartitionKey1, e.PartitionKey2, e.PartitionKey3 }); + b.HasKey(e => new { e.Id1, e.Id2, e.Id3, e.PartitionKey1, e.PartitionKey2, e.PartitionKey3 }); + b.Property(e => e.Id2).ValueGeneratedOnAdd(); + }); + + modelBuilder.Entity(b => + { + b.ToContainer(b.Metadata.ClrType.Name); + b.HasPartitionKey(e => e.Id); + b.HasKey(e => e.Id); + b.Property(e => e.Id).ValueGeneratedOnAdd(); + }); + + modelBuilder.Entity(b => + { + b.ToContainer(b.Metadata.ClrType.Name); + b.HasPartitionKey(e => new { e.Key1, e.Key2, e.Key3 }); + b.HasKey(e => new { e.Key1, e.Key2, e.Key3 }); + b.Property(e => e.Key2).ValueGeneratedOnAdd(); + }); + } + } + + protected class SingleKeySinglePartitionKey + { + public int Id { get; set; } + public int PartitionKey { get; set; } + } + + protected class CompositeKeySinglePartitionKey + { + public int Id1 { get; set; } + public Guid Id2 { get; set; } + public bool Id3 { get; set; } + public int PartitionKey { get; set; } + } + + protected class SingleKeyCompositePartitionKey + { + public int Id { get; set; } + public int PartitionKey1 { get; set; } + public Guid PartitionKey2 { get; set; } + public bool PartitionKey3 { get; set; } + } + + protected class CompositeKeyCompositePartitionKey + { + public int Id1 { get; set; } + public Guid Id2 { get; set; } + public bool Id3 { get; set; } + public int PartitionKey1 { get; set; } + public Guid PartitionKey2 { get; set; } + public bool PartitionKey3 { get; set; } + } + + protected class SingleSameKeyAndPartitionKey + { + public int Id { get; set; } + } + + protected class CompositeSameKeyAndPartitionKey + { + public int Key1 { get; set; } + public Guid Key2 { get; set; } + public bool Key3 { get; set; } + } + + protected class SingleGeneratedKeySinglePartitionKey + { + public Guid Id { get; set; } + public int PartitionKey { get; set; } + } + + protected class CompositeGeneratedKeySinglePartitionKey + { + public int Id1 { get; set; } + public Guid Id2 { get; set; } + public bool Id3 { get; set; } + public int PartitionKey { get; set; } + } + + protected class SingleGeneratedKeyCompositePartitionKey + { + public Guid Id { get; set; } + public int PartitionKey1 { get; set; } + public Guid PartitionKey2 { get; set; } + public bool PartitionKey3 { get; set; } + } + + protected class CompositeGeneratedKeyCompositePartitionKey + { + public int Id1 { get; set; } + public Guid Id2 { get; set; } + public bool Id3 { get; set; } + public int PartitionKey1 { get; set; } + public Guid PartitionKey2 { get; set; } + public bool PartitionKey3 { get; set; } + } + + protected class SingleSameGeneratedKeyAndPartitionKey + { + public Guid Id { get; set; } + } + + protected class CompositeSameGeneratedKeyAndPartitionKey + { + public int Key1 { get; set; } + public Guid Key2 { get; set; } + public bool Key3 { get; set; } + } +} diff --git a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs index 5da83c83566..46066de861d 100644 --- a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs +++ b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs @@ -546,6 +546,9 @@ public TProperty GetOriginalValue(IProperty property) public bool HasTemporaryValue(IProperty property) => throw new NotImplementedException(); + public bool HasExplicitValue(IProperty property) + => throw new NotImplementedException(); + public bool HasStoreGeneratedValue(IProperty property) => throw new NotImplementedException(); diff --git a/test/EFCore.Cosmos.Tests/Diagnostics/CosmosEventIdTest.cs b/test/EFCore.Cosmos.Tests/Diagnostics/CosmosEventIdTest.cs index 6b8bbae1800..77e232dcaf4 100644 --- a/test/EFCore.Cosmos.Tests/Diagnostics/CosmosEventIdTest.cs +++ b/test/EFCore.Cosmos.Tests/Diagnostics/CosmosEventIdTest.cs @@ -14,11 +14,13 @@ public class CosmosEventIdTest : EventIdTestBase public void Every_eventId_has_a_logger_method_and_logs_when_level_enabled() { var model = new Model(); - var entityType = model.AddEntityType(typeof(object), owned: false, ConfigurationSource.Convention); + var entityType = model.AddEntityType(typeof(object), owned: false, ConfigurationSource.Convention)!; + var property = entityType.AddProperty("A", typeof(int), ConfigurationSource.Convention, ConfigurationSource.Convention); var fakeFactories = new Dictionary> { { typeof(IEntityType), () => entityType }, + { typeof(IProperty), () => property }, { typeof(CosmosSqlQuery), () => new CosmosSqlQuery( "Some SQL...", diff --git a/test/EFCore.Specification.Tests/MaterializationInterceptionTestBase.cs b/test/EFCore.Specification.Tests/MaterializationInterceptionTestBase.cs index 9f0b1f44c07..a14aa2762a2 100644 --- a/test/EFCore.Specification.Tests/MaterializationInterceptionTestBase.cs +++ b/test/EFCore.Specification.Tests/MaterializationInterceptionTestBase.cs @@ -163,7 +163,7 @@ public virtual async Task Intercept_query_materialization_for_empty_constructor( } } - private static int _id; + private static int _id = 1; [ConditionalTheory] // Issue #30244 [ClassData(typeof(DataGenerator))] diff --git a/test/EFCore.Tests/ExceptionTest.cs b/test/EFCore.Tests/ExceptionTest.cs index 1b4eba5efbb..d06a5d65b54 100644 --- a/test/EFCore.Tests/ExceptionTest.cs +++ b/test/EFCore.Tests/ExceptionTest.cs @@ -118,6 +118,9 @@ public bool IsModified(IProperty property) public bool HasTemporaryValue(IProperty property) => throw new NotImplementedException(); + public bool HasExplicitValue(IProperty property) + => throw new NotImplementedException(); + public bool HasStoreGeneratedValue(IProperty property) => throw new NotImplementedException();