@malamute/ai-rules 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +270 -121
- package/bin/cli.js +5 -2
- package/configs/_shared/.claude/rules/conventions/documentation.md +324 -0
- package/configs/_shared/.claude/rules/conventions/git.md +265 -0
- package/configs/_shared/.claude/rules/{performance.md → conventions/performance.md} +1 -1
- package/configs/_shared/.claude/rules/conventions/principles.md +334 -0
- package/configs/_shared/.claude/rules/devops/ci-cd.md +262 -0
- package/configs/_shared/.claude/rules/devops/docker.md +275 -0
- package/configs/_shared/.claude/rules/devops/nx.md +194 -0
- package/configs/_shared/.claude/rules/domain/backend/api-design.md +203 -0
- package/configs/_shared/.claude/rules/lang/csharp/async.md +220 -0
- package/configs/_shared/.claude/rules/lang/csharp/csharp.md +314 -0
- package/configs/_shared/.claude/rules/lang/csharp/linq.md +210 -0
- package/configs/_shared/.claude/rules/lang/python/async.md +337 -0
- package/configs/_shared/.claude/rules/lang/python/celery.md +476 -0
- package/configs/_shared/.claude/rules/lang/python/config.md +339 -0
- package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/database/sqlalchemy.md +6 -1
- package/configs/_shared/.claude/rules/lang/python/deployment.md +523 -0
- package/configs/_shared/.claude/rules/lang/python/error-handling.md +330 -0
- package/configs/_shared/.claude/rules/lang/python/migrations.md +421 -0
- package/configs/_shared/.claude/rules/lang/python/python.md +172 -0
- package/configs/_shared/.claude/rules/lang/python/repository.md +383 -0
- package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/testing.md +2 -69
- package/configs/_shared/.claude/rules/lang/typescript/async.md +447 -0
- package/configs/_shared/.claude/rules/lang/typescript/generics.md +356 -0
- package/configs/_shared/.claude/rules/lang/typescript/typescript.md +212 -0
- package/configs/_shared/.claude/rules/quality/error-handling.md +48 -0
- package/configs/_shared/.claude/rules/quality/logging.md +45 -0
- package/configs/_shared/.claude/rules/quality/observability.md +240 -0
- package/configs/_shared/.claude/rules/quality/testing-patterns.md +65 -0
- package/configs/_shared/.claude/rules/security/secrets-management.md +222 -0
- package/configs/_shared/.claude/skills/analysis/explore/SKILL.md +257 -0
- package/configs/_shared/.claude/skills/analysis/security-audit/SKILL.md +184 -0
- package/configs/_shared/.claude/skills/dev/api-endpoint/SKILL.md +126 -0
- package/configs/_shared/.claude/{commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
- package/configs/_shared/.claude/{commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
- package/configs/_shared/.claude/{commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
- package/configs/_shared/.claude/skills/infra/deploy/SKILL.md +139 -0
- package/configs/_shared/.claude/skills/infra/docker/SKILL.md +95 -0
- package/configs/_shared/.claude/skills/infra/migration/SKILL.md +158 -0
- package/configs/_shared/.claude/skills/nx/nx-affected/SKILL.md +72 -0
- package/configs/_shared/.claude/skills/nx/nx-lib/SKILL.md +375 -0
- package/configs/_shared/CLAUDE.md +52 -149
- package/configs/angular/.claude/rules/{components.md → core/components.md} +69 -15
- package/configs/angular/.claude/rules/core/resource.md +285 -0
- package/configs/angular/.claude/rules/core/signals.md +323 -0
- package/configs/angular/.claude/rules/http.md +338 -0
- package/configs/angular/.claude/rules/routing.md +291 -0
- package/configs/angular/.claude/rules/ssr.md +312 -0
- package/configs/angular/.claude/rules/state/signal-store.md +408 -0
- package/configs/angular/.claude/rules/{state.md → state/state.md} +2 -2
- package/configs/angular/.claude/rules/testing.md +7 -7
- package/configs/angular/.claude/rules/ui/aria.md +422 -0
- package/configs/angular/.claude/rules/ui/forms.md +424 -0
- package/configs/angular/.claude/rules/ui/pipes-directives.md +335 -0
- package/configs/angular/.claude/settings.json +1 -0
- package/configs/angular/.claude/skills/ngrx-slice/SKILL.md +362 -0
- package/configs/angular/.claude/skills/signal-store/SKILL.md +445 -0
- package/configs/angular/CLAUDE.md +24 -216
- package/configs/dotnet/.claude/rules/background-services.md +552 -0
- package/configs/dotnet/.claude/rules/configuration.md +426 -0
- package/configs/dotnet/.claude/rules/ddd.md +447 -0
- package/configs/dotnet/.claude/rules/dependency-injection.md +343 -0
- package/configs/dotnet/.claude/rules/mediatr.md +320 -0
- package/configs/dotnet/.claude/rules/middleware.md +489 -0
- package/configs/dotnet/.claude/rules/result-pattern.md +363 -0
- package/configs/dotnet/.claude/rules/validation.md +388 -0
- package/configs/dotnet/.claude/settings.json +21 -3
- package/configs/dotnet/CLAUDE.md +53 -286
- package/configs/fastapi/.claude/rules/background-tasks.md +254 -0
- package/configs/fastapi/.claude/rules/dependencies.md +170 -0
- package/configs/{python → fastapi}/.claude/rules/fastapi.md +61 -1
- package/configs/fastapi/.claude/rules/lifespan.md +274 -0
- package/configs/fastapi/.claude/rules/middleware.md +229 -0
- package/configs/fastapi/.claude/rules/pydantic.md +433 -0
- package/configs/fastapi/.claude/rules/responses.md +251 -0
- package/configs/fastapi/.claude/rules/routers.md +202 -0
- package/configs/fastapi/.claude/rules/security.md +222 -0
- package/configs/fastapi/.claude/rules/testing.md +251 -0
- package/configs/fastapi/.claude/rules/websockets.md +298 -0
- package/configs/fastapi/.claude/settings.json +33 -0
- package/configs/fastapi/CLAUDE.md +144 -0
- package/configs/flask/.claude/rules/blueprints.md +208 -0
- package/configs/flask/.claude/rules/cli.md +285 -0
- package/configs/flask/.claude/rules/configuration.md +281 -0
- package/configs/flask/.claude/rules/context.md +238 -0
- package/configs/flask/.claude/rules/error-handlers.md +278 -0
- package/configs/flask/.claude/rules/extensions.md +278 -0
- package/configs/flask/.claude/rules/flask.md +171 -0
- package/configs/flask/.claude/rules/marshmallow.md +206 -0
- package/configs/flask/.claude/rules/security.md +267 -0
- package/configs/flask/.claude/rules/testing.md +284 -0
- package/configs/flask/.claude/settings.json +33 -0
- package/configs/flask/CLAUDE.md +166 -0
- package/configs/nestjs/.claude/rules/common-patterns.md +300 -0
- package/configs/nestjs/.claude/rules/filters.md +376 -0
- package/configs/nestjs/.claude/rules/interceptors.md +317 -0
- package/configs/nestjs/.claude/rules/middleware.md +321 -0
- package/configs/nestjs/.claude/rules/modules.md +26 -0
- package/configs/nestjs/.claude/rules/pipes.md +351 -0
- package/configs/nestjs/.claude/rules/websockets.md +451 -0
- package/configs/nestjs/.claude/settings.json +16 -2
- package/configs/nestjs/CLAUDE.md +57 -215
- package/configs/nextjs/.claude/rules/api-routes.md +358 -0
- package/configs/nextjs/.claude/rules/authentication.md +355 -0
- package/configs/nextjs/.claude/rules/components.md +52 -0
- package/configs/nextjs/.claude/rules/data-fetching.md +249 -0
- package/configs/nextjs/.claude/rules/database.md +400 -0
- package/configs/nextjs/.claude/rules/middleware.md +303 -0
- package/configs/nextjs/.claude/rules/routing.md +324 -0
- package/configs/nextjs/.claude/rules/seo.md +350 -0
- package/configs/nextjs/.claude/rules/server-actions.md +353 -0
- package/configs/nextjs/.claude/rules/state/zustand.md +6 -6
- package/configs/nextjs/.claude/settings.json +5 -0
- package/configs/nextjs/CLAUDE.md +69 -331
- package/package.json +23 -9
- package/src/cli.js +220 -0
- package/src/config.js +29 -0
- package/src/index.js +13 -0
- package/src/installer.js +361 -0
- package/src/merge.js +116 -0
- package/src/tech-config.json +29 -0
- package/src/utils.js +96 -0
- package/configs/python/.claude/rules/flask.md +0 -332
- package/configs/python/.claude/settings.json +0 -18
- package/configs/python/CLAUDE.md +0 -273
- package/src/install.js +0 -315
- /package/configs/_shared/.claude/rules/{accessibility.md → domain/frontend/accessibility.md} +0 -0
- /package/configs/_shared/.claude/rules/{security.md → security/security.md} +0 -0
- /package/configs/_shared/.claude/skills/{debug → dev/debug}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{learning → dev/learning}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{spec → dev/spec}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{review → git/review}/SKILL.md +0 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/*.Validator.cs"
|
|
4
|
+
- "**/Validators/**/*.cs"
|
|
5
|
+
- "**/Validation/**/*.cs"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# .NET Validation (FluentValidation)
|
|
9
|
+
|
|
10
|
+
## Basic Validator
|
|
11
|
+
|
|
12
|
+
```csharp
|
|
13
|
+
// Validators/CreateUserRequestValidator.cs
|
|
14
|
+
using FluentValidation;
|
|
15
|
+
|
|
16
|
+
public class CreateUserRequestValidator : AbstractValidator<CreateUserRequest>
|
|
17
|
+
{
|
|
18
|
+
public CreateUserRequestValidator()
|
|
19
|
+
{
|
|
20
|
+
RuleFor(x => x.Email)
|
|
21
|
+
.NotEmpty().WithMessage("Email is required")
|
|
22
|
+
.EmailAddress().WithMessage("Invalid email format")
|
|
23
|
+
.MaximumLength(255);
|
|
24
|
+
|
|
25
|
+
RuleFor(x => x.Name)
|
|
26
|
+
.NotEmpty().WithMessage("Name is required")
|
|
27
|
+
.Length(2, 100).WithMessage("Name must be between 2 and 100 characters")
|
|
28
|
+
.Matches(@"^[a-zA-Z\s'-]+$").WithMessage("Name contains invalid characters");
|
|
29
|
+
|
|
30
|
+
RuleFor(x => x.Password)
|
|
31
|
+
.NotEmpty()
|
|
32
|
+
.MinimumLength(8)
|
|
33
|
+
.Matches(@"[A-Z]").WithMessage("Password must contain uppercase letter")
|
|
34
|
+
.Matches(@"[a-z]").WithMessage("Password must contain lowercase letter")
|
|
35
|
+
.Matches(@"[0-9]").WithMessage("Password must contain digit")
|
|
36
|
+
.Matches(@"[^a-zA-Z0-9]").WithMessage("Password must contain special character");
|
|
37
|
+
|
|
38
|
+
RuleFor(x => x.ConfirmPassword)
|
|
39
|
+
.Equal(x => x.Password).WithMessage("Passwords do not match");
|
|
40
|
+
|
|
41
|
+
RuleFor(x => x.DateOfBirth)
|
|
42
|
+
.NotEmpty()
|
|
43
|
+
.LessThan(DateTime.Today).WithMessage("Date of birth must be in the past")
|
|
44
|
+
.Must(BeAtLeast18).WithMessage("Must be at least 18 years old");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private bool BeAtLeast18(DateTime dateOfBirth)
|
|
48
|
+
{
|
|
49
|
+
return dateOfBirth <= DateTime.Today.AddYears(-18);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Async Validation
|
|
55
|
+
|
|
56
|
+
```csharp
|
|
57
|
+
public class CreateUserRequestValidator : AbstractValidator<CreateUserRequest>
|
|
58
|
+
{
|
|
59
|
+
private readonly IUserRepository _userRepository;
|
|
60
|
+
|
|
61
|
+
public CreateUserRequestValidator(IUserRepository userRepository)
|
|
62
|
+
{
|
|
63
|
+
_userRepository = userRepository;
|
|
64
|
+
|
|
65
|
+
RuleFor(x => x.Email)
|
|
66
|
+
.NotEmpty()
|
|
67
|
+
.EmailAddress()
|
|
68
|
+
.MustAsync(BeUniqueEmail).WithMessage("Email already exists");
|
|
69
|
+
|
|
70
|
+
RuleFor(x => x.Username)
|
|
71
|
+
.NotEmpty()
|
|
72
|
+
.MustAsync(BeUniqueUsername).WithMessage("Username is taken");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private async Task<bool> BeUniqueEmail(string email, CancellationToken ct)
|
|
76
|
+
{
|
|
77
|
+
return !await _userRepository.ExistsAsync(u => u.Email == email, ct);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private async Task<bool> BeUniqueUsername(string username, CancellationToken ct)
|
|
81
|
+
{
|
|
82
|
+
return !await _userRepository.ExistsAsync(u => u.Username == username, ct);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Conditional Validation
|
|
88
|
+
|
|
89
|
+
```csharp
|
|
90
|
+
public class OrderValidator : AbstractValidator<CreateOrderRequest>
|
|
91
|
+
{
|
|
92
|
+
public OrderValidator()
|
|
93
|
+
{
|
|
94
|
+
RuleFor(x => x.ShippingAddress)
|
|
95
|
+
.NotEmpty()
|
|
96
|
+
.When(x => x.DeliveryMethod == DeliveryMethod.Shipping);
|
|
97
|
+
|
|
98
|
+
RuleFor(x => x.PickupLocation)
|
|
99
|
+
.NotEmpty()
|
|
100
|
+
.When(x => x.DeliveryMethod == DeliveryMethod.Pickup);
|
|
101
|
+
|
|
102
|
+
// Complex condition
|
|
103
|
+
When(x => x.PaymentMethod == PaymentMethod.CreditCard, () =>
|
|
104
|
+
{
|
|
105
|
+
RuleFor(x => x.CardNumber).NotEmpty().CreditCard();
|
|
106
|
+
RuleFor(x => x.ExpiryDate).NotEmpty().Must(BeValidExpiryDate);
|
|
107
|
+
RuleFor(x => x.Cvv).NotEmpty().Length(3, 4);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Otherwise
|
|
111
|
+
Otherwise(() =>
|
|
112
|
+
{
|
|
113
|
+
RuleFor(x => x.BankAccountNumber).NotEmpty();
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Collection Validation
|
|
120
|
+
|
|
121
|
+
```csharp
|
|
122
|
+
public class OrderValidator : AbstractValidator<CreateOrderRequest>
|
|
123
|
+
{
|
|
124
|
+
public OrderValidator()
|
|
125
|
+
{
|
|
126
|
+
RuleFor(x => x.Items)
|
|
127
|
+
.NotEmpty().WithMessage("Order must contain at least one item");
|
|
128
|
+
|
|
129
|
+
RuleForEach(x => x.Items)
|
|
130
|
+
.SetValidator(new OrderItemValidator());
|
|
131
|
+
|
|
132
|
+
// Custom collection rules
|
|
133
|
+
RuleFor(x => x.Items)
|
|
134
|
+
.Must(items => items.Sum(i => i.Quantity) <= 100)
|
|
135
|
+
.WithMessage("Maximum 100 items per order");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
public class OrderItemValidator : AbstractValidator<OrderItem>
|
|
140
|
+
{
|
|
141
|
+
public OrderItemValidator()
|
|
142
|
+
{
|
|
143
|
+
RuleFor(x => x.ProductId).NotEmpty();
|
|
144
|
+
RuleFor(x => x.Quantity).GreaterThan(0).LessThanOrEqualTo(10);
|
|
145
|
+
RuleFor(x => x.Price).GreaterThan(0);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Nested Objects
|
|
151
|
+
|
|
152
|
+
```csharp
|
|
153
|
+
public class CustomerValidator : AbstractValidator<Customer>
|
|
154
|
+
{
|
|
155
|
+
public CustomerValidator()
|
|
156
|
+
{
|
|
157
|
+
RuleFor(x => x.Name).NotEmpty();
|
|
158
|
+
|
|
159
|
+
RuleFor(x => x.Address)
|
|
160
|
+
.NotNull()
|
|
161
|
+
.SetValidator(new AddressValidator());
|
|
162
|
+
|
|
163
|
+
// Inline nested validation
|
|
164
|
+
RuleFor(x => x.Contact)
|
|
165
|
+
.ChildRules(contact =>
|
|
166
|
+
{
|
|
167
|
+
contact.RuleFor(c => c.Phone).NotEmpty();
|
|
168
|
+
contact.RuleFor(c => c.Email).EmailAddress();
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
public class AddressValidator : AbstractValidator<Address>
|
|
174
|
+
{
|
|
175
|
+
public AddressValidator()
|
|
176
|
+
{
|
|
177
|
+
RuleFor(x => x.Street).NotEmpty().MaximumLength(200);
|
|
178
|
+
RuleFor(x => x.City).NotEmpty().MaximumLength(100);
|
|
179
|
+
RuleFor(x => x.PostalCode).NotEmpty().Matches(@"^\d{5}(-\d{4})?$");
|
|
180
|
+
RuleFor(x => x.Country).NotEmpty().Length(2);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Custom Validators
|
|
186
|
+
|
|
187
|
+
```csharp
|
|
188
|
+
// Extensions/ValidationExtensions.cs
|
|
189
|
+
public static class ValidationExtensions
|
|
190
|
+
{
|
|
191
|
+
public static IRuleBuilderOptions<T, string> PhoneNumber<T>(
|
|
192
|
+
this IRuleBuilder<T, string> ruleBuilder)
|
|
193
|
+
{
|
|
194
|
+
return ruleBuilder
|
|
195
|
+
.Matches(@"^\+?[1-9]\d{1,14}$")
|
|
196
|
+
.WithMessage("Invalid phone number format");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
public static IRuleBuilderOptions<T, string> Slug<T>(
|
|
200
|
+
this IRuleBuilder<T, string> ruleBuilder)
|
|
201
|
+
{
|
|
202
|
+
return ruleBuilder
|
|
203
|
+
.Matches(@"^[a-z0-9]+(?:-[a-z0-9]+)*$")
|
|
204
|
+
.WithMessage("Invalid slug format");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
public static IRuleBuilderOptions<T, decimal> Currency<T>(
|
|
208
|
+
this IRuleBuilder<T, decimal> ruleBuilder)
|
|
209
|
+
{
|
|
210
|
+
return ruleBuilder
|
|
211
|
+
.GreaterThanOrEqualTo(0)
|
|
212
|
+
.PrecisionScale(18, 2, true)
|
|
213
|
+
.WithMessage("Invalid currency format");
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Usage
|
|
218
|
+
RuleFor(x => x.Phone).PhoneNumber();
|
|
219
|
+
RuleFor(x => x.Slug).Slug();
|
|
220
|
+
RuleFor(x => x.Price).Currency();
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Reusable Property Validators
|
|
224
|
+
|
|
225
|
+
```csharp
|
|
226
|
+
// Validators/PropertyValidators/UniqueEmailValidator.cs
|
|
227
|
+
public class UniqueEmailValidator<T> : AsyncPropertyValidator<T, string>
|
|
228
|
+
{
|
|
229
|
+
private readonly IUserRepository _userRepository;
|
|
230
|
+
private readonly Guid? _excludeUserId;
|
|
231
|
+
|
|
232
|
+
public UniqueEmailValidator(IUserRepository userRepository, Guid? excludeUserId = null)
|
|
233
|
+
{
|
|
234
|
+
_userRepository = userRepository;
|
|
235
|
+
_excludeUserId = excludeUserId;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
public override string Name => "UniqueEmailValidator";
|
|
239
|
+
|
|
240
|
+
public override async Task<bool> IsValidAsync(
|
|
241
|
+
ValidationContext<T> context,
|
|
242
|
+
string value,
|
|
243
|
+
CancellationToken ct)
|
|
244
|
+
{
|
|
245
|
+
if (string.IsNullOrEmpty(value)) return true;
|
|
246
|
+
|
|
247
|
+
var existingUser = await _userRepository.FindByEmailAsync(value, ct);
|
|
248
|
+
|
|
249
|
+
if (existingUser == null) return true;
|
|
250
|
+
|
|
251
|
+
return _excludeUserId.HasValue && existingUser.Id == _excludeUserId.Value;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
protected override string GetDefaultMessageTemplate(string errorCode)
|
|
255
|
+
=> "Email is already in use";
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Usage
|
|
259
|
+
RuleFor(x => x.Email).SetAsyncValidator(new UniqueEmailValidator<Request>(_userRepository));
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Registration
|
|
263
|
+
|
|
264
|
+
```csharp
|
|
265
|
+
// Program.cs or Startup.cs
|
|
266
|
+
builder.Services.AddValidatorsFromAssemblyContaining<CreateUserRequestValidator>();
|
|
267
|
+
|
|
268
|
+
// With pipeline behavior for MediatR
|
|
269
|
+
builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## MediatR Validation Behavior
|
|
273
|
+
|
|
274
|
+
```csharp
|
|
275
|
+
// Behaviors/ValidationBehavior.cs
|
|
276
|
+
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
|
|
277
|
+
where TRequest : IRequest<TResponse>
|
|
278
|
+
{
|
|
279
|
+
private readonly IEnumerable<IValidator<TRequest>> _validators;
|
|
280
|
+
|
|
281
|
+
public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
|
|
282
|
+
{
|
|
283
|
+
_validators = validators;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
public async Task<TResponse> Handle(
|
|
287
|
+
TRequest request,
|
|
288
|
+
RequestHandlerDelegate<TResponse> next,
|
|
289
|
+
CancellationToken cancellationToken)
|
|
290
|
+
{
|
|
291
|
+
if (!_validators.Any())
|
|
292
|
+
{
|
|
293
|
+
return await next();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
var context = new ValidationContext<TRequest>(request);
|
|
297
|
+
|
|
298
|
+
var validationResults = await Task.WhenAll(
|
|
299
|
+
_validators.Select(v => v.ValidateAsync(context, cancellationToken)));
|
|
300
|
+
|
|
301
|
+
var failures = validationResults
|
|
302
|
+
.SelectMany(r => r.Errors)
|
|
303
|
+
.Where(f => f != null)
|
|
304
|
+
.ToList();
|
|
305
|
+
|
|
306
|
+
if (failures.Count != 0)
|
|
307
|
+
{
|
|
308
|
+
throw new ValidationException(failures);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return await next();
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## API Endpoint Integration
|
|
317
|
+
|
|
318
|
+
```csharp
|
|
319
|
+
// Using Minimal APIs
|
|
320
|
+
app.MapPost("/users", async (
|
|
321
|
+
CreateUserRequest request,
|
|
322
|
+
IValidator<CreateUserRequest> validator,
|
|
323
|
+
IMediator mediator) =>
|
|
324
|
+
{
|
|
325
|
+
var result = await validator.ValidateAsync(request);
|
|
326
|
+
|
|
327
|
+
if (!result.IsValid)
|
|
328
|
+
{
|
|
329
|
+
return Results.ValidationProblem(result.ToDictionary());
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
var user = await mediator.Send(new CreateUserCommand(request));
|
|
333
|
+
return Results.Created($"/users/{user.Id}", user);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// Extension for automatic validation
|
|
337
|
+
public static class ValidationExtensions
|
|
338
|
+
{
|
|
339
|
+
public static async Task<IResult> ValidateAndExecute<T>(
|
|
340
|
+
this T request,
|
|
341
|
+
IValidator<T> validator,
|
|
342
|
+
Func<Task<IResult>> onValid)
|
|
343
|
+
{
|
|
344
|
+
var result = await validator.ValidateAsync(request);
|
|
345
|
+
|
|
346
|
+
return result.IsValid
|
|
347
|
+
? await onValid()
|
|
348
|
+
: Results.ValidationProblem(result.ToDictionary());
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## Anti-patterns
|
|
354
|
+
|
|
355
|
+
```csharp
|
|
356
|
+
// BAD: Business logic in validators
|
|
357
|
+
public class OrderValidator : AbstractValidator<Order>
|
|
358
|
+
{
|
|
359
|
+
public OrderValidator(IInventoryService inventory)
|
|
360
|
+
{
|
|
361
|
+
RuleFor(x => x.Items)
|
|
362
|
+
.MustAsync(async (items, ct) =>
|
|
363
|
+
{
|
|
364
|
+
await inventory.ReserveItems(items); // Side effect!
|
|
365
|
+
return true;
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// GOOD: Validators only validate, services handle business logic
|
|
371
|
+
|
|
372
|
+
// BAD: Catching exceptions in validators
|
|
373
|
+
RuleFor(x => x.Value)
|
|
374
|
+
.Must(v =>
|
|
375
|
+
{
|
|
376
|
+
try { return Parse(v); }
|
|
377
|
+
catch { return false; } // Silent failure
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// GOOD: Clear validation
|
|
381
|
+
RuleFor(x => x.Value).Must(BeValidFormat).WithMessage("Invalid format");
|
|
382
|
+
|
|
383
|
+
// BAD: Overly specific error messages exposing internals
|
|
384
|
+
.WithMessage($"Query failed: {exception.Message}");
|
|
385
|
+
|
|
386
|
+
// GOOD: User-friendly messages
|
|
387
|
+
.WithMessage("Email validation failed. Please try again.");
|
|
388
|
+
```
|
|
@@ -1,9 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"permissions": {
|
|
3
3
|
"allow": [
|
|
4
|
-
"Bash(dotnet *)",
|
|
5
|
-
"Bash(dotnet
|
|
4
|
+
"Bash(dotnet run *)",
|
|
5
|
+
"Bash(dotnet build *)",
|
|
6
|
+
"Bash(dotnet test *)",
|
|
7
|
+
"Bash(dotnet watch *)",
|
|
8
|
+
"Bash(dotnet ef *)",
|
|
9
|
+
"Bash(dotnet add *)",
|
|
10
|
+
"Bash(dotnet restore)",
|
|
11
|
+
"Bash(dotnet format *)",
|
|
12
|
+
"Read",
|
|
13
|
+
"Edit",
|
|
14
|
+
"Write"
|
|
6
15
|
],
|
|
7
|
-
"deny": [
|
|
16
|
+
"deny": [
|
|
17
|
+
"Bash(rm -rf *)",
|
|
18
|
+
"Read(appsettings.*.json)",
|
|
19
|
+
"Read(**/secrets.json)",
|
|
20
|
+
"Read(**/*.pfx)"
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
"env": {
|
|
24
|
+
"ASPNETCORE_ENVIRONMENT": "Development",
|
|
25
|
+
"DOTNET_WATCH_RESTART_ON_RUDE_EDIT": "true"
|
|
8
26
|
}
|
|
9
27
|
}
|