From d607434cdaab391ff61efae116a79094d1170fb5 Mon Sep 17 00:00:00 2001 From: Atanas Korchev Date: Thu, 25 Jun 2026 14:27:12 +0300 Subject: [PATCH] Serialize enum filter values as numbers instead of FullName casts --- .../ExpressionSerializerTests.cs | 4 +- Radzen.Blazor/ExpressionSerializer.cs | 44 ++++++++++++++++--- 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/Radzen.Blazor.Tests/ExpressionSerializerTests.cs b/Radzen.Blazor.Tests/ExpressionSerializerTests.cs index 76e73769a..9a1660dab 100644 --- a/Radzen.Blazor.Tests/ExpressionSerializerTests.cs +++ b/Radzen.Blazor.Tests/ExpressionSerializerTests.cs @@ -268,7 +268,9 @@ namespace Radzen.Blazor.Tests public void Serializes_EnumArrayInValueOposite() { Expression> expr = e => new[] { Status.Active, Status.Inactive }.Intersect(e.Statuses).Any(); - Assert.Equal("e => (new [] { (Radzen.Blazor.Tests.Status)0, (Radzen.Blazor.Tests.Status)1 }).Intersect(e.Statuses).Any()", _serializer.Serialize(expr)); + // Enum elements serialize as bare numbers; the typed array prefix carries the enum type so the + // parser binds Status[] for Intersect, without per-value casts that force assembly type scanning. + Assert.Equal("e => (new Radzen.Blazor.Tests.Status[] { 0, 1 }).Intersect(e.Statuses).Any()", _serializer.Serialize(expr)); } [Fact] diff --git a/Radzen.Blazor/ExpressionSerializer.cs b/Radzen.Blazor/ExpressionSerializer.cs index 9e4ef60b9..be22ccc58 100644 --- a/Radzen.Blazor/ExpressionSerializer.cs +++ b/Radzen.Blazor/ExpressionSerializer.cs @@ -122,7 +122,7 @@ public class ExpressionSerializer : ExpressionVisitor _sb.Append(", "); } - Visit(node.Arguments[i]); + VisitMethodArgument(node.Arguments[i]); } _sb.Append(")"); @@ -146,7 +146,7 @@ public class ExpressionSerializer : ExpressionVisitor _sb.Append(", "); } - Visit(node.Arguments[i]); + VisitMethodArgument(node.Arguments[i]); } _sb.Append(")"); @@ -155,6 +155,23 @@ public class ExpressionSerializer : ExpressionVisitor return node; } + // FormatValue serializes enums as bare numbers, but a method argument needs its enum type for the + // parser's overload resolution (e.g. DateTime.Parse(s, provider, DateTimeStyles)). Emit the cast here + // (not in FormatValue) so overloads still bind while comparison/array enums stay numeric. + private void VisitMethodArgument(Expression argument) + { + if (argument is ConstantExpression { Value: not null } constant) + { + var type = Nullable.GetUnderlyingType(constant.Type) ?? constant.Type; + if (type.IsEnum) + { + _sb.Append(CultureInfo.InvariantCulture, $"({type.DisplayName(true).Replace("+", ".", StringComparison.Ordinal)})"); + } + } + + Visit(argument); + } + /// protected override Expression VisitUnary(UnaryExpression node) { @@ -218,8 +235,11 @@ public class ExpressionSerializer : ExpressionVisitor TimeOnly timeOnly => $"TimeOnly.Parse(\"{timeOnly.ToString("HH:mm:ss", CultureInfo.InvariantCulture)}\", CultureInfo.InvariantCulture)", Guid guid => $"Guid.Parse(\"{guid.ToString("D", CultureInfo.InvariantCulture)}\")", IEnumerable enumerable when value is not string => FormatEnumerable(enumerable), + // Emit the underlying numeric value (not a (Namespace.EnumType) cast): the cast forces + // ResolveTypeFromAssemblies (AppDomain.GetTypes) on deserialization, which is trim/AOT hostile. + // The enum type is recovered on parse from the compared member / typed array prefix. _ => value.GetType().IsEnum - ? $"({value.GetType()?.FullName?.Replace("+", ".", StringComparison.Ordinal)})" + Convert.ChangeType(value, Enum.GetUnderlyingType(value.GetType()), CultureInfo.InvariantCulture).ToString() + ? Convert.ChangeType(value, Enum.GetUnderlyingType(value.GetType()), CultureInfo.InvariantCulture).ToString() : Convert.ToString(value, CultureInfo.InvariantCulture) }; } @@ -236,8 +256,14 @@ public class ExpressionSerializer : ExpressionVisitor { var arrayType = enumerable.AsQueryable().ElementType; + // Enum elements are serialized as bare numbers (see FormatValue); emit the typed array prefix so the + // parser binds the array as the enum type (required for Intersect/Except). Nullable elements already + // need the prefix to carry the Nullable<> wrapper. + var underlying = Nullable.GetUnderlyingType(arrayType) ?? arrayType; + var needsTypePrefix = Nullable.GetUnderlyingType(arrayType) != null || underlying.IsEnum; + var items = enumerable.Cast().Select(FormatValue); - return $"new {(Nullable.GetUnderlyingType(arrayType) != null ? arrayType.DisplayName(true).Replace("+", ".", StringComparison.Ordinal) : "")}[] {{ {string.Join(", ", items)} }}"; + return $"new {(needsTypePrefix ? arrayType.DisplayName(true).Replace("+", ".", StringComparison.Ordinal) : "")}[] {{ {string.Join(", ", items)} }}"; } /// @@ -252,7 +278,15 @@ public class ExpressionSerializer : ExpressionVisitor _sb.Append("("); } - _sb.Append("new [] { "); + // Enum elements serialize as bare numbers (see FormatValue); emit the typed array prefix so the + // parser binds the array as the enum type (required for Intersect/Except) instead of int[]. + var elementType = node.Type.GetElementType(); + var underlying = elementType != null ? Nullable.GetUnderlyingType(elementType) ?? elementType : null; + var typePrefix = underlying != null && (underlying.IsEnum || Nullable.GetUnderlyingType(elementType!) != null) + ? elementType!.DisplayName(true).Replace("+", ".", StringComparison.Ordinal) + : ""; + + _sb.Append(CultureInfo.InvariantCulture, $"new {typePrefix}[] {{ "); bool first = true; foreach (var expr in node.Expressions) {