Skip to content

Commit 5107899

Browse files
committed
feat: add FluentValidation integration support
- Add FluentValidationAdapter for seamless integration with existing validators - Add extension methods WithFluentValidation() and WithFluentValidator() - Add comprehensive demo page showcasing FluentValidation features - Add detailed documentation for FluentValidation usage - Add unit tests for adapter and extension methods - Update API reference documentation - Register CustomerValidator in DI for demo This enables users to leverage their existing FluentValidation validators within FormCraft's dynamic forms, supporting complex rules, async validation, and nested object validation.
1 parent 1a5fa49 commit 5107899

File tree

14 files changed

+1161
-2
lines changed

14 files changed

+1161
-2
lines changed

.DS_Store

6 KB
Binary file not shown.

.idea/.idea.FormCraft/.idea/inspectionProfiles/Project_Default.xml

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

FormCraft.DemoBlazorApp/Components/Layout/MainLayout.razor

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@
9393
<MudNavLink Href="security-demo" Icon="@Icons.Material.Filled.Security">
9494
Security Features
9595
</MudNavLink>
96+
<MudNavLink Href="fluent-validation-demo" Icon="@Icons.Material.Filled.CheckCircle">
97+
FluentValidation Demo
98+
</MudNavLink>
9699
</MudNavGroup>
97100

