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

.net-core c# entity-framework-core sql-server varchar

Question

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.

1
2
11/28/2019 3:30:45 PM

Accepted Answer

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).

2
11/28/2019 8:08:21 PM

Popular Answer

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

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


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow