dotnet / dotnet/efcore

Allow to change the order of updates when using a filtered unique index

Open
#35,159 5 comments 1 reaction 1 assignee Assigned to @AndriySvyryd View on GitHub
area-save-changes customer-reported needs-design
Dominant language
C#
Stars
14.8k
Forks
3.4k
PR merge metrics
PR metrics pending

Description

## The question
I have a filtered unique index defined as:

```C#
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:

```SQL
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 `Child`ren and it goes like this:

```C#
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 `Child`ren 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:

```SQL
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

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.