Skip to content

feat(core): add Form Request classes for typed binding + validation per endpoint#215

Merged
antosubash merged 6 commits into
mainfrom
worktree-form-requests
May 24, 2026
Merged

feat(core): add Form Request classes for typed binding + validation per endpoint#215
antosubash merged 6 commits into
mainfrom
worktree-form-requests

Conversation

@antosubash
Copy link
Copy Markdown
Owner

Summary

  • Adds Laravel-style FormRequest<TSelf> base class that bundles parameter binding, authorization, validation rules (RuleConfigurator<T> wrapping FluentValidation), and data normalization (Prepare()) into a single class
  • Source generator discovers [FormRequest] types, enforces SM0056 (sealed) and SM0057 (extends base), auto-generates TypeScript interfaces, and applies FormRequestEndpointFilter to all module route groups
  • Pipeline: Bind → Authorize (403) → Prepare → Validate (422) → Handler
  • Converts 4 Settings endpoints and 1 Email endpoint as reference implementations
  • 1,204 tests pass across 19 assemblies, 0 failures

What changed

  • Core framework (SimpleModule.Core/FormRequests/): FormRequest, FormRequest<TSelf>, FormRequestAttribute, RuleConfigurator<T>, FormRequestEndpointFilter, FormRequestExtensions
  • Shared utility (SimpleModule.Core/Inertia/InertiaErrorResult): extracted from duplicated code in GlobalExceptionHandler and the filter
  • Source generator: FormRequestFinder, diagnostics (SM0056/SM0057), DTO/TypeScript generation for [FormRequest] types, filter wiring on route groups
  • Email module: CreateTemplateFormRequest + endpoint conversion (reference implementation)
  • Settings module: UpdateSettingFormRequest, UpdateMySettingFormRequest, CreateMenuItemFormRequest, UpdateMenuItemFormRequest + 4 endpoint conversions with 16 integration tests
  • Documentation: CONSTITUTION.md updated with FormRequest rules and diagnostics

Closes #163

Test plan

  • dotnet build — 0 errors
  • dotnet test — 1,204 tests pass, 0 failures
  • Architecture review: filter coverage, auth metadata, validator caching, escape hatch behavior
  • Code review (2 rounds): regex correctness, DTO dedup, scope separation, cascade fix
  • Reviewer: POST invalid JSON to /api/settings → expect 422 with field errors
  • Reviewer: POST valid JSON to /api/settings/menus → expect 201 with Location header
  • CI is green

…er endpoint (#163)

Introduces Laravel-style FormRequest<TSelf> base class that bundles parameter
binding, authorization, validation rules, and data normalization into a single
class. The endpoint handler receives an already-valid request object.

Pipeline: Bind → Authorize (403) → Prepare → Validate (422) → Handler

- FormRequest base + [FormRequest] attribute in SimpleModule.Core
- Source generator discovers [FormRequest] types, validates shape (SM0056/SM0057),
  emits TypeScript interfaces (same path as [Dto])
- FormRequestEndpointFilter auto-applied to all module route groups
- FluentValidation under the hood via RuleConfigurator<T>
- Validator cached per type for performance
- 422 Unprocessable Entity with RFC 7807 problem+json for validation failures
- Email module CreateTemplateEndpoint refactored as reference implementation
- 16 new xUnit tests covering binding, validation, authorization, and prepare hooks
- Constitution updated with FormRequest rules and SM0056/SM0057 diagnostics
…tia errors, testability

- Fix C3: modules without RoutePrefix now get a group with AddFormRequestFilter()
  instead of mapping directly to app (no-prefix endpoints were silently skipping
  the FormRequest validation pipeline)
- Fix C1: ConfigureEndpoints escape hatch now wraps in a group with
  AddFormRequestFilter() so FormRequest types work in escape-hatch modules
- Fix M4: extract shared InertiaErrorResult utility used by both
  FormRequestEndpointFilter and GlobalExceptionHandler (removes duplication)
- Fix m2: add public ValidateRulesAsync() method on FormRequest so consumers
  can unit-test their validation rules without constructing filter context
- Update generator tests to match new generated code shape
- Fix duplicate TS interfaces when both [Dto] and [FormRequest] on same class
  (DtoFinder now deduplicates FQNs before the [FormRequest] scan)
- Restore .RequirePermission() on CreateTemplateEndpoint for ASP.NET auth
  metadata visibility (OpenAPI/Swagger, policy-based audit); remove redundant
  FormRequest.Authorize() override from CreateTemplateFormRequest
- Revert escape-hatch ConfigureEndpoints to receive WebApplication (not
  RouteGroupBuilder) — avoids breaking contract for modules that cast to
  IApplicationBuilder
- ValidateRulesAsync now calls Prepare() before validation, consistent with
  the filter pipeline
- Add When/Unless forwarding to RuleConfigurator for grouped conditional rules
- Replace ConcurrentDictionary validator cache with volatile + Interlocked
  (single-entry-per-type, simpler, avoids redundant dictionary overhead)
Convert 4 Settings module endpoints to use FormRequest for input validation:
- UpdateSettingEndpoint (PUT /api/settings) — validates key format, scope enum
- UpdateMySettingEndpoint (PUT /api/settings/me) — reuses UpdateSettingFormRequest
- CreateMenuItemEndpoint (POST /api/settings/menus) — validates label, URL length
- UpdateMenuItemEndpoint (PUT /api/settings/menus/{id}) — validates same as create

Demonstrates dual-validation: FormRequest validates shape (key format, field
lengths, enum membership), service layer validates domain (setting type/range
against definitions). FormRequest returns 422, service exceptions return 400.

Symmetric validation: both Create and Update menu endpoints now share the same
rules; both admin and user setting endpoints use the same FormRequest.

Includes 16 new integration tests verifying 422 responses, Prepare normalization,
RFC 7807 shape, and auth-before-validation ordering.
…ntics

- Fix regex to allow camelCase/PascalCase keys (email.defaultFromAddress,
  FileStorage.MaxFileSizeMb, etc.) and enforce segment-based validation that
  rejects trailing dots and empty segments
- Create dedicated UpdateMySettingFormRequest (Key + Value only) for
  PUT /api/settings/me — removes misleading Scope field that was validated
  but silently ignored
- Fix double error messages for empty key by splitting rules with .When()
  guard so regex only fires on non-empty keys
- Rename misleading test: NullValue "clears setting" → actually stores
  empty string (documents real behavior, not aspirational)
- Add tests for camelCase keys and trailing-dot rejection
Upstream PR #211 changed UpdateSettingRequest.Value from string? to
JsonElement and added .RequirePermission(SettingsPermissions.Update).
Update FormRequest integration tests to:
- Use JsonSerializer.Deserialize<JsonElement>() for Value fields
- Pass SettingsPermissions.Update to CreateAuthenticatedClient
- Replace hyphenated test keys with camelCase (regex rejects hyphens)
@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying simplemodule-website with  Cloudflare Pages  Cloudflare Pages

Latest commit: e7e84dc
Status: ✅  Deploy successful!
Preview URL: https://aa4cd267.simplemodule-website.pages.dev
Branch Preview URL: https://worktree-form-requests.simplemodule-website.pages.dev

View logs

@antosubash antosubash merged commit 5121d06 into main May 24, 2026
6 checks passed
@antosubash antosubash deleted the worktree-form-requests branch May 24, 2026 19:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Form Request classes (typed binding + validation per endpoint)

1 participant