abpframework / abpframework/abp

Low-Code: an EntityConfigurations entry whose key matches no descriptor silently creates a phantom CLR-less entity

Open
#26,097 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

AbpDynamicEntityConfig.EntityConfigurations is keyed by entity descriptor name. When an entry's key matches no existing descriptor, FluentDynamicModelLayer does not fail, warn, or ignore it — it synthesizes a brand-new EntityDescriptor under that key with no CLR type. That phantom is then treated as a runtime-only dynamic entity: it is materialized by ConfigureDynamicEntities() as a shared-type DynamicEntity table and shows up as a spurious table in the next migration.

The relevant part of FluentDynamicModelLayer.GetAllEntityDescriptors() (deobfuscated):

var lower = LowerLayer?.GetAllEntityDescriptors() ?? Array.Empty<EntityDescriptor>();

// 1. configured entries that DO match a lower-layer descriptor: applied to a clone — correct
foreach (var descriptor in lower) {
    var clone = descriptor.CreateClone();
    AbpDynamicEntityConfig.EntityConfigurations.TryGetValue(clone.Name, out var configure);
    configure?.Invoke(clone);
    list.Add(clone);
}

// 2. configured entries that match NOTHING: a new, CLR-less descriptor is invented
var orphans = AbpDynamicEntityConfig.EntityConfigurations
    .Where(entry => lower.All(e => e.Name != entry.Key))          // ordinal comparison
    .Select(entry => { var d = new EntityDescriptor(entry.Key); entry.Value?.Invoke(d); return d; });
list.AddRange(orphans);

The failure is silent and the symptom is far from the cause: the developer sees their C#-defined [DynamicEntity] behaving as though it had been dropped from their module's EF model, plus an unexpected table appearing in a migration. Nothing points at the configuration key.

The correct usage — the generic overload, which keys by typeof(T).FullName — works perfectly, so the whole failure mode hinges on a string that nothing validates.

Reproduction Steps

Given a C#-defined low-code entity Ak.Bss.Customer.RegulatoryRule:

Correct — descriptor keeps its CLR binding and the rule is applied:

AbpDynamicEntityConfig.EntityConfigurations.Configure<RegulatoryRule>(
    e => e.AddCrossFieldValidation("ValidTo", "ValidFrom", "greaterThanOrEqual"));
Ak.Bss.Customer.RegulatoryRule | layer=Code | clr=Ak.Bss.Customer.RegulatoryRule | entityType=RegulatoryRule
CrossFieldValidations: 1

Typo / short name — no error anywhere, and a phantom appears:

AbpDynamicEntityConfig.EntityConfigurations.Configure(
    "RegulatoryRule",                                  // not the descriptor name
    e => e.AddCrossFieldValidation("ValidTo", "ValidFrom", "greaterThanOrEqual"));
Ak.Bss.Customer.RegulatoryRule | layer=Code   | clr=Ak.Bss.Customer.RegulatoryRule | entityType=RegulatoryRule
RegulatoryRule                 | layer=Fluent | clr=NULL                           | entityType=DynamicEntity

The real descriptor's CrossFieldValidations is null — the rule went to the phantom — and the phantom becomes a DynamicEntity table named RegulatoryRule.

Both cases dumped via DynamicModelManager.Instance.GetAll() after InitializeAsync().

Expected behavior

One of:

  • throw at model-initialization time for a configuration key that matches no known descriptor (my preference — this is a programming error, and the config is authored in C# where a typo has no other way of being caught); or
  • log a warning and ignore the entry; or
  • if inventing a descriptor really is intended (declaring a new runtime-only entity from the fluent layer), make that explicit through a distinct API such as EntityConfigurations.Define(name, …), so Configure(...) can stay strictly "configure something that exists".
Actual behavior

Silent creation of a CLR-less entity, a lost configuration, and an unexpected table in the next migration. No warning is logged.

Regression?

No — same behavior in 10.6.0 and 10.7.0-rc.3.

Known Workarounds

Always use Configure<TEntity>() rather than the string overload. As a guard, a test can assert the model contains no CLR-less descriptors:

DynamicModelManager.Instance.GetAll()
    .Where(d => d.ClassType == null)
    .ShouldBeEmpty();
Configuration
  • ABP 10.7.0-rc.3 (also verified on 10.6.0)
  • .NET 10
  • Template: app-nolayers, PostgreSQL / EF Core

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.