diff --git a/gridify.sln b/gridify.sln
index e09a5623..d9b975f7 100644
--- a/gridify.sln
+++ b/gridify.sln
@@ -23,6 +23,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EntityFrameworkIntegrationT
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EntityFrameworkSqlProviderIntegrationTests", "test\EntityFrameworkSqlProviderIntegrationTests\EntityFrameworkSqlProviderIntegrationTests.csproj", "{3B9A8E46-1D4D-40EE-89C0-C3C376D9320A}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EntityFrameworkPostgreSqlIntegrationTests", "test\EntityFrameworkPostgreSqlIntegrationTests\EntityFrameworkPostgreSqlIntegrationTests.csproj", "{7C6699E7-7B6E-48D4-920F-6DD3568FBFD9}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -115,6 +117,18 @@ Global
{3B9A8E46-1D4D-40EE-89C0-C3C376D9320A}.Release|x64.Build.0 = Release|Any CPU
{3B9A8E46-1D4D-40EE-89C0-C3C376D9320A}.Release|x86.ActiveCfg = Release|Any CPU
{3B9A8E46-1D4D-40EE-89C0-C3C376D9320A}.Release|x86.Build.0 = Release|Any CPU
+ {7C6699E7-7B6E-48D4-920F-6DD3568FBFD9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7C6699E7-7B6E-48D4-920F-6DD3568FBFD9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7C6699E7-7B6E-48D4-920F-6DD3568FBFD9}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {7C6699E7-7B6E-48D4-920F-6DD3568FBFD9}.Debug|x64.Build.0 = Debug|Any CPU
+ {7C6699E7-7B6E-48D4-920F-6DD3568FBFD9}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {7C6699E7-7B6E-48D4-920F-6DD3568FBFD9}.Debug|x86.Build.0 = Debug|Any CPU
+ {7C6699E7-7B6E-48D4-920F-6DD3568FBFD9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7C6699E7-7B6E-48D4-920F-6DD3568FBFD9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7C6699E7-7B6E-48D4-920F-6DD3568FBFD9}.Release|x64.ActiveCfg = Release|Any CPU
+ {7C6699E7-7B6E-48D4-920F-6DD3568FBFD9}.Release|x64.Build.0 = Release|Any CPU
+ {7C6699E7-7B6E-48D4-920F-6DD3568FBFD9}.Release|x86.ActiveCfg = Release|Any CPU
+ {7C6699E7-7B6E-48D4-920F-6DD3568FBFD9}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{CDFDBB16-1D9F-40FD-B693-96D1D4FB79EE} = {1BBCBA37-25E5-4BFF-A8E8-7EE582E0317F}
@@ -124,5 +138,6 @@ Global
{02F96021-6989-4F09-919D-999F0BE3DEFB} = {41676937-4F05-4794-A2E5-442127927776}
{9A54A635-2B5B-4EDC-98FE-9BF963903BD4} = {1BBCBA37-25E5-4BFF-A8E8-7EE582E0317F}
{3B9A8E46-1D4D-40EE-89C0-C3C376D9320A} = {1BBCBA37-25E5-4BFF-A8E8-7EE582E0317F}
+ {7C6699E7-7B6E-48D4-920F-6DD3568FBFD9} = {1BBCBA37-25E5-4BFF-A8E8-7EE582E0317F}
EndGlobalSection
EndGlobal
diff --git a/src/Gridify/Syntax/SyntaxTreeToQueryConvertor.cs b/src/Gridify/Syntax/SyntaxTreeToQueryConvertor.cs
index 475035ce..f85ccd63 100644
--- a/src/Gridify/Syntax/SyntaxTreeToQueryConvertor.cs
+++ b/src/Gridify/Syntax/SyntaxTreeToQueryConvertor.cs
@@ -164,6 +164,8 @@ private static LambdaExpression GetExpressionWithNullCheck(MemberExpression prop
if (allowNullSearch && op.Kind is SyntaxKind.Equal or SyntaxKind.NotEqual && value.ToString() == "null")
value = null;
+ var isConvertable = true;
+
// type fixer
if (value is not null && body.Type != value.GetType())
{
@@ -173,10 +175,13 @@ private static LambdaExpression GetExpressionWithNullCheck(MemberExpression prop
if (body.Type == typeof(Guid) && !Guid.TryParse(value.ToString(), out _)) value = Guid.NewGuid().ToString();
var converter = TypeDescriptor.GetConverter(body.Type);
- value = converter.ConvertFromString(value.ToString()!);
+ isConvertable = converter.CanConvertFrom(typeof(string));
+ if (isConvertable)
+ value = converter.ConvertFromString(value.ToString()!);
}
catch (FormatException)
{
+ // this code should never run
// return no records in case of any exception in formatting
return Expression.Lambda(Expression.Constant(false), parameter); // q => false
}
@@ -306,7 +311,11 @@ private static LambdaExpression GetExpressionWithNullCheck(MemberExpression prop
var customOperator = GridifyGlobalConfiguration.CustomOperators.Operators.First(q => q.GetOperator() == token!.Text);
var customExp = customOperator.OperatorHandler();
be = new ReplaceExpressionVisitor(customExp.Parameters[0], body).Visit(customExp.Body);
- be = new ReplaceExpressionVisitor(customExp.Parameters[1], Expression.Constant(value, body.Type)).Visit(be);
+ if (isConvertable)
+ be = new ReplaceExpressionVisitor(customExp.Parameters[1], Expression.Constant(value, body.Type)).Visit(be);
+
+ be = new ReplaceExpressionVisitor(customExp.Parameters[1], Expression.Constant(value, typeof(string))).Visit(be);
+
break;
default:
return null;
diff --git a/test/EntityFrameworkPostgreSqlIntegrationTests/EntityFrameworkPostgreSqlIntegrationTests.csproj b/test/EntityFrameworkPostgreSqlIntegrationTests/EntityFrameworkPostgreSqlIntegrationTests.csproj
new file mode 100644
index 00000000..9be9193c
--- /dev/null
+++ b/test/EntityFrameworkPostgreSqlIntegrationTests/EntityFrameworkPostgreSqlIntegrationTests.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/EntityFrameworkPostgreSqlIntegrationTests/Interceptors.cs b/test/EntityFrameworkPostgreSqlIntegrationTests/Interceptors.cs
new file mode 100644
index 00000000..1807a099
--- /dev/null
+++ b/test/EntityFrameworkPostgreSqlIntegrationTests/Interceptors.cs
@@ -0,0 +1,153 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Data.Common;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.EntityFrameworkCore.Diagnostics;
+
+namespace EntityFrameworkIntegrationTests.cs;
+
+public class SuppressConnectionInterceptor : DbConnectionInterceptor
+{
+ public override ValueTask ConnectionOpeningAsync(DbConnection connection, ConnectionEventData eventData,
+ InterceptionResult result,
+ CancellationToken cancellationToken = new())
+ {
+ result = InterceptionResult.Suppress();
+ return base.ConnectionOpeningAsync(connection, eventData, result, cancellationToken);
+ }
+
+ public override InterceptionResult ConnectionOpening(DbConnection connection, ConnectionEventData eventData, InterceptionResult result)
+ {
+ result = InterceptionResult.Suppress();
+ return base.ConnectionOpening(connection, eventData, result);
+ }
+}
+
+public class EmptyMessageDataReader : DbDataReader
+{
+
+ private readonly List _users = new List();
+
+ public EmptyMessageDataReader()
+ {
+ }
+
+ public override int FieldCount
+ => 0;
+
+ public override int RecordsAffected
+ => 0;
+
+ public override bool HasRows
+ => false;
+
+ public override bool IsClosed
+ => true;
+
+ public override int Depth
+ => 0;
+
+ public override bool Read()
+ => false;
+
+ public override int GetInt32(int ordinal)
+ => 0;
+
+ public override bool IsDBNull(int ordinal)
+ => false;
+
+ public override string GetString(int ordinal)
+ => "suppressed message";
+
+ public override bool GetBoolean(int ordinal)
+ => true;
+
+ public override byte GetByte(int ordinal)
+ => 0;
+
+ public override long GetBytes(int ordinal, long dataOffset, byte[] buffer, int bufferOffset, int length)
+ => 0;
+
+ public override char GetChar(int ordinal)
+ => '\0';
+
+ public override long GetChars(int ordinal, long dataOffset, char[] buffer, int bufferOffset, int length)
+ => 0;
+
+ public override string GetDataTypeName(int ordinal)
+ => string.Empty;
+
+ public override DateTime GetDateTime(int ordinal)
+ => DateTime.Now;
+
+ public override decimal GetDecimal(int ordinal)
+ => 0;
+
+ public override double GetDouble(int ordinal)
+ => 0;
+
+ public override Type GetFieldType(int ordinal)
+ => typeof(User);
+
+ public override float GetFloat(int ordinal)
+ => 0;
+
+ public override Guid GetGuid(int ordinal)
+ => Guid.Empty;
+
+ public override short GetInt16(int ordinal)
+ => 0;
+
+ public override long GetInt64(int ordinal)
+ => 0;
+
+ public override string GetName(int ordinal)
+ => "";
+
+ public override int GetOrdinal(string name)
+ => 0;
+
+ public override object GetValue(int ordinal)
+ => new object();
+
+ public override int GetValues(object[] values)
+ => 0;
+
+ public override object this[int ordinal]
+ => new object();
+
+ public override object this[string name]
+ => new object();
+
+ public override bool NextResult()
+ => false;
+
+ public override IEnumerator GetEnumerator()
+ => _users.GetEnumerator();
+}
+
+public class SuppressCommandResultInterceptor : DbCommandInterceptor
+{
+ public override InterceptionResult ReaderExecuting(
+ DbCommand command,
+ CommandEventData eventData,
+ InterceptionResult result)
+ {
+ result = InterceptionResult.SuppressWithResult(new EmptyMessageDataReader());
+
+ return result;
+ }
+
+ public override ValueTask> ReaderExecutingAsync(
+ DbCommand command,
+ CommandEventData eventData,
+ InterceptionResult result,
+ CancellationToken cancellationToken = default)
+ {
+ result = InterceptionResult.SuppressWithResult(new EmptyMessageDataReader());
+
+ return new ValueTask>(result);
+ }
+}
diff --git a/test/EntityFrameworkPostgreSqlIntegrationTests/Issue76Tests.cs b/test/EntityFrameworkPostgreSqlIntegrationTests/Issue76Tests.cs
new file mode 100644
index 00000000..5631d216
--- /dev/null
+++ b/test/EntityFrameworkPostgreSqlIntegrationTests/Issue76Tests.cs
@@ -0,0 +1,46 @@
+using System.Linq.Expressions;
+using System.Reflection.Metadata;
+using EntityFrameworkIntegrationTests.cs;
+using Gridify.Syntax;
+using Microsoft.EntityFrameworkCore;
+using Xunit;
+
+namespace Gridify.Tests;
+
+public class Issue76Tests
+{
+
+ private readonly MyDbContext _dbContext;
+
+ public Issue76Tests()
+ {
+ _dbContext = new MyDbContext();
+ }
+
+ [Fact]
+ public void CustomOperator_EFJsonContains_ShouldGenerateCorrectExpression()
+ {
+ // Arrange
+ GridifyGlobalConfiguration.CustomOperators.Register(new JsonContainsOperator());
+
+ var gm = new GridifyMapper()
+ .AddMap("u", q => q.Users);
+ var expected = _dbContext.ProductsViews.Where(q => EF.Functions.JsonContains(q.Users, new[] { new { Id = 1 } })).ToQueryString();
+
+ // Act
+ var actual = _dbContext.ProductsViews.ApplyFiltering("u #= 1", gm).ToQueryString();
+
+ // Assert
+ Assert.Equal(expected, actual);
+ }
+
+}
+
+public class JsonContainsOperator : IGridifyOperator
+{
+ public string GetOperator() => "#=";
+ public Expression OperatorHandler()
+ {
+ return (prop, value) => EF.Functions.JsonContains(prop, new[] { new { Id = int.Parse(value.ToString()!) } });
+ }
+}
diff --git a/test/EntityFrameworkPostgreSqlIntegrationTests/MyDbContext.cs b/test/EntityFrameworkPostgreSqlIntegrationTests/MyDbContext.cs
new file mode 100644
index 00000000..9c254d53
--- /dev/null
+++ b/test/EntityFrameworkPostgreSqlIntegrationTests/MyDbContext.cs
@@ -0,0 +1,50 @@
+using System;
+using System.ComponentModel.DataAnnotations.Schema;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Diagnostics;
+
+namespace EntityFrameworkIntegrationTests.cs;
+
+public class MyDbContext : DbContext
+{
+ public DbSet Users { get; set; }
+ public DbSet ProductsViews { get; set; }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity().Property("shadow1");
+ base.OnModelCreating(modelBuilder);
+ }
+
+ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+ {
+ optionsBuilder.UseNpgsql("Server=myServerAddress;Database=myDataBase;User Id=myUsername;Password=myPassword;");
+ optionsBuilder.AddInterceptors(new SuppressCommandResultInterceptor());
+ optionsBuilder.AddInterceptors(new SuppressConnectionInterceptor());
+ optionsBuilder.EnableServiceProviderCaching();
+
+ base.OnConfiguring(optionsBuilder);
+ }
+}
+
+public class User
+{
+ public int Id { get; set; }
+ public string Name { get; set; }
+ public DateTime? CreateDate { get; set; }
+ public Guid FkGuid { get; set; }
+}
+public class Products
+{
+ public int Id { get; set; }
+ public string Name { get; set; }
+
+ [Column(TypeName = "jsonb")]
+ public IEnumerable Users { get; set; }
+}
+
+public class ProductUser
+{
+ public int Id { get; set; }
+ public string Name { get; set; }
+}