Skip to content

string.StartsWith(char) with wild char translates to incorrect LIKE pattern on SQL Server #38923

Description

@colinlooser

Bug description

Repro:

Build a DB context with UseSqlServer options having an entity with a string column.

Query using a string.StartsWith(char) filter for values starting with underscore or other LIKE wild char, e.g.:
db.MyEntities.Where(i => i.StringColumn.StartsWith('_'))...

Expect: SQL containing: WHERE [i].[StringColumn] LIKE N'\_%' ESCAPE N'\'

Actual: SQL contains: WHERE [i].[StringColumn] LIKE N'187%' ESCAPE N'\'

Impact:

The query runs, but includes/excludes the wrong rows.

Workaround:

Use the string.StartsWith(string) overload, e.g. .StartsWith("_"), rather than the char overload.

Note, the C# code analyzers encourage the use of the char overload.

Analysis:

This appears to be caused by the escape and wild char being added rather than concatenated,
e.g. 187 = '\' (92) + '_' (95), then the resulting integer being concatenated with "%".

See: src/EFCore.SqlServer/Query/Internal/SqlServerSqlTranslatingExpressionVisitor.cs

In latest main branch, same code in 2 places (for constant and parameter), switch with char s:

L581: StartsEndsWithContains.StartsWith => LikeEscapeChar + s + "%",
L765: StartsEndsWithContains.StartsWith => LikeEscapeChar + s + "%",

Note, EndsWith and Contains appear to work correctly.

Possible fix:

Change similar to how Contains works a couple of lines down, i.e. $"{LikeEscapeChar}{s}%".

Your code

// C# example mostly generated by Claude Code
// <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.11" />

using System.ComponentModel.DataAnnotations;
using System.Linq.Expressions;
using EFUnderscoreIssue;
using Microsoft.EntityFrameworkCore;

var options = new DbContextOptionsBuilder<AppDbContext>()
    .UseSqlServer("Server=.;Database=XXX;Trusted_Connection=True;TrustServerCertificate=True;")
    .Options;

using var db = new AppDbContext(options);

void Show(string label, Expression<Func<MyEntity, bool>> whereClause)
{
    IQueryable<MyEntity> query = db.MyEntities.Where(whereClause);
    string sql = query.ToQueryString();
    // Console.WriteLine(sql);

    // Pull out the DECLARE line (parameterised patterns) and the WHERE clause.
    var declare = sql.Split('\n').FirstOrDefault(l => l.StartsWith("DECLARE"))?.Trim() ?? "";
    var where = sql.Split('\n').FirstOrDefault(l => l.TrimStart().StartsWith("WHERE"))?.Trim() ?? "";

    Console.WriteLine($"{label,-32} {where}");
    if (declare.Length > 0)
    {
        Console.WriteLine($"{"",-32} {declare}");
    }
}

char underscore = '_';
string underscoreText = "_";

Console.WriteLine("=== constant argument ===");
Show("StartsWith('_')   char", i => i.StringColumn.StartsWith('_'));
Show("StartsWith(\"_\")   string", i => i.StringColumn.StartsWith("_"));
Show("StartsWith('%')   char", i => i.StringColumn.StartsWith('%'));
Show("StartsWith('[')   char", i => i.StringColumn.StartsWith('['));
Show("StartsWith('A')   char", i => i.StringColumn.StartsWith('A'));
Show("EndsWith('_')     char", i => i.StringColumn.EndsWith('_'));
Show("Contains('_')     char", i => i.StringColumn.Contains('_'));

Console.WriteLine();
Console.WriteLine("=== captured variable (parameterised) ===");
Show("StartsWith(char)  '_'", i => i.StringColumn.StartsWith(underscore));
Show("StartsWith(string) \"_\"", i => i.StringColumn.StartsWith(underscoreText));
Show("EndsWith(char)    '_'", i => i.StringColumn.EndsWith(underscore));

namespace EFUnderscoreIssue
{
    public class MyEntity
    {
        [Key]
        public int Id { get; set; }
        public string StringColumn { get; set; } = string.Empty;
    }

    public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
    {
        public DbSet<MyEntity> MyEntities { get; set; }
    }
}

Stack traces


Verbose output

=== constant argument ===
StartsWith('_')   char           WHERE [m].[StringColumn] LIKE N'187%' ESCAPE N'\'
StartsWith("_")   string         WHERE [m].[StringColumn] LIKE N'\_%' ESCAPE N'\'
StartsWith('%')   char           WHERE [m].[StringColumn] LIKE N'129%' ESCAPE N'\'
StartsWith('[')   char           WHERE [m].[StringColumn] LIKE N'183%' ESCAPE N'\'
StartsWith('A')   char           WHERE [m].[StringColumn] LIKE N'A%'
EndsWith('_')     char           WHERE [m].[StringColumn] LIKE N'%\_' ESCAPE N'\'
Contains('_')     char           WHERE [m].[StringColumn] LIKE N'%\_%' ESCAPE N'\'

=== captured variable (parameterised) ===
StartsWith(char)  '_'            WHERE [m].[StringColumn] LIKE @underscore_startswith ESCAPE N'\'
                                 DECLARE @underscore_startswith nvarchar(4000) = N'187%';
StartsWith(string) "_"           WHERE [m].[StringColumn] LIKE @underscoreText_startswith ESCAPE N'\'
                                 DECLARE @underscoreText_startswith nvarchar(4000) = N'\_%';
EndsWith(char)    '_'            WHERE [m].[StringColumn] LIKE @underscore_endswith ESCAPE N'\'
                                 DECLARE @underscore_endswith nvarchar(4000) = N'%\_';

EF Core version

10.0.11

Database provider

Microsoft.EntityFrameworkCore.SqlServer

Target framework

.NET 10

Operating system

Windows 11

IDE

Visual Studio 2026 18.6.2

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions