abpframework / abpframework/abp

Low-Code: projected list pipeline unpacks rows with uncached reflection and repeated dictionary materializations

Open
#26,090 0 comments 0 reactions 1 assignee View on GitHub

@salihozkara is already working on this.

Since Aug 31, 2026.

Dominant language
C#
Stars
14.4k
Forks
3.7k
Avg merge
15h 32m
Merged PRs (30d)
106

Description

Description

The generated Low-Code dataGrid list endpoint (DynamicPageController.GetDataAsyncEfCoreDynamicQueryServiceProvider.GetProjectedListAsync) does a significant amount of per-row, per-field work on the hot path that scales with page size × column count on every request:

  1. Uncached reflection to unpack projection rows. The projected list is materialized via a System.Linq.Dynamic.Core Select producing runtime DynamicClass rows, which are then unpacked per row × per field with row.GetType().GetProperty($"Item{i}").GetValue(row) — the PropertyInfo lookup is not cached, and the "Item{i}" string is rebuilt for every row/field. There is no compiled-accessor cache anywhere on this path.
  2. Three dictionary materializations per row before serialization: DynamicClassDynamicProjectionRow (case-insensitive dictionary) → DynamicProjectionCompatibilityMapper.Map (iterates the row, Split('.') on paths, re-nests FK display values) → DynamicEntityDto (another dictionary), followed by a FilterServerOnlyProperties pass over each row's dictionary.
  3. EntityDescriptor.FindProperty is a linear scan (Properties.FirstOrDefault(...) with a closure allocation) and is called repeatedly per row and per filter during mapping/translation — O(rows × fields × propertyCount).
  4. Dynamic LINQ filter/sort/select strings are parsed on every request. (UseParameterizedNamesInDynamicQuery = true is set, so the EF/DB plan caches survive — the SQL side is fine; the parse cost itself repeats per request.)
  5. Child-collection hydration is unbounded: ChildCollectionDtoHydrator issues its batched child query with MaxResultCount = int.MaxValue, so a list page over parents loads all their children regardless of what the page shows.

For comparison, a hand-written CrudAppService on the same data does one compiled-C# filtered query plus one compiled (Mapperly) entity→DTO map per row.

Configuration
  • ABP 10.7.0-rc.3 (Volo.Abp.LowCode.Application / Volo.Abp.LowCode.EntityFrameworkCore), Low-Code module
  • .NET 10, PostgreSQL (Npgsql), app-nolayers modular monolith, ARM64 macOS (dev)
Regression?

Not known to be a regression — observed while evaluating the Low-Code module (preview) for a telecom BSS back-office.

Data

Findings come from static inspection of the shipped assemblies (decompiled for analysis while evaluating the module), not from profiling — no benchmark numbers yet. The reflection unpack, the triple materialization and the int.MaxValue hydration are directly visible in EfCoreDynamicQueryServiceProvider, DynamicProjectionCompatibilityMapper, DynamicProjectionRow and ChildCollectionDtoHydrator.

Analysis

Suggested directions:

  • Cache a compiled accessor array per projection plan (the plan is already computed per request; keying a delegate cache by entity-descriptor version + selected paths would let rows be unpacked without reflection).
  • Collapse the three per-row dictionary materializations into one pass that writes the final DynamicEntityDto directly.
  • Give EntityDescriptor an O(1) name→property lookup.
  • Cache parsed Dynamic LINQ artifacts keyed by (descriptor version, filter/sort signature).
  • Cap or page child-collection hydration instead of MaxResultCount = int.MaxValue.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.