Skip to content

Commit 3dfe5be

Browse files
committed
feat: initial commit with shared validation architecture, api, dataverse plugin, and comprehensive validator tests
0 parents  commit 3dfe5be

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+3500
-0
lines changed

.gitignore

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
## Build artifacts
2+
bin/
3+
obj/
4+
**/bin/
5+
**/obj/
6+
7+
## User-specific
8+
*.user
9+
*.suo
10+
*.userprefs
11+
.vs/
12+
13+
## Logs
14+
logs/
15+
*.log
16+
17+
## OS
18+
Thumbs.db
19+
ehthumbs.db
20+
Desktop.ini
21+
*.DS_Store
22+
23+
## Rider / JetBrains
24+
.idea/
25+
26+
## Dotnet
27+
*.pidb
28+
*.svclog
29+
TestResult*/
30+
*.coverage*
31+
*.lcov
32+
33+
## Secrets / configuration overrides
34+
appsettings.Development.json
35+
appsettings.Local.json
36+
*.Secret.json
37+
secrets.json
38+
39+
## Azure / tooling
40+
.azure/
41+
42+
## Coverage
43+
coverage/
44+
coverage-report/
45+
46+
## Packages
47+
*.nupkg
48+
packages/
49+
50+
## Others
51+
~$*
52+
*.bak

Docs/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Documentation
2+
3+
This folder contains documentation files for the solution.

Docs/implementation-summary.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Implementation Summary
2+
3+
Moved to Docs folder as part of solution reorganization.

Docs/solution-structure.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Solution Structure
2+
3+
Moved to Docs folder as part of solution reorganization.

README.md

Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
# Shared Validation Layer for Dataverse and .NET Applications
2+
3+
This solution demonstrates how to implement a **single source of truth** for business validation rules that can be shared between Dataverse plugins and .NET applications. The same validation logic runs in both contexts, ensuring consistency while avoiding code duplication.
4+
5+
## 🎯 Key Benefits
6+
7+
- **Single Source of Truth**: All business rules live in one shared library
8+
- **Consistent Validation**: Same rules apply whether data comes from Dataverse forms, Power Apps, Power Automate, or your API
9+
- **Fail-Fast Architecture**: API validates early to provide immediate feedback, while plugins provide authoritative server-side enforcement
10+
- **Testable**: Core validation logic can be unit tested in isolation using mocks
11+
- **Maintainable**: Change a business rule once and it applies everywhere
12+
13+
## 🏗️ Architecture Overview
14+
15+
```
16+
┌─────────────────────────────────────────────────────────────────┐
17+
│ Shared.Domain Library │
18+
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
19+
│ │ Domain Models │ │ IOrderRulesData │ │ FluentValidation│ │
20+
│ │ (Commands/DTOs) │ │ (Abstraction) │ │ Validators │ │
21+
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
22+
└─────────────────────────────────────────────────────────────────┘
23+
│ │
24+
▼ ▼
25+
┌─────────────────────────────┐ ┌─────────────────────────────┐
26+
│ Dataverse Plugin │ │ ASP.NET Core API │
27+
│ │ │ │
28+
│ ┌─────────────────────────┐ │ │ ┌─────────────────────────┐ │
29+
│ │ DataverseOrderRulesData │ │ │ │DataverseOrderRulesData │ │
30+
│ │ (IOrganizationService) │ │ │ │ ForApp (ServiceClient) │ │
31+
│ └─────────────────────────┘ │ │ └─────────────────────────┘ │
32+
│ │ │ │
33+
│ • PreValidation/PreOp Stage │ │ • Controller Validation │
34+
│ • Blocks invalid saves │ │ • Early failure (fail-fast) │
35+
│ • Transactional enforcement │ │ • Detailed error responses │
36+
└─────────────────────────────┘ └─────────────────────────────┘
37+
│ │
38+
▼ ▼
39+
┌─────────────────────────────────────────────────────────────────┐
40+
│ Dataverse │
41+
└─────────────────────────────────────────────────────────────────┘
42+
```
43+
44+
## 🚀 Getting Started
45+
46+
### Prerequisites
47+
48+
- .NET 8.0 SDK
49+
- Visual Studio 2022 or VS Code
50+
- Access to a Dataverse environment
51+
- Plugin Registration Tool (for deploying plugins)
52+
53+
### 1. Clone and Build
54+
55+
```bash
56+
git clone <repository-url>
57+
cd SharedValidationExample
58+
dotnet restore
59+
dotnet build
60+
```
61+
62+
### 2. Configure Dataverse Connection
63+
64+
Update `src/Api.Orders/appsettings.json` with your Dataverse connection string:
65+
66+
```json
67+
{
68+
"ConnectionStrings": {
69+
"Dataverse": "AuthType=OAuth;[email protected];Password=your-password;Url=https://your-org.crm.dynamics.com;AppId=51f81489-12ee-4a9e-aaae-a2591f45987d;RedirectUri=app://58145B91-0C36-4500-8554-080854F2AC97;LoginPrompt=Auto"
70+
}
71+
}
72+
```
73+
74+
**For production**, use more secure authentication methods like Client Secrets or Certificates.
75+
76+
### 3. Create Dataverse Schema
77+
78+
Create the following entities in your Dataverse environment:
79+
80+
**Order Entity (`new_order`)**:
81+
- `new_customerid` (Customer lookup to Account)
82+
- `new_orderdate` (Date)
83+
- `new_ordernumber` (Text)
84+
- `new_totalamount` (Currency)
85+
- `new_productid` (Product lookup)
86+
- `new_quantity` (Whole Number)
87+
- `new_unitprice` (Currency)
88+
89+
### 4. Deploy the Plugin
90+
91+
1. Build the `Plugins.Dataverse` project
92+
2. Use Plugin Registration Tool to register:
93+
- **Assembly**: `Plugins.Dataverse.dll`
94+
- **Plugin**: `Plugins.Dataverse.Orders.CreateOrderPlugin`
95+
- **Message**: Create
96+
- **Entity**: new_order
97+
- **Stage**: PreValidation (recommended) or PreOperation
98+
- **Mode**: Synchronous
99+
100+
### 5. Run the API
101+
102+
```bash
103+
cd src/Api.Orders
104+
dotnet run
105+
```
106+
107+
The API will be available at `https://localhost:7000` with Swagger UI at the root.
108+
109+
## 🧪 Testing the Solution
110+
111+
### Unit Tests
112+
113+
Run the shared validation tests:
114+
115+
```bash
116+
cd tests/Shared.Domain.Tests
117+
dotnet test
118+
```
119+
120+
### API Testing
121+
122+
Test the API endpoints:
123+
124+
```bash
125+
# Create an order (will validate using shared rules)
126+
curl -X POST https://localhost:7000/api/orders \
127+
-H "Content-Type: application/json" \
128+
-d '{
129+
"customerId": "customer-guid-here",
130+
"orderNumber": "ORD-001",
131+
"totalAmount": 20.00,
132+
"lines": [{
133+
"productId": "product-guid-here",
134+
"quantity": 2,
135+
"unitPrice": 10.00
136+
}]
137+
}'
138+
139+
# Validate without creating
140+
curl -X POST https://localhost:7000/api/orders/validate \
141+
-H "Content-Type: application/json" \
142+
-d '{...same payload...}'
143+
```
144+
145+
### Plugin Testing
146+
147+
Create an order through:
148+
- Dataverse forms
149+
- Power Apps
150+
- Power Automate
151+
- Direct SDK calls
152+
153+
The same validation rules will apply and block invalid data.
154+
155+
## 📋 How It Works
156+
157+
### 1. Shared Validation Logic
158+
159+
The `Shared.Domain` library contains:
160+
161+
```csharp
162+
public class CreateOrderValidator : AbstractValidator<CreateOrderCommand>
163+
{
164+
public CreateOrderValidator(IOrderRulesData rulesData)
165+
{
166+
// All business rules defined here
167+
RuleFor(x => x.CustomerId)
168+
.NotEmpty()
169+
.MustAsync(async (id, ct) => await rulesData.CustomerExistsAsync(id, ct))
170+
.WithMessage("Customer does not exist.");
171+
172+
// ... more rules
173+
}
174+
}
175+
```
176+
177+
### 2. Dataverse Plugin Enforcement
178+
179+
```csharp
180+
public void Execute(IServiceProvider serviceProvider)
181+
{
182+
// Get Dataverse services
183+
var service = GetOrganizationService(serviceProvider);
184+
var target = GetTargetEntity(context);
185+
186+
// Map to domain command
187+
var command = EntityMapper.MapToCreateOrderCommand(target);
188+
189+
// Use shared validation
190+
var validator = new CreateOrderValidator(new DataverseOrderRulesData(service));
191+
var result = validator.Validate(command);
192+
193+
if (!result.IsValid)
194+
{
195+
// Block the save
196+
throw new InvalidPluginExecutionException(result.GetErrorsAsString());
197+
}
198+
}
199+
```
200+
201+
### 3. API Fail-Fast Validation
202+
203+
```csharp
204+
[HttpPost]
205+
public async Task<IActionResult> CreateOrder(CreateOrderRequest request)
206+
{
207+
var command = request.ToCommand();
208+
209+
// Same validator, different data adapter
210+
var result = await _validator.ValidateAsync(command);
211+
212+
if (!result.IsValid)
213+
{
214+
// Return 400 with detailed errors
215+
return ValidationProblem(result.ToErrorDictionary());
216+
}
217+
218+
// Create the order (plugin will validate again as final guard)
219+
var response = await _orderService.CreateOrderAsync(command);
220+
return CreatedAtAction(nameof(GetOrderById), new { id = response.OrderId }, response);
221+
}
222+
```
223+
224+
## 🔧 Customizing for Your Needs
225+
226+
### Adding New Validation Rules
227+
228+
1. **Update the validator** in `Shared.Domain/Orders/CreateOrderValidator.cs`
229+
2. **Add any new data requirements** to `IOrderRulesData`
230+
3. **Implement in both adapters** (plugin and API)
231+
4. **Add unit tests** in `Shared.Domain.Tests`
232+
233+
### Supporting Different Entities
234+
235+
1. **Create new command models** (e.g., `CreateCustomerCommand`)
236+
2. **Create new validators** (e.g., `CreateCustomerValidator`)
237+
3. **Create new adapters** for data access
238+
4. **Create new plugins and controllers**
239+
240+
### Advanced Scenarios
241+
242+
- **Async validation**: Already supported via `MustAsync` in FluentValidation
243+
- **Cross-entity validation**: Implement in `IOrderRulesData` adapters
244+
- **Conditional validation**: Use FluentValidation's `When` conditions
245+
- **Custom validation**: Implement `CustomAsync` rules for complex logic
246+
247+
## 🎯 Best Practices
248+
249+
### Plugin Development
250+
251+
- **Use PreValidation** stage when possible (cheaper than PreOperation rollbacks)
252+
- **Keep plugins fast** - avoid heavy computations or external calls
253+
- **Use proper error handling** - return user-friendly messages
254+
- **Include tracing** for debugging
255+
- **Filter attributes** in plugin steps to avoid unnecessary executions
256+
257+
### API Development
258+
259+
- **Validate early** in controllers before expensive operations
260+
- **Return structured errors** using ValidationProblemDetails
261+
- **Use async patterns** throughout the validation chain
262+
- **Implement proper logging** for troubleshooting
263+
- **Consider caching** for frequently accessed reference data
264+
265+
### Testing
266+
267+
- **Mock `IOrderRulesData`** for isolated unit tests
268+
- **Test edge cases** thoroughly (null values, boundary conditions)
269+
- **Integration test both paths** (plugin and API) against actual Dataverse
270+
- **Performance test** with realistic data volumes
271+
272+
## 📚 Key Files Reference
273+
274+
| File | Purpose |
275+
|------|---------|
276+
| `Shared.Domain/Orders/CreateOrderValidator.cs` | **Core validation logic** - single source of truth for all business rules |
277+
| `Shared.Domain/Orders/IOrderRulesData.cs` | **Data access abstraction** - defines what data validators need |
278+
| `Plugins.Dataverse/Orders/CreateOrderPlugin.cs` | **Plugin implementation** - enforces validation in Dataverse pipeline |
279+
| `Api.Orders/Controllers/OrdersController.cs` | **API controller** - validates before sending to Dataverse |
280+
| `Plugins.Dataverse/Adapters/DataverseOrderRulesData.cs` | **Plugin data adapter** - implements IOrderRulesData using IOrganizationService |
281+
| `Api.Orders/Adapters/DataverseOrderRulesDataForApp.cs` | **API data adapter** - implements IOrderRulesData using ServiceClient |
282+
283+
## 🤝 Contributing
284+
285+
1. Fork the repository
286+
2. Create a feature branch
287+
3. Add tests for new functionality
288+
4. Ensure all tests pass
289+
5. Submit a pull request
290+
291+
## 📄 License
292+
293+
This project is licensed under the MIT License - see the LICENSE file for details.
294+
295+
## 🆘 Troubleshooting
296+
297+
### Common Issues
298+
299+
**Plugin not firing**: Check plugin registration, message, entity, and stage configuration.
300+
301+
**Connection string errors**: Verify Dataverse URL, credentials, and AppId in connection string.
302+
303+
**Validation not working**: Ensure both adapters implement IOrderRulesData correctly and return consistent results.
304+
305+
**Performance issues**:
306+
- Use column sets to limit data retrieval
307+
- Implement caching for reference data
308+
- Avoid N+1 query patterns
309+
310+
### Getting Help
311+
312+
- Check the unit tests for usage examples
313+
- Review the Swagger documentation at the API root
314+
- Enable detailed logging to troubleshoot validation failures
315+
- Use Dataverse tracing to debug plugin execution

0 commit comments

Comments
 (0)