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; } +}