RadzenDataGrid overuses null propagation in LoadDataArgs for complex objects #1329

Closed
opened 2026-01-29 17:52:09 +00:00 by claunia · 0 comments
Owner

Originally created by @kerajel on GitHub (Aug 4, 2024).

Describe the bug
LoadDataArgs produces redundant null propagation for child properties of complex objects leading to subpar queries

To Reproduce

Consider 2 domain models and a view model to account for aggregation necessities:

public class Order
{
    public int Id { get; set; }
    public string Name { get; set; }

    [InverseProperty("Order")]
    public ICollection<OrderDetail> OrderDetails { get; set; }
}

public class OrderDetail
{
    public int Id { get; set; }

    public string Code { get; set; }

    [ForeignKey("OrderId")]
    public int OrderId { get; set; }

    [InverseProperty("OrderDetails")]
    public Order Order { get; set; }
}

    private class OrderView
    {
        public Order Order { get; set; }
        public int DetailCount { get; set; }
        public string DetailCodes { get; set; } = string.Empty;
    }

Wire up a grid:

<RadzenDataGrid LoadData="LoadOrderViews" TItem="OrderView" FilterMode="FilterMode.Advanced" AllowFiltering="true" AllowSorting="true">
    <Columns>
        <RadzenDataGridColumn Property="Order.Id" Title="Order Id" />
        <RadzenDataGridColumn Property="DetailCount" Title="Detail Count" />
        <RadzenDataGridColumn Property="DetailCodes" Title="Detail Codes" />
    </Columns>
</RadzenDataGrid>

Try to filter on Order.Id and observe the following value of LoadDataArgs.Filter:

'(np(Order.Id)) = 3'

The grid issued 'np' for order id even though it's a primary key of order and can never be null and OrderView.Order is a non nullable property.

This results into a very inefficient query:
(the example uses linq2db, but the provider does not matter)

    async Task LoadOrderViews(LoadDataArgs args)
    {
        var detailCte = dbContext.OrderDetail.GroupBy(x => x.OrderId)
            .Select(x => new
            {
                OrderId = x.Key,
                Count = x.Count(),
                Codes = x.StringAggregate(",", r => r.Code).OrderBy(r => r.Code).ToValue(),
            }).AsCte();

        var query = from order in dbContext.Order
                    from cte in detailCte.LeftJoin(x => x.OrderId == order.Id).DefaultIfEmpty()
                    select new OrderView
                        {
                            Order = order,
                            DetailCount = cte.Count,
                            DetailCodes = cte.Codes,
                        };

        if (!string.IsNullOrEmpty(args.Filter))
        {
            query = query.Where(args.Filter);
        }
        orderViews = await query.ToArrayAsyncLinqToDB();
    }

Resulting SQL:

WITH [CTE_1] ([OrderId], [Count], [Codes])
AS
(
        SELECT
                [t1].[OrderId],
                Count(*),
                STRING_AGG([t1].[Code], N',') WITHIN GROUP (ORDER BY [t1].[Code])
        FROM
                [OrderDetail] [t1]
        GROUP BY
                [t1].[OrderId]
)
SELECT
        [order_1].[Id],
        [order_1].[Name],
        [x].[Count],
        [x].[Codes]
FROM
        [Order] [order_1]
                LEFT JOIN [CTE_1] [x] ON [x].[OrderId] = [order_1].[Id]
WHERE
        IIF(([order_1].[Id] IS NOT NULL OR [x].[Count] IS NOT NULL OR [x].[Codes] IS NOT NULL) AND [order_1].[Id] IS NOT NULL, [order_1].[Id], NULL) = 3

Notice the complete mess in the WHERE clause. Not only it looks horrible, it no longer can use index seek over Order.Id

If we strip the 'np' from the filter (Order.Id= 3) then we get fine SQL:

WITH [CTE_1] ([OrderId], [Count], [Codes])
AS
(
        SELECT
                [t1].[OrderId],
                Count(*),
                STRING_AGG([t1].[Code], N',') WITHIN GROUP (ORDER BY [t1].[Code])
        FROM
                [OrderDetail] [t1]
        GROUP BY
                [t1].[OrderId]
)
SELECT
        [order_1].[Id],
        [order_1].[Name],
        [x].[Count],
        [x].[Codes]
FROM
        [Order] [order_1]
                LEFT JOIN [CTE_1] [x] ON [x].[OrderId] = [order_1].[Id]
WHERE
        [order_1].[Id] = 3

