Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Circular dependency exception when switching values for unique index fields #30560

Closed
cjblomqvist opened this issue Mar 23, 2023 · 2 comments
Closed
Labels
closed-no-further-action The issue is closed and no further action is planned. customer-reported

Comments

@cjblomqvist
Copy link

Ask a question

Question: Is it a bug? If not, how should one deal with below scenarios?

Scenario: I have a class (entity?) with 2 fields, and a unique index for those 2 fields. I have 2 instances of that class. I swap the values of one of the fields between the 2 instances, and try to save.

Actual outcome: EF Core exception "Unable to save changes because a circular dependency was detected in the data to be saved: ..."

Expected outcome: The data to be saved.

Context: SQL Server

Related issues: #29647 (and more along that thread) - but couldn't find any describing this specific scenario.

Include your code

Disclaimer: I haven't tested below code - but have something very similar in our codebase

[Index(nameof(A), nameof(B), IsUnique = true)]
public class Test {
	public string A { get; set; }
	public int B { get; set; }

	public Test(string a, int b) {
		A = a;
		B = b;
	}
}

var instance1 = new Test("A", 1);
var instance2 = new Test("B", 2);

context.SaveChanges(); // All good

instance1.B = 2;
instance1.A = 1;

context.SaveChanges(); // Exception

Include stack traces

Include the full exception message and stack trace for any exception you encounter.

Use triple-tick fences for stack traces. For example:

Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
   at SixFour.Sub() in C:\Stuff\AllTogetherNow\SixFour\SixFour.cs:line 49
   at SixFour.Main() in C:\Stuff\AllTogetherNow\SixFour\SixFour.cs:line 54

Include verbose output

ProductSupplier = Test
ProductId = A
Order = B

System.InvalidOperationException: Unable to save changes because a circular dependency was detected in the data to be saved: 'ProductSupplier [Modified] <-
Index { 'ProductId', 'Order' } ProductSupplier [Modified] <-
Index { 'ProductId', 'Order' } ProductSupplier [Modified]To show additional information call 'DbContextOptionsBuilder.EnableSensitiveDataLogging'.'.
   at Microsoft.EntityFrameworkCore.Utilities.Multigraph`2.ThrowCycle(List`1 cycle, Func`2 formatCycle, Func`2 formatException)
   at Microsoft.EntityFrameworkCore.Utilities.Multigraph`2.TopologicalSortCore(Boolean withBatching, Func`4 tryBreakEdge, Func`2 formatCycle, Func`2 formatException)
   at Microsoft.EntityFrameworkCore.Utilities.Multigraph`2.BatchingTopologicalSort(Func`4 tryBreakEdge, Func`2 formatCycle, Func`2 formatException)
   at Microsoft.EntityFrameworkCore.Update.Internal.CommandBatchPreparer.TopologicalSort(IEnumerable`1 commands)
   at Microsoft.EntityFrameworkCore.Update.Internal.CommandBatchPreparer.BatchCommands(IList`1 entries, IUpdateAdapter updateAdapter)+MoveNext()
   at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(IList`1 entriesToSave, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(StateManager stateManager, Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
   at API.Models.MainContext.SaveChangesAsync(CancellationToken cancellationToken) in <removed>Api\Models\MainContext.cs:line 981
   at API.Controllers.ProductController.UpdateSuppliers(Int32 productId, List`1 suppliersData) in <removed>Api\Controllers\ProductController.cs:line 1121
   at lambda_method6035(Closure, Object)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.AwaitableObjectResultExecutor.Execute(ActionContext actionContext, IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Awaited|12_0(ControllerActionInvoker invoker, ValueTask`1 actionResultValueTask)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeInnerFilterAsync>g__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
   at API.Startup.<>c.<<Configure>b__14_2>d.MoveNext() in <removed>Api\Startup.cs:line 485

Include provider and version information

EF Core version: 7.0.4
Database provider: (e.g. Microsoft.EntityFrameworkCore.SqlServer)
Target framework: (e.g. .NET 7.0)
Operating system: Win 10 64 bit
IDE: (e.g. Visual Studio 2022 17.4.2)

@cjblomqvist
Copy link
Author

I realize there's probably nothing you can do about this (even with just pure SQL this is challenging to handle). Solution for our case was to simply remove the index.

@ajcvickers ajcvickers added the closed-no-further-action The issue is closed and no further action is planned. label Mar 23, 2023
@ajcvickers ajcvickers closed this as not planned Won't fix, can't repro, duplicate, stale Mar 23, 2023
@0xced
Copy link
Contributor

0xced commented Oct 3, 2023

For our case, removing the index was not an option. We went with the following approach instead: within a transaction, delete the two rows, save, perform the modification, add the rows back, save.

var instance1 = new Test("A", 1);
var instance2 = new Test("B", 2);
context.SaveChanges();

using var transaction = context.Database.BeginTransaction();

context.Remove(instance1);
context.Remove(instance2);
context.SaveChanges();

instance1.B = 2;
instance1.A = 1;
context.Add(instance1);
context.Add(instance2);
context.SaveChanges();

transaction.Commit();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
closed-no-further-action The issue is closed and no further action is planned. customer-reported
Projects
None yet
Development

No branches or pull requests

3 participants