Skip to content

Commit 45d0739

Browse files
authored
Fix value conversion of array over nullable (#2197)
Fixes #2189
1 parent e384117 commit 45d0739

File tree

8 files changed

+244
-107
lines changed

8 files changed

+244
-107
lines changed

src/EFCore.PG/Storage/ValueConversion/NpgsqlArrayConverter.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@ public NpgsqlArrayConverter(ValueConverter elementConverter)
1919
throw new ArgumentException("Can only convert between arrays");
2020
}
2121

22-
if (modelElementType.UnwrapNullableType() != elementConverter.ModelClrType)
22+
if (modelElementType.UnwrapNullableType() != elementConverter.ModelClrType.UnwrapNullableType())
2323
{
2424
throw new ArgumentException($"The element's value converter model type ({elementConverter.ModelClrType}), doesn't match the array's ({modelElementType})");
2525
}
2626

27-
if (providerElementType.UnwrapNullableType() != elementConverter.ProviderClrType)
27+
if (providerElementType.UnwrapNullableType() != elementConverter.ProviderClrType.UnwrapNullableType())
2828
{
2929
throw new ArgumentException($"The element's value converter provider type ({elementConverter.ProviderClrType}), doesn't match the array's ({providerElementType})");
3030
}
@@ -54,7 +54,7 @@ private static Expression<Func<TInput, TOutput>> ArrayConversionExpression<TInpu
5454

5555
// elementConversionExpression is always over non-nullable value types. If the array is over nullable types,
5656
// we need to sanitize via an external null check.
57-
if (inputElementType.IsGenericType && inputElementType.GetGenericTypeDefinition() == typeof(Nullable<>))
57+
if (inputElementType.IsNullableType() && outputElementType.IsNullableType())
5858
{
5959
// p => p is null ? null : elementConversionExpression(p)
6060
var p = Expression.Parameter(inputElementType, "foo");
@@ -65,7 +65,12 @@ private static Expression<Func<TInput, TOutput>> ArrayConversionExpression<TInpu
6565
Expression.Convert(
6666
Expression.Invoke(
6767
elementConversionExpression,
68-
Expression.Convert(p, inputElementType.UnwrapNullableType())),
68+
// The user-provided conversion lambda typically accepts non-nullable (value) types, with EF Core doing the
69+
// null-sanitization and conversion to non-nullable; do this here unless the user-provided lambda happens to
70+
// accept a nullable value type parameter.
71+
elementConversionExpression.Parameters[0].Type.IsNullableType()
72+
? p
73+
: Expression.Convert(p, inputElementType.UnwrapNullableType())),
6974
outputElementType)),
7075
p);
7176
}

test/EFCore.PG.FunctionalTests/Query/ArrayArrayQueryTest.cs

Lines changed: 98 additions & 47 deletions
Large diffs are not rendered by default.

test/EFCore.PG.FunctionalTests/Query/ArrayListQueryTest.cs

Lines changed: 97 additions & 46 deletions
Large diffs are not rendered by default.

test/EFCore.PG.FunctionalTests/Query/ArrayQueryFixture.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public IReadOnlyDictionary<Type, object> GetEntityAsserters()
5454
Assert.Equal(ee.NullableStringList, ee.NullableStringList);
5555
Assert.Equal(ee.NullableText, ee.NullableText);
5656
Assert.Equal(ee.NonNullableText, ee.NonNullableText);
57-
Assert.Equal(ee.ValueConvertedScalar, ee.ValueConvertedScalar);
57+
Assert.Equal(ee.EnumConvertedToInt, ee.EnumConvertedToInt);
5858
Assert.Equal(ee.ValueConvertedArray, ee.ValueConvertedArray);
5959
Assert.Equal(ee.ValueConvertedList, ee.ValueConvertedList);
6060
Assert.Equal(ee.Byte, ee.Byte);
@@ -76,4 +76,4 @@ public IReadOnlyDictionary<Type, object> GetEntityAsserters()
7676
}
7777
}
7878
}.ToDictionary(e => e.Key, e => (object)e.Value);
79-
}
79+
}

test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,19 @@ public virtual async Task Nullable_array_column_Contains_literal_item(bool async
245245

246246
[ConditionalTheory]
247247
[MemberData(nameof(IsAsyncData))]
248-
public abstract Task Array_param_Contains_value_converted_column(bool async);
248+
public abstract Task Array_param_Contains_value_converted_column_enum_to_int(bool async);
249+
250+
[ConditionalTheory]
251+
[MemberData(nameof(IsAsyncData))]
252+
public abstract Task Array_param_Contains_value_converted_column_enum_to_string(bool async);
253+
254+
[ConditionalTheory]
255+
[MemberData(nameof(IsAsyncData))]
256+
public abstract Task Array_param_Contains_value_converted_column_nullable_enum_to_string(bool async);
257+
258+
[ConditionalTheory]
259+
[MemberData(nameof(IsAsyncData))]
260+
public abstract Task Array_param_Contains_value_converted_column_nullable_enum_to_string_with_non_nullable_lambda(bool async);
249261

250262
[ConditionalTheory]
251263
[MemberData(nameof(IsAsyncData))]

test/EFCore.PG.FunctionalTests/TestModels/Array/ArrayEntity.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@ public class ArrayEntity
1717
public List<string?> NullableStringList { get; set; } = null!;
1818
public string? NullableText { get; set; }
1919
public string NonNullableText { get; set; } = null!;
20-
public SomeEnum ValueConvertedScalar { get; set; }
20+
public SomeEnum EnumConvertedToInt { get; set; }
21+
public SomeEnum EnumConvertedToString { get; set; }
22+
public SomeEnum? NullableEnumConvertedToString { get; set; }
23+
public SomeEnum? NullableEnumConvertedToStringWithNonNullableLambda { get; set; }
2124
public SomeEnum[] ValueConvertedArray { get; set; } = null!;
2225
public List<SomeEnum> ValueConvertedList { get; set; } = null!;
2326
public byte Byte { get; set; }
24-
}
27+
}

test/EFCore.PG.FunctionalTests/TestModels/Array/ArrayQueryContext.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,18 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
1212
e =>
1313
{
1414
// We do negative to make sure our value converter is properly used, and not the built-in one
15-
e.Property(ae => ae.ValueConvertedScalar)
15+
e.Property(ae => ae.EnumConvertedToInt)
1616
.HasConversion(w => -(int)w, v => (SomeEnum)(-v));
1717

18+
e.Property(ae => ae.EnumConvertedToString)
19+
.HasConversion(w => w.ToString(), v => Enum.Parse<SomeEnum>(v));
20+
21+
e.Property(ae => ae.NullableEnumConvertedToString)
22+
.HasConversion(w => w.ToString(), v => Enum.Parse<SomeEnum>(v));
23+
24+
e.Property(ae => ae.NullableEnumConvertedToStringWithNonNullableLambda)
25+
.HasConversion(new ValueConverter<SomeEnum, string>(w => w.ToString(), v => Enum.Parse<SomeEnum>(v)));
26+
1827
e.Property(ae => ae.ValueConvertedArray)
1928
.HasPostgresArrayConversion(w => -(int)w, v => (SomeEnum)(-v));
2029

@@ -36,4 +45,4 @@ public static void Seed(ArrayQueryContext context)
3645
);
3746
context.SaveChanges();
3847
}
39-
}
48+
}

test/EFCore.PG.FunctionalTests/TestModels/Array/ArrayQueryData.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ public static IReadOnlyList<ArrayEntity> CreateArrayEntities()
4242
NullableStringList = new List<string> { "3", "4", null},
4343
NullableText = "foo",
4444
NonNullableText = "foo",
45-
ValueConvertedScalar = SomeEnum.One,
45+
EnumConvertedToInt = SomeEnum.One,
46+
EnumConvertedToString = SomeEnum.One,
47+
NullableEnumConvertedToString = SomeEnum.One,
48+
NullableEnumConvertedToStringWithNonNullableLambda = SomeEnum.One,
4649
ValueConvertedArray = new[] { SomeEnum.Eight, SomeEnum.Nine },
4750
ValueConvertedList = new List<SomeEnum> { SomeEnum.Eight, SomeEnum.Nine },
4851
Byte = 10
@@ -62,7 +65,10 @@ public static IReadOnlyList<ArrayEntity> CreateArrayEntities()
6265
NullableStringList = new List<string> { "5", "6", "7", "8" },
6366
NullableText = "bar",
6467
NonNullableText = "bar",
65-
ValueConvertedScalar = SomeEnum.Two,
68+
EnumConvertedToInt = SomeEnum.Two,
69+
EnumConvertedToString = SomeEnum.Two,
70+
NullableEnumConvertedToString = SomeEnum.Two,
71+
NullableEnumConvertedToStringWithNonNullableLambda = SomeEnum.Two,
6672
ValueConvertedArray = new[] { SomeEnum.Nine, SomeEnum.Ten },
6773
ValueConvertedList = new List<SomeEnum> { SomeEnum.Nine, SomeEnum.Ten },
6874
Byte = 20
@@ -78,4 +84,4 @@ public static IReadOnlyList<ArrayContainerEntity> CreateContainerEntities()
7884
ArrayEntities = CreateArrayEntities().ToList()
7985
}
8086
};
81-
}
87+
}

0 commit comments

Comments
 (0)