Skip to content

Commit 569aba4

Browse files
committed
docs: add validation flow documentation
1 parent 8a0bbdf commit 569aba4

File tree

1 file changed

+173
-0
lines changed

1 file changed

+173
-0
lines changed

Docs/validation-flow.md

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
# Validation Flow: Dataverse Plugin vs .NET API
2+
3+
This document explains exactly **where** and **how** the shared `CreateOrderValidator` is invoked in both the Dataverse plugin pipeline and the ASP.NET Core API, illustrating the fail-fast plus server-authoritative validation strategy.
4+
5+
---
6+
## 1. Shared Validator Overview
7+
8+
File: `src/Shared.Domain/CreateOrderValidator.cs`
9+
10+
The validator encapsulates all business rules for creating an order (customer existence & status, order date bounds, unique order number, line product checks, pricing rules, total consistency). It depends only on the abstraction `IOrderRulesData`, allowing any host (plugin or API) to supply its own data access implementation.
11+
12+
Key characteristics:
13+
14+
- No direct Dataverse or EF dependencies
15+
- All async lookups funneled through `IOrderRulesData`
16+
- Deterministic, side-effect free (pure validation logic)
17+
18+
```csharp
19+
var validator = new CreateOrderValidator(rulesData);
20+
var result = await validator.ValidateAsync(command, ct);
21+
```
22+
23+
---
24+
25+
## 2. API Validation Flow (Fail-Fast)
26+
27+
Entry point: `OrdersController.CreateOrder` (`src/Api.Orders/Controllers/OrdersController.cs`)
28+
29+
High-level sequence:
30+
31+
```text
32+
Client -> API Controller -> (Model Binding) -> Request DTO -> Command Mapping -> Service -> Validator -> Dataverse (create) -> Plugin (re-validates)
33+
```
34+
35+
### Sequence Diagram
36+
37+
```text
38+
+---------+ +-----------------+ +----------------+ +-------------------+
39+
| Client | ---> | OrdersController| ---> | OrderService | ---> | Dataverse Adapter |
40+
+---------+ +-----------------+ +----------------+ +-------------------+
41+
HTTP POST /api/orders | |
42+
| CreateOrderAsync |
43+
| - Map DTO -> Command |
44+
| - Call validator |
45+
| - Throw on failure |
46+
v |
47+
Shared Validator |
48+
| (IOrderRulesData impl) |
49+
v v
50+
(If valid) Dataverse create (plugin will validate again)
51+
```
52+
53+
Controller excerpt (simplified):
54+
55+
```csharp
56+
var command = request.ToCommand();
57+
var response = await _orderService.CreateOrderAsync(command, cancellationToken); // internally runs validator
58+
```
59+
60+
Service responsibility (conceptual):
61+
62+
```csharp
63+
// Inside service
64+
var validationResult = await _validator.ValidateAsync(command, ct);
65+
if(!validationResult.IsValid) throw new ValidationException(validationResult.Errors);
66+
```
67+
68+
Why fail-fast?
69+
70+
- Avoids unnecessary Dataverse round-trips on obviously invalid data
71+
- Returns structured errors (`ValidationProblemDetails`) immediately
72+
- Still allows the plugin to act as final authoritative gate (defense in depth)
73+
74+
---
75+
76+
## 3. Dataverse Plugin Validation Flow (Authoritative Enforcement)
77+
78+
Entry point: `CreateOrderPlugin.Execute` (`src/Plugins.Dataverse/Orders/CreateOrderPlugin.cs`)
79+
80+
Current plugin skeleton (abridged):
81+
82+
```csharp
83+
var context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext))!;
84+
var serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory))!;
85+
var tracing = (ITracingService)serviceProvider.GetService(typeof(ITracingService))!;
86+
var orgService = serviceFactory.CreateOrganizationService(context.UserId);
87+
88+
// Guard conditions (message, entity, required fields)...
89+
var command = EntityMapper.MapToCreateOrderCommand(targetEntity, tracing);
90+
91+
// Supply plugin-specific data adapter implementing IOrderRulesData
92+
var rulesData = new DataverseOrderRulesData(orgService);
93+
var validator = new CreateOrderValidator(rulesData);
94+
var result = validator.Validate(command);
95+
if(!result.IsValid)
96+
throw new InvalidPluginExecutionException(result.GetErrorsAsString());
97+
```
98+
99+
Where it sits in the pipeline:
100+
101+
```text
102+
User / Integration -> Dataverse Platform -> (PreValidation Plugin) -> Transaction continues if valid -> Core Operation (DB write) -> PostOperation plugins
103+
```
104+
105+
Plugin role:
106+
107+
- Executes inside Dataverse transaction scope
108+
- Blocks invalid records even if API or other client skipped validation
109+
- Ensures invariants are always enforced server-side
110+
111+
---
112+
113+
## 4. Adapter Abstraction Bridging Host Environments
114+
115+
Both hosts implement `IOrderRulesData` differently:
116+
117+
| Host | Adapter File | Data Source | Purpose |
118+
|------|--------------|-------------|---------|
119+
| API | `Api.Orders/Adapters/DataverseOrderRulesDataForApp.cs` | Dataverse ServiceClient | Remote lookup in fail-fast phase |
120+
| Plugin | `Plugins.Dataverse/Adapters/DataverseOrderRulesData.cs` | `IOrganizationService` | In-process transactional validation |
121+
122+
Benefits:
123+
124+
- Each host optimizes data access (caching could be added independently)
125+
- Validator remains agnostic to environment
126+
- Easier unit testing by mocking `IOrderRulesData`
127+
128+
---
129+
130+
## 5. Error Propagation Differences
131+
132+
| Aspect | API | Plugin |
133+
|--------|-----|--------|
134+
| Exception Type | `FluentValidation.ValidationException` caught and converted to HTTP 400 | `InvalidPluginExecutionException` returned to Dataverse client |
135+
| Error Format | `ValidationProblemDetails` (JSON) | Platform error dialog / flow failure message |
136+
| Multiple Errors | Aggregated per property | Combined string (custom formatting helper) |
137+
| Recovery | Client can correct fields immediately | User re-submits after fixing form data |
138+
139+
---
140+
141+
## 6. Defense in Depth Rationale
142+
143+
1. API performs fail-fast validation to reduce load and improve UX
144+
2. Plugin guarantees invariants for any channel (forms, flows, integrations) bypassing the API
145+
3. Shared validator ensures a single modification updates rules everywhere
146+
147+
---
148+
149+
## 7. Extending the Flow
150+
151+
152+
When adding a new rule:
153+
154+
1. Add logic to `CreateOrderValidator`
155+
2. Extend `IOrderRulesData` if new data is required
156+
3. Implement new members in both adapters
157+
4. Add/adjust unit tests in `Shared.Domain.Tests`
158+
5. (Optional) Add an API integration test
159+
160+
Diagram of change propagation:
161+
162+
```text
163+
Update Validator -> Compile -> Tests pass -> Deploy API + Deploy Plugin Assembly -> Rule active everywhere
164+
```
165+
166+
---
167+
168+
## 8. Summary
169+
170+
The architecture centralizes rule intent while letting each runtime handle error transport and context-specific concerns. This keeps business logic DRY, testable, and consistently enforced.
171+
172+
---
173+
*Last updated: autogenerated documentation.*

0 commit comments

Comments
 (0)