abpframework / abpframework/abp
Low-Code: an EntityConfigurations entry whose key matches no descriptor silently creates a phantom CLR-less entity
@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, …), soConfigure(...)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
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.