Onboarding guide
6. Validation
This chapter covers the small, framework-level validation kit that MMCA.Common.Application
ships so that every consuming module validates command input the same way: a set of composable
FluentValidation rule sets (RequiredStringRules<T>, EmailRules<T>, StrongPasswordRules<T>,
the six address-field rules, and the AddressValidator that assembles them), one convention
validator (CommandRequestValidator<TCommand, TRequest>)
that auto-bridges a command to its request's validator, and one failure-mapping extension
(ValidationFailureExtensions) that turns FluentValidation output
into domain Errors. These are the reusable pieces;
the per-feature validators that consume them (e.g. ADC's SessionCreateRequestValidator,
EventUpdateRequestValidator, Identity's RegisterRequestValidator) live in their own module
chapters. The external library underneath everything is FluentValidation 12 (primer §3), the
codebase does not hand-roll validation.
Where validation sits in the request lifecycle. Validation is not invoked by handlers; it is a
cross-cutting stage of the CQRS pipeline (primer §2, "CQRS"). When a command is dispatched it flows
through the decorator chain Logging → Caching → Validating → Transactional → handler. The
ValidatingCommandDecorator<TCommand, TResult>
(G05) is the gate this chapter's types feed: it resolves the first registered IValidator<TCommand>,
calls ValidateAsync, and, critically, sits before the transactional decorator so an invalid
command short-circuits before any database transaction is opened
(ValidatingCommandDecorator.cs:16-20). On failure it never calls the inner handler; it converts the
failures to errors and returns a Result failure immediately (ValidatingCommandDecorator.cs:46-55).
This is the "gate commands before they execute" charter of the group, realized as one pipeline stage
rather than scattered if (!valid) return checks in every handler. [Rubric §6, CQRS & Event-Driven]
(cross-cutting concerns belong in the pipeline) [Rubric §24, Forms, Validation & UX Safety]
(centralized, consistent input validation).
The failure-mapping boundary. FluentValidation speaks in ValidationResult / ValidationFailure;
the rest of the codebase speaks in the Result pattern
(primer §2). ValidationFailureExtensions.ToErrors is the one-line
bridge: a C# extension(ValidationResult) block (primer §4) that projects each failure into an
Error.Validation(errorCode, message, source, propertyName),
tagged with the ErrorType.Validation kind. The
decorator calls it with typeof(TCommand).Name as the source so a downstream consumer (or the API
error mapper) can see which command produced the failures (ValidatingCommandDecorator.cs:52). This
is why a validation failure surfaces to the client as an HTTP 400 with per-property messages: the
ErrorType.Validation tag is what the API layer's HandleFailure maps to 400 Bad Request. Keeping
the FluentValidation type out of the domain Error (and the Error out of FluentValidation) is a
deliberate decoupling, neither library knows about the other.
The reusable rule sets, composition over copy-paste. The eight rules in CommonValidationRules.cs
(RequiredStringRules<T>, OptionalStringRules<T>, EmailRules<T>, PositiveIntRules<T>,
PositiveDecimalRules<T>, NonNegativeIntRules<T>, PasswordRules<T>, StrongPasswordRules<T>) and
the six address rules in AddressValidationRules.cs (AddressLine1Rules<T> … CountryRules<T>) are
each a tiny AbstractValidator<T> generic over the parent type T, taking an
Expression<Func<T, …>> selector in its constructor. Because they're generic-plus-selector, the
same EmailRules<T> can validate a bare value object, a request DTO, or a command, a module composes
them with FluentValidation's Include(...) instead of re-writing the "non-empty + valid format + max
length" logic each time. Length limits are not literals in the rule: the address rules pull theirs
from AddressInvariants constants
(AddressValidationRules.cs:36), so the domain invariant and its validator agree by construction.
AddressValidator is the worked example of this composition, a single
AbstractValidator<Address> that Includes all six field rules (AddressValidationRules.cs:14-22).
[Rubric §1, SOLID] (each rule set has one responsibility; the composite assembles, never
duplicates) [Rubric §15, Best Practices & Code Quality] (no copy-pasted length limits or messages).
Convention over configuration, the request/command bridge. Most ADC commands wrap a request
record (e.g. CreateSessionCommand(CreateSessionRequest Request)) and implement
ICommandWithRequest<TRequest>. Rather
than force module authors to write a validator for both the request and the command,
CommandRequestValidator<TCommand, TRequest> is
auto-registered for every such command: it resolves the registered IValidator<TRequest> and forwards
to it via RuleFor(c => c.Request).SetValidator(...). Module authors thus write one validator,
for the request, and the command is validated for free. This wiring lives in
ScanModuleApplicationServices<TAssemblyMarker>'s
DI scan (DependencyInjection.cs:162-176): it reflects over the assembly for commands implementing
ICommandWithRequest<>, builds the closed CommandRequestValidator<TCommand, TRequest> type, and
registers it with TryAddTransient, so an explicitly-authored IValidator<TCommand> always wins
(DependencyInjection.cs:158-159 doc). The same scan also calls
AddValidatorsFromAssemblyContaining<TAssemblyMarker> (DependencyInjection.cs:156) to discover every
hand-written validator by convention. [Rubric §2, Design Patterns] (convention over configuration)
[Rubric §9, API & Contract Design] (input validation applied uniformly across every endpoint).
End-to-end flow, concretely. A request arrives at a controller, is mapped to a command, and
dispatched. The pipeline reaches ValidatingCommandDecorator, which finds the IValidator<TCommand>
the module scan registered, either a hand-written one or the auto-generated CommandRequestValidator
delegating to the request validator. That validator runs the composed rule sets (a
SessionCreateRequestValidator might Include RequiredStringRules for the title and EmailRules
for a contact field). If valid, the handler runs inside the transaction; if not, ToErrors converts
the failures to ErrorType.Validation errors and the decorator returns a failed Result without
touching the database. The errors propagate up to the API layer, which maps them to 400. Nothing
in the domain or the handler ever references FluentValidation, the validation kit is entirely an
application-layer concern, consistent with Clean Architecture's inward dependency rule (primer §1).
A note on placement and scope: all of this lives in MMCA.Common.Application so that both ADC and
Store inherit the same email/password/address rules and the same auto-validation convention without
duplicating them, the per-module validators only express domain-specific rules and reuse these
primitives. There is no separate ADR for validation; it is governed implicitly by the CQRS decorator
ADRs and the layering rules the architecture fitness tests enforce (primer §4).
RequiredStringRules<T>, OptionalStringRules<T>, EmailRules<T>, PositiveIntRules<T>, PositiveDecimalRules<T>, NonNegativeIntRules<T>, PasswordRules<T>, StrongPasswordRules<T>
MMCA.Common.Application ·
MMCA.Common.Application.Validation·MMCA.Common/Source/Core/MMCA.Common.Application/Validation/CommonValidationRules.cs· Level 0 · classes (AbstractValidator<T>subclasses)
What it is: eight reusable FluentValidation rule fragments, each enforcing exactly one field contract (a required string with a max length, a positive integer, a strong password, …). A command/request validator composes them via FluentValidation's
Include()instead of redeclaring the sameRuleForchain. Each member, with its source line and the rule chain it builds:Type File:Line Rule chain RequiredStringRules<T>CommonValidationRules.cs:12NotEmpty()+MaximumLength(maxLength)OptionalStringRules<T>CommonValidationRules.cs:24MaximumLength(maxLength)only (null/empty allowed)EmailRules<T>CommonValidationRules.cs:35NotEmpty()+EmailAddress()+MaximumLength(maxLength)PositiveIntRules<T>CommonValidationRules.cs:48GreaterThan(0)(int)PositiveDecimalRules<T>CommonValidationRules.cs:59GreaterThan(0)(decimal)NonNegativeIntRules<T>CommonValidationRules.cs:70GreaterThanOrEqualTo(0)PasswordRules<T>CommonValidationRules.cs:82NotEmpty()+MinimumLength(8)+MaximumLength(128)StrongPasswordRules<T>CommonValidationRules.cs:96all of PasswordRules+ fourMatches(...)regexes (uppercase, lowercase, digit, special char)Depends on: FluentValidation's
AbstractValidator<T>(NuGet, primer §3) andSystem.Linq.Expressions.Expression<Func<T,…>>(BCL). No first-party dependencies, these sit at the very bottom of the Application layer. Auto-wired for request types byCommandRequestValidator<TCommand, TRequest>.Concept introduced, reusable FluentValidation rule fragments via
Include(). This is the first place the guide meets FluentValidation's fragment-composition idiom. Rather than each command validator owning a longRuleFor(...)chain, a fragment is a tinyAbstractValidator<T>whose constructor builds one field's rules, and a real validator pulls it in withInclude(new RequiredStringRules<CreateSessionRequest>(r => r.Title, "Title", 200)). Two design choices make the fragment reusable across unrelated types: (1) it is generic overT(the parent type that contains the field), and (2) it takes a selector expressionExpression<Func<T, string>>rather than inheriting from the parent, so the sameEmailRules<T>validates aRegisterRequest, aLoginRequest, or a bare value object without any inheritance coupling.[Rubric §24, Forms, Validation & UX Safety]assesses whether validation is reused rather than copy-pasted across create/update paths; these fragments are the framework's answer, one definition, included everywhere.[Rubric §1, SOLID]: each fragment has a single responsibility (one field contract), and changing, say, the minimum password length is a one-line edit in one place rather than a grep-and-replace across every module.Walkthrough: every constructor is a single expression body (
=>returning the configuredRuleForchain), which is why these classes are so terse.RequiredStringRules<T>(:14) takes(selector, fieldName, maxLength)and chainsNotEmpty().WithMessage($"You must enter a {fieldName}")thenMaximumLength(maxLength).WithMessage(...). ThefieldNameis interpolated into the human-readable message, so the same fragment yields "You must enter a Title" or "You must enter a First Name".OptionalStringRules<T>(:26) drops theNotEmpty, selector isFunc<T, string?>and only the length ceiling is enforced.EmailRules<T>(:37) inserts FluentValidation's built-inEmailAddress()check betweenNotEmptyandMaximumLength.PositiveIntRules<T>/PositiveDecimalRules<T>/NonNegativeIntRules<T>(:50,:61,:72) take only(selector, fieldName), no length, and emit one comparison rule each.PasswordRules<T>(:84) takes only aselector(the message strings are fixed, not parameterised) and enforces non-empty + 8–128 length.StrongPasswordRules<T>(:98) extends that with fourMatches(...)calls whose regex literals are inline in the source:"[A-Z]","[a-z]","\\d", and"[^a-zA-Z\\d]"for uppercase, lowercase, digit, and special character respectively. The doc comment onPasswordRules(:79) explicitly points callers needing complexity atStrongPasswordRulesinstead.
Why it's built this way: these fragments are the DRY core of the validation story. Because they live in
MMCA.Common.Applicationand are generic, both ADC and Store get identical, tested field rules for free.[Rubric §33, Developer Experience]: a new request validator in any module reads as a short list ofInclude(...)calls, and a security tweak (e.g. tightening the password regex) propagates to every consumer on the next package bump rather than being missed in some forgotten validator.Where it's used: request validators throughout ADC and Store
Include()the appropriate fragment; the Conference module's*Rules<T>validators (e.g.SessionTitleRules<T>,SpeakerFirstNameRules<T>in group-18) inherit fromRequiredStringRules<T>to add domain max-length constants.PasswordRules<T>is the standard password path;StrongPasswordRules<T>gates the initial registration / set-password flow where the user chooses their own credential. Whichever validator wins, it is invoked byValidatingCommandDecorator<TCommand, TResult>in the CQRS pipeline before the command handler runs.
CommandRequestValidator<TCommand, TRequest>
MMCA.Common.Application ·
MMCA.Common.Application.Validation·MMCA.Common/Source/Core/MMCA.Common.Application/Validation/CommandRequestValidator.cs:19· Level 1 · class (sealed)
- What it is: an auto-registered
AbstractValidator<TCommand>for any command that implementsICommandWithRequest<out TRequest>. It validates the command by delegating to whateverIValidator<TRequest>is registered for the embedded request payload. - Depends on:
ICommandWithRequest<out TRequest>(Level 0; itsRequestproperty is the bridge) and FluentValidation. Registered automatically byScanModuleApplicationServices<TAssemblyMarker>()in the Application layerDependencyInjection. - Concept, convention-over-configuration validation.
[Rubric §2, Design Patterns](convention over configuration) and[Rubric §9, API & Contract Design](input validation applied uniformly at the edge). Many commands are thin wrappers carrying a request DTO, e.g.CreateSessionCommand(CreateSessionRequest Request). Without this type you would have to register a validator for both the request and the command. Instead the framework auto-bindsCommandRequestValidator<CreateSessionCommand, CreateSessionRequest>and routes its validation into the registeredIValidator<CreateSessionRequest>. The doc comment (:12-15) notes it is registered withTryAddsemantics, so an explicit, hand-written command validator the module ships always takes precedence over this generic fallback. - Walkthrough: the constructor (
CommandRequestValidator.cs:22-27) receivesIEnumerable<IValidator<TRequest>>by DI, takesFirstOrDefault(), and, only if one exists, appliesRuleFor(c => c.Request).SetValidator(validator). An empty validator collection is not an error: it simply means the command's request has no validation rules, and the generic validator becomes a no-op. The generic constraintwhere TCommand : ICommandWithRequest<TRequest>is what guarantees thec.Requestselector compiles. - Why it's built this way: it removes the most common piece of validation boilerplate (re-stating request rules at the command level) while staying overridable, so the convention never blocks a bespoke case.
- Where it's used: the closed generic is discovered and registered per command during module
scanning; at runtime
ValidatingCommandDecorator<TCommand, TResult>resolves and runs allIValidator<TCommand>instances, including this one, before the handler.
AddressLine1Rules<T>, AddressLine2Rules<T>, CityRules<T>, StateRules<T>, ZipCodeRules<T>, CountryRules<T>
MMCA.Common.Application ·
MMCA.Common.Application.Validation·MMCA.Common/Source/Core/MMCA.Common.Application/Validation/AddressValidationRules.cs· Level 4 · classes (sealed,AbstractValidator<T>)
What it is: six composable FluentValidation rule sets, one per address field, each generic over the parent type
Tand configured with a selector expression. Per member:Type File:Line Rule chain AddressLine1Rules<T>AddressValidationRules.cs:30NotEmpty()+MaximumLength(AddressInvariants.AddressLine1MaxLength)(the only required line)AddressLine2Rules<T>AddressValidationRules.cs:41MaximumLength(AddressInvariants.AddressLine2MaxLength)only (optional, nullable selector)CityRules<T>AddressValidationRules.cs:51MaximumLength(AddressInvariants.CityMaxLength)onlyStateRules<T>AddressValidationRules.cs:61MaximumLength(AddressInvariants.StateMaxLength)onlyZipCodeRules<T>AddressValidationRules.cs:71MaximumLength(AddressInvariants.ZipCodeMaxLength)onlyCountryRules<T>AddressValidationRules.cs:81MaximumLength(AddressInvariants.CountryMaxLength)onlyDepends on:
AddressInvariants(theMMCA.Common.Shared.ValueObjectsconstants that supply eachMaximumLength, imported atAddressValidationRules.cs:3) and FluentValidation. These are the same fragment shape as the Level-0 CommonValidationRules but pinned to address semantics; they sit at Level 4 because they reference theAddressInvariantsconstants rather than takingmaxLengthas a constructor argument.Concept introduced, composable validation rule sets bound to domain invariants.
[Rubric §24, Forms, Validation & UX Safety]: each rule set extendsAbstractValidator<T>and callsRuleFor(selector)once in its constructor, so the sameCityRules<T>validates a bareAddressvalue object and any request DTO that embeds loose address fields without anAddresswrapper (the class doc at:25-29calls this out explicitly).[Rubric §1, SOLID](SRP): one rule set per field, and the limit comes from a singleAddressInvariantsconstant so the max-length lives in exactly one place across the whole solution.Walkthrough: every constructor is a single
=>expression body.AddressLine1Rules<T>(:33-36) is the only one withNotEmpty(); the other five take a nullableExpression<Func<T, string?>>selector and enforceMaximumLengthonly, matching the fact that lines 2 / city / state / zip / country are optional on theAddressvalue object. EachWithMessagebakes the actual numeric limit in from its invariant constant (e.g. "City cannot be longer than {AddressInvariants.CityMaxLength} characters").Why it's built this way: different commands (create event, update profile, register) all carry address fields; each can
Include(new CityRules<CreateEventRequest>(p => p.City))without copy-pasting the length limit, and a change to the canonical limit flows throughAddressInvariants.Where it's used: assembled by
AddressValidatorfor theAddressvalue object, and included directly by module request validators in ADC and Store that embed address fields.
AddressValidator
MMCA.Common.Application ·
MMCA.Common.Application.Validation·MMCA.Common/Source/Core/MMCA.Common.Application/Validation/AddressValidationRules.cs:12· Level 5 · class (sealed)
- What it is: a composite
AbstractValidator<Address>that validates the wholeAddressvalue object byInclude()-ing the six field-level rule sets at Level 4. - Depends on:
Address(the validated type) and the six field rule setsAddressLine1Rules<T>…CountryRules<T>. - Concept: composition over duplication: the address validator owns no
RuleForchains of its own; its entire body (AddressValidationRules.cs:14-22) is sixInclude(new XRules<Address>(p => p.Field))calls binding each rule set to the correspondingAddressproperty.[Rubric §15, Best Practices](no copy-paste validation; each field's rules are isolated and independently testable, seeAddressValidationRulesTestsin group-25).[Rubric §24, Forms, Validation & UX Safety]: this lives inMMCA.Common.Applicationso both ADC and Store reuse one framework-level address validator rather than each authoring its own. - Caveats / not-in-source: the prior tier-05 edition described the six rule sets as "derived from
RequiredStringRules<T>"; that is stale. As of the current source each ofAddressLine1Rules<T>…CountryRules<T>extendsAbstractValidator<T>directly (AddressValidationRules.cs:30-87), notRequiredStringRules<T>, they share only the shape, not an inheritance chain. - Where it's used: referenced by command validators that carry an address (address-update flows in
ADC and Store); like every validator here it is invoked by
ValidatingCommandDecorator<TCommand, TResult>in the CQRS pipeline before the handler executes.
ValidationFailureExtensions
MMCA.Common.Application ·
MMCA.Common.Application.Extensions·MMCA.Common/Source/Core/MMCA.Common.Application/Extensions/ValidationFailureExtensions.cs:9· Level 2 · class (static)
What it is: a static class wrapping a C#
extension(ValidationResult)block that adds one method,ToErrors(string source), which converts FluentValidation'sValidationFailureentries into domainErrorinstances. It is the single adapter that lets FluentValidation output flow into the codebase's Result pattern.Depends on:
Error(Level 1) and itsErrorType.Validationclassification;FluentValidation.Results(ValidationResult,ValidationFailure, NuGet).Concept introduced, bridging FluentValidation to the Result pattern.
[Rubric §15, Best Practices & Code Quality](assesses consistent, idiomatic conventions over one-off code; here the convention is validators produce domainErrors, never raw strings or thrown exceptions). FluentValidation validators (AbstractValidator<T>) produce aValidationResultwhoseErrorsare framework-specificValidationFailureobjects; the application pipeline needs those surfaced as aResultfailure carrying domainErrors. This extension is the only boundary where that translation happens, so the rest of the pipeline depends on neither FluentValidation's failure shape nor a hand-rolled mapping. It also touches[Rubric §9, API & Contract Design](assesses uniform, standardized error responses): because each failure becomes anErrorType.Validationerror (which the API layer maps to HTTP 400), validation failures look identical at the boundary regardless of which validator raised them.Walkthrough: the file is a static class (
:9) holding a singleextension(ValidationResult result)block (:11). Inside it,ToErrors(string source)(:19) projectsresult.Errorswith a LINQSelect(:20) into oneError.Validation(...)call per failure (:21), passing four arguments in order: the failure'sErrorCode→ the errorcode, itsErrorMessage→ the errormessage, the caller-suppliedsource→ the errorsource, and itsPropertyName→ the errortarget. (Error.Validationis declared asValidation(string code, string message, string? source = null, string? target = null)inMMCA.Common.Shared/Abstractions/Error.cs:37, soPropertyNamelands in thetargetslot, identifying which field failed.) The method returnsIEnumerable<Error>lazily; the caller materializes it.Why it's built this way: the C#
extension(T)syntax (see primer §4) lets the conversion read as a natural method onValidationResult(result.ToErrors("X")) without subclassing FluentValidation or adding a static helper that consumers must remember to call. It keeps the cross-cutting mapping co-located with its purpose and out of bothFluentValidationandError. LazyIEnumerable<Error>defers the projection until the caller enumerates (the decorator immediately.ToList()s it).Where it's used: the sole call site is
ValidatingCommandDecorator<TCommand, TResult>(MMCA.Common.Application/UseCases/Decorators/ValidatingCommandDecorator.cs:52), which runs the command'sCommandRequestValidator<TCommand, TRequest>and, on failure, callsvalidationResult.ToErrors(typeof(TCommand).Name).ToList(), passing the command type name assource, then returns those errors as aResultfailure, short-circuiting the pipeline before the concrete handler runs.Caveats / not-in-source: the failure's
ErrorCodeis FluentValidation's per-rule code (e.g."NotEmptyValidator") unless a validator overrides it with.WithErrorCode(...); this extension passes it through verbatim and does not normalize or validate it.
⬅ CQRS: Commands, Queries & the Decorator Pipeline • Index • Persistence & EF Core ➡