From b4309ae10bbeb178d4c08ba6b911ff4e01d7776d Mon Sep 17 00:00:00 2001 From: Muhannad Al-Khatib Date: Tue, 13 Oct 2026 13:14:29 +0300 Subject: [PATCH] fix(pgsql): escape % and _ in Contains/StartsWith ILIKE patterns --- .../ExpressionExtensions.cs | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/SW.EfCoreExtensions.PgSql/ExpressionExtensions.cs b/SW.EfCoreExtensions.PgSql/ExpressionExtensions.cs index f5e6a26..7a5fbab 100644 --- a/SW.EfCoreExtensions.PgSql/ExpressionExtensions.cs +++ b/SW.EfCoreExtensions.PgSql/ExpressionExtensions.cs @@ -55,29 +55,10 @@ static Expression BuildFilterExpression(this Expression parameter, ISea switch (filter.Rule) { case SearchyRule.StartsWith: - //MethodInfo iLikeMethod = typeof(NpgsqlDbFunctionsExtensions).GetMethod("ILike", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); - - return Expression.Call( - typeof(NpgsqlDbFunctionsExtensions), - nameof(NpgsqlDbFunctionsExtensions.ILike), - Type.EmptyTypes, - Expression.Property(null, typeof(EF), nameof(EF.Functions)), - fieldNameExpression, - Expression.Constant($"{filter.Value}%") - ); - - //return Expression.Call(fieldNameExpression, typeof(string).GetMethod("StartsWith", new Type[] { typeof(string) }), GetValueAsConstantExpression(filter.Value, fieldType)); + return BuildILikeExpression(fieldNameExpression, $"{EscapeLikePattern(filter.Value)}%"); case SearchyRule.Contains: - - return Expression.Call( - typeof(NpgsqlDbFunctionsExtensions), - nameof(NpgsqlDbFunctionsExtensions.ILike), - Type.EmptyTypes, - Expression.Property(null, typeof(EF), nameof(EF.Functions)), - fieldNameExpression, - Expression.Constant($"%{filter.Value}%") - ); + return BuildILikeExpression(fieldNameExpression, $"%{EscapeLikePattern(filter.Value)}%"); case SearchyRule.EqualsTo: return Expression.Equal(fieldNameExpression, GetValueAsConstantExpression(filter.Value, fieldType)); @@ -119,6 +100,28 @@ static Expression BuildFilterExpression(this Expression parameter, ISea } } + // The value is escaped so a '%' or '_' in it matches literally, as string.Contains/StartsWith + // do. The escape character has to be passed explicitly: ILike without one makes Npgsql emit + // ESCAPE '', which turns escaping off altogether. + const string LikeEscapeCharacter = "\\"; + + static Expression BuildILikeExpression(Expression fieldNameExpression, string pattern) => + Expression.Call( + typeof(NpgsqlDbFunctionsExtensions), + nameof(NpgsqlDbFunctionsExtensions.ILike), + Type.EmptyTypes, + Expression.Property(null, typeof(EF), nameof(EF.Functions)), + fieldNameExpression, + Expression.Constant(pattern), + Expression.Constant(LikeEscapeCharacter) + ); + + static string EscapeLikePattern(object value) => + $"{value}" + .Replace(LikeEscapeCharacter, LikeEscapeCharacter + LikeEscapeCharacter) + .Replace("%", LikeEscapeCharacter + "%") + .Replace("_", LikeEscapeCharacter + "_"); + static Expression GetValueAsConstantExpression(object value, Type type) { Expression constantExpression = Expression.Constant(value.ConvertValueToType(type));