diff --git a/AyCode.Blazor.Components.Tests/AyCode.Blazor.Components.Tests.csproj b/AyCode.Blazor.Components.Tests/AyCode.Blazor.Components.Tests.csproj
index 7d73d4c..11cd5a9 100644
--- a/AyCode.Blazor.Components.Tests/AyCode.Blazor.Components.Tests.csproj
+++ b/AyCode.Blazor.Components.Tests/AyCode.Blazor.Components.Tests.csproj
@@ -8,7 +8,7 @@
-
+
diff --git a/AyCode.Blazor.Components/Components/Grids/MgGridSignalRDataSource.cs b/AyCode.Blazor.Components/Components/Grids/MgGridSignalRDataSource.cs
new file mode 100644
index 0000000..ae92efe
--- /dev/null
+++ b/AyCode.Blazor.Components/Components/Grids/MgGridSignalRDataSource.cs
@@ -0,0 +1,462 @@
+using AyCode.Core.Helpers;
+using AyCode.Core.Interfaces;
+using AyCode.Core.Loggers;
+using AyCode.Services.Server.SignalRs;
+using AyCode.Services.SignalRs;
+using DevExpress.Blazor;
+using DevExpress.Data.Filtering;
+using DevExpress.Data.Linq;
+using DevExpress.Data.Linq.Helpers;
+using System.Collections;
+
+namespace AyCode.Blazor.Components.Components.Grids;
+
+#region Models
+
+///
+/// Sorting information for a single field
+///
+public class SignalRGridSortInfo
+{
+ public string FieldName { get; set; } = "";
+ public bool Descending { get; set; }
+}
+
+#endregion
+
+///
+/// GridCustomDataSource implementation that wraps AcSignalRDataSource.
+/// Provides instant local filtering for previously seen filter criteria,
+/// while refreshing data in background using SignalR callback pattern.
+///
+/// Key features:
+/// - Uses AcSignalRDataSource for caching and background populate
+/// - Tracks seen filter criteria - if already seen, returns local data instantly
+/// - Background refresh with callback/populate pattern (no UI blocking)
+/// - Full GridCustomDataSource support (filter, sort, page, group, summary)
+///
+/// Entity type implementing IId
+/// ID type (int, Guid, long, etc.)
+public class MgGridSignalRDataSource : GridCustomDataSource
+ where TDataItem : class, IId
+ where TId : struct
+{
+ private readonly AcSignalRDataSource> _innerDataSource;
+ private readonly AcLoggerBase? _logger;
+
+ // DevExpress CriteriaOperator to Expression converter
+ private readonly CriteriaToExpressionConverter _criteriaConverter = new();
+
+ // Track filter criteria that have been seen before
+ private readonly HashSet _knownFilterCriteria = new(StringComparer.Ordinal);
+
+ // Lock for thread-safe operations
+ private readonly object _syncLock = new();
+
+ // Event fired when background refresh completes
+ public event Action? OnBackgroundRefreshCompleted;
+
+ ///
+ /// Creates a new MgGridSignalRDataSource wrapping an existing AcSignalRDataSource
+ ///
+ /// The underlying AcSignalRDataSource that handles caching and SignalR communication
+ /// Optional logger for debugging
+ public MgGridSignalRDataSource(
+ AcSignalRDataSource> innerDataSource,
+ AcLoggerBase? logger = null)
+ {
+ _innerDataSource = innerDataSource ?? throw new ArgumentNullException(nameof(innerDataSource));
+ _logger = logger;
+
+ // Subscribe to data source events
+ _innerDataSource.OnDataSourceLoaded += OnInnerDataSourceLoaded;
+ }
+
+ ///
+ /// Specifies the data item type for the grid
+ ///
+ protected override Type DataItemType => typeof(TDataItem);
+
+ ///
+ /// Gets the inner AcSignalRDataSource for direct access if needed
+ ///
+ public AcSignalRDataSource> InnerDataSource => _innerDataSource;
+
+ #region GridCustomDataSource Implementation
+
+ ///
+ /// Gets the total count of items matching the current filter.
+ /// If filter was seen before, returns local count instantly and refreshes in background.
+ ///
+ public override async Task GetItemCountAsync(
+ GridCustomDataSourceCountOptions options,
+ CancellationToken cancellationToken)
+ {
+ var filterKey = GetFilterKey(options.FilterCriteria);
+
+ _logger?.Debug($"[MgGridSignalRDataSource] GetItemCountAsync - Filter: {filterKey}");
+
+ // If we have local data and this filter was seen before, return local count
+ if (_innerDataSource.Count > 0 && IsKnownFilter(filterKey))
+ {
+ var localCount = ApplyLocalFilter(_innerDataSource.ToList(), options.FilterCriteria).Count;
+
+ _logger?.Debug($"[MgGridSignalRDataSource] Returning local count: {localCount}, refreshing in background");
+
+ // Refresh in background (fire-and-forget)
+ _ = RefreshInBackgroundAsync(filterKey);
+
+ return localCount;
+ }
+
+ // First time seeing this filter - must wait for server
+ _logger?.Debug("[MgGridSignalRDataSource] New filter, waiting for server data");
+
+ await LoadFromServerAsync();
+ MarkFilterAsKnown(filterKey);
+
+ return ApplyLocalFilter(_innerDataSource.ToList(), options.FilterCriteria).Count;
+ }
+
+ ///
+ /// Gets items for the current page with filtering and sorting applied.
+ /// If filter was seen before, returns local data instantly and refreshes in background.
+ ///
+ public override async Task GetItemsAsync(
+ GridCustomDataSourceItemsOptions options,
+ CancellationToken cancellationToken)
+ {
+ var filterKey = GetFilterKey(options.FilterCriteria);
+
+ _logger?.Debug($"[MgGridSignalRDataSource] GetItemsAsync - Skip: {options.StartIndex}, Take: {options.Count}, Filter: {filterKey}");
+
+ // If we have local data and this filter was seen before, return local data
+ if (_innerDataSource.Count > 0 && IsKnownFilter(filterKey))
+ {
+ var localResult = GetLocalItems(options);
+
+ _logger?.Debug($"[MgGridSignalRDataSource] Returning {localResult.Count} local items, refreshing in background");
+
+ // Refresh in background (fire-and-forget)
+ _ = RefreshInBackgroundAsync(filterKey);
+
+ return localResult;
+ }
+
+ // First time seeing this filter - must wait for server
+ _logger?.Debug("[MgGridSignalRDataSource] New filter, waiting for server data");
+
+ await LoadFromServerAsync();
+ MarkFilterAsKnown(filterKey);
+
+ return GetLocalItems(options);
+ }
+
+ ///
+ /// Gets unique values for a column (used in filter dropdowns).
+ /// Always returns from local data.
+ ///
+ public override Task GetUniqueValuesAsync(
+ GridCustomDataSourceUniqueValuesOptions options,
+ CancellationToken cancellationToken)
+ {
+ _logger?.Debug($"[MgGridSignalRDataSource] GetUniqueValuesAsync - Field: {options.FieldName}");
+
+ if (_innerDataSource.Count == 0)
+ return Task.FromResult(Array.Empty());
+
+ try
+ {
+ var propertyInfo = typeof(TDataItem).GetProperty(options.FieldName);
+ if (propertyInfo == null)
+ return Task.FromResult(Array.Empty());
+
+ var uniqueValues = _innerDataSource
+ .Select(item => propertyInfo.GetValue(item))
+ .Where(v => v != null)
+ .Distinct()
+ .Cast()
+ .ToArray();
+
+ _logger?.Debug($"[MgGridSignalRDataSource] Found {uniqueValues.Length} unique values for {options.FieldName}");
+
+ return Task.FromResult(uniqueValues);
+ }
+ catch (Exception ex)
+ {
+ _logger?.Error($"[MgGridSignalRDataSource] GetUniqueValuesAsync failed: {ex.Message}", ex);
+ return Task.FromResult(Array.Empty());
+ }
+ }
+
+ ///
+ /// Gets group information for grouped data.
+ /// Currently delegates to base implementation.
+ ///
+ public override async Task> GetGroupInfoAsync(
+ GridCustomDataSourceGroupingOptions options,
+ CancellationToken cancellationToken)
+ {
+ _logger?.Debug("[MgGridSignalRDataSource] GetGroupInfoAsync");
+
+ // TODO: Implement local grouping when needed
+ return await base.GetGroupInfoAsync(options, cancellationToken);
+ }
+
+ ///
+ /// Gets total summary values.
+ /// Calculates from local data.
+ ///
+ public override Task GetTotalSummaryAsync(
+ GridCustomDataSourceTotalSummaryOptions options,
+ CancellationToken cancellationToken)
+ {
+ _logger?.Debug($"[MgGridSignalRDataSource] GetTotalSummaryAsync - Summaries: {options.SummaryInfo?.Count ?? 0}");
+
+ if (options.SummaryInfo == null || options.SummaryInfo.Count == 0 || _innerDataSource.Count == 0)
+ return Task.FromResult(new List());
+
+ var filteredData = ApplyLocalFilter(_innerDataSource.ToList(), options.FilterCriteria);
+ var summaryValues = new List();
+
+ foreach (var summaryInfo in options.SummaryInfo)
+ {
+ var value = CalculateSummary(filteredData, summaryInfo);
+ summaryValues.Add(value);
+ }
+
+ _logger?.Debug($"[MgGridSignalRDataSource] Calculated {summaryValues.Count} summary values");
+
+ return Task.FromResult(summaryValues);
+ }
+
+ #endregion
+
+ #region Local Data Operations
+
+ ///
+ /// Gets items from local cache with filter, sort, and paging applied
+ ///
+ private List GetLocalItems(GridCustomDataSourceItemsOptions options)
+ {
+ var data = _innerDataSource.ToList();
+
+ // Apply filter
+ var filtered = ApplyLocalFilter(data, options.FilterCriteria);
+
+ // Apply sorting
+ var sorted = ApplyLocalSort(filtered, options.SortInfo);
+
+ // Apply paging
+ var paged = sorted
+ .Skip(options.StartIndex)
+ .Take(options.Count)
+ .ToList();
+
+ return paged;
+ }
+
+ ///
+ /// Applies CriteriaOperator filter to local data using DevExpress CriteriaToExpressionConverter
+ ///
+ private List ApplyLocalFilter(List data, CriteriaOperator? criteria)
+ {
+ if (criteria is null || data.Count == 0)
+ return data;
+
+ try
+ {
+ // Use DevExpress built-in CriteriaToExpressionConverter
+ var filteredData = data.AsQueryable().AppendWhere(_criteriaConverter, criteria);
+ return filteredData.Cast().ToList();
+ }
+ catch (Exception ex)
+ {
+ _logger?.Error($"[MgGridSignalRDataSource] Local filter failed: {ex.Message}", ex);
+ return data;
+ }
+ }
+
+ ///
+ /// Applies sorting to local data
+ ///
+ private List ApplyLocalSort(List data, IReadOnlyList? sortInfo)
+ {
+ if (sortInfo == null || sortInfo.Count == 0 || data.Count == 0)
+ return data;
+
+ try
+ {
+ IOrderedEnumerable? ordered = null;
+
+ for (var i = 0; i < sortInfo.Count; i++)
+ {
+ var sort = sortInfo[i];
+ var propertyInfo = typeof(TDataItem).GetProperty(sort.FieldName);
+
+ if (propertyInfo == null)
+ continue;
+
+ Func keySelector = item => propertyInfo.GetValue(item);
+
+ if (i == 0)
+ {
+ ordered = sort.DescendingSortOrder
+ ? data.OrderByDescending(keySelector)
+ : data.OrderBy(keySelector);
+ }
+ else if (ordered != null)
+ {
+ ordered = sort.DescendingSortOrder
+ ? ordered.ThenByDescending(keySelector)
+ : ordered.ThenBy(keySelector);
+ }
+ }
+
+ return ordered?.ToList() ?? data;
+ }
+ catch (Exception ex)
+ {
+ _logger?.Error($"[MgGridSignalRDataSource] Local sort failed: {ex.Message}", ex);
+ return data;
+ }
+ }
+
+ ///
+ /// Calculates a summary value for the given data
+ ///
+ private object? CalculateSummary(List data, GridCustomDataSourceSummaryInfo summaryInfo)
+ {
+ if (data.Count == 0)
+ return null;
+
+ try
+ {
+ var propertyInfo = typeof(TDataItem).GetProperty(summaryInfo.FieldName);
+
+ return summaryInfo.SummaryType switch
+ {
+ GridSummaryItemType.Count => data.Count,
+ GridSummaryItemType.Sum when propertyInfo != null =>
+ data.Sum(item => Convert.ToDecimal(propertyInfo.GetValue(item) ?? 0)),
+ GridSummaryItemType.Min when propertyInfo != null =>
+ data.Min(item => propertyInfo.GetValue(item)),
+ GridSummaryItemType.Max when propertyInfo != null =>
+ data.Max(item => propertyInfo.GetValue(item)),
+ GridSummaryItemType.Avg when propertyInfo != null =>
+ data.Average(item => Convert.ToDecimal(propertyInfo.GetValue(item) ?? 0)),
+ _ => null
+ };
+ }
+ catch (Exception ex)
+ {
+ _logger?.Error($"[MgGridSignalRDataSource] Summary calculation failed: {ex.Message}", ex);
+ return null;
+ }
+ }
+
+ #endregion
+
+ #region Filter Criteria Tracking
+
+ ///
+ /// Gets a unique key for the filter criteria
+ ///
+ private string GetFilterKey(CriteriaOperator? criteria)
+ {
+ if (criteria is null)
+ return string.Empty;
+
+ try
+ {
+ return CriteriaOperator.ToString(criteria);
+ }
+ catch
+ {
+ return string.Empty;
+ }
+ }
+
+ ///
+ /// Checks if this filter has been seen before
+ ///
+ private bool IsKnownFilter(string filterKey)
+ {
+ lock (_syncLock)
+ {
+ return _knownFilterCriteria.Contains(filterKey);
+ }
+ }
+
+ ///
+ /// Marks a filter as known (seen before)
+ ///
+ private void MarkFilterAsKnown(string filterKey)
+ {
+ lock (_syncLock)
+ {
+ _knownFilterCriteria.Add(filterKey);
+ }
+ }
+
+ ///
+ /// Clears the known filter cache
+ ///
+ public void ClearKnownFilters()
+ {
+ lock (_syncLock)
+ {
+ _knownFilterCriteria.Clear();
+ }
+ _logger?.Debug("[MgGridSignalRDataSource] Known filters cleared");
+ }
+
+ #endregion
+
+ #region Server Communication
+
+ ///
+ /// Loads data from server synchronously (blocking)
+ ///
+ private Task LoadFromServerAsync()
+ {
+ _logger?.Debug("[MgGridSignalRDataSource] Loading from server (sync)");
+ return _innerDataSource.LoadDataSource();
+ }
+
+ ///
+ /// Refreshes data in background using callback pattern (non-blocking)
+ ///
+ private Task RefreshInBackgroundAsync(string filterKey)
+ {
+ _logger?.Debug($"[MgGridSignalRDataSource] Starting background refresh for filter: {filterKey}");
+
+ // Use async callback version - this won't block
+ return _innerDataSource.LoadDataSourceAsync();
+ }
+
+ ///
+ /// Called when inner data source finishes loading
+ ///
+ private Task OnInnerDataSourceLoaded()
+ {
+ _logger?.Debug("[MgGridSignalRDataSource] Inner data source loaded, triggering refresh event");
+ OnBackgroundRefreshCompleted?.Invoke();
+ return Task.CompletedTask;
+ }
+
+ #endregion
+
+ #region Cleanup
+
+ ///
+ /// Invalidates all caches and clears known filters
+ ///
+ public void InvalidateCache()
+ {
+ ClearKnownFilters();
+ _logger?.Debug("[MgGridSignalRDataSource] Cache invalidated");
+ }
+
+ #endregion
+}
diff --git a/AyCode.Blazor.Components/Services/ExpressionHelpers/AcExpressionDeserializer.cs b/AyCode.Blazor.Components/Services/ExpressionHelpers/AcExpressionDeserializer.cs
new file mode 100644
index 0000000..e0dda2b
--- /dev/null
+++ b/AyCode.Blazor.Components/Services/ExpressionHelpers/AcExpressionDeserializer.cs
@@ -0,0 +1,356 @@
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace AyCode.Blazor.Components.Services.ExpressionHelpers;
+
+///
+/// Deserializes AcExpressionNode DTO back to Expression tree.
+///
+public class AcExpressionDeserializer
+{
+ private static readonly JsonSerializerOptions JsonOptions = new()
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ Converters = { new JsonStringEnumConverter() }
+ };
+
+ private readonly Dictionary _parameters = new();
+
+ ///
+ /// Deserializes JSON to Expression.
+ ///
+ public static Expression ExpressionFromJson(string json, Type? entityType = null)
+ {
+ var node = JsonSerializer.Deserialize(json, JsonOptions)
+ ?? throw new ArgumentException("Invalid expression JSON", nameof(json));
+
+ var deserializer = new AcExpressionDeserializer();
+ return deserializer.Deserialize(node, entityType);
+ }
+
+ ///
+ /// Deserializes JSON to typed Expression.
+ ///
+ public static Expression> ExpressionFromJson(string json)
+ {
+ var expression = ExpressionFromJson(json, typeof(T));
+ return (Expression>)expression;
+ }
+
+ ///
+ /// Deserializes AcExpressionNode to Expression.
+ ///
+ public Expression Deserialize(AcExpressionNode node, Type? entityType = null)
+ {
+ return node.NodeType switch
+ {
+ ExpressionType.Lambda => DeserializeLambda(node, entityType),
+ ExpressionType.Parameter => DeserializeParameter(node),
+ ExpressionType.Constant => DeserializeConstant(node),
+ ExpressionType.MemberAccess => DeserializeMemberAccess(node, entityType),
+ ExpressionType.Call => DeserializeMethodCall(node, entityType),
+ ExpressionType.Conditional => DeserializeConditional(node, entityType),
+ ExpressionType.New => DeserializeNew(node, entityType),
+ ExpressionType.MemberInit => DeserializeMemberInit(node, entityType),
+ ExpressionType.NewArrayInit or ExpressionType.NewArrayBounds => DeserializeNewArray(node, entityType),
+ ExpressionType.Invoke => DeserializeInvocation(node, entityType),
+ ExpressionType.TypeIs or ExpressionType.TypeAs => DeserializeTypeBinary(node, entityType),
+
+ // Unary expressions
+ ExpressionType.Not or ExpressionType.Negate or ExpressionType.NegateChecked or
+ ExpressionType.Convert or ExpressionType.ConvertChecked or ExpressionType.ArrayLength or
+ ExpressionType.Quote or ExpressionType.UnaryPlus
+ => DeserializeUnary(node, entityType),
+
+ // Binary expressions
+ ExpressionType.Add or ExpressionType.AddChecked or ExpressionType.Subtract or
+ ExpressionType.SubtractChecked or ExpressionType.Multiply or ExpressionType.MultiplyChecked or
+ ExpressionType.Divide or ExpressionType.Modulo or ExpressionType.Power or
+ ExpressionType.And or ExpressionType.AndAlso or ExpressionType.Or or ExpressionType.OrElse or
+ ExpressionType.ExclusiveOr or ExpressionType.Equal or ExpressionType.NotEqual or
+ ExpressionType.LessThan or ExpressionType.LessThanOrEqual or
+ ExpressionType.GreaterThan or ExpressionType.GreaterThanOrEqual or
+ ExpressionType.Coalesce or ExpressionType.ArrayIndex or
+ ExpressionType.LeftShift or ExpressionType.RightShift
+ => DeserializeBinary(node, entityType),
+
+ _ => throw new NotSupportedException($"Expression type '{node.NodeType}' is not supported.")
+ };
+ }
+
+ #region Deserialize Methods
+
+ private LambdaExpression DeserializeLambda(AcExpressionNode node, Type? entityType)
+ {
+ // Create parameters
+ var parameters = new List();
+ if (node.Parameters != null)
+ {
+ foreach (var paramNode in node.Parameters)
+ {
+ var paramType = entityType ?? ResolveType(paramNode.TypeName);
+ var param = Expression.Parameter(paramType, paramNode.Name);
+ _parameters[paramNode.Index] = param;
+ parameters.Add(param);
+
+ // Use entityType only for first parameter
+ entityType = null;
+ }
+ }
+
+ var body = Deserialize(node.Body!, null);
+ return Expression.Lambda(body, parameters);
+ }
+
+ private ParameterExpression DeserializeParameter(AcExpressionNode node)
+ {
+ if (node.ParameterIndex.HasValue && _parameters.TryGetValue(node.ParameterIndex.Value, out var param))
+ return param;
+
+ throw new InvalidOperationException($"Parameter '{node.ParameterName}' not found.");
+ }
+
+ private static ConstantExpression DeserializeConstant(AcExpressionNode node)
+ {
+ var type = ResolveType(node.TypeName ?? "System.Object");
+
+ if (node.Value == null)
+ return Expression.Constant(null, type);
+
+ var value = JsonSerializer.Deserialize(node.Value, type, JsonOptions);
+ return Expression.Constant(value, type);
+ }
+
+ private Expression DeserializeMemberAccess(AcExpressionNode node, Type? entityType)
+ {
+ if (node.Object == null)
+ {
+ // Static member access
+ var declaringType = ResolveType(node.DeclaringType!);
+ var member = declaringType.GetMember(node.MemberName!, BindingFlags.Public | BindingFlags.Static).FirstOrDefault()
+ ?? throw new InvalidOperationException($"Static member '{node.MemberName}' not found on type '{declaringType.Name}'.");
+ return Expression.MakeMemberAccess(null, member);
+ }
+
+ var obj = Deserialize(node.Object, entityType);
+ var memberInfo = obj.Type.GetMember(node.MemberName!, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase).FirstOrDefault()
+ ?? throw new InvalidOperationException($"Member '{node.MemberName}' not found on type '{obj.Type.Name}'.");
+
+ return Expression.MakeMemberAccess(obj, memberInfo);
+ }
+
+ private Expression DeserializeMethodCall(AcExpressionNode node, Type? entityType)
+ {
+ var arguments = node.Arguments?.Select(a => Deserialize(a, entityType)).ToArray() ?? [];
+ var argumentTypes = arguments.Select(a => a.Type).ToArray();
+
+ var declaringType = node.DeclaringType != null ? ResolveType(node.DeclaringType) : null;
+ var instance = node.Object != null ? Deserialize(node.Object, entityType) : null;
+
+ MethodInfo? method = null;
+
+ if (instance != null)
+ {
+ // Instance method
+ method = FindMethod(instance.Type, node.MethodName!, argumentTypes, isStatic: false);
+ }
+ else if (declaringType != null)
+ {
+ // Static method (including extension methods)
+ method = FindMethod(declaringType, node.MethodName!, argumentTypes, isStatic: true);
+ }
+
+ if (method == null)
+ throw new InvalidOperationException($"Method '{node.MethodName}' not found.");
+
+ // Handle generic methods
+ if (method.IsGenericMethodDefinition && node.GenericArguments?.Count > 0)
+ {
+ var genericTypes = node.GenericArguments.Select(ResolveType).ToArray();
+ method = method.MakeGenericMethod(genericTypes);
+ }
+
+ return instance != null
+ ? Expression.Call(instance, method, arguments)
+ : Expression.Call(method, arguments);
+ }
+
+ private Expression DeserializeBinary(AcExpressionNode node, Type? entityType)
+ {
+ var left = Deserialize(node.Left!, entityType);
+ var right = Deserialize(node.Right!, entityType);
+
+ // Handle type mismatches (e.g., nullable comparisons)
+ if (left.Type != right.Type)
+ {
+ if (Nullable.GetUnderlyingType(left.Type) == right.Type)
+ right = Expression.Convert(right, left.Type);
+ else if (Nullable.GetUnderlyingType(right.Type) == left.Type)
+ left = Expression.Convert(left, right.Type);
+ }
+
+ return Expression.MakeBinary(node.NodeType, left, right);
+ }
+
+ private Expression DeserializeUnary(AcExpressionNode node, Type? entityType)
+ {
+ var operand = Deserialize(node.Operand!, entityType);
+ var type = node.TypeName != null ? ResolveType(node.TypeName) : null;
+
+ return node.NodeType switch
+ {
+ ExpressionType.Convert or ExpressionType.ConvertChecked when type != null
+ => Expression.Convert(operand, type),
+ _ => Expression.MakeUnary(node.NodeType, operand, type)
+ };
+ }
+
+ private Expression DeserializeConditional(AcExpressionNode node, Type? entityType)
+ {
+ var test = Deserialize(node.Test!, entityType);
+ var ifTrue = Deserialize(node.IfTrue!, entityType);
+ var ifFalse = Deserialize(node.IfFalse!, entityType);
+ return Expression.Condition(test, ifTrue, ifFalse);
+ }
+
+ private Expression DeserializeNew(AcExpressionNode node, Type? entityType)
+ {
+ var type = ResolveType(node.TypeName!);
+ var args = node.ConstructorArguments?.Select(a => Deserialize(a, entityType)).ToArray() ?? [];
+ var argTypes = args.Select(a => a.Type).ToArray();
+ var ctor = type.GetConstructor(argTypes)
+ ?? throw new InvalidOperationException($"Constructor not found for type '{type.Name}'.");
+ return Expression.New(ctor, args);
+ }
+
+ private Expression DeserializeMemberInit(AcExpressionNode node, Type? entityType)
+ {
+ var type = ResolveType(node.TypeName!);
+ var args = node.ConstructorArguments?.Select(a => Deserialize(a, entityType)).ToArray() ?? [];
+ var argTypes = args.Select(a => a.Type).ToArray();
+ var ctor = type.GetConstructor(argTypes) ?? type.GetConstructor(Type.EmptyTypes)
+ ?? throw new InvalidOperationException($"Constructor not found for type '{type.Name}'.");
+
+ var newExpr = Expression.New(ctor, args);
+ var bindings = node.MemberBindings?.Select(b => DeserializeMemberBinding(b, type, entityType)).ToList()
+ ?? [];
+
+ return Expression.MemberInit(newExpr, bindings);
+ }
+
+ private MemberBinding DeserializeMemberBinding(MemberBindingNode node, Type declaringType, Type? entityType)
+ {
+ var member = declaringType.GetMember(node.MemberName, BindingFlags.Public | BindingFlags.Instance).FirstOrDefault()
+ ?? throw new InvalidOperationException($"Member '{node.MemberName}' not found on type '{declaringType.Name}'.");
+
+ return node.BindingType switch
+ {
+ MemberBindingType.Assignment => Expression.Bind(member, Deserialize(node.Expression!, entityType)),
+ MemberBindingType.MemberBinding => Expression.MemberBind(member,
+ node.Bindings?.Select(b => DeserializeMemberBinding(b, GetMemberType(member), entityType)) ?? []),
+ MemberBindingType.ListBinding => Expression.ListBind(member,
+ node.Initializers?.Select(args => Expression.ElementInit(
+ GetAddMethod(GetMemberType(member)),
+ args.Select(a => Deserialize(a, entityType)))) ?? []),
+ _ => throw new NotSupportedException($"Binding type '{node.BindingType}' is not supported.")
+ };
+ }
+
+ private Expression DeserializeNewArray(AcExpressionNode node, Type? entityType)
+ {
+ var elementType = ResolveType(node.TypeName!).GetElementType()
+ ?? throw new InvalidOperationException("Cannot determine array element type.");
+ var elements = node.Elements?.Select(e => Deserialize(e, entityType)).ToArray() ?? [];
+ return Expression.NewArrayInit(elementType, elements);
+ }
+
+ private Expression DeserializeInvocation(AcExpressionNode node, Type? entityType)
+ {
+ var expression = Deserialize(node.Object!, entityType);
+ var arguments = node.Arguments?.Select(a => Deserialize(a, entityType)).ToArray() ?? [];
+ return Expression.Invoke(expression, arguments);
+ }
+
+ private Expression DeserializeTypeBinary(AcExpressionNode node, Type? entityType)
+ {
+ var expression = Deserialize(node.Operand!, entityType);
+ var type = ResolveType(node.TypeName!);
+ return node.NodeType == ExpressionType.TypeIs
+ ? Expression.TypeIs(expression, type)
+ : Expression.TypeAs(expression, type);
+ }
+
+ #endregion
+
+ #region Helper Methods
+
+ private static MethodInfo? FindMethod(Type type, string methodName, Type[] argumentTypes, bool isStatic)
+ {
+ var bindingFlags = BindingFlags.Public | (isStatic ? BindingFlags.Static : BindingFlags.Instance);
+
+ // Try exact match first
+ var method = type.GetMethod(methodName, bindingFlags, null, argumentTypes, null);
+ if (method != null) return method;
+
+ // Try finding by name and parameter count
+ var candidates = type.GetMethods(bindingFlags)
+ .Where(m => m.Name == methodName && m.GetParameters().Length == argumentTypes.Length)
+ .ToList();
+
+ return candidates.FirstOrDefault();
+ }
+
+ private static Type GetMemberType(MemberInfo member) => member switch
+ {
+ PropertyInfo pi => pi.PropertyType,
+ FieldInfo fi => fi.FieldType,
+ _ => throw new InvalidOperationException($"Cannot get type for member '{member.Name}'.")
+ };
+
+ private static MethodInfo GetAddMethod(Type collectionType)
+ {
+ return collectionType.GetMethod("Add")
+ ?? throw new InvalidOperationException($"Add method not found on type '{collectionType.Name}'.");
+ }
+
+ private static Type ResolveType(string typeName)
+ {
+ var type = typeName switch
+ {
+ "System.String" or "string" => typeof(string),
+ "System.Int32" or "int" => typeof(int),
+ "System.Int64" or "long" => typeof(long),
+ "System.Int16" or "short" => typeof(short),
+ "System.Byte" or "byte" => typeof(byte),
+ "System.Boolean" or "bool" => typeof(bool),
+ "System.Double" or "double" => typeof(double),
+ "System.Single" or "float" => typeof(float),
+ "System.Decimal" or "decimal" => typeof(decimal),
+ "System.DateTime" => typeof(DateTime),
+ "System.DateTimeOffset" => typeof(DateTimeOffset),
+ "System.DateOnly" => typeof(DateOnly),
+ "System.TimeOnly" => typeof(TimeOnly),
+ "System.TimeSpan" => typeof(TimeSpan),
+ "System.Guid" => typeof(Guid),
+ "System.Object" or "object" => typeof(object),
+ _ => Type.GetType(typeName)
+ };
+
+ if (type == null && typeName.Contains("Nullable"))
+ {
+ var match = System.Text.RegularExpressions.Regex.Match(typeName, @"System\.Nullable`1\[\[(.+?),");
+ if (match.Success)
+ {
+ var underlyingType = ResolveType(match.Groups[1].Value);
+ type = typeof(Nullable<>).MakeGenericType(underlyingType);
+ }
+ }
+
+ return type ?? throw new InvalidOperationException($"Cannot resolve type '{typeName}'.");
+ }
+
+ #endregion
+}
diff --git a/AyCode.Blazor.Components/Services/ExpressionHelpers/AcExpressionHelper.cs b/AyCode.Blazor.Components/Services/ExpressionHelpers/AcExpressionHelper.cs
new file mode 100644
index 0000000..4f033da
--- /dev/null
+++ b/AyCode.Blazor.Components/Services/ExpressionHelpers/AcExpressionHelper.cs
@@ -0,0 +1,238 @@
+using System.Linq.Expressions;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace AyCode.Blazor.Components.Services.ExpressionHelpers;
+
+///
+/// Helper class for serializing and deserializing Expression trees and IQueryable queries.
+/// Uses visitor pattern to handle all expression types automatically.
+///
+public static class AcExpressionHelper
+{
+ private static readonly JsonSerializerOptions JsonOptions = new()
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ Converters = { new JsonStringEnumConverter() }
+ };
+
+ #region Expression Serialization
+
+ ///
+ /// Serializes an Expression to AcExpressionNode DTO.
+ ///
+ public static AcExpressionNode ExpressionToNode(Expression expression)
+ {
+ var visitor = new AcExpressionSerializerVisitor();
+ return visitor.Convert(expression);
+ }
+
+ ///
+ /// Serializes an Expression to JSON string.
+ ///
+ public static string ExpressionToJson(Expression expression)
+ {
+ var node = ExpressionToNode(expression);
+ return JsonSerializer.Serialize(node, JsonOptions);
+ }
+
+ ///
+ /// Serializes a typed Expression to JSON string.
+ ///
+ public static string ExpressionToJson(Expression> expression)
+ {
+ return ExpressionToJson((Expression)expression);
+ }
+
+ #endregion
+
+ #region Expression Deserialization
+
+ ///
+ /// Deserializes AcExpressionNode DTO to Expression.
+ ///
+ public static Expression ExpressionFromNode(AcExpressionNode node, Type? entityType = null)
+ {
+ var deserializer = new AcExpressionDeserializer();
+ return deserializer.Deserialize(node, entityType);
+ }
+
+ ///
+ /// Deserializes JSON to Expression.
+ ///
+ public static Expression ExpressionFromJson(string json, Type? entityType = null)
+ {
+ return AcExpressionDeserializer.ExpressionFromJson(json, entityType);
+ }
+
+ ///
+ /// Deserializes JSON to typed Expression.
+ ///
+ public static Expression> ExpressionFromJson(string json)
+ {
+ return AcExpressionDeserializer.ExpressionFromJson(json);
+ }
+
+ #endregion
+
+ #region IQueryable Serialization
+
+ ///
+ /// Serializes an IQueryable's expression tree to JSON.
+ ///
+ public static string QueryToJson(IQueryable query)
+ {
+ return ExpressionToJson(query.Expression);
+ }
+
+ ///
+ /// Serializes an IQueryable's expression tree to AcExpressionNode.
+ ///
+ public static AcExpressionNode QueryToNode(IQueryable query)
+ {
+ return ExpressionToNode(query.Expression);
+ }
+
+ #endregion
+
+ #region IQueryable Deserialization
+
+ ///
+ /// Applies a serialized query expression to an IQueryable source.
+ ///
+ public static IQueryable ApplyQueryFromJson(IQueryable source, string json)
+ {
+ var node = JsonSerializer.Deserialize(json, JsonOptions)
+ ?? throw new ArgumentException("Invalid query JSON", nameof(json));
+
+ return ApplyQueryFromNode(source, node);
+ }
+
+ ///
+ /// Applies an AcExpressionNode query to an IQueryable source.
+ ///
+ public static IQueryable ApplyQueryFromNode(IQueryable source, AcExpressionNode node)
+ {
+ // If the node is a method call (Where, OrderBy, etc.), we need to rebuild it
+ // with the source expression replaced
+ var expression = RebuildQueryExpression(source.Expression, node, typeof(T));
+ return source.Provider.CreateQuery(expression);
+ }
+
+ ///
+ /// Rebuilds a query expression, replacing the source with the provided expression.
+ ///
+ private static Expression RebuildQueryExpression(Expression sourceExpression, AcExpressionNode node, Type entityType)
+ {
+ if (node is { NodeType: ExpressionType.Call, MethodName: not null })
+ {
+ return RebuildMethodCallChain(sourceExpression, node, entityType);
+ }
+
+ // If it's just a lambda (filter expression), wrap it in a Where call
+ if (node.NodeType == ExpressionType.Lambda)
+ {
+ var deserializer = new AcExpressionDeserializer();
+ var lambda = deserializer.Deserialize(node, entityType);
+
+ var whereMethod = typeof(Queryable).GetMethods()
+ .First(m => m.Name == "Where" && m.GetParameters().Length == 2)
+ .MakeGenericMethod(entityType);
+
+ return Expression.Call(whereMethod, sourceExpression, lambda);
+ }
+
+ throw new NotSupportedException($"Cannot apply expression of type '{node.NodeType}' to IQueryable.");
+ }
+
+ ///
+ /// Rebuilds a chain of method calls (Where, OrderBy, Skip, Take, etc.)
+ ///
+ private static Expression RebuildMethodCallChain(Expression sourceExpression, AcExpressionNode node, Type entityType)
+ {
+ // First, process the inner expression (the source of this method call)
+ Expression currentSource;
+
+ if (node.Arguments?.Count > 0 && node.Arguments[0].NodeType == ExpressionType.Call)
+ {
+ // Recursive: rebuild the inner chain first
+ currentSource = RebuildMethodCallChain(sourceExpression, node.Arguments[0], entityType);
+ }
+ else
+ {
+ // Base case: use the provided source
+ currentSource = sourceExpression;
+ }
+
+ // Now apply this method call
+ var methodName = node.MethodName!;
+ var declaringType = node.DeclaringType != null ? Type.GetType(node.DeclaringType) : typeof(Queryable);
+
+ // Find the method
+ var method = FindQueryableMethod(declaringType!, methodName, node.GenericArguments, node.Arguments?.Count ?? 1);
+
+ if (method == null)
+ throw new InvalidOperationException($"Method '{methodName}' not found.");
+
+ // Apply generic type arguments
+ if (method.IsGenericMethodDefinition && node.GenericArguments?.Count > 0)
+ {
+ var genericTypes = node.GenericArguments.Select(t => Type.GetType(t) ?? entityType).ToArray();
+ method = method.MakeGenericMethod(genericTypes);
+ }
+
+ // Build arguments
+ var deserializer = new AcExpressionDeserializer();
+ var arguments = new List { currentSource };
+
+ // Skip first argument (it's the source) and deserialize the rest
+ if (node.Arguments?.Count > 1)
+ {
+ for (var i = 1; i < node.Arguments.Count; i++)
+ {
+ var argNode = node.Arguments[i];
+
+ if (argNode.NodeType == ExpressionType.Quote && argNode.Operand != null)
+ {
+ // Quoted lambda - unquote and deserialize
+ var lambda = deserializer.Deserialize(argNode.Operand, entityType);
+ arguments.Add(Expression.Quote(lambda));
+ }
+ else if (argNode.NodeType == ExpressionType.Lambda)
+ {
+ var lambda = deserializer.Deserialize(argNode, entityType);
+ arguments.Add(Expression.Quote(lambda));
+ }
+ else
+ {
+ arguments.Add(deserializer.Deserialize(argNode, entityType));
+ }
+ }
+ }
+
+ return Expression.Call(method, arguments);
+ }
+
+ private static System.Reflection.MethodInfo? FindQueryableMethod(Type declaringType, string methodName, List? genericArgs, int argCount)
+ {
+ return declaringType.GetMethods()
+ .Where(m => m.Name == methodName)
+ .FirstOrDefault(m =>
+ {
+ var parameters = m.GetParameters();
+ if (parameters.Length != argCount) return false;
+
+ // Check if generic argument count matches
+ if (m.IsGenericMethodDefinition)
+ {
+ var genericCount = genericArgs?.Count ?? 1;
+ if (m.GetGenericArguments().Length != genericCount) return false;
+ }
+
+ return true;
+ });
+ }
+
+ #endregion
+}
diff --git a/AyCode.Blazor.Components/Services/ExpressionHelpers/AcExpressionNode.cs b/AyCode.Blazor.Components/Services/ExpressionHelpers/AcExpressionNode.cs
new file mode 100644
index 0000000..b105c64
--- /dev/null
+++ b/AyCode.Blazor.Components/Services/ExpressionHelpers/AcExpressionNode.cs
@@ -0,0 +1,214 @@
+using System.Linq.Expressions;
+using System.Text.Json.Serialization;
+
+namespace AyCode.Blazor.Components.Services.ExpressionHelpers;
+
+///
+/// Universal DTO representing any Expression node.
+/// Recursively represents the entire Expression tree.
+/// Serializable to JSON for transport over SignalR or HTTP.
+///
+public sealed class AcExpressionNode
+{
+ ///
+ /// The expression node type (Add, Equal, Call, MemberAccess, Lambda, etc.)
+ ///
+ public ExpressionType NodeType { get; set; }
+
+ ///
+ /// The CLR type name of this expression's result.
+ ///
+ public string? TypeName { get; set; }
+
+ #region Binary Expressions (Add, Equal, AndAlso, OrElse, etc.)
+
+ ///
+ /// Left operand for binary expressions.
+ ///
+ public AcExpressionNode? Left { get; set; }
+
+ ///
+ /// Right operand for binary expressions.
+ ///
+ public AcExpressionNode? Right { get; set; }
+
+ #endregion
+
+ #region Unary Expressions (Not, Convert, Negate, etc.)
+
+ ///
+ /// Operand for unary expressions.
+ ///
+ public AcExpressionNode? Operand { get; set; }
+
+ #endregion
+
+ #region Lambda Expressions
+
+ ///
+ /// Body of lambda expression.
+ ///
+ public AcExpressionNode? Body { get; set; }
+
+ ///
+ /// Parameter definitions for lambda expressions.
+ ///
+ public List? Parameters { get; set; }
+
+ #endregion
+
+ #region Member Access
+
+ ///
+ /// Member/property/field name for MemberAccess expressions.
+ ///
+ public string? MemberName { get; set; }
+
+ ///
+ /// Object expression for member access or instance method calls.
+ ///
+ public AcExpressionNode? Object { get; set; }
+
+ ///
+ /// Declaring type for static members.
+ ///
+ public string? DeclaringType { get; set; }
+
+ #endregion
+
+ #region Method Call
+
+ ///
+ /// Method name for Call expressions.
+ ///
+ public string? MethodName { get; set; }
+
+ ///
+ /// Arguments for method calls.
+ ///
+ public List? Arguments { get; set; }
+
+ ///
+ /// Generic type arguments for generic method calls.
+ ///
+ public List? GenericArguments { get; set; }
+
+ #endregion
+
+ #region Constant
+
+ ///
+ /// Serialized constant value (JSON).
+ ///
+ public string? Value { get; set; }
+
+ #endregion
+
+ #region Parameter
+
+ ///
+ /// Parameter name for Parameter expressions.
+ ///
+ public string? ParameterName { get; set; }
+
+ ///
+ /// Parameter index (for matching parameters in lambda).
+ ///
+ public int? ParameterIndex { get; set; }
+
+ #endregion
+
+ #region Conditional (Ternary)
+
+ ///
+ /// Test expression for conditional expressions.
+ ///
+ public AcExpressionNode? Test { get; set; }
+
+ ///
+ /// IfTrue branch for conditional expressions.
+ ///
+ public AcExpressionNode? IfTrue { get; set; }
+
+ ///
+ /// IfFalse branch for conditional expressions.
+ ///
+ public AcExpressionNode? IfFalse { get; set; }
+
+ #endregion
+
+ #region New Expression
+
+ ///
+ /// Constructor arguments for New expressions.
+ ///
+ public List? ConstructorArguments { get; set; }
+
+ ///
+ /// Member bindings for MemberInit expressions.
+ ///
+ public List? MemberBindings { get; set; }
+
+ #endregion
+
+ #region Array/Collection
+
+ ///
+ /// Elements for NewArrayInit expressions.
+ ///
+ public List? Elements { get; set; }
+
+ #endregion
+}
+
+///
+/// Represents a parameter definition in a lambda expression.
+///
+public sealed class ParameterNode
+{
+ ///
+ /// Parameter name.
+ ///
+ public string Name { get; set; } = "";
+
+ ///
+ /// Parameter type name.
+ ///
+ public string TypeName { get; set; } = "";
+
+ ///
+ /// Parameter index in the lambda.
+ ///
+ public int Index { get; set; }
+}
+
+///
+/// Represents a member binding in MemberInit expressions.
+///
+public sealed class MemberBindingNode
+{
+ ///
+ /// The member name being bound.
+ ///
+ public string MemberName { get; set; } = "";
+
+ ///
+ /// The binding type (Assignment, MemberBinding, ListBinding).
+ ///
+ public MemberBindingType BindingType { get; set; }
+
+ ///
+ /// The expression being assigned (for Assignment bindings).
+ ///
+ public AcExpressionNode? Expression { get; set; }
+
+ ///
+ /// Nested bindings (for MemberMemberBinding).
+ ///
+ public List? Bindings { get; set; }
+
+ ///
+ /// Element initializers (for ListBinding).
+ ///
+ public List>? Initializers { get; set; }
+}
diff --git a/AyCode.Blazor.Components/Services/ExpressionHelpers/AcExpressionSerializerVisitor.cs b/AyCode.Blazor.Components/Services/ExpressionHelpers/AcExpressionSerializerVisitor.cs
new file mode 100644
index 0000000..1774295
--- /dev/null
+++ b/AyCode.Blazor.Components/Services/ExpressionHelpers/AcExpressionSerializerVisitor.cs
@@ -0,0 +1,429 @@
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace AyCode.Blazor.Components.Services.ExpressionHelpers;
+
+///
+/// Expression visitor that serializes an Expression tree to AcExpressionNode DTO.
+/// Handles all common expression types recursively.
+///
+public class AcExpressionSerializerVisitor : ExpressionVisitor
+{
+ private static readonly JsonSerializerOptions JsonOptions = new()
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ Converters = { new JsonStringEnumConverter() }
+ };
+
+ private readonly Dictionary _parameterIndexes = new();
+ private int _nextParameterIndex;
+
+ // Stack to collect converted nodes
+ private readonly Stack _nodeStack = new();
+
+ ///
+ /// Converts an Expression to an AcExpressionNode DTO.
+ ///
+ public AcExpressionNode Convert(Expression expression)
+ {
+ _nodeStack.Clear();
+ _parameterIndexes.Clear();
+ _nextParameterIndex = 0;
+
+ VisitAndConvert(expression);
+
+ return _nodeStack.Count != 1 ? throw new InvalidOperationException($"Expected 1 node on stack, found {_nodeStack.Count}") : _nodeStack.Pop();
+ }
+
+ ///
+ /// Serializes an Expression to JSON string.
+ ///
+ public string ToJson(Expression expression)
+ {
+ var node = Convert(expression);
+ return JsonSerializer.Serialize(node, JsonOptions);
+ }
+
+ private void VisitAndConvert(Expression expression)
+ {
+ Visit(expression);
+ }
+
+ protected override Expression VisitBinary(BinaryExpression node)
+ {
+ VisitAndConvert(node.Left);
+ var left = _nodeStack.Pop();
+
+ VisitAndConvert(node.Right);
+ var right = _nodeStack.Pop();
+
+ _nodeStack.Push(new AcExpressionNode
+ {
+ NodeType = node.NodeType,
+ TypeName = node.Type.FullName,
+ Left = left,
+ Right = right
+ });
+
+ return node;
+ }
+
+ protected override Expression VisitUnary(UnaryExpression node)
+ {
+ VisitAndConvert(node.Operand);
+ var operand = _nodeStack.Pop();
+
+ _nodeStack.Push(new AcExpressionNode
+ {
+ NodeType = node.NodeType,
+ TypeName = node.Type.FullName,
+ Operand = operand
+ });
+
+ return node;
+ }
+
+ protected override Expression VisitLambda(Expression node)
+ {
+ // Register parameters with indexes
+ var parameters = new List();
+ foreach (var param in node.Parameters)
+ {
+ var index = _nextParameterIndex++;
+ _parameterIndexes[param] = index;
+ parameters.Add(new ParameterNode
+ {
+ Name = param.Name ?? $"p{index}",
+ TypeName = param.Type.FullName ?? param.Type.Name,
+ Index = index
+ });
+ }
+
+ VisitAndConvert(node.Body);
+ var body = _nodeStack.Pop();
+
+ _nodeStack.Push(new AcExpressionNode
+ {
+ NodeType = ExpressionType.Lambda,
+ TypeName = node.Type.FullName,
+ Body = body,
+ Parameters = parameters
+ });
+
+ return node;
+ }
+
+ protected override Expression VisitParameter(ParameterExpression node)
+ {
+ var index = _parameterIndexes.GetValueOrDefault(node, -1);
+
+ _nodeStack.Push(new AcExpressionNode
+ {
+ NodeType = ExpressionType.Parameter,
+ TypeName = node.Type.FullName,
+ ParameterName = node.Name,
+ ParameterIndex = index
+ });
+
+ return node;
+ }
+
+ protected override Expression VisitMember(MemberExpression node)
+ {
+ // Check if this is a closure variable access (captured variable)
+ if (IsClosureAccess(node))
+ {
+ var value = EvaluateClosureValue(node);
+ _nodeStack.Push(new AcExpressionNode
+ {
+ NodeType = ExpressionType.Constant,
+ TypeName = node.Type.FullName,
+ Value = SerializeValue(value)
+ });
+ return node;
+ }
+
+ AcExpressionNode? objectNode = null;
+ if (node.Expression != null)
+ {
+ VisitAndConvert(node.Expression);
+ objectNode = _nodeStack.Pop();
+ }
+
+ _nodeStack.Push(new AcExpressionNode
+ {
+ NodeType = ExpressionType.MemberAccess,
+ TypeName = node.Type.FullName,
+ MemberName = node.Member.Name,
+ Object = objectNode,
+ DeclaringType = node.Member.DeclaringType?.FullName
+ });
+
+ return node;
+ }
+
+ protected override Expression VisitConstant(ConstantExpression node)
+ {
+ _nodeStack.Push(new AcExpressionNode
+ {
+ NodeType = ExpressionType.Constant,
+ TypeName = node.Type.FullName,
+ Value = SerializeValue(node.Value)
+ });
+
+ return node;
+ }
+
+ protected override Expression VisitMethodCall(MethodCallExpression node)
+ {
+ AcExpressionNode? objectNode = null;
+ if (node.Object != null)
+ {
+ VisitAndConvert(node.Object);
+ objectNode = _nodeStack.Pop();
+ }
+
+ var arguments = new List();
+ foreach (var arg in node.Arguments)
+ {
+ VisitAndConvert(arg);
+ arguments.Add(_nodeStack.Pop());
+ }
+
+ var genericArgs = node.Method.IsGenericMethod
+ ? node.Method.GetGenericArguments().Select(t => t.FullName ?? t.Name).ToList()
+ : null;
+
+ _nodeStack.Push(new AcExpressionNode
+ {
+ NodeType = ExpressionType.Call,
+ TypeName = node.Type.FullName,
+ MethodName = node.Method.Name,
+ Object = objectNode,
+ Arguments = arguments,
+ DeclaringType = node.Method.DeclaringType?.FullName,
+ GenericArguments = genericArgs
+ });
+
+ return node;
+ }
+
+ protected override Expression VisitConditional(ConditionalExpression node)
+ {
+ VisitAndConvert(node.Test);
+ var test = _nodeStack.Pop();
+
+ VisitAndConvert(node.IfTrue);
+ var ifTrue = _nodeStack.Pop();
+
+ VisitAndConvert(node.IfFalse);
+ var ifFalse = _nodeStack.Pop();
+
+ _nodeStack.Push(new AcExpressionNode
+ {
+ NodeType = ExpressionType.Conditional,
+ TypeName = node.Type.FullName,
+ Test = test,
+ IfTrue = ifTrue,
+ IfFalse = ifFalse
+ });
+
+ return node;
+ }
+
+ protected override Expression VisitNew(NewExpression node)
+ {
+ var arguments = new List();
+ foreach (var arg in node.Arguments)
+ {
+ VisitAndConvert(arg);
+ arguments.Add(_nodeStack.Pop());
+ }
+
+ _nodeStack.Push(new AcExpressionNode
+ {
+ NodeType = ExpressionType.New,
+ TypeName = node.Type.FullName,
+ ConstructorArguments = arguments
+ });
+
+ return node;
+ }
+
+ protected override Expression VisitMemberInit(MemberInitExpression node)
+ {
+ var arguments = new List();
+ foreach (var arg in node.NewExpression.Arguments)
+ {
+ VisitAndConvert(arg);
+ arguments.Add(_nodeStack.Pop());
+ }
+
+ _nodeStack.Push(new AcExpressionNode
+ {
+ NodeType = ExpressionType.MemberInit,
+ TypeName = node.Type.FullName,
+ ConstructorArguments = arguments,
+ MemberBindings = node.Bindings.Select(ConvertMemberBinding).ToList()
+ });
+
+ return node;
+ }
+
+ protected override Expression VisitNewArray(NewArrayExpression node)
+ {
+ var elements = new List();
+ foreach (var expr in node.Expressions)
+ {
+ VisitAndConvert(expr);
+ elements.Add(_nodeStack.Pop());
+ }
+
+ _nodeStack.Push(new AcExpressionNode
+ {
+ NodeType = node.NodeType,
+ TypeName = node.Type.FullName,
+ Elements = elements
+ });
+
+ return node;
+ }
+
+ protected override Expression VisitTypeBinary(TypeBinaryExpression node)
+ {
+ VisitAndConvert(node.Expression);
+ var operand = _nodeStack.Pop();
+
+ _nodeStack.Push(new AcExpressionNode
+ {
+ NodeType = node.NodeType,
+ TypeName = node.TypeOperand.FullName,
+ Operand = operand
+ });
+
+ return node;
+ }
+
+ protected override Expression VisitInvocation(InvocationExpression node)
+ {
+ VisitAndConvert(node.Expression);
+ var objectNode = _nodeStack.Pop();
+
+ var arguments = new List();
+ foreach (var arg in node.Arguments)
+ {
+ VisitAndConvert(arg);
+ arguments.Add(_nodeStack.Pop());
+ }
+
+ _nodeStack.Push(new AcExpressionNode
+ {
+ NodeType = ExpressionType.Invoke,
+ TypeName = node.Type.FullName,
+ Object = objectNode,
+ Arguments = arguments
+ });
+
+ return node;
+ }
+
+ #region Helper Methods
+
+ private MemberBindingNode ConvertMemberBinding(MemberBinding binding)
+ {
+ return binding switch
+ {
+ MemberAssignment assignment => ConvertMemberAssignment(assignment),
+ MemberMemberBinding memberBinding => new MemberBindingNode
+ {
+ MemberName = memberBinding.Member.Name,
+ BindingType = MemberBindingType.MemberBinding,
+ Bindings = memberBinding.Bindings.Select(ConvertMemberBinding).ToList()
+ },
+ MemberListBinding listBinding => new MemberBindingNode
+ {
+ MemberName = listBinding.Member.Name,
+ BindingType = MemberBindingType.ListBinding,
+ Initializers = listBinding.Initializers
+ .Select(i => i.Arguments.Select(ConvertArgument).ToList())
+ .ToList()
+ },
+ _ => throw new NotSupportedException($"Member binding type '{binding.BindingType}' is not supported.")
+ };
+ }
+
+ private MemberBindingNode ConvertMemberAssignment(MemberAssignment assignment)
+ {
+ VisitAndConvert(assignment.Expression);
+ var expr = _nodeStack.Pop();
+
+ return new MemberBindingNode
+ {
+ MemberName = assignment.Member.Name,
+ BindingType = MemberBindingType.Assignment,
+ Expression = expr
+ };
+ }
+
+ private AcExpressionNode ConvertArgument(Expression expression)
+ {
+ VisitAndConvert(expression);
+ return _nodeStack.Pop();
+ }
+
+ private static bool IsClosureAccess(MemberExpression node)
+ {
+ return node.Expression switch
+ {
+ ConstantExpression => true,
+ MemberExpression nested => IsClosureAccess(nested),
+ _ => false
+ };
+ }
+
+ private static object? EvaluateClosureValue(MemberExpression node)
+ {
+ var objectStack = new Stack();
+ Expression? current = node;
+
+ while (current is MemberExpression me)
+ {
+ objectStack.Push(me);
+ current = me.Expression;
+ }
+
+ if (current is not ConstantExpression constant)
+ throw new InvalidOperationException("Expected constant at root of closure access.");
+
+ object? value = constant.Value;
+
+ while (objectStack.Count > 0)
+ {
+ var me = objectStack.Pop();
+ value = me.Member switch
+ {
+ FieldInfo fi => fi.GetValue(value),
+ PropertyInfo pi => pi.GetValue(value),
+ _ => throw new InvalidOperationException($"Unsupported member type: {me.Member.GetType()}")
+ };
+ }
+
+ return value;
+ }
+
+ private static string? SerializeValue(object? value)
+ {
+ if (value == null) return null;
+
+ // Handle IQueryable source - serialize as placeholder
+ if (value is IQueryable)
+ return null;
+
+ return JsonSerializer.Serialize(value, JsonOptions);
+ }
+
+ #endregion
+}