Skip to content

String properties with indexes are compared case insensitively on SQL Server #32898

@GeorgDangl

Description

@GeorgDangl

As the title states, we've had an endpoint in our ASP.NET Core Web API project covered by an integration test. The test itself was pretty simply, we're issuing a PUT request to update some database model. The failing tests tried to set the name to all uppercase and all lowercase via string.ToUpperInvariant() and string.ToLowerInvariant(). The tests started failing after I did the update to .NET 8 with EF Core 8.

Include your code

Here's a very minimal example from our code:

string originalName;
string updatedName;
using (var context = _testHelper.GetNewDanglIdentityDbContext())
{
    var dbModel = await context.OneClickRegistrationScenarios.SingleAsync(s => s.Id == _scenarioId);
    originalName = dbModel.Name;
    updatedName = dbModel.Name.ToUpperInvariant();
    dbModel.Name = updatedName;
    await context.SaveChangesAsync(); // No UPDATE statement issued in SQL
}

// Getting a new, fresh context
using (var scope = _testHelper.GetScope())
{
    using var context = scope.ServiceProvider.GetRequiredService<DanglIdentityDbContext>();
    var dbModel = await context.OneClickRegistrationScenarios.SingleAsync(s => s.Id == _scenarioId);
    Assert.Equal(updatedName, dbModel.Name); // Fails here
    Assert.NotEqual(originalName, dbModel.Name);
}

In the tests, we're running against MS SQL Server 2017 on Linux (in a Docker container on a tmpfs filesystem).

For the code block above, I'm getting the following log output. Which pretty clearly states that it saved zero changes.

Microsoft.EntityFrameworkCore.Infrastructure: Debug: Entity Framework Core 8.0.0 initialized 'DanglIdentityDbContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer:8.0.0' with options: MigrationsAssembly=Dangl.Identity 
Microsoft.EntityFrameworkCore.Query: Debug: Compiling query expression: 
'DbSet<OneClickRegistrationScenario>()
    .Single(s => s.Id == ___scenarioId_0)'
Microsoft.EntityFrameworkCore.Query: Debug: Generated query execution expression: 
'queryContext => ShapedQueryCompilingExpressionVisitor.SingleAsync<OneClickRegistrationScenario>(
    asyncEnumerable: new SingleQueryingEnumerable<OneClickRegistrationScenario>(
        (RelationalQueryContext)queryContext, 
        RelationalCommandCache.QueryExpression(
            Projection Mapping:
                EmptyProjectionMember -> Dictionary<IProperty, int> { [Property: OneClickRegistrationScenario.Id (Guid) Required PK AfterSave:Throw ValueGenerated.OnAdd, 0], [Property: OneClickRegistrationScenario.ClientRequired (bool) Required, 1], [Property: OneClickRegistrationScenario.HtmlActivationMessage (string) Required, 2], [Property: OneClickRegistrationScenario.HtmlDescription (string) Required, 3], [Property: OneClickRegistrationScenario.Name (string) Required Index MaxLength(256), 4] }
            SELECT TOP(2) o.Id, o.ClientRequired, o.HtmlActivationMessage, o.HtmlDescription, o.Name
            FROM OneClickRegistrationScenarios AS o
            WHERE o.Id == @___scenarioId_0), 
        null, 
        Func<QueryContext, DbDataReader, ResultContext, SingleQueryResultCoordinator, OneClickRegistrationScenario>, 
        Dangl.Identity.Data.DanglIdentityDbContext, 
        False, 
        False, 
        True
    ), 
    cancellationToken: queryContext.CancellationToken)'
