Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Write a search query which is case insensitive in EF Core?

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.

like image 837
Samet Öz Avatar asked Nov 28 '19 20:11

Samet Öz


2 Answers

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.

like image 196
Eldar Avatar answered Oct 05 '22 23:10

Eldar


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

like image 44
Tieson T. Avatar answered Oct 06 '22 01:10

Tieson T.