FluentValidation for gRPC requests with Calzolari integration. Covers AbstractValidator for proto requests, resource-based messages, and pre-handler validation. Trigger: gRPC validation, FluentValidation, Calzolari, request validation.
From dotnet-ai-kitnpx claudepluginhub faysilalshareef/dotnet-ai-kit --plugin dotnet-ai-kitThis skill uses the workspace's default tool permissions.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Compares coding agents like Claude Code and Aider on custom YAML-defined codebase tasks using git worktrees, measuring pass rate, cost, time, and consistency.
AbstractValidator<TRequest> validates gRPC requests before handlers executeStatusCode.InvalidArgument with detailsPhrases.xxx) for localizationAddAppValidators() scans assembly for all validatorsnamespace {Company}.{Domain}.Grpc.Validators;
public sealed class CreateOrderRequestValidator
: AbstractValidator<CreateOrderRequest>
{
public CreateOrderRequestValidator()
{
RuleFor(x => x.CustomerName)
.NotEmpty()
.WithMessage(Phrases.CustomerNameRequired)
.MaximumLength(200)
.WithMessage(Phrases.CustomerNameTooLong);
RuleFor(x => x.Total)
.GreaterThan(0)
.WithMessage(Phrases.InvalidTotal);
RuleFor(x => x.Items)
.NotEmpty()
.WithMessage(Phrases.ItemsRequired);
RuleForEach(x => x.Items).ChildRules(item =>
{
item.RuleFor(x => x.ProductId)
.NotEmpty()
.WithMessage(Phrases.ProductIdRequired);
item.RuleFor(x => x.Quantity)
.GreaterThan(0)
.WithMessage(Phrases.InvalidQuantity);
item.RuleFor(x => x.UnitPrice)
.GreaterThan(0)
.WithMessage(Phrases.InvalidUnitPrice);
});
}
}
namespace {Company}.{Domain}.Grpc.Validators;
public sealed class UpdateOrderRequestValidator
: AbstractValidator<UpdateOrderRequest>
{
public UpdateOrderRequestValidator()
{
RuleFor(x => x.OrderId)
.NotEmpty()
.Must(BeValidGuid)
.WithMessage(Phrases.InvalidOrderId);
// Conditional validation
When(x => x.CustomerName is not null, () =>
{
RuleFor(x => x.CustomerName.Value)
.NotEmpty()
.MaximumLength(200);
});
}
private static bool BeValidGuid(string value)
=> Guid.TryParse(value, out _);
}
// Phone number validation (Libyan format)
public sealed class PhoneValidator : AbstractValidator<string>
{
public PhoneValidator()
{
RuleFor(x => x)
.Matches(@"^0(91|92|93|94|95)\d{7}$")
.WithMessage(Phrases.InvalidPhoneNumber);
}
}
namespace {Company}.{Domain}.Grpc;
public static class ValidationRegistration
{
public static IServiceCollection AddAppValidators(
this IServiceCollection services)
{
// Calzolari gRPC validation integration
services.AddGrpcValidation();
// Scan assembly for all validators
services.AddValidatorsFromAssemblyContaining<CreateOrderRequestValidator>();
return services;
}
}
// In Program.cs
builder.Services.AddGrpc(options =>
{
options.EnableMessageValidation();
});
builder.Services.AddAppValidators();
When validation fails, Calzolari returns:
StatusCode: InvalidArgument
Detail: "Validation failed"
Trailers:
- validation-errors-text: JSON array of validation failures
[
{ "propertyName": "CustomerName", "errorMessage": "Customer name is required" },
{ "propertyName": "Total", "errorMessage": "Total must be greater than 0" }
]
// For cases needing manual validation control
public sealed class OrderCommandsService(
IMediator mediator,
IValidator<CreateOrderRequest> validator)
: OrderCommands.OrderCommandsBase
{
public override async Task<CreateOrderResponse> CreateOrder(
CreateOrderRequest request, ServerCallContext context)
{
var validationResult = await validator.ValidateAsync(request);
if (!validationResult.IsValid)
{
var errors = validationResult.Errors
.Select(e => $"{e.PropertyName}: {e.ErrorMessage}");
throw new RpcException(new Status(
StatusCode.InvalidArgument,
string.Join("; ", errors)));
}
var command = request.ToCommand();
var output = await mediator.Send(command);
return output.ToCreateResponse();
}
}
| Anti-Pattern | Correct Approach |
|---|---|
| Hardcoded error messages | Use resource strings (Phrases.xxx) |
| Validation in handler instead of validator | Validate at gRPC boundary |
| Missing validator for a request type | Every request needs a validator |
| Not using ChildRules for repeated | Use RuleForEach with ChildRules |
| Catching validation exceptions | Let Calzolari handle the response |
# Find validators
grep -r "AbstractValidator<.*Request" --include="*.cs" src/
# Find Calzolari registration
grep -r "AddGrpcValidation\|EnableMessageValidation" --include="*.cs" src/
# Find AddAppValidators
grep -r "AddAppValidators\|AddValidatorsFrom" --include="*.cs" src/
# Find resource strings in validators
grep -r "Phrases\." --include="*.cs" src/Grpc/Validators/
Grpc/Validators/ directory matching request namePhrases resource for error messagesAddAppValidators() is called (scans assembly automatically)