'EntityFramework is very slow to compare strings because create a nvarchar sqlparameter instead of varchar

I have this sample query:

context.BarcodeTipiDoc.AsQueryable().Where(d => d.Barcode.CompareTo(minBarcode) > 0);

That query runs very slow because Entity Framework creates SqlParameter for "minBarcode" as nvarchar instead of varchar.

I tried to set column mapping:

[Column("Barcode", TypeName = "varchar(21)")]   
public string Barcode { get; set; }

but nothing changed.

There is a way to tell to Entity Framework the right type of the sqlparameter?

This query is almost instantaneous:

DECLARE @__minBarcode_0 AS Varchar(21)

SET @__minBarcode_0 = 'aa'

SELECT TOP(100) [d].[Barcode], [d].[contenttype], [d].[idvolume], [d].[path_documento], [d].[Progressivo], [d].[Stato]
    FROM BarcodeTipiDoc AS [d]
    WHERE [d].[Barcode] > @__minBarcode_0

Same query generated by Entity Framework, takes several minutes because of nvarchar:

DECLARE @__minBarcode_0 AS nvarchar(21)

SET @__minBarcode_0 = 'aa'

SELECT TOP(100) [d].[Barcode], [d].[contenttype], [d].[idvolume], [d].[path_documento], [d].[Progressivo], [d].[Stato]
    FROM BarcodeTipiDoc AS [d]
    WHERE [d].[Barcode] > @__minBarcode_0

table schema:

Barcode varchar(21) Unchecked
tipodoc char(4) Unchecked
codutenteinserimento    uniqueidentifier    Checked
dataacquisizione    datetime    Checked
firmato bit Checked
tipodocdescrizione  varchar(50) Checked
Stato   int Unchecked
originedoc  tinyint Checked Unchecked

I am not allowed to change anything of the database, I just need to change generated sql code from LINQ by entity framework.

If there are no ways, I'll be forced to write and the execute the select as pure string.

The version is entity framework 2.2, but I can upgrade.



Solution 1:[1]

There is a way to tell to Entity Framework the right type of the sqlparameter?

Currently (EF Core 2.x, 3.0) there is no such way. EF Core tries to infer the parameter type from the usage inside expressions.

So TypeName = "varchar(21) or .IsUnicode(false).HasMaxLength(21) column mapping is step in the right direction.

Unfortunately 2.x parameter type inference succeeds for comparison operators like ==, > etc. but fails for methods like string.CompareTo, string.Compare etc.

This has been fixed in 3.0, but now the translation is far from optimal (CASE WHEN ... > 0 rather than simply >) and also there are many breaking changes, so upgrading just because of that doesn't make sense and is risky.

What I can offer is a solution based on a custom mapped database scalar methods similar to Entity Framework Core: Guid Greater Than for Paging. It introduces several string custom methods which are mapped to string comparison operators:

public static class StringFunctions
{
    public static bool IsGreaterThan(this string left, string right) => string.Compare(left, right) > 0;
    public static bool IsGreaterThanOrEqual(this string left, string right) => string.Compare(left, right) >= 0;
    public static bool IsLessThan(this string left, string right) => string.Compare(left, right) < 0;
    public static bool IsLessThanOrEqual(this string left, string right) => string.Compare(left, right) <= 0;
    public static ModelBuilder RegisterStringFunctions(this ModelBuilder modelBuilder) => modelBuilder
        .RegisterFunction(nameof(IsGreaterThan), ExpressionType.GreaterThan)
        .RegisterFunction(nameof(IsGreaterThanOrEqual), ExpressionType.GreaterThanOrEqual)
        .RegisterFunction(nameof(IsLessThan), ExpressionType.LessThan)
        .RegisterFunction(nameof(IsLessThanOrEqual), ExpressionType.LessThanOrEqual);
    static ModelBuilder RegisterFunction(this ModelBuilder modelBuilder, string name, ExpressionType type)
    {
        var method = typeof(StringFunctions).GetMethod(name, new[] { typeof(string), typeof(string) });
        modelBuilder.HasDbFunction(method).HasTranslation(parameters =>
        {
            var left = parameters.ElementAt(0);
            var right = parameters.ElementAt(1);
            // EF Core 2.x
            return Expression.MakeBinary(type, left, right, false, method);
        });
        return modelBuilder;
    }
}

For EF Core 3.0 replace

return Expression.MakeBinary(type, left, right, false, method);

with (plus respective usings)

if (right is SqlParameterExpression rightParam)
    right = rightParam.ApplyTypeMapping(left.TypeMapping);
else if (left is SqlParameterExpression leftParam)
    left = leftParam.ApplyTypeMapping(right.TypeMapping);
return new SqlBinaryExpression(type, left, right, typeof(bool), null);

Now all you need is to call

modelBuilder.RegisterStringFunctions();

inside your OnModelCreating override.

Then inside your query, instead of

d => d.Barcode.CompareTo(minBarcode) > 0

use

d => d.Barcode.IsGreaterThan(minBarcode)

and it will be translated to

[d].[Barcode] > @__minBarcode_0

with correct db parameter type (same as the db type of BarCode column).

Solution 2:[2]

You can define the column as non-unicode in the context's OnModelCreating override:

modelBuilder.Entity<BarcodeTipiDoc>().Property(x => x.Barcode).IsUnicode(false);

Solution 3:[3]

In your column mapping, you originally declared this:

[Column("Barcode", TypeName = "varchar(21)")]   
public string Barcode { get; set; }

Can you try this:

[Column(TypeName = "VARCHAR(21)")]
public string Barcode { get; set; }

Or you can specify in the Model Builder:

modelBuilder.Entity<BarCodeTipiDoc>()
            .Property(x=> x.BarCode)
            .HasColumnType("varchar(21)");

It would also help if you could post the model for your object BarcodeTipiDoc.

Update: Just saw that you were using EF Core.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Ivan Stoev
Solution 2 Gert Arnold
Solution 3