Skip to content

Commit 6bf44a1

Browse files
Brekhofclaude
authored andcommitted
Recognise PersistentBase and XPBaseObject as persistent bases
The XPO hierarchy is PersistentBase -> XPBaseObject -> XPCustomObject -> XPObject, with XPLiteObject also under XPBaseObject. BaseTypeNames held the three leaves and neither of the classes above them, so the hole sat in the middle of an API that is used rather than at its edge: DevExpress documents all five as bases a persistent class may derive from, and recommends PersistentBase. Deriving from the higher bases is what you do when the table already brings its own key -- the same population of legacy schemas the DbSet roster was added for, which is why this turned up beside it. FeatureCenter.NET.XPO gains OidGenerator, NoKeyPropertyNamedBaseObject and LayoutDemoObject, all of them ": XPBaseObject". Nothing changes for MainDemo in either ORM, for OutlookInspired, or for a private 221-entity EF Core application. No demo derives from PersistentBase directly, so that half is covered by the fixture rather than by a corpus. The list also stops being duplicated. The CLI, the MCP server and the test harness each passed their own copy, so the default in Core was four names while every caller passed five -- four copies and three chances for them to disagree about what an entity is. They now use the default. Fixes peopleworks#6 in part; the transitive walk in peopleworks#9 is the rest. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 935c194 commit 6bf44a1

11 files changed

Lines changed: 101 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
## [Unreleased]
99

10+
### Changed
11+
12+
- **`ExtractionOptions.BaseTypeNames` is the single source of the list.** The CLI, the MCP server
13+
and the test harness each passed their own copy, so the default in `Core` was four names while
14+
every caller passed five — four copies with three chances to disagree about what an entity is.
15+
The callers now use the default.
16+
1017
### Fixed
1118

1219
- **`Unknown` now reaches every place the ORM is reported.** The agent files learned it; the HTML
@@ -28,6 +35,15 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
2835
omitted rather than guessed. Reading text also counted a *mention*: a comment naming the
2936
namespace was enough, which is how the fixture for this fix first passed against the old code.
3037

38+
- **`PersistentBase` and `XPBaseObject` are recognised as persistent bases.** The XPO hierarchy is
39+
`PersistentBase``XPBaseObject``XPCustomObject``XPObject`, with `XPLiteObject` also under
40+
`XPBaseObject`. The list held the three leaves and neither of the classes above them, so a hole
41+
sat in the middle of a documented API — DevExpress names all five as bases a persistent class may
42+
derive from, and recommends `PersistentBase`. Deriving from the higher bases is what you do when
43+
the table brings its own key, which is the same population as the legacy schemas the DbSet roster
44+
was added for. `FeatureCenter.NET.XPO` gains `OidGenerator`, `NoKeyPropertyNamedBaseObject` and
45+
`LayoutDemoObject`.
46+
3147
## [0.12.1] — 2026-08-13
3248

3349
Entities the application declares, rather than the ones that inherit from the right class.

src/XafLogicExplainer.Cli/Program.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1838,7 +1838,6 @@ static ExtractionOptions BuildExtractionOptions(string language, string? orm = n
18381838
{
18391839
var options = new ExtractionOptions
18401840
{
1841-
BaseTypeNames = ["XPCustomObject", "BaseObject", "XPObject", "XPLiteObject", "PermissionPolicyUser"],
18421841
IncludeSourceCode = true,
18431842
IncludeMethodBodies = true,
18441843
IncludeComments = true,

src/XafLogicExplainer.Core/Models/ExtractionOptions.cs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,28 @@ public class ExtractionOptions
6464
/// <summary>
6565
/// Known business object base types used to classify entity classes.
6666
/// </summary>
67+
/// <remarks>
68+
/// The XPO names are the whole documented hierarchy —
69+
/// <c>PersistentBase</c> → <c>XPBaseObject</c> → <c>XPCustomObject</c> → <c>XPObject</c>, with
70+
/// <c>XPLiteObject</c> also under <c>XPBaseObject</c> — because DevExpress names all five as
71+
/// bases a persistent class may derive from, and recommends <c>PersistentBase</c>. Listing the
72+
/// leaves and not the two classes above them leaves a hole in the middle of an API that is
73+
/// used: mapping onto a table that brings its own key is exactly when the higher bases are the
74+
/// right choice.
75+
/// <para>
76+
/// Callers are expected to use this list rather than pass their own. It was previously
77+
/// duplicated in the CLI, the MCP server and the test harness, which is three chances for the
78+
/// four copies to disagree about what an entity is.
79+
/// </para>
80+
/// </remarks>
6781
public string[] BaseTypeNames { get; set; } = [
82+
"PersistentBase",
83+
"XPBaseObject",
6884
"XPCustomObject",
69-
"BaseObject",
7085
"XPObject",
71-
"XPLiteObject"
86+
"XPLiteObject",
87+
"BaseObject",
88+
"PermissionPolicyUser",
7289
];
7390

7491
/// <summary>

src/XafLogicExplainer.Mcp/XafProjectContext.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,6 @@ private static ExtractionOptions BuildOptions(XafProjectSource source)
159159
{
160160
var options = new ExtractionOptions
161161
{
162-
BaseTypeNames = ["XPCustomObject", "BaseObject", "XPObject", "XPLiteObject", "PermissionPolicyUser"],
163162
IncludeSourceCode = true,
164163
IncludeMethodBodies = true,
165164
IncludeComments = true,

tests/XafLogicExplainer.Tests/ExtractionTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public void FindsEveryEntity()
1212
{
1313
var names = SampleProjects.Xpo.Entities.Select(e => e.ClassName).OrderBy(n => n).ToList();
1414

15-
Assert.Equal(["Customer", "Order", "OrderLine"], names);
15+
Assert.Equal(["AuditEntry", "Customer", "Order", "OrderLine", "SequenceCounter"], names);
1616
}
1717

1818
[Fact]
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using System;
2+
using System.ComponentModel;
3+
using DevExpress.Persistent.Base;
4+
using DevExpress.Xpo;
5+
6+
namespace SampleApp.Module.BusinessObjects;
7+
8+
/// <summary>
9+
/// A row written when something changes. Derived from <c>XPBaseObject</c>, which carries no key
10+
/// of its own, so the class supplies one.
11+
/// </summary>
12+
/// <remarks>
13+
/// <c>XPBaseObject</c> is the ancestor of <c>XPCustomObject</c> and a documented base for
14+
/// persistent classes in its own right — mapping onto a table that already has its own key is
15+
/// exactly when it is the right choice.
16+
/// </remarks>
17+
[DefaultClassOptions]
18+
[NavigationItem("Audit")]
19+
public class AuditEntry : XPBaseObject
20+
{
21+
public AuditEntry(Session session) : base(session) { }
22+
23+
[Key(AutoGenerate = true), Browsable(false)]
24+
public int Oid { get; set; }
25+
26+
[Size(80)]
27+
public string ChangedBy { get; set; }
28+
29+
public DateTime ChangedOn { get; set; }
30+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using System.ComponentModel;
2+
using DevExpress.Persistent.Base;
3+
using DevExpress.Xpo;
4+
5+
namespace SampleApp.Module.BusinessObjects;
6+
7+
/// <summary>
8+
/// The next number to hand out for a document type. Derived from <c>PersistentBase</c>, the root
9+
/// of the XPO hierarchy and the class DevExpress recommends as a base for persistent classes.
10+
/// </summary>
11+
[DefaultClassOptions]
12+
public class SequenceCounter : PersistentBase
13+
{
14+
public SequenceCounter(Session session) : base(session) { }
15+
16+
[Key, Size(60)]
17+
public string TypeName { get; set; }
18+
19+
public long NextNumber { get; set; }
20+
}

tests/XafLogicExplainer.Tests/HtmlExplainerTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ public void GraphPlacesEveryEntity()
123123
{
124124
var graph = EntityGraph.Build(SampleProjects.Xpo);
125125

126-
Assert.Equal(3, graph.Nodes.Count);
126+
Assert.Equal(5, graph.Nodes.Count);
127127
Assert.All(graph.Nodes, n => Assert.InRange(n.X, 0, graph.Width));
128128
Assert.All(graph.Nodes, n => Assert.InRange(n.Y, 0, graph.Height));
129129
}

tests/XafLogicExplainer.Tests/McpToolTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ public async Task AbsentEntityReturnsTheCompleteInventoryAndSaysItDoesNotExist()
105105
var result = await Detail.EntityAsync("PurchaseOrder", cancellationToken: TestContext.Current.CancellationToken);
106106

107107
Assert.Contains("There is no entity called 'PurchaseOrder'", result);
108-
Assert.Contains("complete list of 3 entities", result);
108+
Assert.Contains("complete list of 5 entities", result);
109109
Assert.Contains("Customer", result);
110110
Assert.Contains("has not been created yet", result);
111111
}

tests/XafLogicExplainer.Tests/OrmDetectionTests.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,19 @@ public class OrmDetectionTests
1414
public void DetectsXpo() =>
1515
Assert.Equal("XPO", SampleProjects.Xpo.OrmType, ignoreCase: true);
1616

17+
[Fact]
18+
public void FindsEntitiesOnEveryDocumentedXpoBase()
19+
{
20+
// The hierarchy is PersistentBase -> XPBaseObject -> XPCustomObject -> XPObject, with
21+
// XPLiteObject also under XPBaseObject. Recognising the leaves and not the two classes
22+
// above them leaves a hole in the middle of a documented API: DevExpress names all five
23+
// as bases you may derive persistent classes from, and recommends PersistentBase.
24+
var names = SampleProjects.Xpo.Entities.Select(entity => entity.ClassName);
25+
26+
Assert.Contains("AuditEntry", names); // : XPBaseObject
27+
Assert.Contains("SequenceCounter", names); // : PersistentBase
28+
}
29+
1730
[Fact]
1831
public void DetectsEfCoreFromItsNamespace() =>
1932
Assert.Contains("EF", SampleProjects.EfCore.OrmType, StringComparison.OrdinalIgnoreCase);

0 commit comments

Comments
 (0)