Skip to content

fix(Table): support QueryPageOptions serialization#7323

Merged
ArgoZhang merged 14 commits intomainfrom
bak-op
Dec 13, 2025
Merged

fix(Table): support QueryPageOptions serialization#7323
ArgoZhang merged 14 commits intomainfrom
bak-op

Conversation

@ArgoZhang
Copy link
Copy Markdown
Member

@ArgoZhang ArgoZhang commented Dec 13, 2025

Link issues

fixes #7320

Summary By Copilot

Regression?

  • Yes
  • No

Risk

  • High
  • Medium
  • Low

Verification

  • Manual (required)
  • Automated

Packaging changes reviewed?

  • Yes
  • No
  • N/A

☑️ Self Check before Merge

⚠️ Please check all items below before review. ⚠️

  • Doc is updated/provided or not needed
  • Demo is updated/provided or not needed
  • Merge the latest code from the main branch

Summary by Sourcery

Update QueryPageOptions and related converters to fully support JSON serialization of search and filter options in tables.

New Features:

  • Introduce SerializeFilterAction and a dedicated JsonFilterKeyValueActionConverter to enable serializable filter actions and filter key-value definitions.
  • Persist QueryPageOptions search and filter collections (searches, customerSearches, advanceSearches, filters) in JSON to support round-tripping of table query state.

Bug Fixes:

  • Fix QueryPageOptions JSON converter to correctly read and write individual search and filter collections instead of using a combined filterKeyValueAction payload.
  • Adjust table filtering logic and lambda-based property access tests to handle additional scenarios and avoid null access issues during filtering.

Enhancements:

  • Seal JsonQueryPageOptionsConverter and FilterKeyValueAction JSON handling for safer, more explicit serialization behavior.
  • Improve tests for QueryPageOptions serialization, SerializeFilterAction behavior, and lambda property access to validate the new JSON contract and edge cases.

Copilot AI review requested due to automatic review settings December 13, 2025 10:47
@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Dec 13, 2025

Reviewer's Guide

Refactors QueryPageOptions filtering serialization to be fully JSON-serializable via dedicated converters and a new SerializeFilterAction implementation, replaces the old ObjectWithTypeConverter and FilterKeyValueAction aggregation approach, and updates tests and sample usage accordingly.

Sequence diagram for QueryPageOptions JSON write with filters and searches

sequenceDiagram
    participant Caller
    participant Converter as JsonQueryPageOptionsConverter
    participant Writer as Utf8JsonWriter
    participant Options as JsonSerializerOptions
    participant Qpo as QueryPageOptions
    participant SFA as SerializeFilterAction
    participant FKA as FilterKeyValueAction

    Caller->>Converter: Write(Writer, Qpo, Options)
    activate Converter
    Converter->>Writer: WriteStartObject()

    alt has IsVirtualScroll
        Converter->>Writer: WriteBoolean(isVirtualScroll, Qpo.IsVirtualScroll)
    end

    alt Searches not empty
        Converter->>Writer: WriteStartArray(searches)
        loop each filter in Qpo.Searches
            Converter->>Writer: WriteRawValue(Serialize(filter, Options))
        end
        Converter->>Writer: WriteEndArray()
    end

    alt CustomerSearches not empty
        Converter->>Writer: WriteStartArray(customerSearches)
        loop each filter in Qpo.CustomerSearches
            Converter->>Writer: WriteRawValue(Serialize(filter, Options))
        end
        Converter->>Writer: WriteEndArray()
    end

    alt AdvanceSearches not empty
        Converter->>Writer: WriteStartArray(advanceSearches)
        loop each filter in Qpo.AdvanceSearches
            Converter->>Writer: WriteRawValue(Serialize(filter, Options))
        end
        Converter->>Writer: WriteEndArray()
    end

    alt Filters not empty
        Converter->>Writer: WriteStartArray(filters)
        loop each filter in Qpo.Filters
            Converter->>SFA: new SerializeFilterAction()
            activate SFA
            SFA->>FKA: GetFilterConditions()
            SFA-->>Converter: FilterKeyValueAction
            deactivate SFA
            Converter->>Writer: WriteRawValue(Serialize(SFA, Options))
        end
        Converter->>Writer: WriteEndArray()
    end

    Converter->>Writer: WriteEndObject()
    deactivate Converter