98101
<MudNavGroup Title="Documentation" Icon="@Icons.Material.Filled.MenuBook" Expanded="true">
@@ -108,6 +111,9 @@
108111
<MudNavLink Href="docs/customization" Icon="@Icons.Material.Filled.Palette">
109112
Customization
110113
</MudNavLink>
114+
<MudNavLink Href="docs/fluent-validation" Icon="@Icons.Material.Filled.CheckCircle">
115+
FluentValidation
116+
</MudNavLink>
111117
<MudNavLink Href="docs/security" Icon="@Icons.Material.Filled.Security">
112118
Security
113119
</MudNavLink>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
@page "/docs/fluent-validation"
2+
3+
<DocumentationPage DocumentName="fluent-validation" Title="FluentValidation Integration" />
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
@page "/fluent-validation-demo"
2+
3+
<PageTitle>FluentValidation Integration Demo</PageTitle>
4+
5+
<DemoPageLayout
6+
Title="FluentValidation Integration"
7+
Icon="@Icons.Material.Filled.CheckCircle"
8+
Description="Seamlessly integrate your existing FluentValidation validators with FormCraft's dynamic forms. This demo shows how to leverage FluentValidation's powerful rule engine within FormCraft."
9+
FormDemoIcon="@Icons.Material.Filled.FactCheck">
10+
<FormDemoContent>
11+
<FormDemoSection
12+
TModel="CustomerModel"
13+
Model="@_model"
14+
Configuration="@_formConfig"
15+
FormTitle="Customer Registration with FluentValidation"
16+
FormIcon="@Icons.Material.Filled.PersonAdd"
17+
IsSubmitted="@_submitted"
18+
IsSubmitting="@_isSubmitting"
19+
OnValidSubmit="@HandleValidSubmit"
20+
OnFieldChanged="@HandleFieldChanged"
21+
OnReset="@ResetForm"
22+
DataDisplayItems="@GetDataDisplayItems()"
23+
ShowSubmitButton="true"
24+
SubmitButtonText="Register Customer"
25+
SubmittingText="Validating...">
26+
<SidebarContent>
27+
<FormGuidelines Guidelines="@_sidebarFeatures" Title="FluentValidation Features" />
28+
29+
<MudCard Elevation="1" Class="mt-3">
30+
<MudCardContent>
31+
<MudText Typo="Typo.h6" Class="mb-3">
32+
<MudIcon Icon="@Icons.Material.Filled.Rule" Class="me-1" />
33+
Active Validation Rules
34+
</MudText>
35+
<MudList T="string" Dense="true">
36+
<MudListItem T="string" Icon="@Icons.Material.Filled.TextFields">
37+
Name: 3-50 characters
38+
</MudListItem>
39+
<MudListItem T="string" Icon="@Icons.Material.Filled.Email">
40+
Email: Valid format required
41+
</MudListItem>
42+
<MudListItem T="string" Icon="@Icons.Material.Filled.Cake">
43+
Age: Must be 18 or older
44+
</MudListItem>
45+
</MudList>
46+
</MudCardContent>
47+
</MudCard>
48+
</SidebarContent>
49+
<AdditionalContent>
50+
@if (_validationAttempts > 0)
51+
{
52+
<MudCard Class="mt-4" Elevation="2">
53+
<MudCardContent>
54+
<MudText Typo="Typo.h6" GutterBottom="true" Class="d-flex align-items-center">
55+
<MudIcon Icon="@Icons.Material.Filled.Analytics" Class="me-2 text-info" />
56+
Validation Statistics
57+
</MudText>
58+
<MudGrid>
59+
<MudItem xs="6">
60+
<MudText Typo="Typo.body2">Total Attempts: @_validationAttempts</MudText>
61+
</MudItem>
62+
<MudItem xs="6">
63+
<MudText Typo="Typo.body2">Successful: @_successfulValidations</MudText>
64+
</MudItem>
65+
</MudGrid>
66+
</MudCardContent>
67+
</MudCard>
68+
}
69+
</AdditionalContent>
70+
</FormDemoSection>
71+
</FormDemoContent>
72+
73+
<CodeExampleContent>
74+
<CodeExample
75+
Title="FluentValidation Integration"
76+
Language="csharp"
77+
Code="@GetCodeExample()" />
78+
</CodeExampleContent>
79+
80+
<GuidelinesContent>
81+
<ApiGuidelinesTable
82+
Title="FluentValidation API"
83+
Items="@_apiGuidelineTableItems" />
84+
</GuidelinesContent>
85+
</DemoPageLayout>
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
using FluentValidation;
2+
using FormCraft.DemoBlazorApp.Components.Shared;
3+
using FormCraft.DemoBlazorApp.Models;
4+
using Microsoft.AspNetCore.Components;
5+
using MudBlazor;
6+
7+
namespace FormCraft.DemoBlazorApp.Components.Pages;
8+
9+
public partial class FluentValidationDemo : ComponentBase
10+
{
11+
private CustomerModel _model = new();
12+
private IFormConfiguration<CustomerModel>? _formConfig;
13+
private bool _submitted;
14+
private bool _isSubmitting;
15+
private int _validationAttempts;
16+
private int _successfulValidations;
17+
18+
private readonly List<FormGuidelines.GuidelineItem> _sidebarFeatures =
19+
[
20+
new()
21+
{
22+
Icon = Icons.Material.Filled.Merge,
23+
Color = Color.Primary,
24+
Text = "Seamless integration with existing validators"
25+
},
26+
27+
new()
28+
{
29+
Icon = Icons.Material.Filled.Rule,
30+
Color = Color.Secondary,
31+
Text = "Support for complex validation rules"
32+
},
33+
34+
new()
35+
{
36+
Icon = Icons.Material.Filled.CloudSync,
37+
Color = Color.Tertiary,
38+
Text = "Async validation support"
39+
},
40+
41+
new()
42+
{
43+
Icon = Icons.Material.Filled.AccountTree,
44+
Color = Color.Info,
45+
Text = "Nested object validation"
46+
},
47+
48+
new()
49+
{
50+
Icon = Icons.Material.Filled.Message,
51+
Color = Color.Success,
52+
Text = "Custom error messages"
53+
},
54+
55+
new()
56+
{
57+
Icon = Icons.Material.Filled.FilterAlt,
58+
Color = Color.Warning,
59+
Text = "Conditional validation logic"
60+
}
61+
];
62+
63+
private readonly List<GuidelineItem> _apiGuidelineTableItems =
64+
[
65+
new()
66+
{
67+
Feature = "WithFluentValidation()",
68+
Usage = "Uses validator registered in DI",
69+
Example = "field.WithFluentValidation(x => x.Email)"
70+
},
71+
72+
new()
73+
{
74+
Feature = "WithFluentValidator()",
75+
Usage = "Uses specific validator instance",
76+
Example = "field.WithFluentValidator(validator, x => x.Name)"
77+
},
78+
79+
new()
80+
{
81+
Feature = "Register Validators",
82+
Usage = "Add to dependency injection",
83+
Example = "services.AddScoped<IValidator<Model>, ModelValidator>()"
84+
},
85+
86+
new()
87+
{
88+
Feature = "Combine Validators",
89+
Usage = "Mix with FormCraft validators",
90+
Example = "field.Required().WithFluentValidation(x => x.Name)"
91+
},
92+
93+
new()
94+
{
95+
Feature = "Nested Validation",
96+
Usage = "Supports SetValidator rules",
97+
Example = "RuleFor(x => x.Address).SetValidator(new AddressValidator())"
98+
},
99+
100+
new()
101+
{
102+
Feature = "Async Rules",
103+
Usage = "Supports MustAsync",
104+
Example = "RuleFor(x => x.Email).MustAsync(async (email, ct) => ...)"
105+
}
106+
];
107+
108+
protected override void OnInitialized()
109+
{
110+
// Create form configuration using FluentValidation
111+
_formConfig = FormBuilder<CustomerModel>
112+
.Create()
113+
.AddField(x => x.Name, field => field
114+
.WithLabel("Customer Name")
115+
.WithPlaceholder("Enter your full name")
116+
.WithFluentValidation(x => x.Name)
117+
.WithHelpText("Name must be between 3 and 50 characters"))
118+
.AddField(x => x.Email, field => field
119+
.WithLabel("Email Address")
120+
.WithPlaceholder("your.email@example.com")
121+
.WithFluentValidation(x => x.Email)
122+
.WithHelpText("We'll never share your email with anyone"))
123+
.AddField(x => x.Age, field => field
124+
.WithLabel("Age")
125+
.WithPlaceholder("18")
126+
.WithFluentValidation(x => x.Age)
127+
.WithHelpText("Must be 18 or older"))
128+
.Build();
129+
}
130+
131+
private async Task HandleValidSubmit(CustomerModel model)
132+
{
133+
_isSubmitting = true;
134+
_validationAttempts++;
135+
StateHasChanged();
136+
137+
// Simulate async operation
138+
await Task.Delay(1500);
139+
140+
_submitted = true;
141+
_successfulValidations++;
142+
_isSubmitting = false;
143+
StateHasChanged();
144+
}
145+
146+
private Task HandleFieldChanged((string fieldName, object? value) args)
147+
{
148+
// Track field changes if needed
149+
return Task.CompletedTask;
150+
}
151+
152+
private void ResetForm()
153+
{
154+
_model = new CustomerModel();
155+
_submitted = false;
156+
StateHasChanged();
157+
}
158+
159+
private List<FormSuccessDisplay.DataDisplayItem> GetDataDisplayItems()
160+
{
161+
return new List<FormSuccessDisplay.DataDisplayItem>
162+
{
163+
new() { Label = "Customer Name", Value = _model.Name },
164+
new() { Label = "Email Address", Value = _model.Email },
165+
new() { Label = "Age", Value = _model.Age.ToString() },
166+
new() { Label = "Validation Type", Value = "FluentValidation" },
167+
new() { Label = "Validator Class", Value = "CustomerValidator" }
168+
};
169+
}
170+
171+
private string GetCodeExample()
172+
{
173+
return @"// 1. Define your FluentValidation validator
174+
public class CustomerValidator : AbstractValidator<Customer>
175+
{
176+
public CustomerValidator()
177+
{
178+
RuleFor(x => x.Name)
179+
.NotEmpty().WithMessage(""Name is required"")
180+
.MinimumLength(3).WithMessage(""Name must be at least 3 characters"")
181+
.MaximumLength(50).WithMessage(""Name cannot exceed 50 characters"");
182+
183+
RuleFor(x => x.Email)
184+
.NotEmpty().WithMessage(""Email is required"")
185+
.EmailAddress().WithMessage(""Invalid email format"");
186+
187+
RuleFor(x => x.Age)
188+
.GreaterThanOrEqualTo(18).WithMessage(""Must be 18 or older"");
189+
}
190+
}
191+
192+
// 2. Register the validator in DI
193+
services.AddScoped<IValidator<Customer>, CustomerValidator>();
194+
195+
// 3. Use in FormCraft
196+
var formConfig = FormBuilder<Customer>
197+
.Create()
198+
.AddField(x => x.Name, field => field
199+
.WithLabel(""Customer Name"")
200+
.WithFluentValidation(x => x.Name))
201+
.AddField(x => x.Email, field => field
202+
.WithLabel(""Email Address"")
203+
.WithFluentValidation(x => x.Email))
204+
.Build();";
205+
}
206+
207+
// Model class
208+
public class CustomerModel
209+
{
210+
public string Name { get; set; } = "";
211+
public string Email { get; set; } = "";
212+
public int Age { get; set; }
213+
}
214+
215+
// FluentValidation validator
216+
public class CustomerValidator : AbstractValidator<CustomerModel>
217+
{
218+
public CustomerValidator()
219+
{
220+
RuleFor(x => x.Name)
221+
.NotEmpty().WithMessage("Name is required")
222+
.MinimumLength(3).WithMessage("Name must be at least 3 characters")
223+
.MaximumLength(50).WithMessage("Name cannot exceed 50 characters");
224+
225+
RuleFor(x => x.Email)
226+
.NotEmpty().WithMessage("Email is required")
227+
.EmailAddress().WithMessage("Please enter a valid email address");
228+
229+
RuleFor(x => x.Age)
230+
.GreaterThanOrEqualTo(18).WithMessage("You must be 18 or older");
231+
}
232+
}
233+
}

