#26095·abp

Low-Code: fixed per-mutation overhead — distributed lock + blob manifest read, post-mutation re-read, uncached descriptor hash

Author: mgochmuradovCreated Aug 27, 2026Updated Aug 31, 2026

Description

Every Low-Code create/update/delete carries fixed overhead that is paid even in the common case where none of it is needed:

  1. Distributed lock + blob read per mutation. DynamicEntityMutationGate.ExecuteAsync wraps every mutation and (a) reads the entity-blob-cleanup manifest from the blob container (EntityBlobCleanupManifestStore.GetIntentAsync — a blob read + JSON parse), then (b) acquires IAbpDistributedLock.TryAcquireAsync. With a Redis lock provider and remote blob storage this is 2+ extra network round-trips on every mutation of every dynamic entity — including entities that have no file/image properties at all, which is what the manifest/gate machinery exists for.
  2. Full re-read to build the response. After a create or update, the pipeline re-reads the record via a full SingleEntityQuery (with relation joins and collection hydrators) to build the returned DynamicEntityDto — an extra SELECT (plus hydrator queries) per mutation, where a conventional CrudAppService maps the tracked entity it already has.
  3. Uncached descriptor-version hash per SaveChanges. DynamicEntityStaleModelSaveChangesInterceptor compares model versions on every SaveChanges that touches dynamic entities; computing the current descriptor version (DynamicModelManager.GetEntityDescriptorVersion) serializes the descriptor to JSON and hashes it on every call — the result is deterministic for a given model Version and could be cached until the model changes. (The DB round-trip correctly happens only on version mismatch, and SuppressStaleModelCheck() exists — this point is only about the recomputed hash.)

Configuration

  • ABP 10.7.0-rc.3 (Volo.Abp.LowCode.Application / .EntityFrameworkCore / .Domain), Low-Code module
  • .NET 10, PostgreSQL, app-nolayers modular monolith

Regression?

Not known to be a regression.

Data

Static inspection of the shipped assemblies (decompiled for analysis), not yet profiled. The gate path (manifest read + TryAcquireAsync), the post-mutation SingleEntityQuery re-read, and the per-call serialize+hash in GetEntityDescriptorVersion are directly visible in the mutation pipeline.

Analysis

Suggested directions:

  • Skip the mutation gate (manifest read + distributed lock) entirely for entity types whose descriptor declares no file/image/blob-backed properties — the descriptor is available before the gate runs.
  • Build the response DTO from the tracked entity plus the already-known FK display values where possible, falling back to the re-read only when deferred/computed projections require it.
  • Cache the descriptor-version hash keyed by (entity name, model Version), invalidated by the existing model-changed notification.