|
| 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