Skip to content

Commit fbfd59a

Browse files
peopleworksclaude
andcommitted
Add a demo application, and record the domain map
The map is the thing worth showing, and the existing fixture has three entities: enough to test the layout, nothing to look at. This adds a fourteen-entity dispensing pharmacy -- a shape XAF developers recognise, with prescriptions, stock batches, insurance and payments -- so the diagram reads as a real application rather than as Foo and Bar. It belongs to nobody: publishing a client's schema to demonstrate a diagram is a poor trade. Recording it exposed a real weakness in the layout. The walk was depth-first, so it left a hub after its first spoke and never came back; a sale's lines and payments ended up halfway around the circle with their edges crossing everything between. Breadth-first places a node and then all of its neighbours, which keeps hubs and spokes together -- and hub-and-spoke is the shape most XAF domain models have. The difference is visible at a glance. The GIF is assembled from deterministic frames rather than filmed: hover each entity, screenshot the figure, move on. Even timing, small file, and it can be re-recorded when the diagram changes. Quantised against one shared palette, because per-frame palettes make a flat dark background shimmer. The sample's updater also carries version-gated migration blocks, which is how XAF teams actually write them and which the extractor does not yet read. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 51b9b68 commit fbfd59a

22 files changed

Lines changed: 811 additions & 12 deletions

File tree

README.md

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -166,10 +166,23 @@ attachment on a machine with no internet, which is how handovers actually happen
166166

167167
It draws **a map of your domain model** from the association attributes scattered across your
168168
codebase. Most teams have never seen theirs: it exists in one person's head, which is exactly the
169-
knowledge that leaves when they do. Alongside it: every entity and what each property is, every
170-
action with the code it runs, validation with the message the user will actually see, the Model
171-
Editor settings that appear in no C# file, and **an index of every criteria expression in the
172-
application** — a dialect that is neither SQL nor C# and is otherwise collected nowhere.
169+
knowledge that leaves when they do.
170+
171+
![The domain model of a sample XAF application. Hovering an entity dims everything it does not touch, leaving only its own relationships lit — purple where deleting the parent deletes the child.](https://raw.githubusercontent.com/peopleworks/XAFLogicExplainer/main/docs/assets/domain-map.gif)
172+
173+
<sub>Real output, from the sample application in this repository. Hover an entity and everything it
174+
does not touch fades; purple means deleting the parent deletes the child.</sub>
175+
176+
Alongside it: every entity and what each property is, every action with the code it runs,
177+
validation with the message the user will actually see, the Model Editor settings that appear in no
178+
C# file, and **an index of every criteria expression in the application** — a dialect that is
179+
neither SQL nor C# and is otherwise collected nowhere.
180+
181+
Try it on the sample without touching your own code:
182+
183+
```bash
184+
xaflogic explain --project tests/XafLogicExplainer.Tests/Fixtures/DemoSolution/PharmacyDemo.Module --open
185+
```
173186

174187
## Optional: tell your code apart from DevExpress's
175188

docs/assets/domain-map.gif

265 KB
Loading

site/index.html

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,7 @@
591591
<a href="#gap" class="nav__hideable">The gap</a>
592592
<a href="#output" class="nav__hideable">Output</a>
593593
<a href="#mcp" class="nav__hideable">MCP</a>
594+
<a href="#explainer" class="nav__hideable">Explainer</a>
594595
<a href="#install" class="is-cta">Install</a>
595596
</nav>
596597

@@ -974,6 +975,40 @@ <h2>Or let the agent<br>ask questions directly.</h2>
974975
</p>
975976
</section>
976977

978+
<!-- ------------------------------------------------------- explainer -->
979+
<section id="explainer" class="reveal">
980+
<p class="eyebrow">And for the person who inherits it</p>
981+
<h2>Most teams have never seen<br>their own domain model.</h2>
982+
<p>
983+
It exists as association attributes scattered across twenty files, and the map of it lives in
984+
one person's head — which is exactly the knowledge that leaves when they do.
985+
</p>
986+
987+
<figure class="figure">
988+
<div class="fig-frame">
989+
<img src="https://raw.githubusercontent.com/peopleworks/XAFLogicExplainer/main/docs/assets/domain-map.gif"
990+
alt="The domain model of a sample XAF application. Hovering an entity dims everything it does not touch, leaving only its own relationships lit."
991+
loading="lazy" style="display:block;width:100%;height:auto">
992+
</div>
993+
<figcaption>
994+
Real output. Hover an entity and everything it does not touch fades; purple means deleting
995+
the parent deletes the child.
996+
</figcaption>
997+
</figure>
998+
999+
<p style="margin-top:2.4rem">
1000+
<code>xaflogic explain</code> writes one self-contained HTML file — no server, no build step,
1001+
no request to the network. It opens from an email attachment on a machine with no internet,
1002+
which is how handovers actually happen.
1003+
</p>
1004+
<p>
1005+
Alongside the map: every entity and what each property is, every action with the code it
1006+
runs, validation with the message the user will actually see, and <strong>an index of every
1007+
criteria expression in the application</strong> — a dialect that is neither SQL nor C# and is
1008+
otherwise collected nowhere.
1009+
</p>
1010+
</section>
1011+
9771012
<!-- --------------------------------------------------------- catalog -->
9781013
<section id="catalog" class="reveal">
9791014
<p class="eyebrow">Optional, for licensees</p>

src/XafLogicExplainer.Core/Generators/EntityGraph.cs

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,16 @@ public static EntityGraph Build(ExtractedProject project, double? width = null,
135135
/// <remarks>
136136
/// Alphabetical order scatters an association's two ends to opposite sides, and every chord
137137
/// then crosses the middle — the diagram becomes a ball of string at about eight entities.
138-
/// Walking the graph greedily from its most connected node keeps most relationships as short
139-
/// arcs near the rim. It is a heuristic, not an optimum; the aim is legibility, not a minimal
140-
/// crossing count.
138+
/// <para>
139+
/// The walk is breadth-first on purpose. Following one neighbour at a time leaves a hub after
140+
/// its first spoke and never returns, so its remaining children land halfway around the circle
141+
/// and their edges cross everything. Placing a node and then <em>all</em> of its neighbours
142+
/// keeps a hub and its spokes together, which is the shape most XAF domain models have: an
143+
/// order with its lines, a sale with its payments.
144+
/// </para>
145+
/// <para>
146+
/// A heuristic, not an optimum. The aim is legibility, not a minimal crossing count.
147+
/// </para>
141148
/// </remarks>
142149
private static List<ExtractedEntity> OrderToReduceCrossings(
143150
List<ExtractedEntity> entities,
@@ -163,21 +170,31 @@ private static List<ExtractedEntity> OrderToReduceCrossings(
163170
{
164171
// Start each connected group at its busiest entity, so the hub of a cluster anchors it.
165172
// Ties break alphabetically to keep the layout stable across runs.
166-
var current = remaining
173+
var seed = remaining
167174
.OrderByDescending(n => degrees.GetValueOrDefault(n))
168175
.ThenBy(n => n, StringComparer.Ordinal)
169176
.First();
170177

171-
while (current is not null)
178+
var queue = new Queue<string>();
179+
queue.Enqueue(seed);
180+
remaining.Remove(seed);
181+
182+
while (queue.Count > 0)
172183
{
184+
var current = queue.Dequeue();
173185
order.Add(byName[current]);
174-
remaining.Remove(current);
175186

176-
current = neighbours[current]
187+
var children = neighbours[current]
177188
.Where(remaining.Contains)
178189
.OrderByDescending(n => degrees.GetValueOrDefault(n))
179190
.ThenBy(n => n, StringComparer.Ordinal)
180-
.FirstOrDefault();
191+
.ToList();
192+
193+
foreach (var child in children)
194+
{
195+
remaining.Remove(child);
196+
queue.Enqueue(child);
197+
}
181198
}
182199
}
183200

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using System;
2+
using System.ComponentModel;
3+
using DevExpress.ExpressApp.ConditionalAppearance;
4+
using DevExpress.Persistent.Base;
5+
using DevExpress.Persistent.Validation;
6+
using DevExpress.Xpo;
7+
8+
namespace PharmacyDemo.Module.BusinessObjects;
9+
10+
/// <summary>Who pays, and how much of it.</summary>
11+
[DefaultClassOptions]
12+
[NavigationItem("Care")]
13+
[XafDefaultProperty(nameof(Name))]
14+
[Description("Who pays, and how much of it.")]
15+
public class InsurancePlan : XPCustomObject
16+
{
17+
public InsurancePlan(Session session) : base(session) { }
18+
19+
[Size(120)]
20+
private string _name;
21+
public string Name { get => _name; set => SetPropertyValue(nameof(Name), ref _name, value); }
22+
23+
[Description("Share of the price the insurer pays.")]
24+
private decimal _coveragePercent;
25+
public decimal CoveragePercent { get => _coveragePercent; set => SetPropertyValue(nameof(CoveragePercent), ref _coveragePercent, value); }
26+
27+
[Association("Plan-Patients")]
28+
public XPCollection<Patient> Patients => GetCollection<Patient>(nameof(Patients));
29+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using System;
2+
using System.ComponentModel;
3+
using DevExpress.ExpressApp.ConditionalAppearance;
4+
using DevExpress.Persistent.Base;
5+
using DevExpress.Persistent.Validation;
6+
using DevExpress.Xpo;
7+
8+
namespace PharmacyDemo.Module.BusinessObjects;
9+
10+
/// <summary>A person the pharmacy dispenses to.</summary>
11+
[DefaultClassOptions]
12+
[NavigationItem("Care")]
13+
[XafDefaultProperty(nameof(FullName))]
14+
[Description("A person the pharmacy dispenses to.")]
15+
public class Patient : XPCustomObject
16+
{
17+
public Patient(Session session) : base(session) { }
18+
19+
[Size(140)]
20+
[RuleRequiredField("Patient_Name", DefaultContexts.Save, CustomMessageTemplate = "A patient must have a name.")]
21+
private string _fullName;
22+
public string FullName { get => _fullName; set => SetPropertyValue(nameof(FullName), ref _fullName, value); }
23+
24+
private DateTime _dateOfBirth;
25+
public DateTime DateOfBirth { get => _dateOfBirth; set => SetPropertyValue(nameof(DateOfBirth), ref _dateOfBirth, value); }
26+
27+
[Size(400)]
28+
[Description("Checked before dispensing.")]
29+
private string _allergies;
30+
public string Allergies { get => _allergies; set => SetPropertyValue(nameof(Allergies), ref _allergies, value); }
31+
32+
[Association("Plan-Patients")]
33+
private InsurancePlan _plan;
34+
public InsurancePlan Plan { get => _plan; set => SetPropertyValue(nameof(Plan), ref _plan, value); }
35+
36+
[Association("Patient-Prescriptions")]
37+
public XPCollection<Prescription> Prescriptions => GetCollection<Prescription>(nameof(Prescriptions));
38+
39+
[Association("Patient-Sales")]
40+
public XPCollection<Sale> Sales => GetCollection<Sale>(nameof(Sales));
41+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using System;
2+
using System.ComponentModel;
3+
using DevExpress.ExpressApp.ConditionalAppearance;
4+
using DevExpress.Persistent.Base;
5+
using DevExpress.Persistent.Validation;
6+
using DevExpress.Xpo;
7+
8+
namespace PharmacyDemo.Module.BusinessObjects;
9+
10+
/// <summary>Money taken against a sale.</summary>
11+
[DefaultClassOptions]
12+
[XafDefaultProperty(nameof(Reference))]
13+
[Description("Money taken against a sale.")]
14+
public class Payment : XPCustomObject
15+
{
16+
public Payment(Session session) : base(session) { }
17+
18+
[Size(40)]
19+
private string _reference;
20+
public string Reference { get => _reference; set => SetPropertyValue(nameof(Reference), ref _reference, value); }
21+
22+
private decimal _amount;
23+
public decimal Amount { get => _amount; set => SetPropertyValue(nameof(Amount), ref _amount, value); }
24+
25+
[Association("Method-Payments")]
26+
private PaymentMethod _method;
27+
public PaymentMethod Method { get => _method; set => SetPropertyValue(nameof(Method), ref _method, value); }
28+
29+
[Association("Sale-Payments")]
30+
private Sale _sale;
31+
public Sale Sale { get => _sale; set => SetPropertyValue(nameof(Sale), ref _sale, value); }
32+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using System;
2+
using System.ComponentModel;
3+
using DevExpress.ExpressApp.ConditionalAppearance;
4+
using DevExpress.Persistent.Base;
5+
using DevExpress.Persistent.Validation;
6+
using DevExpress.Xpo;
7+
8+
namespace PharmacyDemo.Module.BusinessObjects;
9+
10+
/// <summary>Cash, card, or an insurer.</summary>
11+
[DefaultClassOptions]
12+
[NavigationItem("Sales")]
13+
[XafDefaultProperty(nameof(Name))]
14+
[Description("Cash, card, or an insurer.")]
15+
public class PaymentMethod : XPCustomObject
16+
{
17+
public PaymentMethod(Session session) : base(session) { }
18+
19+
[Size(60)]
20+
private string _name;
21+
public string Name { get => _name; set => SetPropertyValue(nameof(Name), ref _name, value); }
22+
23+
private bool _requiresAuthorization;
24+
public bool RequiresAuthorization { get => _requiresAuthorization; set => SetPropertyValue(nameof(RequiresAuthorization), ref _requiresAuthorization, value); }
25+
26+
[Association("Method-Payments")]
27+
public XPCollection<Payment> Payments => GetCollection<Payment>(nameof(Payments));
28+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using System;
2+
using System.ComponentModel;
3+
using DevExpress.ExpressApp.ConditionalAppearance;
4+
using DevExpress.Persistent.Base;
5+
using DevExpress.Persistent.Validation;
6+
using DevExpress.Xpo;
7+
8+
namespace PharmacyDemo.Module.BusinessObjects;
9+
10+
/// <summary>Staff licensed to dispense.</summary>
11+
[DefaultClassOptions]
12+
[NavigationItem("Staff")]
13+
[XafDefaultProperty(nameof(FullName))]
14+
[Description("Staff licensed to dispense.")]
15+
public class Pharmacist : XPCustomObject
16+
{
17+
public Pharmacist(Session session) : base(session) { }
18+
19+
[Size(140)]
20+
private string _fullName;
21+
public string FullName { get => _fullName; set => SetPropertyValue(nameof(FullName), ref _fullName, value); }
22+
23+
[Size(40)]
24+
private string _licenceNumber;
25+
public string LicenceNumber { get => _licenceNumber; set => SetPropertyValue(nameof(LicenceNumber), ref _licenceNumber, value); }
26+
27+
private bool _isOnDuty;
28+
public bool IsOnDuty { get => _isOnDuty; set => SetPropertyValue(nameof(IsOnDuty), ref _isOnDuty, value); }
29+
30+
[Association("Pharmacist-Sales")]
31+
public XPCollection<Sale> Sales => GetCollection<Sale>(nameof(Sales));
32+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using System;
2+
using System.ComponentModel;
3+
using DevExpress.ExpressApp.ConditionalAppearance;
4+
using DevExpress.Persistent.Base;
5+
using DevExpress.Persistent.Validation;
6+
using DevExpress.Xpo;
7+
8+
namespace PharmacyDemo.Module.BusinessObjects;
9+
10+
/// <summary>A doctor who writes prescriptions.</summary>
11+
[DefaultClassOptions]
12+
[NavigationItem("Care")]
13+
[XafDefaultProperty(nameof(FullName))]
14+
[Description("A doctor who writes prescriptions.")]
15+
public class Prescriber : XPCustomObject
16+
{
17+
public Prescriber(Session session) : base(session) { }
18+
19+
[Size(140)]
20+
private string _fullName;
21+
public string FullName { get => _fullName; set => SetPropertyValue(nameof(FullName), ref _fullName, value); }
22+
23+
[Size(40)]
24+
private string _licenceNumber;
25+
public string LicenceNumber { get => _licenceNumber; set => SetPropertyValue(nameof(LicenceNumber), ref _licenceNumber, value); }
26+
27+
[Association("Prescriber-Prescriptions")]
28+
public XPCollection<Prescription> Prescriptions => GetCollection<Prescription>(nameof(Prescriptions));
29+
}

0 commit comments

Comments
 (0)