abpframework / abpframework/abp
Low-Code: projected list pipeline unpacks rows with uncached reflection and repeated dictionary materializations
@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.GetDataAsync → EfCoreDynamicQueryServiceProvider.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:
- Uncached reflection to unpack projection rows. The projected list is materialized via a System.Linq.Dynamic.Core
Selectproducing runtimeDynamicClassrows, which are then unpacked per row × per field withrow.GetType().GetProperty($"Item{i}").GetValue(row)— thePropertyInfolookup is not cached, and the"Item{i}"string is rebuilt for every row/field. There is no compiled-accessor cache anywhere on this path. - Three dictionary materializations per row before serialization:
DynamicClass→DynamicProjectionRow(case-insensitive dictionary) →DynamicProjectionCompatibilityMapper.Map(iterates the row,Split('.')on paths, re-nests FK display values) →DynamicEntityDto(another dictionary), followed by aFilterServerOnlyPropertiespass over each row's dictionary. EntityDescriptor.FindPropertyis 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).- Dynamic LINQ filter/sort/select strings are parsed on every request. (
UseParameterizedNamesInDynamicQuery = trueis set, so the EF/DB plan caches survive — the SQL side is fine; the parse cost itself repeats per request.) - Child-collection hydration is unbounded:
ChildCollectionDtoHydratorissues its batched child query withMaxResultCount = 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-nolayersmodular 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
DynamicEntityDtodirectly. - Give
EntityDescriptoran 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.