6

I want to ask a question about SQL Server and EF Core. Collation in the database is Latin1_CI_AS and I want to write a search query that contains Turkish characters.

In the database, there is a record named "SELİM" in the 'personnel' table. When I write a query like this in EF Core:

    public async Task<IList<PersonnelGetDto>> Get(PersonnelGetPayload payload)
        {
           if (payload.Name != null)
                query = query.Where(x => x.Name.Contains(payload.Name)); 
        }

The list is empty if my search condition is "selim".

I don't have a chance to change the collation in the database to Turkish because our application is multilingual. I think there will be a problem with other languages. Or am I wrong?

I also wrote the string extension. However, when converting a LINQ query to SQL, all records come to the service layer because the LIKE operator does not assign the WHERE clause. It's very important to run this condition on the sql side. If I take all the dataset to the service layer and query it, it will cost me a lot.

I can solve the problem when I type a query in the database like this:

SELECT * FROM Personnel WHERE Name LIKE 'selim' COLLATE Turkish_CI_AS

I think if I can manipulate collate on EF Core I will solve the problem.

Samet Öz
  • 91
  • 1
  • 5
  • 1
    Does this answer your question? [Entity Framework core - Contains is case sensitive or case insensitive?](https://stackoverflow.com/questions/43277868/entity-framework-core-contains-is-case-sensitive-or-case-insensitive) – Eldar Nov 28 '19 at 20:52
  • No, not this. Because İ and i is different letter. I tried this solution but didn't work. – Samet Öz Nov 28 '19 at 20:53
  • Is using `indexOf` an option? `query = query.Where(x => x.Name.IndexOf(payload.Name, StringComparison.OrdinalIgnoreCase));` – Angelo Nov 28 '19 at 20:57
  • When I implement this, i got this errors: "Cannot implicitly convert type 'int' to 'bool' " and "Cannot convert lambda expression to intended delegate type because some of the return types in the block are not implicitly convertible to the delegate return type" – Samet Öz Nov 28 '19 at 20:59
  • `...Where(x =>x.Name.IndexOf(payload.Name, StringComparison.OrdinalIgnoreCase) > -1)` – Eldar Nov 28 '19 at 21:02
  • I didn't get an error. but when I type "wil" to search for "WILLIAM" in the database, I can't get results. but other "wil" containing records coming. Except William. – Samet Öz Nov 28 '19 at 21:11
  • Maybe this https://dba.stackexchange.com/questions/100643/why-these-characters-are-all-equal-in-sql-server gives some hints. If you are not allowed to modify SQL-Server settings you are lost, but maybe you find a setting that just supports any language. – Holger Nov 28 '19 at 22:14

2 Answers2

4

You're after EF.Functions.Like, which requires adding using Microsoft.EntityFrameworkCore;, if you haven't already. Then, your query would look something like:

query.Where(x => EF.Functions.Like(x.Name, $"%{payload.Name}%"))

This translates directly to the LIKE operator in the generated SQL statement. It's not available for every DBMS, but as long as you've added Microsoft.EntityFrameworkCore.SqlServer, you're good-to-go (assuming your question is tagged correctly).

Tieson T.
  • 20,774
  • 6
  • 77
  • 92
  • Hi Tieson, thank you for your interest. When implement your solution, I was launch sql profiler and observed queries to the database. But I've seen that 'like' is not included in the query. It's very important to run this condition on the sql side. If I take all the dataset to the service layer and query it, it will cost me a lot. – Samet Öz Nov 29 '19 at 05:49
  • @SametÖz As long as you haven't enumerated your values, this _would_ translate to a LIKE clause in the generated SQL. There isn't enough in your question to show where `query` comes from or what it is, so I can't really say what's going on when you run the profiler. If you're trying to build up a complex query, you'd probably want to look into LinqKit, specifically, [PredicateBuilder](http://www.albahari.com/nutshell/predicatebuilder.aspx). – Tieson T. Dec 01 '19 at 04:57
  • 1
    let me add another aspect for this, lets also add collation for the column: `query.Where(x => EF.Functions.Like(EF.Functions.Collate(x.Name,"Turkish_CI_AS"), $"%{payload.Name}%"))` – mkb Feb 06 '22 at 14:27
3

I have tested the like function but it is not resulting correct as op stated. So only one option left remains. Which is to create an interceptor and implement custom logic. I have created a sample like below :

   public class Suffixes
    {
        public const string Collate = "--Collate";
    }

    public class CollationDbCommandInterceptor : DbCommandInterceptor
    {
        private const string CollateSyntax = " collate turkish_ci_as";

        public override InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
        {
            var args = command.Parameters.OfType<DbParameter>()
                           .Where(t => t.DbType == DbType.String && t.Value.ToString().EndsWith(Suffixes.Collate)).ToList();
            if (args.Count <= 0)
                return base.ReaderExecuting(command, eventData, result);

            foreach (var parameter in args)
            {
                parameter.Value = parameter.Value.ToString().Replace(Suffixes.Collate, "");
                var equality = $"= {parameter.ParameterName}";

                var ixs = AllIndexesOf(command.CommandText, equality);

#pragma warning disable CA2100 // Review SQL queries for security vulnerabilities
                foreach (var eq in ixs)
                {
                    command.CommandText = command.CommandText.Insert(eq+equality.Length,CollateSyntax);

                }
#pragma warning restore CA2100 // Review SQL queries for security vulnerabilities

            }



            return base.ReaderExecuting(command, eventData, result);
        }

        private static IEnumerable<int> AllIndexesOf(string str, string value)
        {
            if (string.IsNullOrEmpty(value))
                throw new ArgumentException("the string to find may not be empty", nameof(value));
            var indexes = new List<int>();
            for (var index = 0; ; index += value.Length)
            {
                index = str.IndexOf(value, index);
                if (index == -1)
                    return indexes;
                indexes.Insert(0,index);
            }
        }
    }

Configuration :

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            if (!optionsBuilder.IsConfigured)
            {
               ....  
                optionsBuilder.AddInterceptors(new CollationDbCommandInterceptor());
                ....
            }
        }

Usage :

var kadayif = $"kadayıf{Suffixes.Collate}";
var william = $"Wİlliam{Suffixes.Collate}";            
var auths = ctx.Authors.Where(t =>   t.FirstName == william ||t.LastName == kadayif).ToList(); 
// returns William Shakespeare and Abuzer Kadayıf

The logic is to create an interceptor that seeks a specific suffix in sql parameters passed in the query. Injects query specific collation in to the final sql command text. I tried to cover some advanced scenarios like parameter reuse. It may require more improvements.

Please note that this example is for Entity Framework Core 3.0 which is the version that interceptors introduced. Interception in earlier ef core versions is a bit trick. You can refer to this link for further information.

Eldar
  • 9,781
  • 2
  • 10
  • 35
  • 1
    thank you very much!! this helps me a lot :) project is based on Core 2.1 but [that like](https://weblogs.asp.net/ricardoperes/interception-in-entity-framework-core) helps me in that case – coder Jun 29 '21 at 10:52