Microsoft.EntityFrameworkCore.Database.Connection: Debug: Creating DbConnection.
Microsoft.EntityFrameworkCore.Database.Connection: Debug: Created DbConnection. (1ms).
Microsoft.EntityFrameworkCore.Database.Connection: Debug: Opening connection to database '10479797-d8a3-4a94-bb3c-e07a9b6d6414' on server 'localhost,51129'.
The thread 0xbb24 has exited with code 0 (0x0).
Microsoft.EntityFrameworkCore.Database.Connection: Debug: Opened connection to database '10479797-d8a3-4a94-bb3c-e07a9b6d6414' on server 'localhost,51129'.
Microsoft.EntityFrameworkCore.Database.Command: Debug: Creating DbCommand for 'ExecuteReader'.
Microsoft.EntityFrameworkCore.Database.Command: Debug: Created DbCommand for 'ExecuteReader' (1ms).
Microsoft.EntityFrameworkCore.Database.Command: Debug: Initialized DbCommand for 'ExecuteReader' (4ms).
Microsoft.EntityFrameworkCore.Database.Command: Debug: Executing DbCommand [Parameters=[@___scenarioId_0='?' (DbType = Guid)], CommandType='Text', CommandTimeout='30']
SELECT TOP(2) [o].[Id], [o].[ClientRequired], [o].[HtmlActivationMessage], [o].[HtmlDescription], [o].[Name]
FROM [OneClickRegistrationScenarios] AS [o]
WHERE [o].[Id] = @___scenarioId_0
Microsoft.EntityFrameworkCore.Database.Command: Information: Executed DbCommand (9ms) [Parameters=[@___scenarioId_0='?' (DbType = Guid)], CommandType='Text', CommandTimeout='30']
SELECT TOP(2) [o].[Id], [o].[ClientRequired], [o].[HtmlActivationMessage], [o].[HtmlDescription], [o].[Name]
FROM [OneClickRegistrationScenarios] AS [o]
WHERE [o].[Id] = @___scenarioId_0
Microsoft.EntityFrameworkCore.ChangeTracking: Debug: Context 'DanglIdentityDbContext' started tracking 'OneClickRegistrationScenario' entity. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see key values.
Microsoft.EntityFrameworkCore.Database.Command: Debug: Closing data reader to '10479797-d8a3-4a94-bb3c-e07a9b6d6414' on server 'localhost,51129'.
Microsoft.EntityFrameworkCore.Database.Command: Debug: A data reader for '10479797-d8a3-4a94-bb3c-e07a9b6d6414' on server 'localhost,51129' is being disposed after spending 3ms reading results.
Microsoft.EntityFrameworkCore.Database.Connection: Debug: Closing connection to database '10479797-d8a3-4a94-bb3c-e07a9b6d6414' on server 'localhost,51129'.
Microsoft.EntityFrameworkCore.Database.Connection: Debug: Closed connection to database '10479797-d8a3-4a94-bb3c-e07a9b6d6414' on server 'localhost,51129' (1ms).
Microsoft.EntityFrameworkCore.Update: Debug: SaveChanges starting for 'DanglIdentityDbContext'.
Microsoft.EntityFrameworkCore.ChangeTracking: Debug: DetectChanges starting for 'DanglIdentityDbContext'.
Microsoft.EntityFrameworkCore.ChangeTracking: Debug: DetectChanges completed for 'DanglIdentityDbContext'.
Microsoft.EntityFrameworkCore.Update: Debug: SaveChanges completed for 'DanglIdentityDbContext' with 0 entities written to the database.

I've tried the exact same code with another entity, an user from the ASP.NET Core Identity EF storage. There, I could change the username as expected to an all uppercase representation.

The model we're updating looks like this, the string property in question (Name) is marked as required and has a max length of 256:

public class OneClickRegistrationScenario
{
    [Required]
    public Guid Id { get; set; }

    [Required]
    public string Name { get; set; }

    [Required]
    public string HtmlDescription { get; set; }

    [Required]
    public string HtmlActivationMessage { get; set; }

    public bool ClientRequired { get; set; }

    public List<OneClickRegistrationScenarioDeveloperService> DeveloperServices { get; set; }

    public List<OneClickRegistrationScenarioClientScope> RequiredClientScopes { get; set; }

    public static void OnModelCreating(ModelBuilder builder)
    {
        builder.Entity<OneClickRegistrationScenario>()
            .HasIndex(x => x.Name)
            .IsUnique();

        builder.Entity<OneClickRegistrationScenario>()
            .Property(x => x.Name)
            .HasMaxLength(256);

        builder.Entity<OneClickRegistrationScenario>()
            .HasMany(x => x.RequiredClientScopes)
            .WithOne(y => y.OneClickRegistrationScenario);
    }
}

I couldn't find anything in the GitHub repo, except this issue which might seem like it's related, but I don't see anything in the PR that would affect the change tracker to not detect changes there. But, I'm also not very deep into the EF Core codebase:
#27526

Edit: I just took a look at the Debug view of the ChangeTracker, and there I saw that it was tracking the entity via it's Id property, and I saw the difference there:

OneClickRegistrationScenario {Id: fd0ba8c8-58d2-482b-af56-994338d723d1} Unchanged
    Id: 'fd0ba8c8-58d2-482b-af56-994338d723d1' PK
    ClientRequired: 'True'
    HtmlActivationMessage: 'OneClickRegistrationScenario01ActivationMessage'
    HtmlDescription: 'OneClickRegistrationScenario01Description'
    Name: 'AVACLOUD SAAS' Originally 'AVACloud SaaS'
  DeveloperServices: <null>
  RequiredClientScopes: <null>

Also, when I set another value, e.g. just Guid.NewGuid.ToString(), the change was saved correctly to the database.

Include provider and version information

EF Core version: 8.0.0
Database provider: Microsoft.EntityFrameworkCore.SqlServer
Target framework: .NET 6.0
Operating system:
IDE: Visual Studio 2022 17.8.0

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions