-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path01-findings.html
More file actions
207 lines (197 loc) · 12.3 KB
/
Copy path01-findings.html
File metadata and controls
207 lines (197 loc) · 12.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>01 · Findings — Multi-Tenant Platform</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Fraunces:opsz,wght@9..144,500;9..144,600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css" />
<link rel="stylesheet" href="../assets/styles.css" />
</head>
<body>
<div class="layout">
<aside class="sidebar">
<a href="../index.html" class="brand">
<span class="brand-mark">MTP</span>
<span class="brand-text">Multi-Tenant Platform</span>
</a>
<nav>
<div class="nav-group">
<h4>Foundations</h4>
<ul>
<li><a href="../index.html"><span class="nav-num">00</span>Overview</a></li>
<li><a href="01-findings.html" class="active"><span class="nav-num">01</span>Findings</a></li>
<li><a href="02-architecture.html"><span class="nav-num">02</span>Architecture</a></li>
<li><a href="10-tenancy-model.html"><span class="nav-num">10</span>Tenancy Model</a></li>
</ul>
</div>
<div class="nav-group">
<h4>Validation</h4>
<ul>
<li><a href="03-code-sketch.html"><span class="nav-num">03</span>Code Sketch</a></li>
<li><a href="04-ui-rule-config.html"><span class="nav-num">04</span>UI Rule Config</a></li>
<li><a href="05-result-pattern.html"><span class="nav-num">05</span>Result Pattern</a></li>
<li><a href="06-reconciliation.html"><span class="nav-num">06</span>Reconciliation</a></li>
</ul>
</div>
<div class="nav-group">
<h4>Tenant Surface</h4>
<ul>
<li><a href="07-ui-schema.html"><span class="nav-num">07</span>UI Schema</a></li>
<li><a href="08-i18n-l10n.html"><span class="nav-num">08</span>i18n / l10n</a></li>
<li><a href="09-extension-fields.html"><span class="nav-num">09</span>Extension Fields</a></li>
</ul>
</div>
<div class="nav-group">
<h4>Frontend</h4>
<ul>
<li><a href="../mockups/classic.html"><span class="nav-num">UI1</span>AppShell — Classic</a></li>
<li><a href="../mockups/zoho.html"><span class="nav-num">UI2</span>AppShell — 2-Stage</a></li>
<li><a href="../mockups/exam-setup.html"><span class="nav-num">UI3</span>AppShell — Exam Setup</a></li>
<li><a href="../mockups/ux-showcase-1.html"><span class="nav-num">UI4</span>AppShell — UX Showcase</a></li>
<li><a href="../mockups/ux-showcase-2.html"><span class="nav-num">UI5</span>AppShell — UX Showcase 2</a></li>
<li><a href="../mockups/ux-showcase-3.html"><span class="nav-num">UI6</span>AppShell — UX Showcase 3</a></li>
<li><a href="../mockups/ux-showcase-4.html"><span class="nav-num">UI8</span>AppShell — UX Showcase 4</a></li>
<li><a href="../mockups/ux-catalog.html"><span class="nav-num">UI7</span>UX Pattern Catalog</a></li>
<li><a href="../mockups/notes.html"><span class="nav-num">UIN</span>AppShell Notes</a></li>
</ul>
</div>
<div class="nav-group">
<h4>Decisions</h4>
<ul>
<li><a href="../adr/0001-hybrid-validation-strategy.html"><span class="nav-num">ADR-1</span>Hybrid Validation</a></li>
<li><a href="../adr/0002-database-per-client.html"><span class="nav-num">ADR-2</span>DB-per-Client</a></li>
<li><a href="../adr/0003-frontend-stack.html"><span class="nav-num">ADR-3</span>Frontend Stack</a></li>
<li><a href="../adr/0004-authorization-permission-model.html"><span class="nav-num">ADR-4</span>Authz & Permissions</a></li>
<li><a href="../adr/0005-repo-topology.html"><span class="nav-num">ADR-5</span>Repo Topology</a></li>
<li><a href="../adr/0006-no-module-federation.html"><span class="nav-num">ADR-6</span>No Module Federation</a></li>
<li><a href="../adr/0007-ui-stack-radix-cssmodules.html"><span class="nav-num">ADR-7</span>UI Stack — Radix</a></li>
<li><a href="../adr/0008-mtp-web-folder-structure.html"><span class="nav-num">ADR-8</span>mtp-web Folder Structure</a></li>
<li><a href="../adr/0009-hr-payroll-three-aggregates.html"><span class="nav-num">ADR-9</span>HR-Payroll — 3 Aggregates, 1 Module</a></li>
</ul>
</div>
<div class="nav-group">
<h4>GNUMS — Conventions</h4>
<ul>
<li><a href="../gnums/module-catalog.html"><span class="nav-num">MOD</span>Module Catalog</a></li>
<li><a href="../gnums/all-modules-inventory.html"><span class="nav-num">REF</span>Procedure Inventory</a></li>
<li><a href="../gnums/org-hierarchy.html"><span class="nav-num">ORG</span>Org Hierarchy</a></li>
<li><a href="../gnums/hierarchy-comparison.html"><span class="nav-num">CMP</span>Hierarchy Comparison</a></li>
</ul>
</div>
<div class="nav-group">
<h4>GNUMS — Modules</h4>
<ul>
<li><a href="../gnums/methodology.html"><span class="nav-num">HOW</span>Study Methodology</a></li>
<li><a href="../gnums/accounting/scope.html"><span class="nav-num">ACC</span>Accounting — Scope</a></li>
<li><a href="../gnums/staff/scope.html"><span class="nav-num">STF</span>Staff — Scope</a></li>
<li><a href="../gnums/hr-payroll/scope.html"><span class="nav-num">HRP</span>HR-Payroll — Scope</a></li>
<li><a href="../gnums/exam/scope.html"><span class="nav-num">EXM</span>Exam — Scope</a></li>
<li><a href="../gnums/exam/strategy.html"><span class="nav-num">PLAY</span>Exam — Strategy</a></li>
<li><a href="../gnums/student/scope.html"><span class="nav-num">STU</span>Student — Scope</a></li>
<li><a href="../gnums/student/strategy.html"><span class="nav-num">SPL</span>Student — Strategy</a></li>
<li><a href="../gnums/student/cluster-01-core-identity.html"><span class="nav-num">SC1</span>Student / Core Identity</a></li>
<li><a href="../gnums/student/cluster-02-subordinate-entities.html"><span class="nav-num">SC2</span>Student / Subordinate Entities</a></li>
<li><a href="../gnums/student/cluster-03-master-config.html"><span class="nav-num">SC3</span>Student / Master & Config</a></li>
<li><a href="../gnums/student/cluster-04-certificate-workflow.html"><span class="nav-num">SC4</span>Student / Certificate Workflow</a></li>
<li><a href="../gnums/student/cluster-05-bvm-activities.html"><span class="nav-num">SC5</span>Student / NCC NSS Yoga</a></li>
<li><a href="../gnums/student/cluster-06-mentoring.html"><span class="nav-num">SC6</span>Student / Mentoring</a></li>
</ul>
</div>
</nav>
</aside>
<main>
<span class="eyebrow">01 · Analysis</span>
<h1>Is FluentValidation enough?</h1>
<p class="lede">
A frank look at where FluentValidation shines, where it strains, and why a multi-tenant
app with UI-configurable flows needs more than one tool.
</p>
<h2>Context</h2>
<p>Enterprise application serves multiple client tenants. Each tenant may have:</p>
<ul>
<li>Different required or forbidden fields per feature</li>
<li>Different workflow steps (approval chain length, gating conditions)</li>
<li>Rules edited by client administrators through the admin UI — not by deploying code</li>
<li>Multiple flow versions coexisting during phased rollouts</li>
</ul>
<h2>FluentValidation strengths</h2>
<table>
<thead><tr><th>Area</th><th>Notes</th></tr></thead>
<tbody>
<tr><td>Property validation</td><td>Declarative, composable, well-tested</td></tr>
<tr><td>DI integration</td><td>Constructor-inject repositories, clients, current-request services</td></tr>
<tr><td>Async rules</td><td>Database and API checks via <code>MustAsync</code></td></tr>
<tr><td>Rule sets</td><td><code>RuleSet("TenantA", ...)</code> selectable at runtime</td></tr>
<tr><td>ASP.NET Core integration</td><td><code>AddFluentValidationAutoValidation()</code> runs validators on model binding</td></tr>
<tr><td>Testability</td><td>Unit-testable in isolation with <code>TestValidate</code></td></tr>
</tbody>
</table>
<h2>FluentValidation weaknesses (for this use case)</h2>
<table>
<thead><tr><th>Pain</th><th>Detail</th></tr></thead>
<tbody>
<tr><td>Rule explosion</td><td><code>When(tenant.Is("A"))</code> matrix grows as <em>N tenants × M features</em></td></tr>
<tr><td>One validator per DTO</td><td>DI resolves a single <code>IValidator<T></code> per request type; multi-tenant resolution needs a custom factory</td></tr>
<tr><td>Code-first</td><td>Rules compile-time. UI-driven rules require a dynamic builder, which fights idiomatic FV</td></tr>
<tr><td>Cross-aggregate invariants</td><td>Belong in the domain layer, not a validator</td></tr>
<tr><td>Versioning</td><td>No first-class concept of "flow v1 vs v2"</td></tr>
<tr><td>Discoverability</td><td>Hard to answer "what rules apply for tenant X, feature Y" without running the validator</td></tr>
</tbody>
</table>
<h2>Variation type vs tool fit</h2>
<table>
<thead>
<tr><th>Variation</th><th>FluentValidation</th><th>Strategy Pattern</th><th>Rule Engine</th></tr>
</thead>
<tbody>
<tr><td>Same shape, different limits</td><td>Good</td><td>Overkill</td><td>Overkill</td></tr>
<tr><td>Optional vs required fields by tenant</td><td>OK (<code>When</code>)</td><td>Better</td><td>Better</td></tr>
<tr><td>Entirely different fields</td><td>Poor</td><td>Good</td><td>Good</td></tr>
<tr><td>Different workflow steps</td><td>Wrong tool</td><td>Required</td><td>Optional</td></tr>
<tr><td>Rules edited in UI</td><td>Bad</td><td>Partial</td><td>Required</td></tr>
<tr><td>Complex domain invariants</td><td>Wrong layer</td><td>Wrong layer</td><td>Wrong layer (use domain)</td></tr>
</tbody>
</table>
<h2>Recommendation: a three-layer hybrid</h2>
<p>Three tools, each used where it shines:</p>
<ol>
<li><strong>FluentValidation</strong> — syntactic/shape validation only (format, range, regex, required-by-default). Tenant-agnostic.</li>
<li><strong>Tenant Flow Strategy</strong> — semantic validation + workflow orchestration. Resolved per <em>(tenant, feature, version)</em>.</li>
<li><strong>Rule Engine (lightweight, JSON-driven)</strong> — rules editable from the admin UI. Lives <em>inside</em> the strategy as one of its inputs.</li>
</ol>
<p>Domain invariants remain in the domain layer regardless of tenant.</p>
<h2>Why not "just FluentValidation"</h2>
<ul>
<li>20 tenants × 10 features × 3 flow versions = 600 conditional branches if everything is forced into FV.</li>
<li>UI-driven rules need persistence, versioning, and audit — FV provides none of that.</li>
<li>Workflow is not validation. Strategy separates flow from rule checks.</li>
</ul>
<h2>Why not "just a rule engine"</h2>
<ul>
<li>Overhead for trivial format checks.</li>
<li>Loss of compile-time safety for stable logic.</li>
<li>Devs need IDE assistance for the common 90%; UI editors only for the configurable 10%.</li>
</ul>
<div class="callout">
<strong>Architectural stance:</strong> traditional layered ASP.NET Core. Controllers call
application services directly. <em>Not</em> CQRS. <em>No</em> MediatR.
</div>
<div class="doc-footer">
<a href="../index.html">
<span class="label">← Back</span>
<span class="title">Overview</span>
</a>
<a href="02-architecture.html" class="next">
<span class="label">Next</span>
<span class="title">02 · Architecture →</span>
</a>
</div>
</main>
</div>
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
<script>mermaid.initialize({ startOnLoad: true, theme: 'default' });</script>
</body>
</html>