Loading

Sequence diagram for FilterKeyValueAction JSON read via JsonFilterKeyValueActionConverter

sequenceDiagram
    participant Caller
    participant Converter as JsonFilterKeyValueActionConverter
    participant Reader as Utf8JsonReader
    participant Options as JsonSerializerOptions
    participant FKA as FilterKeyValueAction

    Caller->>Converter: Read(Reader, typeof(FilterKeyValueAction), Options)
    activate Converter
    Converter->>FKA: new FilterKeyValueAction()

    alt Reader at StartObject
        loop properties
            Converter->>Reader: Read()
            alt property fieldKey
                Converter->>Reader: GetString()
                Converter->>FKA: FieldKey = value
            else property fieldValueType
                Converter->>Reader: GetString()
                Converter->>Converter: Type.GetType(typeName)
            else property fieldValue
                alt fieldValueType resolved
                    Converter->>Converter: JsonSerializer.Deserialize(Reader, fieldValueType, Options)
                    Converter->>FKA: FieldValue = result
                else no fieldValueType
                    Converter->>Reader: GetString()
                    Converter->>FKA: FieldValue = value
                end
            else property fieldAction
                Converter->>Converter: JsonSerializer.Deserialize~FilterAction~(Reader, Options)
                Converter->>FKA: FilterAction = result
            else property filterLogic
                Converter->>Converter: JsonSerializer.Deserialize~FilterLogic~(Reader, Options)
                Converter->>FKA: FilterLogic = result
            else property filters
                Converter->>Converter: JsonSerializer.Deserialize~List~FilterKeyValueAction~~(Reader, Options)
                Converter->>FKA: Filters.AddRange(list)
            end
        end
    end

    Converter-->>Caller: FKA
    deactivate Converter
Loading

Class diagram for updated QueryPageOptions filtering serialization