FormCraft.DemoBlazorApp/Program.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
using FormCraft;
66
using FormCraft.ForMudBlazor.Extensions;
77
using MudBlazor.Services;
8+
using FluentValidation;
9+
using FormCraft.DemoBlazorApp.Components.Pages;
810

911
var builder = WebAssemblyHostBuilder.CreateDefault(args);
1012
builder.RootComponents.Add<App>("#app");
@@ -21,6 +23,9 @@
2123
builder.Services.AddScoped<IVersionService>(sp =>
2224
new VersionService(sp.GetRequiredService<HttpClient>()));
2325

26+
// Register FluentValidation validators
27+
builder.Services.AddScoped<IValidator<FluentValidationDemo.CustomerModel>, FluentValidationDemo.CustomerValidator>();
28+
2429
// Custom field renderers are now registered by AddFormCraftMudBlazor
2530

2631
await builder.Build().RunAsync();

FormCraft.DemoBlazorApp/wwwroot/docs/api-reference.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,23 @@ Adds email format validation.
217217
.WithEmailValidation("Please enter a valid email address"))
218218
```
219219

220+
#### WithFluentValidation()
221+
Integrates FluentValidation validators registered in DI.
222+
223+
```csharp
224+
.AddField(x => x.Email, field => field
225+
.WithFluentValidation(x => x.Email))
226+
```
227+
228+
#### WithFluentValidator()
229+
Uses a specific FluentValidation validator instance.
230+
231+
```csharp
232+
var validator = new CustomerValidator();
233+
.AddField(x => x.Name, field => field
234+
.WithFluentValidator(validator, x => x.Name))
235+
```
236+
220237
### Appearance
221238

222239
#### WithLabel()

0 commit comments

Comments
 (0)