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

How do we manage the order of updates when using a filtered unique index? #35159

Open
sebdesalvador opened this issue Nov 20, 2024 · 0 comments

Comments

@sebdesalvador
Copy link

sebdesalvador commented Nov 20, 2024

The question

I have a filtered unique index defined as:

builder
    .HasIndex(e => new { e.UserId, e.IsDefault })
    .HasFilter($"[{nameof(MyEntity.IsDefault)}] = 1")
    .IsUnique()
    .HasDatabaseName($"UX_{nameof(MyEntity)}_{nameof(MyEntity.UserId)}_{nameof(MyEntity.IsDefault)}");

It translates into:

CREATE UNIQUE NONCLUSTERED INDEX [UX_MyEntity_UserId_IsDefault] ON [dbo].[MyEntity]
(
    [UserId] ASC,
    [IsDefault] ASC
)
WHERE ([IsDefault]=(1))

Because I can only have 1 record with the flag IsDefault set to true, the order of updates is important, I first need to set non default records to false, and then set the default one. Since the EF Core doesn't really know the condition/filter, it doesn't order the updates correclty and I often get an error with the message:

Cannot insert duplicate key row in object 'dbo.MyEntity' with unique index 'UX_MyEntity_UserId_IsDefault'.

Example

In the database I have:

Id UserId IsDefault
1 5 0
2 5 1
3 5 0

In the code we have a Parent and a Child object, the Parent has a collection of Children and it goes like this:

var parent = await parentEfRepository.GetByIdAsync(5, cancellationToken);
var newDefaultChild = parent.Children.Where(c => ...);
newDefaultChild.SetAsDefault();
await context.SaveChangesAsync(cancellationToken);

The SetAsDefault() method contains the logic to set other Children as non default and the result is that only 1 Child is marked as being the default one.
When we get to SaveChanges(), EF generates a SQL script that contains the updates which look like this:

SET NOCOUNT ON;
UPDATE [MyEntity] SET [IsDefault] = @p0
OUTPUT INSERTED.[PeriodEnd], INSERTED.[PeriodStart]
WHERE [Id] = @p1;
UPDATE [MyEntity] SET [IsDefault] = @p2
OUTPUT INSERTED.[PeriodEnd], INSERTED.[PeriodStart]
WHERE [Id] = @p3;

Because it's trying to update record with ID 1 first, we would end up with the following:

Id UserId IsDefault
1 5 1
2 5 1
3 5 0

And that's when the unique constraint kicks in and throws an error.
I have started experimenting with interceptors but it's getting dirty, surely someone had this issue before me?

Thanks!

Provider and version information

EF Core version: 8.0.11
Database provider: Microsoft.EntityFrameworkCore.SqlServer
Target framework: .NET 8.0
Operating system: Windows
IDE: Rider

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants