Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Changed

- **`ExtractionOptions.BaseTypeNames` is the single source of the list.** 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 with three chances to disagree about what an entity is.
The callers now use the default.

### Fixed

- **`Unknown` now reaches every place the ORM is reported.** The agent files learned it; the HTML
Expand Down Expand Up @@ -40,6 +47,15 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
`OutlookInspiredDemo.NET.EFCore` 23 → 24 — the last of which is an EF Core application, where an
entity that is not registered as a `DbSet<T>` had no fallback either.

- **`PersistentBase` and `XPBaseObject` are recognised as persistent bases.** The XPO hierarchy is
`PersistentBase` → `XPBaseObject` → `XPCustomObject` → `XPObject`, with `XPLiteObject` also under
`XPBaseObject`. The list held the three leaves and neither of the classes above them, so a hole
sat in the middle of a documented API — DevExpress names 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 brings its own key, which is the same population as the legacy schemas the DbSet roster
was added for. `FeatureCenter.NET.XPO` gains `OidGenerator`, `NoKeyPropertyNamedBaseObject` and
`LayoutDemoObject`.

## [0.12.1] — 2026-08-13

Entities the application declares, rather than the ones that inherit from the right class.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ applications. The agent-facing surface is what is landing now, in the open.
| ✅ | Pluggable publishing targets (`IDocumentationSink`) |
| ✅ | **MCP server** — 10 tools, live against your source |
| ✅ | **Installable Claude Code plugin** with skill and MCP server |
| ✅ | **289 tests** over synthetic XPO and EF Core fixtures — no DevExpress needed |
| ✅ | **290 tests** over synthetic XPO and EF Core fixtures — no DevExpress needed |
| ✅ | **DevExpress ground-truth catalog**, generated locally by licensees |

PeopleWorks Copilot, where this tool grew up, is now one sink among several rather than the
Expand Down
1 change: 0 additions & 1 deletion src/XafLogicExplainer.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1838,7 +1838,6 @@ static ExtractionOptions BuildExtractionOptions(string language, string? orm = n
{
var options = new ExtractionOptions
{
BaseTypeNames = ["XPCustomObject", "BaseObject", "XPObject", "XPLiteObject", "PermissionPolicyUser"],
IncludeSourceCode = true,
IncludeMethodBodies = true,
IncludeComments = true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ public async Task<SyncResult> SyncAsync(SyncConfiguration config, ExtractionOpti
onProgress?.Invoke("Extracting project logic...");
extractionOptions ??= new ExtractionOptions
{
BaseTypeNames = ["XPCustomObject", "BaseObject", "XPObject", "XPLiteObject", "PermissionPolicyUser"],
IncludeSourceCode = true,
IncludeMethodBodies = true,
IncludeComments = true,
Expand Down
21 changes: 19 additions & 2 deletions src/XafLogicExplainer.Core/Models/ExtractionOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,28 @@ public class ExtractionOptions
/// <summary>
/// Known business object base types used to classify entity classes.
/// </summary>
/// <remarks>
/// The XPO names are the whole documented hierarchy —
/// <c>PersistentBase</c> → <c>XPBaseObject</c> → <c>XPCustomObject</c> → <c>XPObject</c>, with
/// <c>XPLiteObject</c> also under <c>XPBaseObject</c> — because DevExpress names all five as
/// bases a persistent class may derive from, and recommends <c>PersistentBase</c>. Listing the
/// leaves and not the two classes above them leaves a hole in the middle of an API that is
/// used: mapping onto a table that brings its own key is exactly when the higher bases are the
/// right choice.
/// <para>
/// Callers are expected to use this list rather than pass their own. It was previously
/// duplicated in the CLI, the MCP server and the test harness, which is three chances for the
/// four copies to disagree about what an entity is.
/// </para>
/// </remarks>
public string[] BaseTypeNames { get; set; } = [
"PersistentBase",
"XPBaseObject",
"XPCustomObject",
"BaseObject",
"XPObject",
"XPLiteObject"
"XPLiteObject",
"BaseObject",
"PermissionPolicyUser",
];

/// <summary>
Expand Down
1 change: 0 additions & 1 deletion src/XafLogicExplainer.Mcp/XafProjectContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,6 @@ private static ExtractionOptions BuildOptions(XafProjectSource source)
{
var options = new ExtractionOptions
{
BaseTypeNames = ["XPCustomObject", "BaseObject", "XPObject", "XPLiteObject", "PermissionPolicyUser"],
IncludeSourceCode = true,
IncludeMethodBodies = true,
IncludeComments = true,
Expand Down
2 changes: 1 addition & 1 deletion tests/XafLogicExplainer.Tests/ExtractionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public void FindsEveryEntity()
{
var names = SampleProjects.Xpo.Entities.Select(e => e.ClassName).OrderBy(n => n).ToList();

Assert.Equal(["Customer", "Order", "OrderLine"], names);
Assert.Equal(["AuditEntry", "Customer", "Order", "OrderLine", "SequenceCounter"], names);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System;
using System.ComponentModel;
using DevExpress.Persistent.Base;
using DevExpress.Xpo;

namespace SampleApp.Module.BusinessObjects;

/// <summary>
/// A row written when something changes. Derived from <c>XPBaseObject</c>, which carries no key
/// of its own, so the class supplies one.
/// </summary>
/// <remarks>
/// <c>XPBaseObject</c> is the ancestor of <c>XPCustomObject</c> and a documented base for
/// persistent classes in its own right — mapping onto a table that already has its own key is
/// exactly when it is the right choice.
/// </remarks>
[DefaultClassOptions]
[NavigationItem("Audit")]
public class AuditEntry : XPBaseObject
{
public AuditEntry(Session session) : base(session) { }

[Key(AutoGenerate = true), Browsable(false)]
public int Oid { get; set; }

[Size(80)]
public string ChangedBy { get; set; }

public DateTime ChangedOn { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.ComponentModel;
using DevExpress.Persistent.Base;
using DevExpress.Xpo;

namespace SampleApp.Module.BusinessObjects;

/// <summary>
/// The next number to hand out for a document type. Derived from <c>PersistentBase</c>, the root
/// of the XPO hierarchy and the class DevExpress recommends as a base for persistent classes.
/// </summary>
[DefaultClassOptions]
public class SequenceCounter : PersistentBase
{
public SequenceCounter(Session session) : base(session) { }

[Key, Size(60)]
public string TypeName { get; set; }

public long NextNumber { get; set; }
}
2 changes: 1 addition & 1 deletion tests/XafLogicExplainer.Tests/HtmlExplainerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ public void GraphPlacesEveryEntity()
{
var graph = EntityGraph.Build(SampleProjects.Xpo);

Assert.Equal(3, graph.Nodes.Count);
Assert.Equal(5, graph.Nodes.Count);
Assert.All(graph.Nodes, n => Assert.InRange(n.X, 0, graph.Width));
Assert.All(graph.Nodes, n => Assert.InRange(n.Y, 0, graph.Height));
}
Expand Down
2 changes: 1 addition & 1 deletion tests/XafLogicExplainer.Tests/McpToolTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ public async Task AbsentEntityReturnsTheCompleteInventoryAndSaysItDoesNotExist()
var result = await Detail.EntityAsync("PurchaseOrder", cancellationToken: TestContext.Current.CancellationToken);

Assert.Contains("There is no entity called 'PurchaseOrder'", result);
Assert.Contains("complete list of 3 entities", result);
Assert.Contains("complete list of 5 entities", result);
Assert.Contains("Customer", result);
Assert.Contains("has not been created yet", result);
}
Expand Down
13 changes: 13 additions & 0 deletions tests/XafLogicExplainer.Tests/OrmDetectionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,19 @@ public class OrmDetectionTests
public void DetectsXpo() =>
Assert.Equal("XPO", SampleProjects.Xpo.OrmType, ignoreCase: true);

[Fact]
public void FindsEntitiesOnEveryDocumentedXpoBase()
{
// The hierarchy is PersistentBase -> XPBaseObject -> XPCustomObject -> XPObject, with
// XPLiteObject also under XPBaseObject. Recognising the leaves and not the two classes
// above them leaves a hole in the middle of a documented API: DevExpress names all five
// as bases you may derive persistent classes from, and recommends PersistentBase.
var names = SampleProjects.Xpo.Entities.Select(entity => entity.ClassName);

Assert.Contains("AuditEntry", names); // : XPBaseObject
Assert.Contains("SequenceCounter", names); // : PersistentBase
}

[Fact]
public void DetectsEfCoreFromItsNamespace() =>
Assert.Contains("EF", SampleProjects.EfCore.OrmType, StringComparison.OrdinalIgnoreCase);
Expand Down
1 change: 0 additions & 1 deletion tests/XafLogicExplainer.Tests/SampleProjects.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,6 @@ private static ExtractedProject Extract(string path)

var options = new ExtractionOptions
{
BaseTypeNames = ["XPCustomObject", "BaseObject", "XPObject", "XPLiteObject", "PermissionPolicyUser"],
IncludeSourceCode = true,
IncludeMethodBodies = true,
IncludeComments = true,
Expand Down
Loading