Proposed solution:
If a child entity of a complex object is not marked as nullable then do not use null propagation on this child entity

Desktop (please complete the following information):

  • OS: Windows 11
  • Version: 127.0.6533.88/89
  • Blazor Version: 5.0.7
Originally created by @kerajel on GitHub (Aug 4, 2024). **Describe the bug** LoadDataArgs produces redundant null propagation for child properties of complex objects leading to subpar queries **To Reproduce** Consider 2 domain models and a view model to account for aggregation necessities: ``` public class Order { public int Id { get; set; } public string Name { get; set; } [InverseProperty("Order")] public ICollection<OrderDetail> OrderDetails { get; set; } } public class OrderDetail { public int Id { get; set; } public string Code { get; set; } [ForeignKey("OrderId")] public int OrderId { get; set; } [InverseProperty("OrderDetails")] public Order Order { get; set; } } private class OrderView { public Order Order { get; set; } public int DetailCount { get; set; } public string DetailCodes { get; set; } = string.Empty; } ``` Wire up a grid: ``` <RadzenDataGrid LoadData="LoadOrderViews" TItem="OrderView" FilterMode="FilterMode.Advanced" AllowFiltering="true" AllowSorting="true"> <Columns> <RadzenDataGridColumn Property="Order.Id" Title="Order Id" /> <RadzenDataGridColumn Property="DetailCount" Title="Detail Count" /> <RadzenDataGridColumn Property="DetailCodes" Title="Detail Codes" /> </Columns> </RadzenDataGrid> ``` Try to filter on Order.Id and observe the following value of LoadDataArgs.Filter: ``` '(np(Order.Id)) = 3' ``` The grid issued 'np' for order id even though it's a primary key of order and can never be null and OrderView.Order is a non nullable property. This results into a very inefficient query: (the example uses linq2db, but the provider does not matter) ``` async Task LoadOrderViews(LoadDataArgs args) { var detailCte = dbContext.OrderDetail.GroupBy(x => x.OrderId) .Select(x => new { OrderId = x.Key, Count = x.Count(), Codes = x.StringAggregate(",", r => r.Code).OrderBy(r => r.Code).ToValue(), }).AsCte(); var query = from order in dbContext.Order from cte in detailCte.LeftJoin(x => x.OrderId == order.Id).DefaultIfEmpty() select new OrderView { Order = order, DetailCount = cte.Count, DetailCodes = cte.Codes, }; if (!string.IsNullOrEmpty(args.Filter)) { query = query.Where(args.Filter); } orderViews = await query.ToArrayAsyncLinqToDB(); } ``` Resulting SQL: ``` WITH [CTE_1] ([OrderId], [Count], [Codes]) AS ( SELECT [t1].[OrderId], Count(*), STRING_AGG([t1].[Code], N',') WITHIN GROUP (ORDER BY [t1].[Code]) FROM [OrderDetail] [t1] GROUP BY [t1].[OrderId] ) SELECT [order_1].[Id], [order_1].[Name], [x].[Count], [x].[Codes] FROM [Order] [order_1] LEFT JOIN [CTE_1] [x] ON [x].[OrderId] = [order_1].[Id] WHERE IIF(([order_1].[Id] IS NOT NULL OR [x].[Count] IS NOT NULL OR [x].[Codes] IS NOT NULL) AND [order_1].[Id] IS NOT NULL, [order_1].[Id], NULL) = 3 ``` Notice the complete mess in the WHERE clause. Not only it looks horrible, it no longer can use index seek over Order.Id If we strip the 'np' from the filter (Order.Id= 3) then we get fine SQL: ``` WITH [CTE_1] ([OrderId], [Count], [Codes]) AS ( SELECT [t1].[OrderId], Count(*), STRING_AGG([t1].[Code], N',') WITHIN GROUP (ORDER BY [t1].[Code]) FROM [OrderDetail] [t1] GROUP BY [t1].[OrderId] ) SELECT [order_1].[Id], [order_1].[Name], [x].[Count], [x].[Codes] FROM [Order] [order_1] LEFT JOIN [CTE_1] [x] ON [x].[OrderId] = [order_1].[Id] WHERE [order_1].[Id] = 3 ``` Proposed solution: If a child entity of a complex object is not marked as nullable then do not use null propagation on this child entity **Desktop (please complete the following information):** - OS: Windows 11 - Version: 127.0.6533.88/89 - Blazor Version: 5.0.7
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/radzen-blazor#1329