From 1c5e98e3f9527226a05077072eac6f1fc71de865 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Klari=C4=87?= Date: Fri, 24 Jul 2026 13:53:07 +0200 Subject: [PATCH 1/2] fix: quote nullable Guid values in IN filter Selector.In generic fallback joined values via plain ToString, so IEnumerable (unlike IEnumerable) never got the ' ' quoting and produced invalid SQL: IN (guid1,guid2). Unwrap Nullable before picking the format. --- .../QueryBuilder/GuidTest.cs | 16 ++++++++++++++++ src/Simpleverse.Repository.Db/Selector.cs | 17 +++++++++++++---- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/Simpleverse.Repository.Db.Test/QueryBuilder/GuidTest.cs b/src/Simpleverse.Repository.Db.Test/QueryBuilder/GuidTest.cs index 23be796..69c70ad 100644 --- a/src/Simpleverse.Repository.Db.Test/QueryBuilder/GuidTest.cs +++ b/src/Simpleverse.Repository.Db.Test/QueryBuilder/GuidTest.cs @@ -31,5 +31,21 @@ public void TestGuid_NotIn() $"WHERE [Guid] NOT IN ('{Value}','{Value}')", Array.Empty() ); + + [Fact] + public void TestGuidNullable_In() + => Test( + queryBuilder => queryBuilder.Where(x => x.GuidNullable, new List() { Value, Value }), + $"WHERE [GuidNullable] IN ('{Value}','{Value}')", + Array.Empty() + ); + + [Fact] + public void TestGuidNullable_NotIn() + => Test( + queryBuilder => queryBuilder.WhereNot(x => x.GuidNullable, new List() { Value, Value }), + $"WHERE [GuidNullable] NOT IN ('{Value}','{Value}')", + Array.Empty() + ); } } diff --git a/src/Simpleverse.Repository.Db/Selector.cs b/src/Simpleverse.Repository.Db/Selector.cs index 205218c..450253f 100644 --- a/src/Simpleverse.Repository.Db/Selector.cs +++ b/src/Simpleverse.Repository.Db/Selector.cs @@ -105,10 +105,19 @@ public Selector In(IEnumerable values, bool not = false) { if (values == null || !values.Any()) return this; - var valuesJoined = values.Join(','); - var tType = typeof(T); - if (tType.IsEnum) - valuesJoined = string.Join(',', values.Select(x => Enum.Format(tType, x, "d"))); + + var underlyingType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T); + var nonNullValues = values.Where(x => x != null); + + string valuesJoined; + if (underlyingType == typeof(Guid)) + valuesJoined = string.Join(',', nonNullValues.Select(x => $"'{x}'")); + else if (underlyingType == typeof(DateTime)) + valuesJoined = string.Join(',', nonNullValues.Select(x => $"'{Convert.ToDateTime(x):yyyy-MM-ddTHH:mm:ss.fff}'")); + else if (underlyingType.IsEnum) + valuesJoined = string.Join(',', nonNullValues.Select(x => Enum.Format(underlyingType, x, "d"))); + else + valuesJoined = nonNullValues.Join(','); return In(valuesJoined, not); } From 17c9353462f02c1d62cd81474795c1f55e15b2d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Klari=C4=87?= Date: Fri, 24 Jul 2026 15:12:16 +0200 Subject: [PATCH 2/2] test: add DB-backed nullable Guid IN filter tests to EntityTest Verifies the Selector.In fix against real SQL Server: filters DataTypeNullable via Entity.ListAsync with IEnumerable, incl. a null-containing list (skipped). Filter property named Guids so it doesn't collide with the model's Guid column and skip base auto-filter handling without needing a GetFilterConditions override. --- .../SqlServer/Entity/EntityTest.cs | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/src/Simpleverse.Repository.Db.Test/SqlServer/Entity/EntityTest.cs b/src/Simpleverse.Repository.Db.Test/SqlServer/Entity/EntityTest.cs index 9060f1a..6f8f1eb 100644 --- a/src/Simpleverse.Repository.Db.Test/SqlServer/Entity/EntityTest.cs +++ b/src/Simpleverse.Repository.Db.Test/SqlServer/Entity/EntityTest.cs @@ -295,6 +295,54 @@ public async Task UpsertAsync_WhenProvidedSqlRepositoryAndIdentityExists_Updates Assert.NotNull(fetchedIdentity); } } + + [Fact] + public async Task ListAsync_WhenFilteredByNullableGuidList_ReturnsMatchingRecords() + { + using (var profiler = Profile()) + using (var connection = _fixture.GetProfiledConnection()) + { + // arrange + connection.Open(); + connection.Truncate(); + var records = TestData.DataTypeNullableData(4).ToList(); + connection.Insert(records); + var recordsWithGuid = records.Where(x => x.Guid.HasValue).ToList(); + var guidFilter = recordsWithGuid.Select(x => x.Guid).ToList(); + var entity = new DataTypeNullableEntity(_sqlRepository); + + // act + var returned = (await entity.ListAsync(filter => filter.Guids = guidFilter)).ToList(); + + // assert + Assert.Equal(recordsWithGuid.Count, returned.Count); + Assert.All(returned, x => Assert.Contains(x.Guid, guidFilter)); + } + } + + [Fact] + public async Task ListAsync_WhenFilteredByNullableGuidListContainingNull_SkipsNullAndReturnsMatchingRecord() + { + using (var profiler = Profile()) + using (var connection = _fixture.GetProfiledConnection()) + { + // arrange + connection.Open(); + connection.Truncate(); + var records = TestData.DataTypeNullableData(4).ToList(); + connection.Insert(records); + var recordWithGuid = records.First(x => x.Guid.HasValue); + var guidFilter = new List { recordWithGuid.Guid, null }; + var entity = new DataTypeNullableEntity(_sqlRepository); + + // act + var returned = (await entity.ListAsync(filter => filter.Guids = guidFilter)).ToList(); + + // assert + Assert.Single(returned); + Assert.Equal(recordWithGuid.Guid, returned[0].Guid); + } + } } public class IdentityEntity : Entity @@ -362,4 +410,23 @@ protected override IEnumerable GetFilterConditions(IdentityDateOfBirth f return changed; } } + + public class DataTypeNullableFilter + { + public virtual IEnumerable Guids { get; set; } + } + + public class DataTypeNullableEntity : Entity + { + public DataTypeNullableEntity(DbRepository repository) + : base(repository, new Table("DT")) + { + } + + protected override void Filter(QueryBuilder builder, DataTypeNullableFilter filter) + { + builder.Where(x => x.Guid, filter.Guids); + base.Filter(builder, filter); + } + } }