classDiagram
    direction LR

    class QueryPageOptions {
        +object SearchModel
        +List~IFilterAction~ Searches
        +List~IFilterAction~ CustomerSearches
        +List~IFilterAction~ AdvanceSearches
        +List~IFilterAction~ Filters
        +bool IsFirstQuery
        +bool IsVirtualScroll
    }

    class IFilterAction {
        <<interface>>
        +void Reset()
        +Task SetFilterConditionsAsync(FilterKeyValueAction filter)
        +FilterKeyValueAction GetFilterConditions()
    }

    class SearchFilterAction {
    }

    class SerializeFilterAction {
        +FilterKeyValueAction Filter
        +void Reset()
        +Task SetFilterConditionsAsync(FilterKeyValueAction filter)
        +FilterKeyValueAction GetFilterConditions()
    }

    class FilterKeyValueAction {
        +string FieldKey
        +object FieldValue
        +FilterAction FilterAction
        +FilterLogic FilterLogic
        +List~FilterKeyValueAction~ Filters
    }

    class JsonQueryPageOptionsConverter {
        +QueryPageOptions Read(Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        +void Write(Utf8JsonWriter writer, QueryPageOptions value, JsonSerializerOptions options)
    }

    class JsonFilterKeyValueActionConverter {
        +FilterKeyValueAction Read(Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        +void Write(Utf8JsonWriter writer, FilterKeyValueAction value, JsonSerializerOptions options)
    }

    QueryPageOptions "1" --> "*" IFilterAction : Searches
    QueryPageOptions "1" --> "*" IFilterAction : CustomerSearches
    QueryPageOptions "1" --> "*" IFilterAction : AdvanceSearches
    QueryPageOptions "1" --> "*" IFilterAction : Filters

    IFilterAction <|.. SerializeFilterAction
    IFilterAction <|.. SearchFilterAction

    SerializeFilterAction --> FilterKeyValueAction : Filter

    JsonQueryPageOptionsConverter --> QueryPageOptions : serialize/deserialize
    JsonQueryPageOptionsConverter --> SerializeFilterAction : uses
    JsonQueryPageOptionsConverter --> SearchFilterAction : uses
    JsonQueryPageOptionsConverter --> FilterKeyValueAction : uses via SerializeFilterAction

    JsonFilterKeyValueActionConverter --> FilterKeyValueAction : serialize/deserialize
    FilterKeyValueAction ..> JsonFilterKeyValueActionConverter : JsonConverter
Loading

File-Level Changes

Change Details Files
Make QueryPageOptions filter-related collections and FilterKeyValueAction fully serializable via custom JSON converters and a new IFilterAction implementation.
  • Seal JsonQueryPageOptionsConverter and change its read logic to deserialize searches, customerSearches, advanceSearches, and filters arrays instead of a single filterKeyValueAction object.
  • Update JsonQueryPageOptionsConverter write logic to emit searches, customerSearches, advanceSearches, and filters arrays using SerializeFilterAction and FilterKeyValueAction rather than a combined FilterKeyValueAction property.
  • Introduce JsonFilterKeyValueActionConverter to control how FilterKeyValueAction (including nested Filters and typed FieldValue) is serialized and deserialized, and apply it via an attribute on FilterKeyValueAction.
  • Add SerializeFilterAction implementing IFilterAction to wrap a FilterKeyValueAction for serialization, and adjust QueryPageOptions to expose non-ignored Lists of IFilterAction without internal FilterKeyValueAction caching.
  • Change IFilterAction JsonDerivedType mapping from SearchFilterAction to SerializeFilterAction and remove the now-unused ObjectWithTypeConverter and FilterKeyValueAction-related Json attributes and internal property.
src/BootstrapBlazor/Converter/JsonQueryPageOptionsConverter.cs
src/BootstrapBlazor/Converter/JsonFilterKeyValueActionConverter.cs
src/BootstrapBlazor/Components/Filters/SerializeFilterAction.cs
src/BootstrapBlazor/Components/Filters/IFilterAction.cs
src/BootstrapBlazor/Components/Filters/FilterKeyValueAction.cs
src/BootstrapBlazor/Options/QueryPageOptions.cs
src/BootstrapBlazor/Converter/ObjectWithTypeConverter.cs
src/BootstrapBlazor/Extensions/QueryPageOptionsExtensions.cs
Align tests and sample usages with the new serialization behavior and improve LambdaExtensions coverage.
  • Update QueryPageOptionsExtensionsTest.Serialize_Ok to build filters using DateTimeFilter and SerializeFilterAction, assert that search-related collections round-trip with single entries, and adapt to async usage.
  • Add SerializeFilterAction_Ok unit test to verify setting, retrieving, and resetting FilterKeyValueAction via SerializeFilterAction.
  • Extend LambdaExtensions tests to cover property access of nested Foo.Count and null-safe navigation on Dummy.Foo.
  • Adjust TablesColumnDrag sample to use a concrete Count check on Filters instead of LINQ Any for filtering detection.
  • Apply minor BOM/license header tweaks to a few files.
test/UnitTest/Extensions/QueryPageOptionsExtensionsTest.cs
test/UnitTest/Extensions/LambadaExtensionsTest.cs
src/BootstrapBlazor.Server/Components/Samples/Table/TablesColumnDrag.razor.cs
src/BootstrapBlazor/Extensions/QueryPageOptionsExtensions.cs
src/BootstrapBlazor/Options/QueryPageOptions.cs
src/BootstrapBlazor/Components/Filters/IFilterAction.cs
src/BootstrapBlazor/Components/Filters/FilterKeyValueAction.cs

Assessment against linked issues

Issue Objective Addressed Explanation
#7320 Ensure QueryPageOptions fully supports JSON serialization and deserialization of its search/filter-related properties (Searches, CustomerSearches, AdvanceSearches, Filters) including their underlying filter conditions.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@bb-auto bb-auto Bot added the bug Something isn't working label Dec 13, 2025
@bb-auto bb-auto Bot added this to the v10.1.0 milestone Dec 13, 2025
Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes and found some issues that need to be addressed.

  • In JsonFilterKeyValueActionConverter, you serialize fieldValueType using value.FieldValue?.GetType().FullName but later resolve it via Type.GetType(typeName); for non-BCL or non-current-assembly types this will often return null—consider persisting the assembly-qualified name instead (e.g., AssemblyQualifiedName) or using a custom type resolver.
  • QueryPageOptions.SearchModel no longer uses ObjectWithTypeConverter and will now serialize as a plain object without type metadata; this may break round-tripping of strongly-typed search models—if polymorphic deserialization is still needed, you might want to keep a converter or introduce an alternative approach to preserve the concrete type.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In JsonFilterKeyValueActionConverter, you serialize fieldValueType using value.FieldValue?.GetType().FullName but later resolve it via Type.GetType(typeName); for non-BCL or non-current-assembly types this will often return null—consider persisting the assembly-qualified name instead (e.g., AssemblyQualifiedName) or using a custom type resolver.
- QueryPageOptions.SearchModel no longer uses ObjectWithTypeConverter and will now serialize as a plain object without type metadata; this may break round-tripping of strongly-typed search models—if polymorphic deserialization is still needed, you might want to keep a converter or introduce an alternative approach to preserve the concrete type.

## Individual Comments

### Comment 1
<location> `src/BootstrapBlazor/Converter/JsonQueryPageOptionConverter.cs:107-112` </location>
<code_context>
                     }
-                    
-                    else if (propertyName == "filterKeyValueAction")
+                    else if (propertyName == "searches")
                     {
                         reader.Read();
-                        var val = JsonSerializer.Deserialize<FilterKeyValueAction>(ref reader, options);
-                        if (val != null)
+                        if (reader.TokenType == JsonTokenType.StartArray)
+                        {
</code_context>

<issue_to_address>
**issue (bug_risk):** Consider keeping deserialization support for legacy `filterKeyValueAction` to avoid breaking existing payloads.

With this change, any incoming JSON that still uses `filterKeyValueAction` will now be silently ignored. If this converter is used for persisted data or older clients, that’s a breaking change in behavior. Consider retaining an `else if (propertyName == "filterKeyValueAction")` branch that adapts the legacy payload into the new `searches`/`customerSearches`/`advanceSearches`/`filters` collections so both formats remain deserializable.
</issue_to_address>

### Comment 2
<location> `src/BootstrapBlazor/Converter/JsonFilterKeyValueActionConverter.cs:44-53` </location>
<code_context>
+                        case "fieldKey":
+                            action.FieldKey = reader.GetString();
+                            break;
+                        case "fieldValueType":
+                            var typeName = reader.GetString();
+                            if (!string.IsNullOrEmpty(typeName))
+                            {
+                                fieldValueType = Type.GetType(typeName);
+                            }
+                            break;
+                        case "fieldValue":
+                            if (fieldValueType != null)
+                            {
+                                action.FieldValue = JsonSerializer.Deserialize(ref reader, fieldValueType, options);
+                            }
+                            else
+                            {
+                                action.FieldValue = reader.GetString();
+                            }
+                            break;
</code_context>

<issue_to_address>
**issue:** Deserialization of `fieldValue` can fail when `Type.GetType` cannot resolve `fieldValueType`.

Because `fieldValueType` is obtained via `Type.GetType(typeName)`, it can be null for unresolved type names (e.g., types from other assemblies, renamed types, or trimmed assemblies). In that case, falling back to `reader.GetString()` will throw or distort non-string values (numbers, booleans, objects, arrays). Consider instead (a) storing an assembly-qualified or otherwise resolvable type name, and/or (b) when `fieldValueType` is null, deserializing as `JsonElement` or `object` via `JsonSerializer.Deserialize` to preserve the original value shape and avoid runtime errors.
</issue_to_address>

### Comment 3
<location> `src/BootstrapBlazor/Components/Filters/IFilterAction.cs:13` </location>
<code_context>
 /// IFilterAction 接口
 /// </summary>
-[JsonDerivedType(typeof(SearchFilterAction))]
+[JsonDerivedType(typeof(SerializeFilterAction))]
 public interface IFilterAction
 {
</code_context>

<issue_to_address>
**question (bug_risk):** Switching the `JsonDerivedType` from `SearchFilterAction` to `SerializeFilterAction` may affect polymorphic JSON behavior.

This changes the polymorphic mapping for `IFilterAction` from `SearchFilterAction` to `SerializeFilterAction`. Please verify that all consumers now expect `SerializeFilterAction` in polymorphic (de)serialization of `IFilterAction`, and that any existing payloads using `SearchFilterAction`-specific shape or type metadata are either migrated or remain readable via other configuration.
</issue_to_address>

### Comment 4
<location> `test/UnitTest/Extensions/QueryPageOptionsExtensionsTest.cs:155-164` </location>
<code_context>

     [Fact]
-    public void Serialize_Ok()
+    public async Task Serialize_Ok()
     {
+        var cut = Context.Render<DateTimeFilter>(pb =>
</code_context>

<issue_to_address>
**suggestion (testing):** Strengthen `Serialize_Ok` to verify round-tripped filter/search contents, not just collection counts.

Currently this test only proves that the four collections exist and are non-empty after JSON round-trip; it doesn’t verify that the filter/search data itself is preserved. Since we now depend on `SerializeFilterAction` and `JsonFilterKeyValueActionConverter` to round-trip `FieldKey`, `FieldValue` (including runtime type like `DateTime`), and nested filter structures, it would be good to assert this explicitly.

Consider extending the test to:
- Verify that `Searches/AdvanceSearches/CustomerSearches` contain `SerializeFilterAction` instances.
- Inspect `SerializeFilterAction.Filter` and assert `FieldKey`/`FieldValue` (and the concrete type of `FieldValue`) match what was set before serialization.
- For the `DateTimeFilter` in `model.Filters`, assert that after round-trip there are still two `FilterKeyValueAction` entries with the expected `FilterAction` values.

This will ensure the JSON contract truly round-trips filter/search options, not just collection counts.

Suggested implementation:

```csharp
    [Fact]
    public async Task Serialize_Ok()
    {
        var cut = Context.Render<DateTimeFilter>(pb =>
        {
            pb.Add(a => a.FieldKey, "DateTime");
        });
        var filter = cut.Instance;

        // Use a deterministic DateTime so we can verify exact round-tripped value and type.
        var expectedDateTime = new DateTime(2020, 1, 2, 3, 4, 5, DateTimeKind.Utc);

        var conditions = new FilterKeyValueAction()
        {
            Filters =
            [
                // This value should round-trip through SerializeFilterAction/JsonFilterKeyValueActionConverter.
                new FilterKeyValueAction()
                {
                    FieldValue = expectedDateTime,
                    FilterAction = FilterAction.GreaterThanOrEqual
                },

```

To fully implement your suggestion, the rest of the `Serialize_Ok` test (which is not visible in the snippet) should be updated to:

1. Perform the JSON round-trip of the query/filter model as it already does, but then:
   - Assert that `Searches`, `AdvanceSearches`, and `CustomerSearches` collections contain elements of type `SerializeFilterAction`.
   - For each `SerializeFilterAction`, inspect its `Filter` property and assert:
     - `FieldKey` is `"DateTime"` (or the expected key you set via the component).
     - `FieldValue` is equal to `expectedDateTime` and `FieldValue.GetType()` is `typeof(DateTime)`.
2. For the `DateTimeFilter` stored in `model.Filters`:
   - After deserialization, locate the corresponding filter entry.
   - Assert that it still has two `FilterKeyValueAction` entries (if you create two in this test) and that their `FilterAction` values are the expected ones (e.g. `GreaterThanOrEqual`, `LessThanOrEqual`).
3. If the test uses an intermediate `SerializeFilterAction` wrapper object:
   - Add explicit `Assert.IsType<SerializeFilterAction>(...)`/`Assert.All(...)` checks on `Searches`, `AdvanceSearches`, and `CustomerSearches` collections.
   - Assert that the nested `FilterKeyValueAction` structure (including `Filters` lists) has the same shape after round-trip as before serialization.

You will need to integrate these assertions at the end of `Serialize_Ok`, after the JSON deserialize step, using the `expectedDateTime` variable introduced in the change above as the source of truth for value and type comparison.
</issue_to_address>

### Comment 5
<location> `test/UnitTest/Extensions/QueryPageOptionsExtensionsTest.cs:154-163` </location>
<code_context>
     [Fact]
     public void GetPropertyValueLambda_Ok()
     {
</code_context>

<issue_to_address>
**suggestion (testing):** Add dedicated tests for JSON serialization of `SerializeFilterAction` / `FilterKeyValueAction`.

The current test only verifies in-memory behavior and doesnt exercise the new JSON converter or the use of `SerializeFilterAction` as the `IFilterAction` implementation. Please add tests that:
- Serialize and deserialize a `SerializeFilterAction` with a `FilterKeyValueAction` using non-string `FieldValue` types (e.g. `DateTime`, `int`) and nested `Filters`, then assert that `FieldValue`’s runtime type, `FilterAction`, `FilterLogic`, and nested `Filters` are preserved.
- Optionally cover an empty `Filters` collection and a `null` `FieldValue`.

This will directly verify the JSON shape and converter behavior relied on for persisting table query state.

Suggested implementation:

```csharp
    }

    [Fact]
    public void SerializeFilterAction_WithNestedFilters_PreservesTypes()
    {
        // Arrange
        var createdAt = new DateTime(2024, 01, 02, 03, 04, 05, DateTimeKind.Utc);

        var rootFilter = new FilterKeyValueAction
        {
            FieldKey = "CreatedAt",
            FieldValue = createdAt,
            FilterAction = TableFilterAction.Equal,
            FilterLogic = TableFilterLogic.And,
            Filters =
            [
                new FilterKeyValueAction
                {
                    FieldKey = "Count",
                    FieldValue = 42,
                    FilterAction = TableFilterAction.GreaterThan,
                    FilterLogic = TableFilterLogic.Or
                }
            ]
        };

        var serializeFilterAction = new SerializeFilterAction
        {
            FilterAction = rootFilter
        };

        var options = new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase
        };
        options.Converters.Add(new SerializeFilterActionJsonConverter());

        // Act
        var json = JsonSerializer.Serialize(serializeFilterAction, options);
        var roundTripped = JsonSerializer.Deserialize<SerializeFilterAction>(json, options);

        // Assert
        Assert.NotNull(roundTripped);
        var deserializedRoot = Assert.IsType<FilterKeyValueAction>(roundTripped!.FilterAction);

        // Root filter assertions
        Assert.Equal("CreatedAt", deserializedRoot.FieldKey);
        Assert.Equal(TableFilterAction.Equal, deserializedRoot.FilterAction);
        Assert.Equal(TableFilterLogic.And, deserializedRoot.FilterLogic);

        Assert.NotNull(deserializedRoot.FieldValue);
        var deserializedCreatedAt = Assert.IsType<DateTime>(deserializedRoot.FieldValue);
        Assert.Equal(createdAt, deserializedCreatedAt);

        // Nested filters assertions
        Assert.NotNull(deserializedRoot.Filters);
        var nested = Assert.Single(deserializedRoot.Filters);
        var nestedFilter = Assert.IsType<FilterKeyValueAction>(nested);

        Assert.Equal("Count", nestedFilter.FieldKey);
        Assert.Equal(TableFilterAction.GreaterThan, nestedFilter.FilterAction);
        Assert.Equal(TableFilterLogic.Or, nestedFilter.FilterLogic);

        Assert.NotNull(nestedFilter.FieldValue);
        var nestedCount = Assert.IsType<int>(nestedFilter.FieldValue);
        Assert.Equal(42, nestedCount);
    }

    [Fact]
    public void SerializeFilterAction_WithNullFieldValueAndEmptyFilters_Ok()
    {
        // Arrange
        var rootFilter = new FilterKeyValueAction
        {
            FieldKey = "Status",
            FieldValue = null,
            FilterAction = TableFilterAction.Equal,
            FilterLogic = TableFilterLogic.And,
            Filters = []
        };

        var serializeFilterAction = new SerializeFilterAction
        {
            FilterAction = rootFilter
        };

        var options = new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase
        };
        options.Converters.Add(new SerializeFilterActionJsonConverter());

        // Act
        var json = JsonSerializer.Serialize(serializeFilterAction, options);
        var roundTripped = JsonSerializer.Deserialize<SerializeFilterAction>(json, options);

        // Assert
        Assert.NotNull(roundTripped);
        var deserializedRoot = Assert.IsType<FilterKeyValueAction>(roundTripped!.FilterAction);

        Assert.Equal("Status", deserializedRoot.FieldKey);
        Assert.Equal(TableFilterAction.Equal, deserializedRoot.FilterAction);
        Assert.Equal(TableFilterLogic.And, deserializedRoot.FilterLogic);

        // FieldValue should remain null
        Assert.Null(deserializedRoot.FieldValue);

        // Filters should remain an empty collection
        Assert.NotNull(deserializedRoot.Filters);
        Assert.Empty(deserializedRoot.Filters);
    }

```

These tests assume the following are available and correctly implemented in your codebase:

1. `SerializeFilterAction` with a `FilterAction` property of type `IFilterAction`.
2. `FilterKeyValueAction` implementing `IFilterAction` with:
   - `string FieldKey { get; set; }`
   - `object? FieldValue { get; set; }`
   - `TableFilterAction FilterAction { get; set; }`
   - `TableFilterLogic FilterLogic { get; set; }`
   - `ICollection<IFilterAction> Filters { get; set; }`
3. A JSON converter type named `SerializeFilterActionJsonConverter` that is responsible for correctly (de)serializing `SerializeFilterAction` and its `IFilterAction` graph.
4. Enums `TableFilterAction` and `TableFilterLogic` used by `FilterKeyValueAction`.
5. `using System.Text.Json;` and `using System.Text.Json.Serialization;` at the top of the test file (or imported via global usings).

If the converter or enum names differ, adjust the test code to use the concrete names from your project. Also, if you keep the original `Serialize_Ok` test, ensure it remains elsewhere in the file or reintroduce it alongside these new tests.
</issue_to_address>

### Comment 6
<location> `test/UnitTest/Extensions/LambadaExtensionsTest.cs:522-526` </location>
<code_context>
         var invoker1 = LambdaExtensions.GetPropertyValueLambda<Dummy, string>(dummy, "Foo.Name").Compile();
         Assert.Equal("Test1", invoker1(dummy));
+
+        var invoker2 = LambdaExtensions.GetPropertyValueLambda<Dummy, int>(dummy, "Foo.Count").Compile();
+        Assert.Equal(10, invoker2(dummy));
+
+        dummy.Foo = null;
+        Assert.Null(invoker1(dummy));
+
         Assert.Throws<InvalidOperationException>(() => LambdaExtensions.GetPropertyValueLambda<Dummy, string>(dummy, "Foo.Test1"));
</code_context>

<issue_to_address>
**suggestion (testing):** Add coverage for value-type property access when the intermediate target becomes null.

We currently only exercise `invoker2` (`Foo.Count`) when `dummy.Foo` is non-null. Please also add an assertion for `invoker2` after `dummy.Foo = null`, verifying the expected behavior for value-type access (e.g., specific exception vs. default value). This will align coverage between reference- and value-type properties when intermediate members are null.

Suggested implementation:

```csharp
        var invoker2 = LambdaExtensions.GetPropertyValueLambda<Dummy, int>(dummy, "Foo.Count").Compile();
        Assert.Equal(10, invoker2(dummy));

        dummy.Foo = null;
        Assert.Null(invoker1(dummy));
        Assert.Equal(default(int), invoker2(dummy));

        Assert.Throws<InvalidOperationException>(() => LambdaExtensions.GetPropertyValueLambda<Dummy, string>(dummy, "Foo.Test1"));

```

If the intended behavior of `GetPropertyValueLambda` for value-type properties with a null intermediate target is to throw instead of returning the default value, replace `Assert.Equal(default(int), invoker2(dummy));` with an appropriate `Assert.Throws<...>(() => invoker2(dummy));` using the actual exception type.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/BootstrapBlazor/Converter/JsonQueryPageOptionConverter.cs
Comment thread src/BootstrapBlazor/Converter/JsonFilterKeyValueActionConverter.cs
Comment thread test/UnitTest/Extensions/LambadaExtensionsTest.cs
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes QueryPageOptions serialization support by refactoring the JSON serialization approach for filter collections. The changes remove the generic ObjectWithTypeConverter in favor of a specialized JsonFilterKeyValueActionConverter and introduce a new SerializeFilterAction class to handle serialization of filter actions.

Key changes:

  • Replaced generic ObjectWithTypeConverter with specialized JsonFilterKeyValueActionConverter for handling FilterKeyValueAction serialization with type information
  • Added SerializeFilterAction class as a serialization-specific implementation of IFilterAction
  • Modified JsonQueryPageOptionsConverter to properly serialize and deserialize filter collections (Searches, CustomerSearches, AdvanceSearches, Filters)

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/BootstrapBlazor/Converter/ObjectWithTypeConverter.cs Removed generic object converter that was previously handling type-aware serialization
src/BootstrapBlazor/Converter/JsonFilterKeyValueActionConverter.cs New specialized converter for FilterKeyValueAction with custom handling of FieldValue type information
src/BootstrapBlazor/Converter/JsonQueryPageOptionConverter.cs Updated to serialize/deserialize filter collections as arrays instead of using the removed FilterKeyValueAction property
src/BootstrapBlazor/Options/QueryPageOptions.cs Removed JsonIgnore attributes from filter lists and removed internal FilterKeyValueAction property
src/BootstrapBlazor/Extensions/QueryPageOptionsExtensions.cs Removed code checking for internal FilterKeyValueAction property
src/BootstrapBlazor/Components/Filters/FilterKeyValueAction.cs Added JsonConverter attribute to use new specialized converter; removed ObjectWithTypeConverter from FieldValue
src/BootstrapBlazor/Components/Filters/IFilterAction.cs Changed JsonDerivedType from SearchFilterAction to SerializeFilterAction
src/BootstrapBlazor/Components/Filters/SerializeFilterAction.cs New sealed class implementing IFilterAction for serialization purposes
test/UnitTest/Extensions/QueryPageOptionsExtensionsTest.cs Updated test to use SerializeFilterAction and verify proper serialization/deserialization of filter collections
test/UnitTest/Extensions/LambadaExtensionsTest.cs Added additional test cases for nested property access with null handling
src/BootstrapBlazor.Server/Components/Samples/Table/TablesColumnDrag.razor.cs Updated to use Count property instead of Any() for checking if filters exist

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/BootstrapBlazor/Converter/JsonFilterKeyValueActionConverter.cs Outdated
Comment thread src/BootstrapBlazor/Components/Filters/IFilterAction.cs
Comment thread src/BootstrapBlazor/Converter/JsonFilterKeyValueActionConverter.cs
Comment thread src/BootstrapBlazor/Converter/JsonFilterKeyValueActionConverter.cs
Comment thread test/UnitTest/Extensions/QueryPageOptionsExtensionsTest.cs
@ArgoZhang ArgoZhang merged commit dbae1d0 into main Dec 13, 2025
4 checks passed
@ArgoZhang ArgoZhang deleted the bak-op branch December 13, 2025 12:37
@codecov
Copy link
Copy Markdown

codecov Bot commented Dec 13, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (2e5c675) to head (e287546).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
@@             Coverage Diff             @@
##             main     #7323      +/-   ##
===========================================
+ Coverage   99.98%   100.00%   +0.01%     
===========================================
  Files         746       747       +1     
  Lines       32561     32714     +153     
  Branches     4500      4534      +34     
===========================================
+ Hits        32556     32714     +158     
+ Misses          1         0       -1     
+ Partials        4         0       -4     
Flag Coverage Δ
BB 100.00% <100.00%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(Table): support QueryPageOptions serialization

3 participants