diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs index 0c93c65b14b..00c282e6907 100644 --- a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs +++ b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseCreator.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Runtime.CompilerServices; using Microsoft.EntityFrameworkCore.Cosmos.Internal; using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal; @@ -20,6 +21,8 @@ public class CosmosDatabaseCreator : IDatabaseCreator private readonly IDatabase _database; private readonly ICurrentDbContext _currentContext; private readonly IDbContextOptions _contextOptions; + private readonly IExecutionStrategy _executionStrategy; + private readonly IDiagnosticsLogger _logger; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -33,7 +36,9 @@ public CosmosDatabaseCreator( IUpdateAdapterFactory updateAdapterFactory, IDatabase database, ICurrentDbContext currentContext, - IDbContextOptions contextOptions) + IDbContextOptions contextOptions, + IExecutionStrategy executionStrategy, + IDiagnosticsLogger logger) { _cosmosClient = cosmosClient; _designTimeModel = designTimeModel; @@ -41,6 +46,8 @@ public CosmosDatabaseCreator( _database = database; _currentContext = currentContext; _contextOptions = contextOptions; + _executionStrategy = executionStrategy; + _logger = logger; } /// @@ -49,26 +56,56 @@ public CosmosDatabaseCreator( /// 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 async Task EnsureCreatedAsync(CancellationToken cancellationToken = default) + public virtual Task EnsureCreatedAsync(CancellationToken cancellationToken = default) { - var model = _designTimeModel.Model; - var created = await _cosmosClient.CreateDatabaseIfNotExistsAsync(model.GetThroughput(), cancellationToken) - .ConfigureAwait(false); - - foreach (var container in GetContainersToCreate(model)) + if (_currentContext.Context.ChangeTracker.Entries().Any( + e => e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted)) { - created |= await _cosmosClient.CreateContainerIfNotExistsAsync(container, cancellationToken) - .ConfigureAwait(false); + _logger.EnsureCreatedWithTrackedEntitiesWarning(); } - if (created) - { - await InsertDataAsync(cancellationToken).ConfigureAwait(false); - } + var created = new StrongBox(false); + var dataInserted = new StrongBox(false); + var retrying = new StrongBox(false); + return _executionStrategy.ExecuteAsync( + (Creator: this, Created: created, DataInserted: dataInserted, Retrying: retrying), + static async (_, state, ct) => + { + var creator = state.Creator; + + if (state.Retrying.Value) + { + creator._currentContext.Context.ChangeTracker.Clear(); + } + + state.Retrying.Value = true; + + if (!state.DataInserted.Value) + { + var model = creator._designTimeModel.Model; + state.Created.Value |= await creator._cosmosClient + .CreateDatabaseIfNotExistsAsync(model.GetThroughput(), ct) + .ConfigureAwait(false); + + foreach (var container in GetContainersToCreate(model)) + { + state.Created.Value |= await creator._cosmosClient + .CreateContainerIfNotExistsAsync(container, ct) + .ConfigureAwait(false); + } + + if (state.Created.Value) + { + await creator.InsertDataAsync(ct).ConfigureAwait(false); + state.DataInserted.Value = true; + } + } - await SeedDataAsync(created, cancellationToken).ConfigureAwait(false); + await creator.SeedDataAsync(state.Created.Value, cancellationToken: ct) + .ConfigureAwait(false); - return created; + return state.Created.Value; + }, verifySucceeded: null, cancellationToken); } private static IEnumerable GetContainersToCreate(IModel model) @@ -193,17 +230,17 @@ private IUpdateAdapter AddModelData() /// 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 async Task SeedDataAsync(bool created, CancellationToken cancellationToken = default) + public virtual async Task SeedDataAsync( + bool created, CancellationToken cancellationToken = default) { var coreOptionsExtension = - _contextOptions.FindExtension() - ?? new CoreOptionsExtension(); + _contextOptions.FindExtension(); - if (coreOptionsExtension.AsyncSeeder is not null) + if (coreOptionsExtension?.AsyncSeeder is not null) { await coreOptionsExtension.AsyncSeeder(_currentContext.Context, created, cancellationToken).ConfigureAwait(false); } - else if (coreOptionsExtension.Seeder != null) + else if (coreOptionsExtension?.Seeder is not null) { throw new InvalidOperationException(CoreStrings.MissingSeeder); } diff --git a/src/EFCore.Relational/EFCore.Relational.baseline.json b/src/EFCore.Relational/EFCore.Relational.baseline.json index f776a93b79b..9e5f52bc2f0 100644 --- a/src/EFCore.Relational/EFCore.Relational.baseline.json +++ b/src/EFCore.Relational/EFCore.Relational.baseline.json @@ -7304,56 +7304,6 @@ } ] }, - { - "Type": "class Microsoft.EntityFrameworkCore.Infrastructure.StructuredJsonPath", - "Methods": [ - { - "Member": "StructuredJsonPath(System.Collections.Generic.IReadOnlyList segments, int[] indices);" - }, - { - "Member": "virtual System.Text.StringBuilder AppendTo(System.Text.StringBuilder builder);" - }, - { - "Member": "override string ToString();" - } - ], - "Properties": [ - { - "Member": "virtual bool IsRoot { get; }" - }, - { - "Member": "virtual int[] Indices { get; }" - }, - { - "Member": "static Microsoft.EntityFrameworkCore.Infrastructure.StructuredJsonPath Root { get; }" - }, - { - "Member": "virtual System.Collections.Generic.IReadOnlyList Segments { get; }" - } - ] - }, - { - "Type": "sealed class Microsoft.EntityFrameworkCore.Metadata.StructuredJsonPathSegment", - "Methods": [ - { - "Member": "StructuredJsonPathSegment(string propertyName);" - }, - { - "Member": "override string ToString();" - } - ], - "Properties": [ - { - "Member": "static Microsoft.EntityFrameworkCore.Metadata.StructuredJsonPathSegment Array { get; }" - }, - { - "Member": "bool IsArray { get; }" - }, - { - "Member": "string? PropertyName { get; }" - } - ] - }, { "Type": "class Microsoft.EntityFrameworkCore.Query.JsonQueryExpression : System.Linq.Expressions.Expression, Microsoft.EntityFrameworkCore.Query.IPrintableExpression", "Methods": [ @@ -7970,6 +7920,9 @@ { "Member": "int LastCommittedCommandIndex { get; set; }" }, + { + "Member": "bool SeedingAttempted { get; set; }" + }, { "Member": "Microsoft.EntityFrameworkCore.Storage.IDbContextTransaction? Transaction { get; set; }" } @@ -11327,6 +11280,9 @@ { "Member": "Microsoft.EntityFrameworkCore.Storage.IExecutionStrategy ExecutionStrategy { get; }" }, + { + "Member": "Microsoft.EntityFrameworkCore.Diagnostics.IDiagnosticsLogger Logger { get; init; }" + }, { "Member": "Microsoft.EntityFrameworkCore.Migrations.IMigrationCommandExecutor MigrationCommandExecutor { get; init; }" }, @@ -16941,7 +16897,8 @@ ], "Properties": [ { - "Member": "static string BadSequenceString { get; }" + "Member": "static string BadSequenceString { get; }", + "Stage": "Obsolete" }, { "Member": "static string BadSequenceType { get; }" @@ -19928,6 +19885,56 @@ } ] }, + { + "Type": "class Microsoft.EntityFrameworkCore.Infrastructure.StructuredJsonPath", + "Methods": [ + { + "Member": "StructuredJsonPath(System.Collections.Generic.IReadOnlyList segments, int[] indices);" + }, + { + "Member": "virtual System.Text.StringBuilder AppendTo(System.Text.StringBuilder builder);" + }, + { + "Member": "override string ToString();" + } + ], + "Properties": [ + { + "Member": "virtual int[] Indices { get; }" + }, + { + "Member": "virtual bool IsRoot { get; }" + }, + { + "Member": "static Microsoft.EntityFrameworkCore.Infrastructure.StructuredJsonPath Root { get; }" + }, + { + "Member": "virtual System.Collections.Generic.IReadOnlyList Segments { get; }" + } + ] + }, + { + "Type": "sealed class Microsoft.EntityFrameworkCore.Metadata.StructuredJsonPathSegment", + "Methods": [ + { + "Member": "StructuredJsonPathSegment(string propertyName);" + }, + { + "Member": "override string ToString();" + } + ], + "Properties": [ + { + "Member": "static Microsoft.EntityFrameworkCore.Metadata.StructuredJsonPathSegment Array { get; }" + }, + { + "Member": "bool IsArray { get; }" + }, + { + "Member": "string? PropertyName { get; }" + } + ] + }, { "Type": "class Microsoft.EntityFrameworkCore.Metadata.Builders.TableBuilder : Microsoft.EntityFrameworkCore.Infrastructure.IInfrastructure", "Methods": [ diff --git a/src/EFCore.Relational/Migrations/Internal/Migrator.cs b/src/EFCore.Relational/Migrations/Internal/Migrator.cs index 55661d495eb..1ec0609d7b1 100644 --- a/src/EFCore.Relational/Migrations/Internal/Migrator.cs +++ b/src/EFCore.Relational/Migrations/Internal/Migrator.cs @@ -192,6 +192,12 @@ private bool MigrateImplementation( var seed = coreOptionsExtension.Seeder; if (seed != null) { + if (state.SeedingAttempted) + { + context.ChangeTracker.Clear(); + } + + state.SeedingAttempted = true; seed(context, state.AnyOperationPerformed); } else if (coreOptionsExtension.AsyncSeeder != null) @@ -328,6 +334,12 @@ await _migrationCommandExecutor.ExecuteNonQueryAsync( var seedAsync = coreOptionsExtension.AsyncSeeder; if (seedAsync != null) { + if (state.SeedingAttempted) + { + context.ChangeTracker.Clear(); + } + + state.SeedingAttempted = true; await seedAsync(context, state.AnyOperationPerformed, cancellationToken).ConfigureAwait(false); } else if (coreOptionsExtension.Seeder != null) diff --git a/src/EFCore.Relational/Migrations/MigrationExecutionState.cs b/src/EFCore.Relational/Migrations/MigrationExecutionState.cs index 2f3b3e469a4..ff3e82d15ba 100644 --- a/src/EFCore.Relational/Migrations/MigrationExecutionState.cs +++ b/src/EFCore.Relational/Migrations/MigrationExecutionState.cs @@ -32,4 +32,9 @@ public sealed class MigrationExecutionState /// The transaction that is in use. /// public IDbContextTransaction? Transaction { get; set; } + + /// + /// Indicates whether seeding has been attempted. + /// + public bool SeedingAttempted { get; set; } } diff --git a/src/EFCore.Relational/Storage/RelationalDatabaseCreator.cs b/src/EFCore.Relational/Storage/RelationalDatabaseCreator.cs index fffd11eac4a..3fbad717c5d 100644 --- a/src/EFCore.Relational/Storage/RelationalDatabaseCreator.cs +++ b/src/EFCore.Relational/Storage/RelationalDatabaseCreator.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Runtime.CompilerServices; using System.Text; using System.Transactions; @@ -237,6 +238,12 @@ public virtual bool EnsureCreated() using var transactionScope = new TransactionScope( TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled); + if (Dependencies.CurrentContext.Context.ChangeTracker.Entries().Any( + e => e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted)) + { + Dependencies.Logger.EnsureCreatedWithTrackedEntitiesWarning(); + } + var operationsPerformed = false; if (!Exists()) { @@ -250,24 +257,34 @@ public virtual bool EnsureCreated() operationsPerformed = true; } - var coreOptionsExtension = - Dependencies.ContextOptions.FindExtension() - ?? new CoreOptionsExtension(); - - var seed = coreOptionsExtension.Seeder; - if (seed != null) - { - var context = Dependencies.CurrentContext.Context; - using var transaction = context.Database.BeginTransaction(); - seed(context, operationsPerformed); - transaction.Commit(); - } - else if (coreOptionsExtension.AsyncSeeder != null) - { - throw new InvalidOperationException(CoreStrings.MissingSeeder); - } - - return operationsPerformed; + return Dependencies.ExecutionStrategy.Execute( + (Creator: this, Created: operationsPerformed, Retrying: new StrongBox(false)), + static (context, state) => + { + if (state.Retrying.Value) + { + context.ChangeTracker.Clear(); + } + + state.Retrying.Value = true; + + var coreOptionsExtension = + state.Creator.Dependencies.ContextOptions.FindExtension(); + + var seed = coreOptionsExtension?.Seeder; + if (seed != null) + { + using var transaction = context.Database.BeginTransaction(); + seed(context, state.Created); + transaction.Commit(); + } + else if (coreOptionsExtension?.AsyncSeeder != null) + { + throw new InvalidOperationException(CoreStrings.MissingSeeder); + } + + return state.Created; + }, verifySucceeded: null); } /// @@ -285,6 +302,12 @@ public virtual async Task EnsureCreatedAsync(CancellationToken cancellatio { using var transactionScope = new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled); + if (Dependencies.CurrentContext.Context.ChangeTracker.Entries().Any( + e => e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted)) + { + Dependencies.Logger.EnsureCreatedWithTrackedEntitiesWarning(); + } + var operationsPerformed = false; if (!await ExistsAsync(cancellationToken).ConfigureAwait(false)) { @@ -300,25 +323,35 @@ public virtual async Task EnsureCreatedAsync(CancellationToken cancellatio operationsPerformed = true; } - var coreOptionsExtension = - Dependencies.ContextOptions.FindExtension() - ?? new CoreOptionsExtension(); - - var seedAsync = coreOptionsExtension.AsyncSeeder; - if (seedAsync != null) - { - var context = Dependencies.CurrentContext.Context; - var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); - await using var _ = transaction.ConfigureAwait(false); - await seedAsync(context, operationsPerformed, cancellationToken).ConfigureAwait(false); - await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); - } - else if (coreOptionsExtension.Seeder != null) - { - throw new InvalidOperationException(CoreStrings.MissingSeeder); - } - - return operationsPerformed; + return await Dependencies.ExecutionStrategy.ExecuteAsync( + (Creator: this, Created: operationsPerformed, Retrying: new StrongBox(false)), + static async (context, state, ct) => + { + if (state.Retrying.Value) + { + context.ChangeTracker.Clear(); + } + + state.Retrying.Value = true; + + var coreOptionsExtension = + state.Creator.Dependencies.ContextOptions.FindExtension(); + + var seedAsync = coreOptionsExtension?.AsyncSeeder; + if (seedAsync != null) + { + var transaction = await context.Database.BeginTransactionAsync(ct).ConfigureAwait(false); + await using var _ = transaction.ConfigureAwait(false); + await seedAsync(context, state.Created, ct).ConfigureAwait(false); + await transaction.CommitAsync(ct).ConfigureAwait(false); + } + else if (coreOptionsExtension?.Seeder != null) + { + throw new InvalidOperationException(CoreStrings.MissingSeeder); + } + + return state.Created; + }, verifySucceeded: null, cancellationToken).ConfigureAwait(false); } /// diff --git a/src/EFCore.Relational/Storage/RelationalDatabaseCreatorDependencies.cs b/src/EFCore.Relational/Storage/RelationalDatabaseCreatorDependencies.cs index 0ada3276fec..d2d00a7af1e 100644 --- a/src/EFCore.Relational/Storage/RelationalDatabaseCreatorDependencies.cs +++ b/src/EFCore.Relational/Storage/RelationalDatabaseCreatorDependencies.cs @@ -55,7 +55,8 @@ public RelationalDatabaseCreatorDependencies( ICurrentDbContext currentContext, IDbContextOptions contextOptions, IRelationalCommandDiagnosticsLogger commandLogger, - IExceptionDetector exceptionDetector) + IExceptionDetector exceptionDetector, + IDiagnosticsLogger logger) { Connection = connection; ModelDiffer = modelDiffer; @@ -67,6 +68,7 @@ public RelationalDatabaseCreatorDependencies( ContextOptions = contextOptions; CommandLogger = commandLogger; ExceptionDetector = exceptionDetector; + Logger = logger; } /// @@ -118,4 +120,9 @@ public RelationalDatabaseCreatorDependencies( /// Gets the exception detector. /// public IExceptionDetector ExceptionDetector { get; init; } + + /// + /// Gets the logger. + /// + public IDiagnosticsLogger Logger { get; init; } } diff --git a/src/EFCore/Diagnostics/CoreEventId.cs b/src/EFCore/Diagnostics/CoreEventId.cs index 62a2cad329b..e68acbf39c9 100644 --- a/src/EFCore/Diagnostics/CoreEventId.cs +++ b/src/EFCore/Diagnostics/CoreEventId.cs @@ -91,6 +91,7 @@ private enum Id RedundantAddServicesCallWarning = CoreBaseId + 410, OldModelVersionWarning = CoreBaseId + 411, CompiledModelProviderMismatchWarning = CoreBaseId + 412, + EnsureCreatedWithTrackedEntitiesWarning = CoreBaseId + 413, // Model and ModelValidation events ShadowPropertyCreated = CoreBaseId + 600, @@ -505,6 +506,20 @@ private static EventId MakeInfraId(Id id) /// public static readonly EventId CompiledModelProviderMismatchWarning = MakeInfraId(Id.CompiledModelProviderMismatchWarning); + /// + /// + /// was called on a context that is already tracking + /// added, modified, or deleted entities. These changes would be lost if a retry occurs due to a transient failure. + /// + /// + /// This event is in the category. + /// + /// + /// This event uses the payload when used with a . + /// + /// + public static readonly EventId EnsureCreatedWithTrackedEntitiesWarning = MakeInfraId(Id.EnsureCreatedWithTrackedEntitiesWarning); + private static readonly string _modelPrefix = DbLoggerCategory.Model.Name + "."; private static EventId MakeModelId(Id id) diff --git a/src/EFCore/Diagnostics/CoreLoggerExtensions.cs b/src/EFCore/Diagnostics/CoreLoggerExtensions.cs index 171ff92845c..3b76077e276 100644 --- a/src/EFCore/Diagnostics/CoreLoggerExtensions.cs +++ b/src/EFCore/Diagnostics/CoreLoggerExtensions.cs @@ -270,6 +270,30 @@ private static string CompiledModelProviderMismatch(EventDefinitionBase definiti return d.GenerateMessage(p.MismatchedProviderName, p.CurrentProviderName); } + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + public static void EnsureCreatedWithTrackedEntitiesWarning( + this IDiagnosticsLogger diagnostics) + { + var definition = CoreResources.LogEnsureCreatedWithTrackedEntities(diagnostics); + + if (diagnostics.ShouldLog(definition)) + { + definition.Log(diagnostics); + } + + if (diagnostics.NeedsEventData(definition, out var diagnosticSourceEnabled, out var simpleLogEnabled)) + { + var eventData = new EventData( + definition, + (d, _) => ((EventDefinition)d).GenerateMessage()); + + diagnostics.DispatchEventData(definition, eventData, diagnosticSourceEnabled, simpleLogEnabled); + } + } + /// /// Logs for the event. /// diff --git a/src/EFCore/Diagnostics/LoggingDefinitions.cs b/src/EFCore/Diagnostics/LoggingDefinitions.cs index 30b462e0aae..55133e004e3 100644 --- a/src/EFCore/Diagnostics/LoggingDefinitions.cs +++ b/src/EFCore/Diagnostics/LoggingDefinitions.cs @@ -115,6 +115,15 @@ public abstract class LoggingDefinitions [EntityFrameworkInternal] public EventDefinitionBase? LogCompiledModelProviderMismatch; + /// + /// 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. + /// + [EntityFrameworkInternal] + public EventDefinitionBase? LogEnsureCreatedWithTrackedEntities; + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore/EFCore.baseline.json b/src/EFCore/EFCore.baseline.json index 8ae23aac4cd..b52c3b307fa 100644 --- a/src/EFCore/EFCore.baseline.json +++ b/src/EFCore/EFCore.baseline.json @@ -2930,6 +2930,9 @@ { "Member": "static readonly Microsoft.Extensions.Logging.EventId DuplicateDependentEntityTypeInstanceWarning" }, + { + "Member": "static readonly Microsoft.Extensions.Logging.EventId EnsureCreatedWithTrackedEntitiesWarning" + }, { "Member": "static readonly Microsoft.Extensions.Logging.EventId ExecutionStrategyRetrying" }, @@ -3178,6 +3181,9 @@ { "Member": "static void DuplicateDependentEntityTypeInstanceWarning(this Microsoft.EntityFrameworkCore.Diagnostics.IDiagnosticsLogger diagnostics, Microsoft.EntityFrameworkCore.Metadata.IEntityType dependent1, Microsoft.EntityFrameworkCore.Metadata.IEntityType dependent2);" }, + { + "Member": "static void EnsureCreatedWithTrackedEntitiesWarning(this Microsoft.EntityFrameworkCore.Diagnostics.IDiagnosticsLogger diagnostics);" + }, { "Member": "static void ExecutionStrategyRetrying(this Microsoft.EntityFrameworkCore.Diagnostics.IDiagnosticsLogger diagnostics, System.Collections.Generic.IReadOnlyList exceptionsEncountered, System.TimeSpan delay, bool async);" }, @@ -4151,9 +4157,6 @@ { "Member": "static string InvalidIncludeExpression(object? expression);" }, - { - "Member": "static string InvalidStructuredJsonPathIndexCount(object? indicesCount, object? arraySegmentCount);" - }, { "Member": "static string InvalidKeyValue(object? entityType, object? keyProperty);" }, @@ -4196,6 +4199,9 @@ { "Member": "static string InvalidSetTypeOwned(object? typeName, object? ownerType);" }, + { + "Member": "static string InvalidStructuredJsonPathIndexCount(object? indicesCount, object? arraySegmentCount);" + }, { "Member": "static string InvalidSwitch(object? name, object? value);" }, @@ -20777,10 +20783,10 @@ ], "Properties": [ { - "Member": "virtual string MismatchedProviderName { get; }" + "Member": "virtual string CurrentProviderName { get; }" }, { - "Member": "virtual string CurrentProviderName { get; }" + "Member": "virtual string MismatchedProviderName { get; }" } ] }, diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index 5ce3819559d..bf9d212a078 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -3894,6 +3894,31 @@ public static EventDefinition LogCompiledModelProviderMismatch(I return (EventDefinition)definition; } + /// + /// 'EnsureCreated' was called on a context that is already tracking added, modified, or deleted entities. These tracked changes would be lost if a retry occurs due to a transient failure. + /// + public static EventDefinition LogEnsureCreatedWithTrackedEntities(IDiagnosticsLogger logger) + { + var definition = ((LoggingDefinitions)logger.Definitions).LogEnsureCreatedWithTrackedEntities; + if (definition == null) + { + definition = NonCapturingLazyInitializer.EnsureInitialized( + ref ((LoggingDefinitions)logger.Definitions).LogEnsureCreatedWithTrackedEntities, + logger, + static logger => new EventDefinition( + logger.Options, + CoreEventId.EnsureCreatedWithTrackedEntitiesWarning, + LogLevel.Warning, + "CoreEventId.EnsureCreatedWithTrackedEntitiesWarning", + level => LoggerMessage.Define( + level, + CoreEventId.EnsureCreatedWithTrackedEntitiesWarning, + _resourceManager.GetString("LogEnsureCreatedWithTrackedEntities")!))); + } + + return (EventDefinition)definition; + } + /// /// The unchanged property '{typePath}.{property}' was detected as changed and will be marked as modified. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see property values. /// diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index ab03f9b8cab..673e2719b39 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -948,6 +948,10 @@ A compiled model was found but it was built for the database provider '{compiledProviderName}'. The current context is using the database provider '{currentProviderName}'. The compiled model was ignored. Regenerate the compiled model with the correct provider. Warning CoreEventId.CompiledModelProviderMismatchWarning string string + + 'EnsureCreated' was called on a context that is already tracking added, modified, or deleted entities. These tracked changes would be lost if a retry occurs due to a transient failure. Call 'EnsureCreated' before making changes to the context, or disable this warning by using 'ConfigureWarnings(w => w.Ignore(CoreEventId.EnsureCreatedWithTrackedEntitiesWarning))'. + Warning CoreEventId.EnsureCreatedWithTrackedEntitiesWarning + The unchanged property '{typePath}.{property}' was detected as changed and will be marked as modified. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see property values. Debug CoreEventId.ComplexElementPropertyChangeDetected string string diff --git a/test/EFCore.Cosmos.FunctionalTests/EmbeddedDocumentsTest.cs b/test/EFCore.Cosmos.FunctionalTests/EmbeddedDocumentsTest.cs index 82032072fb4..3bd6976bbd7 100644 --- a/test/EFCore.Cosmos.FunctionalTests/EmbeddedDocumentsTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/EmbeddedDocumentsTest.cs @@ -163,7 +163,7 @@ await context.AddAsync( new Person { Id = 3, Addresses = new List
{ existingAddress1Person3, existingAddress2Person3 } }); await context.SaveChangesAsync(); - var people = await context.Set().ToListAsync(); + var people = await context.Set().OrderBy(o => o.Id).ToListAsync(); Assert.Empty(people[0].Addresses); @@ -190,7 +190,7 @@ await context.AddAsync( using (var context = new EmbeddedTransportationContext(options)) { - var people = await context.Set().ToListAsync(); + var people = await context.Set().OrderBy(o => o.Id).ToListAsync(); addedAddress1 = new Address { Street = "First", diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs index f3e2da296dd..fda17714951 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs @@ -2215,7 +2215,11 @@ public override Task Select_expression_date_add_milliseconds_below_the_range(boo => Fixture.NoSyncTest( async, async a => { - await base.Select_expression_date_add_milliseconds_below_the_range(a); + await AssertQuery( + a, + ss => ss.Set().Where(o => o.OrderDate != null) + .Select(o => new Order { OrderDate = o.OrderDate.Value.AddMilliseconds(-1000000000000) }), + elementSorter: e => e.OrderDate); AssertSql( """ @@ -3692,7 +3696,17 @@ public override Task Checked_context_with_arithmetic_does_not_fail(bool async) => Fixture.NoSyncTest( async, async a => { - await base.Checked_context_with_arithmetic_does_not_fail(async); + checked + { + await AssertQuery( + a, + ss => ss.Set() + .Where(w => w.Quantity + 1 == 5 && w.Quantity - 1 == 3 && w.Quantity * 1 == w.Quantity) + .OrderBy(o => o.OrderID) + .Select(o => o), + elementSorter: e => (e.OrderID, e.ProductID), + elementAsserter: (e, a2) => { AssertEqual(e, a2); }); + } AssertSql( """ @@ -4363,7 +4377,10 @@ public override Task Convert_to_nullable_on_nullable_value_is_ignored(bool async => Fixture.NoSyncTest( async, async a => { - await base.Convert_to_nullable_on_nullable_value_is_ignored(a); + await AssertQuery( + a, + ss => ss.Set().Select(o => new Order { OrderDate = o.OrderDate.Value }), + elementSorter: e => e.OrderDate); AssertSql( """ diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs index 614a138ed0c..f08d13fc970 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs @@ -2217,7 +2217,10 @@ ORDER BY c["Id"] public override async Task Project_inline_collection() { - await base.Project_inline_collection(); + await AssertQuery( + ss => ss.Set().Select(x => new[] { x.String, "foo" }), + elementAsserter: (e, a) => AssertCollection(e, a, ordered: true), + elementSorter: e => e[0]); // The following should be SELECT VALUE [c["String"], "foo"], #33779 AssertSql( diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/ReadItemPartitionKeyQueryFixtureBase.cs b/test/EFCore.Cosmos.FunctionalTests/Query/ReadItemPartitionKeyQueryFixtureBase.cs index e451dfb03d7..5b9ff1577a2 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/ReadItemPartitionKeyQueryFixtureBase.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/ReadItemPartitionKeyQueryFixtureBase.cs @@ -108,7 +108,7 @@ public override ISetSource GetExpectedData() { { typeof(HierarchicalPartitionKeyEntity), e => ((HierarchicalPartitionKeyEntity?)e)?.Id }, { typeof(OnlyHierarchicalPartitionKeyEntity), e => ((OnlyHierarchicalPartitionKeyEntity?)e)?.Payload }, - { typeof(SinglePartitionKeyEntity), e => ((SinglePartitionKeyEntity?)e)?.Id }, + { typeof(SinglePartitionKeyEntity), e => (((SinglePartitionKeyEntity?)e)?.Id, ((SinglePartitionKeyEntity?)e)?.PartitionKey) }, { typeof(FancyDiscriminatorEntity), e => ((FancyDiscriminatorEntity?)e)?.Id }, { typeof(OnlySinglePartitionKeyEntity), e => ((OnlySinglePartitionKeyEntity?)e)?.Payload }, { typeof(NoPartitionKeyEntity), e => ((NoPartitionKeyEntity?)e)?.Id }, diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/ReadItemPartitionKeyQueryInheritanceFixtureBase.cs b/test/EFCore.Cosmos.FunctionalTests/Query/ReadItemPartitionKeyQueryInheritanceFixtureBase.cs index 648715675c6..714390b2764 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/ReadItemPartitionKeyQueryInheritanceFixtureBase.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/ReadItemPartitionKeyQueryInheritanceFixtureBase.cs @@ -68,7 +68,7 @@ public ReadItemPartitionKeyQueryInheritanceFixtureBase() = new Func(e => ((DerivedOnlyHierarchicalPartitionKeyEntity)e).DerivedPayload); sorters[typeof(DerivedSinglePartitionKeyEntity)] - = new Func(e => ((DerivedSinglePartitionKeyEntity)e).Id); + = new Func(e => (((DerivedSinglePartitionKeyEntity)e).Id, ((DerivedSinglePartitionKeyEntity)e).PartitionKey)); sorters[typeof(DerivedOnlySinglePartitionKeyEntity)] = new Func(e => ((DerivedOnlySinglePartitionKeyEntity)e).DerivedPayload); diff --git a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs index 8db814b5c87..2f6502998df 100644 --- a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs +++ b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs @@ -1,8 +1,10 @@ // 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.Concurrent; using System.Net; using System.Net.Sockets; +using System.Runtime.CompilerServices; using Azure; using Azure.Core; using Azure.ResourceManager; @@ -24,6 +26,41 @@ public class CosmosTestStore : TestStore private static readonly Guid _runId = Guid.NewGuid(); private static bool? _connectionAvailable; + // The Northwind database is shared across multiple test fixtures and is deleted at process exit + // to avoid one fixture's disposal racing with another fixture's queries. + private const string DeferredDeletionStoreName = "Northwind"; + private static readonly ConcurrentDictionary _deferredStores = new(); + + static CosmosTestStore() + { + AppDomain.CurrentDomain.ProcessExit += static (_, _) => + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + try + { + Task.WhenAll(_deferredStores.Select( + async entry => + { + var store = entry.Value; + try + { + store.GetTestStoreIndex(store.ServiceProvider) + .RemoveShared(store.GetType().Name + store.Name); + await store.EnsureDeletedAsync(store._storeContext, cts.Token).ConfigureAwait(false); + } + catch + { + } + + store._storeContext.Dispose(); + })).GetAwaiter().GetResult(); + } + catch + { + } + }; + } + public static CosmosTestStore Create(string name, Action? extensionConfiguration = null) => new(name, shared: false, extensionConfiguration: extensionConfiguration); @@ -58,6 +95,18 @@ private CosmosTestStore( }; _storeContext = new TestStoreContext(this); + + if (shared && name == DeferredDeletionStoreName) + { + _deferredStores.TryAdd(Name, this); + } + else if (shared) + { + Check.DebugAssert( + !_deferredStores.ContainsKey(Name) && !_deferredStores.Values.Any(s => s.Name == Name), + $"Cosmos database '{name}' is shared across multiple fixture types. " + + "Add it to the deferred deletion list or give each fixture a unique StoreName."); + } } private static string CreateName(string name) @@ -250,12 +299,21 @@ private async Task EnsureDeletedAsync(DbContext context, CancellationToken } public override Task CleanAsync(DbContext context, bool createTables = true) - => new TestCosmosExecutionStrategy().ExecuteAsync( - (context, createTables), async (_, state, ct) => + { + context.ChangeTracker.Clear(); + return new TestCosmosExecutionStrategy().ExecuteAsync( + (context, createTables, Retrying: new StrongBox(false)), async (_, state, ct) => { + if (state.Retrying.Value) + { + state.context.ChangeTracker.Clear(); + } + + state.Retrying.Value = true; await CleanAsyncImpl(state.context, state.createTables).ConfigureAwait(false); return true; }, null, default); + } private async Task CleanAsyncImpl(DbContext context, bool createTables) { @@ -481,21 +539,27 @@ private static async Task SeedAsync(DbContext context) public override async ValueTask DisposeAsync() { - if (_initialized) + if (!_initialized || _connectionAvailable == false) { - if (_connectionAvailable == false) - { - return; - } + return; + } - if (Shared) + if (_deferredStores.TryGetValue(Name, out var canonical)) + { + if (!ReferenceEquals(this, canonical)) { - GetTestStoreIndex(ServiceProvider).RemoveShared(GetType().Name + Name); + _storeContext.Dispose(); } - await EnsureDeletedAsync(_storeContext).ConfigureAwait(false); + return; + } + + if (Shared) + { + GetTestStoreIndex(ServiceProvider).RemoveShared(GetType().Name + Name); } + await EnsureDeletedAsync(_storeContext).ConfigureAwait(false); _storeContext.Dispose(); } diff --git a/test/EFCore.Cosmos.FunctionalTests/Update/CosmosBulkConcurrencyTest.cs b/test/EFCore.Cosmos.FunctionalTests/Update/CosmosBulkConcurrencyTest.cs index 59bc6392de3..c9a03fd6f43 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Update/CosmosBulkConcurrencyTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Update/CosmosBulkConcurrencyTest.cs @@ -17,6 +17,9 @@ public override Task Updating_then_updating_the_same_entity_results_in_DbUpdateC public class ConcurrencyFixture : CosmosConcurrencyTest.CosmosFixture { + protected override string StoreName + => nameof(CosmosBulkConcurrencyTest); + public override ConcurrencyContext CreateContext() { var context = base.CreateContext(); diff --git a/test/EFCore.Cosmos.FunctionalTests/Update/CosmosBulkExecutionTest.cs b/test/EFCore.Cosmos.FunctionalTests/Update/CosmosBulkExecutionTest.cs index 1ce5cc30df7..94b9b15708d 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Update/CosmosBulkExecutionTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Update/CosmosBulkExecutionTest.cs @@ -1,9 +1,13 @@ // 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.Update; -public class CosmosBulkExecutionTest(NonSharedFixture fixture) : NonSharedModelTestBase(fixture), IClassFixture +public class CosmosBulkExecutionTest(NonSharedFixture nonSharedFixture, CosmosBulkExecutionTest.BulkFixture fixture) + : NonSharedModelTestBase(nonSharedFixture), IClassFixture, IClassFixture { protected override string NonSharedStoreName => nameof(CosmosBulkExecutionTest); @@ -14,54 +18,106 @@ public class CosmosBulkExecutionTest(NonSharedFixture fixture) : NonSharedModelT [CosmosCondition(CosmosCondition.IsNotLinuxEmulator)] public virtual async Task DoesNotBatchSingleBatchableWrite() { - var contextFactory = await InitializeNonSharedTest(onConfiguring: (cfg) => cfg.UseCosmos(c => c.BulkExecutionAllowed()).ConfigureWarnings(x => x.Ignore(CosmosEventId.BulkExecutionWithTransactionalBatch))); - using var context = contextFactory.CreateDbContext(); + using var context = fixture.CreateContext(); - context.Add(new Customer() { PartitionKey = "4" }); + context.Add(new Customer { PartitionKey = "4" }); context.AddRange(Enumerable.Range(0, 3).Select(x => new Customer())); - context.AddRange(Enumerable.Range(0, 3).Select(x => new Customer() { PartitionKey = "2"})); - context.Add(new Customer() { PartitionKey = "3" }); + context.AddRange(Enumerable.Range(0, 3).Select(x => new Customer { PartitionKey = "2" })); + context.Add(new Customer { PartitionKey = "3" }); + + fixture.ListLoggerFactory.Clear(); + + await context.SaveChangesAsync(); + Assert.Equal(CosmosEventId.ExecutedCreateItem, fixture.ListLoggerFactory.Log[0].Id); + Assert.Equal(CosmosEventId.ExecutedCreateItem, fixture.ListLoggerFactory.Log[1].Id); + Assert.Equal(CosmosEventId.ExecutedTransactionalBatch, fixture.ListLoggerFactory.Log[2].Id); + Assert.Equal(CosmosEventId.ExecutedTransactionalBatch, fixture.ListLoggerFactory.Log[3].Id); + } - ListLoggerFactory.Log.Clear(); + [ConditionalFact] + public virtual async Task AutoTransactionBehaviorNever_DoesNotThrow() + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: cfg => cfg.UseCosmos(x => x.BulkExecutionAllowed())); + using var context = contextFactory.CreateDbContext(); + context.Database.AutoTransactionBehavior = AutoTransactionBehavior.Never; + context.AddRange(Enumerable.Range(0, 100).Select(x => new Customer())); await context.SaveChangesAsync(); - Assert.Equal(CosmosEventId.ExecutedCreateItem, ListLoggerFactory.Log[0].Id); - Assert.Equal(CosmosEventId.ExecutedCreateItem, ListLoggerFactory.Log[1].Id); - Assert.Equal(CosmosEventId.ExecutedTransactionalBatch, ListLoggerFactory.Log[2].Id); - Assert.Equal(CosmosEventId.ExecutedTransactionalBatch, ListLoggerFactory.Log[3].Id); + } + + [ConditionalFact] + // https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/292 (Transactional batch limits not enforced) + [CosmosCondition(CosmosCondition.IsNotLinuxEmulator)] + public virtual async Task AutoTransactionBehaviorWhenNeeded_Throws() + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: cfg => cfg.UseCosmos(x => x.BulkExecutionAllowed())); + using var context = contextFactory.CreateDbContext(); + context.Database.AutoTransactionBehavior = AutoTransactionBehavior.WhenNeeded; + + context.AddRange(Enumerable.Range(0, 200).Select(x => new Customer())); + var ex = await Assert.ThrowsAsync(() => context.SaveChangesAsync()); + Assert.Equal(BulkExecutionWithTransactionalBatchMessage, ex.Message); + } + + [ConditionalFact] + // https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/292 (Transactional batch limits not enforced) + [CosmosCondition(CosmosCondition.IsNotLinuxEmulator)] + public virtual async Task AutoTransactionBehaviorAlways_Throws() + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: cfg => cfg.UseCosmos(x => x.BulkExecutionAllowed())); + using var context = contextFactory.CreateDbContext(); + context.Database.AutoTransactionBehavior = AutoTransactionBehavior.Always; + + context.AddRange(Enumerable.Range(0, 200).Select(x => new Customer())); + var ex = await Assert.ThrowsAsync(() => context.SaveChangesAsync()); + Assert.Equal(BulkExecutionWithTransactionalBatchMessage, ex.Message); } [ConditionalFact] public async Task SessionEnabled_Throws() { - var contextFactory = await InitializeNonSharedTest(onConfiguring: (cfg) => cfg.UseCosmos(c => c.BulkExecutionAllowed().SessionTokenManagementMode(Cosmos.Infrastructure.SessionTokenManagementMode.SemiAutomatic))); + var contextFactory = await InitializeNonSharedTest( + onConfiguring: cfg => cfg.UseCosmos( + c => c.BulkExecutionAllowed() + .SessionTokenManagementMode(Cosmos.Infrastructure.SessionTokenManagementMode.SemiAutomatic))); using var context = contextFactory.CreateDbContext(); context.Database.AutoTransactionBehavior = AutoTransactionBehavior.Never; context.Database.UseSessionToken("0:-1#1"); context.Add(new Customer()); var ex = await Assert.ThrowsAsync(() => context.SaveChangesAsync()); var inner = Assert.IsType(ex.InnerException); - Assert.Contains("Consistency, Session, Properties, and Triggers are not allowed when AllowBulkExecution is set to true.", inner.Message); + Assert.Contains( + "Consistency, Session, Properties, and Triggers are not allowed when AllowBulkExecution is set to true.", + inner.Message); } [ConditionalFact] public async Task Trigger_Throws() { - var contextFactory = await InitializeNonSharedTest(onModelCreating: (b) => b.Entity().HasTrigger(NonSharedStoreName, Azure.Cosmos.Scripts.TriggerType.Post, Azure.Cosmos.Scripts.TriggerOperation.Create), onConfiguring: (cfg) => cfg.UseCosmos(c => c.BulkExecutionAllowed())); + var contextFactory = await InitializeNonSharedTest( + onModelCreating: b => b.Entity().HasTrigger( + NonSharedStoreName, Azure.Cosmos.Scripts.TriggerType.Post, Azure.Cosmos.Scripts.TriggerOperation.Create), + onConfiguring: cfg => cfg.UseCosmos(c => c.BulkExecutionAllowed())); using var context = contextFactory.CreateDbContext(); context.Database.AutoTransactionBehavior = AutoTransactionBehavior.Never; context.Add(new Customer()); var ex = await Assert.ThrowsAsync(() => context.SaveChangesAsync()); var inner = Assert.IsType(ex.InnerException); - Assert.Contains("Consistency, Session, Properties, and Triggers are not allowed when AllowBulkExecution is set to true.", inner.Message); + Assert.Contains( + "Consistency, Session, Properties, and Triggers are not allowed when AllowBulkExecution is set to true.", + inner.Message); } - public class CosmosBulkExecutionContext : DbContext - { - public CosmosBulkExecutionContext(DbContextOptions options) : base(options) - { - } + private string BulkExecutionWithTransactionalBatchMessage => CoreStrings.WarningAsErrorTemplate( + CosmosEventId.BulkExecutionWithTransactionalBatch.ToString(), + CosmosResources.LogBulkExecutionWithTransactionalBatch(new TestLogger()).GenerateMessage(), + "CosmosEventId.BulkExecutionWithTransactionalBatch"); + public class CosmosBulkExecutionContext(DbContextOptions options) : DbContext(options) + { public DbSet Customers { get; } = null!; protected override void OnModelCreating(ModelBuilder modelBuilder) @@ -77,7 +133,7 @@ public class Customer public string PartitionKey { get; set; } = "1"; } - public class CosmosFixture : SharedStoreFixtureBase + public class BulkFixture : SharedStoreFixtureBase { protected override string StoreName => nameof(CosmosBulkExecutionTest); @@ -85,6 +141,15 @@ protected override string StoreName protected override ITestStoreFactory TestStoreFactory => CosmosTestStoreFactory.Instance; - public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) => base.AddOptions(builder).UseCosmos(x => x.BulkExecutionAllowed()).ConfigureWarnings(x => x.Ignore(CosmosEventId.BulkExecutionWithTransactionalBatch)); + protected override bool UsePooling + => false; + + protected override bool ShouldLogCategory(string logCategory) + => logCategory == DbLoggerCategory.Database.Command.Name; + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + => base.AddOptions(builder) + .UseCosmos(x => x.BulkExecutionAllowed()) + .ConfigureWarnings(x => x.Ignore(CosmosEventId.BulkExecutionWithTransactionalBatch)); } } diff --git a/test/EFCore.Cosmos.FunctionalTests/Update/CosmosBulkWarningTest.cs b/test/EFCore.Cosmos.FunctionalTests/Update/CosmosBulkWarningTest.cs deleted file mode 100644 index 30729e11826..00000000000 --- a/test/EFCore.Cosmos.FunctionalTests/Update/CosmosBulkWarningTest.cs +++ /dev/null @@ -1,63 +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.Diagnostics.Internal; -using Microsoft.EntityFrameworkCore.Cosmos.Internal; -using static Microsoft.EntityFrameworkCore.Update.CosmosBulkExecutionTest; - -namespace Microsoft.EntityFrameworkCore.Update; - -public class CosmosBulkWarningTest(CosmosBulkWarningTest.ThrowingFixture fixture) : IClassFixture -{ - [ConditionalFact] - public virtual async Task AutoTransactionBehaviorNever_DoesNotThrow() - { - using var context = fixture.CreateContext(); - context.Database.AutoTransactionBehavior = AutoTransactionBehavior.Never; - - context.AddRange(Enumerable.Range(0, 100).Select(x => new Customer())); - await context.SaveChangesAsync(); - } - - [ConditionalFact] - // https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/292 (Transactional batch limits not enforced) - [CosmosCondition(CosmosCondition.IsNotLinuxEmulator)] - public virtual async Task AutoTransactionBehaviorWhenNeeded_Throws() - { - using var context = fixture.CreateContext(); - context.Database.AutoTransactionBehavior = AutoTransactionBehavior.WhenNeeded; - - context.AddRange(Enumerable.Range(0, 200).Select(x => new Customer())); - var ex = await Assert.ThrowsAsync(() => context.SaveChangesAsync()); - Assert.Equal(BulkExecutionWithTransactionalBatchMessage, ex.Message); - } - - [ConditionalFact] - // https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/292 (Transactional batch limits not enforced) - [CosmosCondition(CosmosCondition.IsNotLinuxEmulator)] - public virtual async Task AutoTransactionBehaviorAlways_Throws() - { - using var context = fixture.CreateContext(); - context.Database.AutoTransactionBehavior = AutoTransactionBehavior.Always; - - context.AddRange(Enumerable.Range(0, 200).Select(x => new Customer())); - var ex = await Assert.ThrowsAsync(() => context.SaveChangesAsync()); - Assert.Equal(BulkExecutionWithTransactionalBatchMessage, ex.Message); - } - - private string BulkExecutionWithTransactionalBatchMessage => CoreStrings.WarningAsErrorTemplate( - CosmosEventId.BulkExecutionWithTransactionalBatch.ToString(), - CosmosResources.LogBulkExecutionWithTransactionalBatch(new TestLogger()).GenerateMessage(), - "CosmosEventId.BulkExecutionWithTransactionalBatch"); - - public class ThrowingFixture : SharedStoreFixtureBase - { - protected override string StoreName - => nameof(CosmosBulkExecutionTest); - - protected override ITestStoreFactory TestStoreFactory - => CosmosTestStoreFactory.Instance; - - public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) => base.AddOptions(builder).UseCosmos(x => x.BulkExecutionAllowed()); - } -}