Files
radzen-blazor/Radzen.Blazor/ExpressionSerializer.cs

480 lines
15 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
/// <summary>
/// Serializes LINQ Expression Trees into C# string representations.
/// </summary>
public class ExpressionSerializer : ExpressionVisitor
{
private readonly StringBuilder _sb = new StringBuilder();
/// <summary>
/// Serializes a given LINQ Expression into a C# string.
/// </summary>
/// <param name="expression">The expression to serialize.</param>
/// <returns>A string representation of the expression.</returns>
public string Serialize(Expression expression)
{
_sb.Clear();
Visit(expression);
return _sb.ToString();
}
/// <inheritdoc/>
protected override Expression VisitLambda<T>(Expression<T> node)
{
ArgumentNullException.ThrowIfNull(node);
if (node.Parameters.Count > 1)
{
_sb.Append("(");
for (int i = 0; i < node.Parameters.Count; i++)
{
if (i > 0) _sb.Append(", ");
_sb.Append(node.Parameters[i].Name);
}
_sb.Append(") => ");
}
else
{
_sb.Append(node.Parameters[0].Name);
_sb.Append(" => ");
}
Visit(node.Body);
return node;
}
/// <inheritdoc/>
protected override Expression VisitParameter(ParameterExpression node)
{
ArgumentNullException.ThrowIfNull(node);
_sb.Append(node.Name);
return node;
}
/// <inheritdoc/>
protected override Expression VisitMember(MemberExpression node)
{
ArgumentNullException.ThrowIfNull(node);
if (node.Expression != null)
{
Visit(node.Expression);
_sb.Append(CultureInfo.InvariantCulture, $".{node.Member.Name}");
}
else
{
_sb.Append(node.Member.Name);
}
return node;
}
/// <inheritdoc/>
protected override Expression VisitMethodCall(MethodCallExpression node)
{
ArgumentNullException.ThrowIfNull(node);
if (node.Method.IsStatic && node.Arguments.Count > 0 &&
(node.Method.DeclaringType == typeof(Enumerable) ||
node.Method.DeclaringType == typeof(Queryable)))
{
Visit(node.Arguments[0]);
_sb.Append(CultureInfo.InvariantCulture, $".{node.Method.Name}(");
for (int i = 1; i < node.Arguments.Count; i++)
{
if (i > 1) _sb.Append(", ");
if (node.Arguments[i] is NewArrayExpression arrayExpr)
{
VisitNewArray(arrayExpr);
}
else
{
Visit(node.Arguments[i]);
}
}
_sb.Append(")");
}
else if (node.Method.IsStatic)
{
_sb.Append(CultureInfo.InvariantCulture, $"{node.Method.DeclaringType?.Name}.{node.Method.Name}(");
for (int i = 0; i < node.Arguments.Count; i++)
{
if (i > 0) _sb.Append(", ");
Visit(node.Arguments[i]);
}
_sb.Append(")");
}
else
{
if (node.Object != null)
{
Visit(node.Object);
_sb.Append(CultureInfo.InvariantCulture, $".{node.Method.Name}(");
}
else
{
_sb.Append(CultureInfo.InvariantCulture, $"{node.Method.Name}(");
}
for (int i = 0; i < node.Arguments.Count; i++)
{
if (i > 0) _sb.Append(", ");
Visit(node.Arguments[i]);
}
_sb.Append(")");
}
return node;
}
/// <inheritdoc/>
protected override Expression VisitUnary(UnaryExpression node)
{
ArgumentNullException.ThrowIfNull(node);
if (node.NodeType == ExpressionType.Not)
{
_sb.Append("(!(");
Visit(node.Operand);
_sb.Append("))");
}
else if (node.NodeType == ExpressionType.Convert)
{
if (node.Operand is IndexExpression indexExpr)
{
_sb.Append(CultureInfo.InvariantCulture, $"({node.Type.DisplayName(true).Replace("+", ".", StringComparison.Ordinal)})");
Visit(indexExpr.Object);
_sb.Append("[");
Visit(indexExpr.Arguments[0]);
_sb.Append("]");
return node;
}
Visit(node.Operand);
}
else
{
_sb.Append(node.NodeType switch
{
ExpressionType.Negate => "-",
ExpressionType.UnaryPlus => "+",
_ => throw new NotSupportedException($"Unsupported unary operator: {node.NodeType}")
});
Visit(node.Operand);
}
return node;
}
/// <inheritdoc/>
protected override Expression VisitConstant(ConstantExpression node)
{
ArgumentNullException.ThrowIfNull(node);
_sb.Append(FormatValue(node.Value));
return node;
}
internal static string? FormatValue(object? value)
{
return value switch
{
string s when s.Length == 0 => @"""""",
null => "null",
string s => @$"""{s.Replace("\"", "\\\"", StringComparison.Ordinal)}""",
char c => $"'{c}'",
bool b => b.ToString().ToLowerInvariant(),
DateTime dt => FormatDateTime(dt),
DateTimeOffset dto => $"DateTime.Parse(\"{dto.UtcDateTime:yyyy-MM-ddTHH:mm:ss.fffZ}\")",
DateOnly dateOnly => $"DateOnly.Parse(\"{dateOnly.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)}\", CultureInfo.InvariantCulture)",
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),
_ => value.GetType().IsEnum
? $"({value.GetType()?.FullName?.Replace("+", ".", StringComparison.Ordinal)})" + Convert.ChangeType(value, Enum.GetUnderlyingType(value.GetType()), CultureInfo.InvariantCulture).ToString()
: Convert.ToString(value, CultureInfo.InvariantCulture)
};
}
private static string FormatDateTime(DateTime dateTime)
{
var finalDate = dateTime.TimeOfDay == TimeSpan.Zero ? dateTime.Date : dateTime;
var dateFormat = dateTime.TimeOfDay == TimeSpan.Zero ? "yyyy-MM-dd" : "yyyy-MM-ddTHH:mm:ss.fffZ";
return $"DateTime.SpecifyKind(DateTime.Parse(\"{finalDate.ToString(dateFormat, CultureInfo.InvariantCulture)}\", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind), DateTimeKind.{Enum.GetName(finalDate.Kind)})";
}
private static string FormatEnumerable(IEnumerable enumerable)
{
var arrayType = enumerable.AsQueryable().ElementType;
var items = enumerable.Cast<object>().Select(FormatValue);
return $"new {(Nullable.GetUnderlyingType(arrayType) != null ? arrayType.DisplayName(true).Replace("+", ".", StringComparison.Ordinal) : "")}[] {{ {string.Join(", ", items)} }}";
}
/// <inheritdoc/>
protected override Expression VisitNewArray(NewArrayExpression node)
{
ArgumentNullException.ThrowIfNull(node);
bool needsParentheses = node.NodeType == ExpressionType.NewArrayInit &&
(node.Expressions.Count > 1 || node.Expressions[0].NodeType != ExpressionType.Constant);
if (needsParentheses) _sb.Append("(");
_sb.Append("new [] { ");
bool first = true;
foreach (var expr in node.Expressions)
{
if (!first) _sb.Append(", ");
first = false;
Visit(expr);
}
_sb.Append(" }");
if (needsParentheses) _sb.Append(")");
return node;
}
/// <inheritdoc/>
protected override Expression VisitBinary(BinaryExpression node)
{
ArgumentNullException.ThrowIfNull(node);
_sb.Append("(");
Visit(node.Left);
_sb.Append(CultureInfo.InvariantCulture, $" {GetOperator(node.NodeType)} ");
Visit(node.Right);
_sb.Append(")");
return node;
}
/// <inheritdoc/>
protected override Expression VisitConditional(ConditionalExpression node)
{
ArgumentNullException.ThrowIfNull(node);
_sb.Append("(");
Visit(node.Test);
_sb.Append(" ? ");
Visit(node.IfTrue);
_sb.Append(" : ");
Visit(node.IfFalse);
_sb.Append(")");
return node;
}
/// <summary>
/// Maps an ExpressionType to its corresponding C# operator.
/// </summary>
/// <param name="type">The ExpressionType to map.</param>
/// <returns>A string representation of the corresponding C# operator.</returns>
private static string GetOperator(ExpressionType type)
{
return type switch
{
ExpressionType.Add => "+",
ExpressionType.Subtract => "-",
ExpressionType.Multiply => "*",
ExpressionType.Divide => "/",
ExpressionType.AndAlso => "&&",
ExpressionType.OrElse => "||",
ExpressionType.Equal => "==",
ExpressionType.NotEqual => "!=",
ExpressionType.LessThan => "<",
ExpressionType.LessThanOrEqual => "<=",
ExpressionType.GreaterThan => ">",
ExpressionType.GreaterThanOrEqual => ">=",
ExpressionType.Coalesce => "??",
_ => throw new NotSupportedException($"Unsupported operator: {type}")
};
}
}
/// <summary>
/// Provides an extension method for displaying type names.
/// </summary>
public static class SharedTypeExtensions
{
private static readonly Dictionary<Type, string> BuiltInTypeNames = new()
{
{ typeof(bool), "bool" },
{ typeof(byte), "byte" },
{ typeof(char), "char" },
{ typeof(decimal), "decimal" },
{ typeof(double), "double" },
{ typeof(float), "float" },
{ typeof(int), "int" },
{ typeof(long), "long" },
{ typeof(object), "object" },
{ typeof(sbyte), "sbyte" },
{ typeof(short), "short" },
{ typeof(string), "string" },
{ typeof(uint), "uint" },
{ typeof(ulong), "ulong" },
{ typeof(ushort), "ushort" },
{ typeof(void), "void" }
};
/// <summary>
/// Unwraps nullable type.
/// </summary>
public static Type UnwrapNullableType(this Type type)
=> Nullable.GetUnderlyingType(type) ?? type;
/// <summary>
/// Returns a display name for the given type.
/// </summary>
/// <param name="type">The type to display.</param>
/// <param name="fullName">Indicates whether to use the full name.</param>
/// <param name="compilable">Indicates whether to use a compilable format.</param>
/// <returns>A string representing the type name.</returns>
public static string DisplayName(this Type type, bool fullName = true, bool compilable = false)
{
ArgumentNullException.ThrowIfNull(type);
var stringBuilder = new StringBuilder();
ProcessType(stringBuilder, type, fullName, compilable);
return stringBuilder.ToString();
}
private static void ProcessType(StringBuilder builder, Type type, bool fullName, bool compilable)
{
if (type.IsGenericType)
{
var genericArguments = type.GetGenericArguments();
ProcessGenericType(builder, type, genericArguments, genericArguments.Length, fullName, compilable);
}
else if (type.IsArray)
{
ProcessArrayType(builder, type, fullName, compilable);
}
else if (BuiltInTypeNames.TryGetValue(type, out var builtInName))
{
builder.Append(builtInName);
}
else if (!type.IsGenericParameter)
{
if (compilable)
{
if (type.IsNested)
{
ProcessType(builder, type.DeclaringType!, fullName, compilable);
builder.Append('.');
}
else if (fullName)
{
builder.Append(type.Namespace).Append('.');
}
builder.Append(type.Name);
}
else
{
builder.Append(fullName ? type.FullName : type.Name);
}
}
}
private static void ProcessArrayType(StringBuilder builder, Type type, bool fullName, bool compilable)
{
var innerType = type;
while (innerType.IsArray)
{
innerType = innerType.GetElementType()!;
}
ProcessType(builder, innerType, fullName, compilable);
while (type.IsArray)
{
builder.Append('[');
builder.Append(',', type.GetArrayRank() - 1);
builder.Append(']');
type = type.GetElementType()!;
}
}
private static void ProcessGenericType(
StringBuilder builder,
Type type,
Type[] genericArguments,
int length,
bool fullName,
bool compilable)
{
if (type.IsConstructedGenericType
&& type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
ProcessType(builder, type.UnwrapNullableType(), fullName, compilable);
builder.Append('?');
return;
}
var offset = type.IsNested ? type.DeclaringType!.GetGenericArguments().Length : 0;
if (compilable)
{
if (type.IsNested)
{
ProcessType(builder, type.DeclaringType!, fullName, compilable);
builder.Append('.');
}
else if (fullName)
{
builder.Append(type.Namespace);
builder.Append('.');
}
}
else
{
if (fullName)
{
if (type.IsNested)
{
ProcessGenericType(builder, type.DeclaringType!, genericArguments, offset, fullName, compilable);
builder.Append('+');
}
else
{
builder.Append(type.Namespace);
builder.Append('.');
}
}
}
var genericPartIndex = type.Name.IndexOf('`', StringComparison.Ordinal);
if (genericPartIndex <= 0)
{
builder.Append(type.Name);
return;
}
builder.Append(type.Name, 0, genericPartIndex);
builder.Append('<');
for (var i = offset; i < length; i++)
{
ProcessType(builder, genericArguments[i], fullName, compilable);
if (i + 1 == length)
{
continue;
}
builder.Append(',');
if (!genericArguments[i + 1].IsGenericParameter)
{
builder.Append(' ');
}
}
builder.Append('>');
}
}