Onboarding guide
18. ADC Conference - Application & Use Cases
What this chapter covers. This is the application layer of the Conference module, the largest
single application assembly in the codebase (this group holds 211 types). It sits between the
REST/gRPC edge (G20, Conference API & gRPC) and the domain
aggregates (G17, Conference Domain), and it is where the conference's
use cases actually live: create an event, publish it, add a room or a speaker, import the whole
agenda from Sessionize.com, export the schedule as an .ics calendar, answer "what is happening right
now", and run the AI-assisted analytics that help organizers decide which session proposals to accept.
Everything here is engine-agnostic and framework-light: it depends on the abstractions introduced
by MMCA.Common.Application (handlers, mappers, validators, query services, navigation populators)
and on the Conference domain, but never on EF Core, ASP.NET, or a broker SDK directly. Read the
primer's tour of CQRS and Vertical Slice
first; this chapter shows those styles at full scale in one module.
The vertical-slice anatomy of a use case
Open any feature folder under Sessions/UseCases/, Events/UseCases/, Speakers/UseCases/,
Categories/UseCases/, or Questions/UseCases/ and you will find the same cohesive slice: a
command or query record, its handler, its FluentValidation validator, and (for creates) a request
record plus a request mapper, all co-located. Adding a feature means adding a folder, not threading an
edit through horizontal Services/, Validators/, and Repositories/ directories. This is the
Vertical Slice discipline made
physical. [Rubric §5, Vertical Slice] (assesses whether a feature is one navigable unit rather than
scattered horizontally), and the folder layout is the evidence.
A command mutates and returns a Result; a query
is side-effect-free and returns a Result<TDTO>. Both implement the Common contracts
ICommandHandler<in TCommand, TResult>
and IQueryHandler<in TQuery, TResult>,
so every handler in this assembly flows through the same decorator pipeline (Logging, Caching,
Transactional, then the handler) without knowing it exists. Commands that change cached read data
implement ICacheInvalidating; commands that must be
atomic implement ITransactional. The controller injects
the handler interface and calls HandleAsync; the concrete type is invisible to it. [Rubric §6, CQRS & Event-Driven] (assesses a clean command/query split through well-defined handler boundaries):
this module is the canonical demonstration, dozens of single-responsibility handlers, each one slice
wide, all dispatched uniformly.
The CRUD-shaped handlers (CreateEventHandler,
CreateSessionHandler, CreateSpeakerHandler,
CreateQuestionHandler,
CreateConferenceCategoryHandler, and the matching
Update*/Delete*/Add*/Remove* families) share one shape: they delegate object construction to an
IEntityRequestMapper<TEntity, TCreateRequest, TIdentifierType>
(which runs the validator and the domain Create(...) factory, returning Result<TEntity>), then
persist through IUnitOfWork and map the saved entity
back to a DTO with an
IEntityDTOMapper<TEntity, TEntityDTO, TIdentifierType>.
The handler owns orchestration only (load, validate, persist, map, log) while the business rule
("an event's end date cannot precede its start date") lives in the domain factory and the invariant
classes. The richer handlers add what genuinely needs orchestration context:
UpdateSessionHandler and UpdateEventHandler stamp
the client's concurrency token before mutating
(MMCA.ADC.Conference.Application/Sessions/UseCases/Update/UpdateSessionHandler.cs:34,
MMCA.ADC.Conference.Application/Events/UseCases/Update/UpdateEventHandler.cs:33) and reject
immutable-field edits with Error.UnprocessableEntity
(UpdateSessionHandler.cs:39), and both the create and update session paths run a server-side
double-booking check through SessionRoomScheduling.BuildOverlapPredicate
(MMCA.ADC.Conference.Application/Sessions/Validation/SessionRoomScheduling.cs:26,
called at CreateSessionHandler.cs:99 and UpdateSessionHandler.cs:120), which builds a
SQL-translatable half-open interval overlap predicate so back-to-back sessions in one room do not
collide (SessionRoomScheduling.cs:36-39).
Manual mapping, validation rule fragments, and authorization specifications
Three sibling families recur across every aggregate. DTO mappers
(SessionDTOMapper, EventDTOMapper,
SpeakerDTOMapper, RoomDTOMapper,
CategoryItemDTOMapper, and the question-answer / category-item link
mappers) implement the Common mapper contract and assign each field by hand, the deliberate choice
of ADR-001 (manual/Mapperly mapping over reflection-based AutoMapper) so a renamed property is a
compile error, not a silent null. [Rubric §9, API & Contract Design] (assesses explicit, traceable
contracts): the mapping is code you can read and test, not convention magic.
Validation is composed, not inherited. Small generic rule fragments
(EventDateRangeRules<T>, EventNameRules<T>,
RoomCapacityRules<T>, SessionTitleRules<T>,
SpeakerFirstNameRules<T>,
CategoryItemNameRules<T>, and a dozen siblings) each encapsulate one
validated concern behind a property selector: the plain string ones subclass the framework's
RequiredStringRules<T> and pass the domain's
max-length invariant through (MMCA.ADC.Conference.Application/Events/Validation/EventValidationRules.cs:13-17),
while the ones with real logic derive from AbstractValidator<T> directly, such as
EventTimeZoneRules<T>, which additionally proves the value is a resolvable
IANA identifier (EventValidationRules.cs:34-48, BR-87). The per-use-case validators
(EventUpdateRequestValidator,
SessionCreateRequestValidator, and the rest) pull the fragments
together with FluentValidation's Include(...) and add only what is local to the request
(MMCA.ADC.Conference.Application/Events/UseCases/Update/EventUpdateRequestValidator.cs:11-18).
EventDateRangeRules<T> is the richest, compiling the StartDate selector
into a delegate (EventValidationRules.cs:68) and reading it inside a cross-property Must on
EndDate (EventValidationRules.cs:69-71). The pattern mirrors the framework's rule-fragment
families in G06, Validation. [Rubric §24, Forms, Validation & UX Safety] and [Rubric §1, SOLID] (fragments compose without an inheritance chain; a
new constraint is a new fragment, touching no existing validator).
Authorization specifications (PublishedEventSpecification,
OwnEventQuestionAnswerSpecification,
OwnSessionQuestionAnswerSpecification) are
ISpecification<TEntity, TIdentifierType>
implementations passed into the query services to scope what a caller may read (an anonymous visitor
sees only published events; a speaker sees only their own answers). This keeps the authorization
predicate a reusable, testable expression rather than an if buried in a controller. [Rubric §11, Security] (assesses authorization that is data-scoped, not just endpoint-gated). The public-session
visibility rule is produced by the GetPublicSessionFilterHandler
use case via the framework's
CrossSourceSpecification helper
(MMCA.ADC.Conference.Application/Sessions/UseCases/GetPublicSessionFilter/GetPublicSessionFilterHandler.cs:26-33),
because Session may live in a different data source from Event (ADR-018): the helper resolves the
published Event ids and returns a translatable Session.EventId IN (...) filter, ANDed with the
local status check that excludes declined and cancelled sessions
(GetPublicSessionFilterHandler.cs:31), so the published-event check is no longer a navigation join.
Query services, navigation populators, and the composition root
Read paths do not get bespoke handlers for the common cases; they go through the framework's generic
IEntityQueryService<TEntity, TEntityDTO, TIdentifierType>,
which supplies filtering, sorting, paging, and field projection. The one local specialization is
SpeakerEntityQueryService, a thin subclass that overrides only the
DTO-to-entity property map so API consumers can sort and filter on the computed FullName while the
pipeline translates it to (FirstName + " " + LastName)
(MMCA.ADC.Conference.Application/Speakers/SpeakerEntityQueryService.cs:28-34). Eager-loading of
child graphs is delegated to per-aggregate
INavigationPopulator<in TEntity>
implementations (EventNavigationPopulator,
SessionNavigationPopulator,
SpeakerNavigationPopulator,
ConferenceCategoryNavigationPopulator), which encapsulate
which navigations to include and how to batch-load cross-source relationships (ADR-002); child
entities and the childless Question aggregate use the framework's
NullNavigationPopulator<TEntity>
because they are never the root of a full graph load
(MMCA.ADC.Conference.Application/DependencyInjection.cs:61-88).
All of this is wired by DependencyInjection, the module's composition
root (MMCA.ADC.Conference.Application/DependencyInjection.cs:32, with the registration surface
exposed as a C# extension(IServiceCollection) member at DependencyInjection.cs:34-36). It
explicitly binds the closed generics Scrutor cannot infer (the cascade-deletion domain service, then
each aggregate's navigation populator, query service, and delete handler, plus the two cross-module
validation services, DependencyInjection.cs:41-94) and then calls
ScanModuleApplicationServices<ClassReference>() (DependencyInjection.cs:98) to discover the "many
small things" (every handler, mapper, validator, and event handler) by convention.
AssemblyReference and ClassReference are the marker types
that anchor that scan. [Rubric §7, Microservices Readiness] (assesses clean, explicit module
boundaries): every internal concrete type is reachable only through an interface registered here,
which is exactly what lets the Conference module boot as its own service host. [Rubric §33, Developer Experience]: one file is the single place a new aggregate gets registered.
Event-driven reactions: domain and integration handlers
The application layer is also where the module reacts to events. Domain event handlers implement
IDomainEventHandler<in TDomainEvent>
and run in-process after the aggregate's SaveChangesAsync. Two of the three are deliberately
observability-only: SessionCreatedHandler filters SessionChanged down to
the Added state and writes one structured log line
(MMCA.ADC.Conference.Application/Sessions/DomainEventHandlers/SessionCreatedHandler.cs:17-21), and
RoomChangedHandler logs every room add/update/delete
(MMCA.ADC.Conference.Application/Events/DomainEventHandlers/RoomChangedHandler.cs:17), which is the
[Rubric §13, Observability & Operability] story: the event stream is where lifecycle telemetry is
emitted, not the entity. SpeakerDeletedHandler is the one with a real side
effect: on a Deleted state with a previously linked user it opens its own DI scope (the handler is a
singleton) and publishes SpeakerUnlinkedFromUser so Identity can clear User.LinkedSpeakerId
(BR-70, MMCA.ADC.Conference.Application/Speakers/DomainEventHandlers/SpeakerDeletedHandler.cs:38-45).
The integration event handler UserRegisteredHandler implements
IIntegrationEventHandler<in TIntegrationEvent>
and is the cross-module boundary in the other direction: when Identity publishes UserRegistered over
the broker, Conference auto-links a speaker to that user (BR-207). It tries an email match first and
falls back to a unique-name match for Sessionize-imported speakers whose Email is null (the public
Sessionize feed omits PII), linking only when exactly one unlinked candidate matches
(MMCA.ADC.Conference.Application/Users/IntegrationEventHandlers/UserRegisteredHandler.cs:58-64,
with the ambiguity guard at :169-173), and publishes SpeakerLinkedToUser back to Identity on a hit
(UserRegisteredHandler.cs:102-104). The handler is deliberately best-effort: it runs in its own DI
scope because the handler is a singleton (UserRegisteredHandler.cs:52), and any non-cancellation
failure is swallowed so the already-committed registration never rolls back
(UserRegisteredHandler.cs:108-113). Publishing flows through the
IIntegrationEventPublisher abstraction and
the outbox (ADR-003), so the application code never references MassTransit. [Rubric §6, CQRS & Event-Driven] and [Rubric §7, Microservices Readiness]: the module collaborates through events and
interfaces, never direct cross-module type references.
Two in-process services close the loop with the Engagement module.
SessionBookmarkValidationService
(MMCA.ADC.Conference.Application/Sessions/SessionBookmarkValidationService.cs:12) and
EventLiveValidationService
(MMCA.ADC.Conference.Application/Events/EventLiveValidationService.cs:18) implement the
Engagement-facing contracts
ISessionBookmarkValidationService
and IEventLiveValidationService, which
Engagement calls via gRPC when the modules run as separate services (ADR-007): the former gates
session bookmarking (BR-49/BR-91), the latter computes an event or session live window in UTC from the
event's dates and IANA time zone, and the session variant adds the assigned speakers (BR-236), the
plenum flag, and the event's question-moderation default (BR-233)
(EventLiveValidationService.cs:21-59). The traffic also runs the other way:
GetSessionBookmarkCountsHandler re-verifies server-side that
every requested session really belongs to the speaker before delegating the counting to Engagement's
batched IBookmarkCountService, silently
dropping ids that are not the speaker's rather than failing the whole batch
(MMCA.ADC.Conference.Application/Speakers/UseCases/GetSessionBookmarkCounts/GetSessionBookmarkCountsHandler.cs:33-45),
which is [Rubric §11, Security] (never trust the client's id list) and [Rubric §12, Performance & Scalability] (one cross-service round-trip instead of one per session) in a single handler.
Attendee-facing read models: calendar export and Now/Next
A small cluster of queries serves the public schedule surfaces without going through the generic query
service, because their output is not a DTO list.
ExportEventCalendarHandler and
ExportSessionCalendarHandler return a Result<string> holding an
.ics document: the event variant loads the event with its rooms, refuses unpublished or unknown
events with Error.NotFound, and turns every exportable session (scheduled, non-service, not
declined/cancelled) into one VEVENT with the room as its location
(MMCA.ADC.Conference.Application/Sessions/UseCases/ExportCalendar/ExportEventCalendarHandler.cs:25-40),
with CalendarExportMapper doing the entity-to-entry shaping.
GetNowNextHandler builds the conference-day "happening now plus next up"
snapshot for one published event or, when no id is given, the auto-selected current-or-next published
event using the domain's CurrentEventSelector
rule, reusing the same eligibility rules and DST-aware wall-clock conversion as the calendar export
(MMCA.ADC.Conference.Application/Sessions/UseCases/NowNext/GetNowNextHandler.cs:20-41).
GetNowNextHandler injects TimeProvider rather than reading the clock directly
(GetNowNextHandler.cs:20-22), which is what makes its "now" unit-testable at a fixed instant; the two
export handlers instead stamp the .ics DTSTAMP from DateTimeOffset.UtcNow directly
(ExportEventCalendarHandler.cs:50, ExportSessionCalendarHandler.cs:51), so their timestamp is not
injectable. [Rubric §14, Testability].
The Sessionize import: Strategy-pattern orchestration
The single most involved use case is importing a conference agenda from Sessionize.com. Sessionize
returns one JSON payload covering five interdependent entity families (categories, rooms, questions,
speakers, and sessions, where sessions reference rooms and speakers and speakers reference categories).
The HTTP shape is captured by a set of deserialization records
(SessionizeResponse, SessionizeSession,
SessionizeSpeaker, SessionizeRoom,
SessionizeCategory, SessionizeCategoryItem,
SessionizeQuestion,
SessionizeQuestionAnswer, SessionizeLink)
confined entirely to this layer, an anti-corruption boundary so a Sessionize API change never
touches a domain entity.
The import is fetched through the ISessionizeService port (implemented by the
HTTP client in G19, Conference Infrastructure)
and orchestrated by RefreshFromSessionizeHandler
(MMCA.ADC.Conference.Application/Events/UseCases/RefreshFromSessionize/RefreshFromSessionizeHandler.cs:16).
That handler does orchestration only: load the Event with its rooms and speakers eagerly
(:41-45), enforce BR-6 (a Sessionize code must be configured, :54) and the BR-63 five-minute
throttle (:64-65), call the external API inside a try/catch (HttpRequestException) that converts
a network failure into an Error (:80-87), then run five
ISessionizeSyncStrategy implementations in dependency order
(CategorySyncStrategy, RoomSyncStrategy,
QuestionSyncStrategy, SpeakerSyncStrategy,
SessionSyncStrategy, :25-32), each carrying its work via a shared
SessionizeSyncContext and returning a
SessionizeSyncResult count. An empty response is treated as success rather
than an error (:90-105). Each strategy bulk-loads its entity family in one call (no N+1), upserts via
the domain's Create/Update methods, skips soft-deleted rows (BR-136, warned once at :122-125),
and accumulates warnings instead of aborting on one bad record. A final RequestIdentityInsert()
(:132) lets SQL Server accept Sessionize's own integer IDs before one batched SaveChangesAsync
(:133). [Rubric §2, Design Patterns] (Strategy solving a real Open/Closed problem: a new entity
family is a new strategy, not an edit to the orchestrator), [Rubric §12, Performance & Scalability]
(bulk loads plus a single save round-trip), and [Rubric §17, DevOps & Deployment] (the throttle and
graceful per-entity degradation make a re-import safe to run repeatedly).
Decision support: AI scoring and content analytics
The last cluster is session-selection decision support, analytics that help organizers triage
proposals. GetSessionSelectionDashboardHandler is a
composite query: it validates the event, loads the event's non-service sessions with their speakers
and category items, the categories, and the referenced speakers once
(MMCA.ADC.Conference.Application/Sessions/UseCases/DecisionSupport/GetSessionSelectionDashboard/GetSessionSelectionDashboardHandler.cs:29-57),
then computes summary counts, category distribution, speaker-session overlap, content similarity, and
speaker locality into one dashboard DTO. The narrower queries
(GetCategoryDistributionQuery,
GetContentSimilarityQuery,
GetSpeakerSessionOverlapQuery) remain dispatchable individually.
Content similarity is computed by the pure-static
SessionSimilarityCalculator, a weighted sum of two Jaccard indices,
category-item overlap at 60%
(MMCA.ADC.Conference.Application/Sessions/UseCases/DecisionSupport/GetContentSimilarity/SessionSimilarityCalculator.cs:11)
and span-tokenized keyword overlap at 40% (:12, with a FrozenSet stop-word filter at :14-34 and a
three-character minimum token length at :62), combined at :97-105, above a tunable threshold whose
default is 0.3
(MMCA.ADC.Conference.Application/Sessions/UseCases/DecisionSupport/GetContentSimilarity/GetContentSimilarityQuery.cs:6),
flagging proposals that would compete for the same audience.
SpeakerLocalityHelper resolves a speaker's locality tier through the
"where are you traveling from" category assignment rather than a Speaker field
(MMCA.ADC.Conference.Application/Sessions/UseCases/DecisionSupport/SpeakerLocalityHelper.cs:19-33),
and the two private per-handler StatusBucket enums map
SessionStatuses values into display groupings.
The flagship is AI scoring. ScoreEventSessionsCommand (handled by
ScoreEventSessionsHandler) is a full re-score: it loads every
non-service session for the event, deletes the existing scores in one set-based ExecuteDeleteAsync
so the dashboard visibly resets
(MMCA.ADC.Conference.Application/Sessions/UseCases/DecisionSupport/ScoreEventSessions/ScoreEventSessionsHandler.cs:39-41),
batch-loads the speakers (:56-60), then scores session by session through
IAiScoringService, a port implemented in infrastructure by
AnthropicScoringService, which calls
the Anthropic Messages API. Each successful score is saved immediately (:102-103) so the UI can show
real-time progress, a per-session save failure only increments a counter (:108-112), and the command
fails as a whole only when every session failed (:117-123). The contract is deliberately
never-throw: ScoreSessionAsync returns a SessionScoringResult with a
Success flag and seven 1.0-10.0 sub-scores (overall, topic relevance, description quality,
novelty, actionable takeaways, depth/insight quality, credibility/experience) in all cases
(MMCA.ADC.Conference.Application/Sessions/UseCases/DecisionSupport/ScoreEventSessions/IAiScoringService.cs:9,40-71),
so one bad session never aborts the scoring loop. The input shapes are
SessionScoringInput and SpeakerInfo
(IAiScoringService.cs:23-37); the result is mapped onto the SessionAiScore aggregate for
persistence (ScoreEventSessionsHandler.cs:87-91). Keeping the port here and the HTTP/LLM adapter
in infrastructure is textbook [Rubric §3, Clean Architecture] (the application layer depends on an
interface, never the SDK) and [Rubric §7, Microservices Readiness] (the AI provider is swappable
behind one interface).
Taken together, this assembly is the codebase's most complete picture of how a use case is built here: a slice per feature, generic framework machinery for the repetitive 80% (query, validate, map, persist, populate), and bespoke handlers reserved for the genuinely complex 20% (the Sessionize import, the attendee-facing read models, and the decision-support analytics), each isolated behind a port or a strategy so it can evolve, be tested, and ultimately be extracted without disturbing the rest.
AssemblyReference
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/AssemblyReference.cs:5· Level 0 · class (static)
- What it is: a tiny static class exposing the Conference Application assembly and its short name as two
static readonlyfields, so scanners and registrars have a strongly-typed handle on this assembly. - Depends on:
System.Reflection(BCL) only. - Concept, the assembly-anchor type.
[Rubric §5, Vertical Slice]assesses whether a module is a self-contained, discoverable unit; a per-assembly anchor type is how the framework's reflection-based wiring finds "everything in the Conference Application layer" without hard-coding a namespace string. The two fields (AssemblyReference.cs:7-8) areAssembly = typeof(AssemblyReference).AssemblyandAssemblyName = Assembly.GetName().Name ?? string.Empty, both computed once at type-load. There is a sibling anchor in every layer (see the identically-named types in the Conference API and Domain groups), so a caller can name the exact assembly it means. - Walkthrough: two fields only, no methods.
Assemblyresolves the containing assembly viatypeof(this).Assembly;AssemblyNamereadsAssembly.GetName().Name, defaulting tostring.Emptywhen the runtime reports a null simple name. - Why it's built this way: taking
typeof(AssemblyReference).Assemblyis refactor-safe (renaming the assembly or moving the file changes nothing), which is why the framework prefers an anchor type over a hard-codedAssembly.Load("..."). - Where it's used: assembly-scoped tooling and OpenAPI/validator discovery that needs the Conference Application assembly by reference. The scan performed inside
DependencyInjectioninstead anchors onClassReference(atypeof-friendly type argument).
CategoryItemSortRules<T>
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Categories.Validation·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Categories/Validation/ConferenceCategoryValidationRules.cs:39· Level 0 · class (sealed)
- What it is: a reusable FluentValidation rule fragment that enforces a non-negative sort order (
>= 0) on any request field selected as anint. - Depends on:
FluentValidation(AbstractValidator<T>, NuGet),System.Linq.Expressions(BCL). - Concept introduced, the generic reusable validator rule.
[Rubric §1, SOLID](single-responsibility, do-not-repeat) and[Rubric §16, Maintainability & Evolvability](one place to change a constraint) both apply. Rather than re-declare the same "sort must be non-negative" check inside every create/update validator, the rule is written once as a genericAbstractValidator<T>that takes anExpression<Func<T, int>>selector (ConferenceCategoryValidationRules.cs:42) pointing at whichever property carries the sort value. A concrete command validator then composes this fragment against its own request shape. This is the FluentValidation "child rules / include" composition idiom applied generically. - Walkthrough: a constructor
CategoryItemSortRules(Expression<Func<T, int>> selector)(ConferenceCategoryValidationRules.cs:42) whose body is a singleRuleFor(selector).GreaterThanOrEqualTo(0)with message "Sort order must be greater than or equal to 0" and error codeCategoryItem.Sort.Negative(ConferenceCategoryValidationRules.cs:43-44). Stable error codes (not just messages) let clients and tests key off the failure without string-matching prose. - Why it's built this way: generic over
Tso the same fragment serves the add-item and update-item request types;sealedbecause it is a leaf composition unit with no intended subclassing. - Where it's used: composed by the Category use-case validators (add/update category-item commands) in this module. Sibling fragments in the same file are
CategoryItemNameRules<T>andConferenceCategoryTitleRules<T>.
ClassReference
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/AssemblyReference.cs:11· Level 0 · class
- What it is: an empty marker class used purely as a
typeofanchor for assembly scanning; it declares no members. - Depends on: nothing first-party.
- Concept, the scan-anchor marker.
[Rubric §2, Design Patterns](assesses idiomatic registration wiring). Scrutor-style scanning APIs are commonly generic over an anchor type:ScanModuleApplicationServices<ClassReference>()tells the scanner "start from the assembly that containsClassReference", i.e. this Application layer. The class body is empty (AssemblyReference.cs:11); its only job is to be a compile-time-checked stand-in for the assembly. - Walkthrough:
public class ClassReference { }, no fields, no methods. - Why it's built this way: a dedicated marker keeps the scan call site refactor-safe and avoids anchoring the scan on a real domain type that might later move to another assembly.
- Where it's used: passed as the type argument to
ScanModuleApplicationServices<ClassReference>()inDependencyInjection(DependencyInjection.cs:100).
SessionizeCategoryItem
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.Sessionize·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/Sessionize/SessionizeModels.cs:60· Level 0 · record (sealed)
- What it is: the leaf DTO for one category value from the Sessionize "View All" API (for example "Beginner" under the "Level" category, or ".NET" under "Track"): an
Id, aName, and aSortorder. - Depends on:
System.Text.Json.Serialization([JsonPropertyName], BCL) only. - Concept introduced, the external-API contract DTO.
[Rubric §9, API & Contract Design](assesses explicit boundary contracts) and[Rubric §32, Dependency & Supply-Chain](assesses controlling the shape of data crossing a third-party boundary). The wholeSessionize*family models the JSON wire format of an external system the conference is imported from. Each type is asealed recordwithinit-only properties defaulted to a non-null empty value (Name { get; init; } = string.Empty,SessionizeModels.cs:66), and every property carries a[JsonPropertyName("...")]mapping the C# name to the exact Sessionize field (SessionizeModels.cs:62-69). Modeling the external contract as its own dedicated, immutable type (rather than binding straight onto domain entities) is the anti-corruption discipline: the messy outside shape is captured here, then translated into the domain by the sync strategies. The rest of theSessionize*records in this file share this exact shape; their sections cross-reference back here. - Walkthrough: three
initproperties,Id(int, line 63),Name(string, empty default, line 66),Sort(int, line 69), each JSON-mapped. No behavior; it is a pure data-transfer record. - Why it's built this way:
recordgives structural equality and immutability for free (see theValueObjectdiscussion of records);initplus non-null defaults means System.Text.Json can populate it while callers cannot mutate it afterward and never see a null string. - Where it's used: nested inside
SessionizeCategory'sItems; consumed by the category sync path of the Sessionize import (CategorySyncStrategy).
SessionizeLink
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.Sessionize·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/Sessionize/SessionizeModels.cs:126· Level 0 · record (sealed)
- What it is: the DTO for one speaker social link from Sessionize: a
Title, aUrl, and aLinkType(for example "Twitter", "LinkedIn"). - Depends on:
System.Text.Json.Serialization(BCL);System.Diagnostics.CodeAnalysis(a scoped analyzer suppression). - Concept: an external-API contract DTO, the pattern taught on
SessionizeCategoryItem.[Rubric §9, API & Contract Design]. - Walkthrough: three
initstring properties, all empty-defaulted and JSON-mapped (SessionizeModels.cs:128-136).Urlcarries[SuppressMessage("Design", "CA1056:URI-like properties should not be strings", ...)](SessionizeModels.cs:131) with the justification "Deserialized from Sessionize JSON": the analyzer wants aUri, but the field must round-trip whatever string Sessionize sends, so the suppression is scoped and justified inline. - Why it's built this way: keeping
UrlastringavoidsUriparsing failing the deserialization when Sessionize sends a non-canonical value; validation/parsing, if needed, happens after import, not at the wire boundary. - Where it's used: nested inside
SessionizeSpeaker'sLinks.
SessionizeQuestion
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.Sessionize·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/Sessionize/SessionizeModels.cs:25· Level 0 · record (sealed)
- What it is: the DTO for one custom-question definition from Sessionize:
Id,Questiontext,QuestionType, and aSortorder. - Depends on:
System.Text.Json.Serialization(BCL) only. - Concept: an external-API contract DTO (
SessionizeCategoryItem).[Rubric §9, API & Contract Design]. - Walkthrough: four
initproperties,Id(int, line 28),Question(string, empty default, line 31),QuestionType(string, empty default, line 34),Sort(int, line 37), each JSON-mapped. - Where it's used: nested inside
SessionizeResponse'sQuestions; imported by the question sync path of the Sessionize refresh (RefreshFromSessionizeCommand).
SessionizeQuestionAnswer
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.Sessionize·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/Sessionize/SessionizeModels.cs:195· Level 0 · record (sealed)
- What it is: the DTO for one answer to a Sessionize custom question: a
QuestionIdand itsAnswerValue. - Depends on:
System.Text.Json.Serialization(BCL) only. - Concept: an external-API contract DTO (
SessionizeCategoryItem).[Rubric §9, API & Contract Design]. - Walkthrough: two
initproperties,QuestionId(int, line 198) andAnswerValue(string, empty default, line 201), each JSON-mapped. - Where it's used: nested inside both
SessionizeSpeaker's andSessionizeSession'sQuestionAnswerscollections.
SessionizeRoom
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.Sessionize·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/Sessionize/SessionizeModels.cs:73· Level 0 · record (sealed)
- What it is: the DTO for one room from Sessionize:
Id,Name,Sort. - Depends on:
System.Text.Json.Serialization(BCL) only. - Concept: an external-API contract DTO (
SessionizeCategoryItem).[Rubric §9, API & Contract Design]. - Walkthrough: three
initproperties,Id(int, line 75),Name(string, empty default, line 78),Sort(int, line 81), each JSON-mapped; structurally identical toSessionizeCategoryItem. - Where it's used: nested inside
SessionizeResponse'sRooms; imported by the room sync path of the Sessionize refresh.
SessionizeCategory
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.Sessionize·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/Sessionize/SessionizeModels.cs:41· Level 1 · record (sealed)
- What it is: the DTO for one Sessionize category (for example "Level" or "Track"), owning a nested list of its
SessionizeCategoryItemvalues. - Depends on:
SessionizeCategoryItem(itsItemscollection);System.Text.Json.Serialization(BCL). - Concept: an external-API contract DTO (
SessionizeCategoryItem); this is the firstSessionize*type that nests another, which is why it sits one level up.[Rubric §9, API & Contract Design]. - Walkthrough: five
initproperties (SessionizeModels.cs:43-56),Id(int),Title(string, empty default),Sort(int), a nullableType(string?, line 53, so an absent JSON field stays null), andItems(IReadOnlyList<SessionizeCategoryItem>defaulted to[], line 56). Defaulting the list to an empty collection literal means a category with no items deserializes to an empty list rather than null. - Where it's used: nested inside
SessionizeResponse'sCategories; its items drive category-item import.
SessionizeSession
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.Sessionize·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/Sessionize/SessionizeModels.cs:140· Level 1 · record (sealed)
- What it is: the richest Sessionize DTO, one conference session with its schedule, room, speakers, category assignments, question answers, and live/recording metadata.
- Depends on:
SessionizeQuestionAnswer(itsQuestionAnswerslist);System.Text.Json.Serialization(BCL);System.Diagnostics.CodeAnalysis(scoped suppressions). - Concept: an external-API contract DTO (
SessionizeCategoryItem), here at full width.[Rubric §9, API & Contract Design]. - Walkthrough: many
initproperties (SessionizeModels.cs:142-191). Three carry extra handling worth knowing:Id(int) is annotated[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)](SessionizeModels.cs:143): Sessionize sometimes serializes the session id as a JSON string rather than a number, and this permits reading it either way.StartsAt/EndsAtareDateTime?(lines 152-156), so an unscheduled session round-trips with nulls instead of failing.SpeakersisIReadOnlyList<Guid>andCategoryItemsisIReadOnlyList<int>(lines 164-168): the session references speakers and category items by their Sessionize ids, not by nesting the full objects, so cross-references are resolved during import.LiveUrlandRecordingUrlare nullable strings with the sameCA1056URI-suppression asSessionizeLink'sUrl(lines 176-182).
- Why it's built this way: the id-reference lists (rather than nested objects) mirror how Sessionize normalizes its own payload; the import strategies join
Speakers/CategoryItemsids back to the already-imported entities. - Where it's used: nested inside
SessionizeResponse'sSessions; imported by the session sync path ofRefreshFromSessionizeCommand.
SessionizeSpeaker
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.Sessionize·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/Sessionize/SessionizeModels.cs:86· Level 1 · record (sealed)
- What it is: the DTO for one speaker from Sessionize, with profile fields, social
SessionizeLinks, question answers, and id references to the speaker's sessions and category items. - Depends on:
SessionizeLink(Links),SessionizeQuestionAnswer(QuestionAnswers);System.Text.Json.Serialization(BCL). - Concept: an external-API contract DTO (
SessionizeCategoryItem).[Rubric §9, API & Contract Design]. - Walkthrough: many
initproperties (SessionizeModels.cs:88-122).Idis aGuid(line 89) (unlike theintids of most other Sessionize entities, matching the Conference domain'sSpeakerIdentifierType = Guid). Optional profile fields (Bio,TagLine,ProfilePicture,FullName) are nullable strings;IsTopSpeakeris abool;Links(line 110),Sessions(IReadOnlyList<int>, line 113),CategoryItems(IReadOnlyList<int>, line 119), andQuestionAnswers(line 122) are all empty-defaulted collections. - Why it's built this way: the
Guidspeaker id lines up with the Conference domain's speaker identifier alias, so the import can map a Sessionize speaker to a domainSpeakerwithout a type conversion. - Where it's used: nested inside
SessionizeResponse'sSpeakers; imported by the speaker sync path of the Sessionize refresh.
SessionizeResponse
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.Sessionize·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/Sessionize/SessionizeModels.cs:6· Level 2 · record (sealed)
- What it is: the top-level envelope for the Sessionize "View All" API response, holding the five parallel collections (
Categories,Rooms,Speakers,Sessions,Questions) that make up an entire conference import payload. - Depends on:
SessionizeCategory,SessionizeRoom,SessionizeSpeaker,SessionizeSession,SessionizeQuestion;System.Text.Json.Serialization(BCL). - Concept: the root of the external-API contract DTO tree (
SessionizeCategoryItemtaught the shape).[Rubric §9, API & Contract Design]. This is the objectISessionizeService'sGetAllAsyncreturns and the single input every Sessionize sync strategy reads from. - Walkthrough: five
initIReadOnlyList<...>properties, each empty-defaulted and JSON-mapped to the lowercased Sessionize field (SessionizeModels.cs:8-21). "View All" is Sessionize's denormalized endpoint: it returns every entity type in one document, which is why this envelope has one collection per entity kind rather than a paged, per-type shape. - Why it's built this way: one immutable envelope makes the import atomic to reason about, the strategies receive the whole snapshot and reconcile the domain against it, and empty-list defaults mean a payload missing a section is still a valid, non-null response.
- Where it's used: returned (nullable) by
ISessionizeService; consumed by the refresh command and its sync strategies (RefreshFromSessionizeCommand,SessionizeSyncContext).
ISessionizeService
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.Sessionize·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/Sessionize/ISessionizeService.cs:6· Level 3 · interface
- What it is: the one-method contract for fetching a whole conference from the Sessionize "View All" API, returning a
SessionizeResponse(ornullwhen the response is empty). - Depends on:
SessionizeResponse(its return type). - Concept introduced, the outbound-port interface (dependency inversion at the external boundary).
[Rubric §3, Clean Architecture](assesses whether the Application layer depends on abstractions it owns, with concrete adapters living outward) and[Rubric §7, Microservices Readiness](assesses isolating third-party calls behind a swappable boundary). The Application layer declares what it needs from Sessionize (this interface) while the HTTP client that actually calls the API lives in the Infrastructure layer and implements it. That inversion keeps the import use cases testable (a fakeISessionizeServicefeeds a canned response) and keeps theHttpClient/retry concerns out of the Application layer. - Walkthrough: a single method
Task<SessionizeResponse?> GetAllAsync(string sessionizeCode, CancellationToken cancellationToken = default)(ISessionizeService.cs:12). ThesessionizeCodeis the per-event Sessionize code (the doc comment gives the example"kqf8l42a",ISessionizeService.cs:9); the nullable return signals an empty/absent response rather than throwing; and the trailingCancellationTokenfollows the codebase convention that every async boundary is cancelable. - Why it's built this way: a narrow, single-purpose port is the smallest surface the import needs, which makes both the real HTTP adapter and its test double trivial to write.
- Where it's used: injected by the Sessionize refresh use case and its strategies; the concrete HTTP implementation is registered from the Conference Infrastructure layer (Group 19).
CategoryItemNameRules<T>
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Categories.Validation·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Categories/Validation/ConferenceCategoryValidationRules.cs:26· Level 6 · class (sealed)
- What it is: a reusable FluentValidation rule fragment enforcing that a category-item name is non-empty and within the max length defined by the domain.
- Depends on:
FluentValidation(AbstractValidator<T>),CategoryInvariants(the sharedCategoryItemNameMaxLengthconstant),System.Linq.Expressions(BCL). - Concept: the generic reusable validator rule, taught on
CategoryItemSortRules<T>.[Rubric §1, SOLID]and[Rubric §16, Maintainability & Evolvability]. - Walkthrough: constructor
CategoryItemNameRules(Expression<Func<T, string>> selector)(ConferenceCategoryValidationRules.cs:29) whose body is one chainedRuleFor(selector):.NotEmpty()with error codeCategoryItem.Name.Required, then.MaximumLength(CategoryInvariants.CategoryItemNameMaxLength)with error codeCategoryItem.Name.MaxLength(ConferenceCategoryValidationRules.cs:30-32). The max-length bound is read fromCategoryInvariants, the single source of truth that the domain, the EF configuration, and this validator all share, so the length rule cannot drift between layers. - Why it's built this way: pulling the constant from the domain invariants (rather than a literal here) is exactly the "one place to change a constraint" discipline; generic over
Tso it composes into whichever request carries the name. - Where it's used: composed by the add/update category-item command validators. Siblings:
CategoryItemSortRules<T>,ConferenceCategoryTitleRules<T>.
ConferenceCategoryTitleRules<T>
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Categories.Validation·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Categories/Validation/ConferenceCategoryValidationRules.cs:12· Level 6 · class (sealed)
- What it is: a reusable FluentValidation rule fragment enforcing that a conference-category title is non-empty and within the domain-defined max length.
- Depends on:
FluentValidation(AbstractValidator<T>),CategoryInvariants(theTitleMaxLengthconstant),System.Linq.Expressions(BCL). - Concept: the generic reusable validator rule, taught on
CategoryItemSortRules<T>.[Rubric §1, SOLID]and[Rubric §16, Maintainability & Evolvability]. - Walkthrough: constructor
ConferenceCategoryTitleRules(Expression<Func<T, string>> selector)(ConferenceCategoryValidationRules.cs:15) with one chainedRuleFor(selector):.NotEmpty()(error codeCategory.Title.Required) then.MaximumLength(CategoryInvariants.TitleMaxLength)(error codeCategory.Title.MaxLength) (ConferenceCategoryValidationRules.cs:16-18). Structurally identical toCategoryItemNameRules<T>, differing only in the target property and its constant/error codes. - Where it's used: composed by the create/update conference-category command validators.
DependencyInjection
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/DependencyInjection.cs:34· Level 11 · class (static, extension block)
- What it is: the Conference module's application-layer composition root: a static class exposing the
AddModuleConferenceApplication(ApplicationSettings)extension method that registers every application service for the module into the DI container. - Depends on:
ApplicationSettings, the Conference domain aggregates (Event,Session,Speaker,Category/CategoryItemand their children), the framework genericsEntityQueryService<TEntity, TEntityDTO, TIdentifierType>andINavigationPopulator<in TEntity>, the cross-module portsISessionBookmarkValidationServiceandIEventLiveValidationService, andClassReference;Microsoft.Extensions.DependencyInjection(NuGet), Scrutor (assembly scanning). - Concept introduced, the module composition root via
extension(IServiceCollection).[Rubric §5, Vertical Slice](assesses whether each module wires its own slice) and[Rubric §2, Design Patterns](assesses idiomatic registration). The registration method is written as a C#extension(IServiceCollection services)block (DependencyInjection.cs:36) so callers invokeservices.AddModuleConferenceApplication(...)(the sameextension(T)member pattern taught in primer and used across the codebase for DI). It mixes two registration styles the class comment (DependencyInjection.cs:29-33) names: explicitTryAdd*registrations for the generic per-entity services, and convention-based Scrutor scanning for the many hand-written handlers, mappers, and validators. - Walkthrough (registration order in the method body):
applicationSettingsis accepted but currently only stored via_ = applicationSettingswith a "reserved for future use" note (DependencyInjection.cs:40), so the parameter is part of the module contract even though this module does not yet branch on it.- Domain service:
IEventCascadeDeletionDomainServiceis registered as a singleton (DependencyInjection.cs:43). - Aggregate roots with custom navigation populators (
Event,Session,Speaker,Category) each get three registrations,INavigationPopulator<T>,IEntityQueryService<T, TDTO, TId>, and aDeleteEntityCommandhandler (DependencyInjection.cs:46-60).Eventuses a bespokeDeleteEventHandlerwhileSession/Speaker/Categoryuse the genericDeleteEntityHandler<,>; the cascade-delete rule for events is whyEventis special-cased. - Aggregate roots with no child navigations (
Question) get the same trio but with aNullNavigationPopulator<Question>(DependencyInjection.cs:63-65). - Child entities (
Room,CategoryItem,EventSpeaker,EventQuestionAnswer,SessionSpeaker,SessionCategoryItem,SessionQuestionAnswer,SpeakerCategoryItem) each get aNullNavigationPopulatorplus the baseEntityQueryService(DependencyInjection.cs:68-90), no delete handler, because children are removed through their aggregate root. - Cross-module ports:
ISessionBookmarkValidationService→SessionBookmarkValidationServiceandIEventLiveValidationService→EventLiveValidationService(DependencyInjection.cs:93-96), the in-process interfaces the Engagement service later reaches over gRPC. - Finally
services.ScanModuleApplicationServices<ClassReference>()(DependencyInjection.cs:100) sweeps this assembly for domain-event handlers, DTO/request mappers, command/query handlers, and validators, then the method returnsservicesfor fluent chaining.
- Why it's built this way:
TryAdd*(rather thanAdd) lets a host override any single registration without a duplicate-registration conflict; splitting explicit generics from convention scanning keeps the file short while still registering dozens of hand-written handlers. Registering the cross-module validation services here (in-process) is what lets the same code run either co-located or split behind gRPC without change (ADR-007/008). - Where it's used: called by the Conference module registration (
IModule.Register) during host startup; the module is discovered and ordered by theModuleLoader(Group 14). - Caveats / not-in-source: the
applicationSettingsparameter is currently unused beyond the discard; the comment flags it as reserved for profiler decorators, which are not wired here today.
SessionizeSyncResult
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.RefreshFromSessionize·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/UseCases/RefreshFromSessionize/ISessionizeSyncStrategy.cs:21· Level 0 · record
- What it is: the small immutable value returned by every Sessionize sync strategy: a pair of counters reporting how many entities that strategy touched. It is co-located in the same file as
ISessionizeSyncStrategybecause it is that interface's return type. - Depends on: nothing first-party; it is a plain
sealed recordwith twointinitproperties. - Walkthrough: two members:
PrimarySynced(ISessionizeSyncStrategy.cs:24), the count of the strategy's main entity (categories, rooms, questions, speakers, or sessions), andSecondarySynced(ISessionizeSyncStrategy.cs:27), an optional count of a nested child entity synced in the same pass (today onlyCategorySyncStrategyuses it, to report category items synced alongside categories). Both default to0, so a strategy that has no secondary entity simply leaves it unset. - Why it's built this way: returning a record rather than a bare
intleaves room to grow the result (more counters, per-entity warnings) without breaking the five implementors. The two-field shape is deliberately generic so one type serves all five strategies. - Where it's used: produced by each
SyncAsyncimplementation and accumulated into aList<SessionizeSyncResult>byRefreshFromSessionizeHandler, which reads the counters positionally to build its result DTO.
RefreshFromSessionizeCommand
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.RefreshFromSessionize·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/UseCases/RefreshFromSessionize/RefreshFromSessionizeCommand.cs:18· Level 6 · record
- What it is: the CQRS command that requests a full refresh of one event's data (categories, rooms, questions, speakers, sessions) from the external Sessionize API. It carries a single field: the
EventIdto refresh (RefreshFromSessionizeCommand.cs:18). - Depends on:
Event(only for thetypeof(Event).FullNamecache-prefix expression),ConferenceFeatures(the feature-flag constant), and three marker interfaces fromMMCA.Common.Application:ICacheInvalidating,ITransactional, andIFeatureGated. - Concept introduced: marker interfaces that opt a command into pipeline behavior. The command itself has no logic; it is a request record whose interfaces tell the decorator pipeline how to treat it (the pipeline is taught in Group 05). Implementing three markers stacks three cross-cutting behaviors declaratively:
IFeatureGatedexposesFeatureName => ConferenceFeatures.SessionizeIntegration(RefreshFromSessionizeCommand.cs:24); the FeatureGate decorator short-circuits the command to a no-op when that flag is off, a runtime circuit breaker for the whole Sessionize integration. [Rubric §10, Cross-Cutting Concerns] assesses whether concerns like feature flags are handled centrally rather than scattered; here the flag check is inherited from the pipeline, not coded in the handler.ICacheInvalidatingexposesCachePrefix => $"{typeof(Event).FullName}:"(RefreshFromSessionizeCommand.cs:21); on success the Caching decorator evicts every cache entry under theEventprefix, so freshly imported data is not masked by a stale read cache.ITransactionalmakes the Transactional decorator wrap the handler in one database transaction, so the five per-entity syncs commit atomically or roll back together. [Rubric §6, CQRS & Event-Driven] assesses whether mutations flow through well-defined command boundaries; this command is the boundary and its markers are how it configures the pipeline around itself.
- Walkthrough: a one-parameter positional record (
RefreshFromSessionizeCommand.cs:18) plus two expression-bodiedgetproperties satisfying the marker contracts (CachePrefixat line 21,FeatureNameat line 24). No constructor body, no validation: an event id is the only input the use case needs. - Why it's built this way: keeping the behavior in interfaces, not in the command body, is what lets one small record participate in feature-gating, cache invalidation, and transactions without repeating that plumbing per use case (ADR-014 for the decorator ordering).
- Where it's used: handled by
RefreshFromSessionizeHandler; dispatched by the Conference REST controller that exposes the refresh endpoint.
SessionizeSyncContext
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.RefreshFromSessionize·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/UseCases/RefreshFromSessionize/SessionizeSyncContext.cs:11· Level 8 · record
- What it is: the mutable "parameter object" passed to every sync strategy for one import run. It bundles the four things a strategy needs (the parsed API payload, the target event, the unit of work, and a shared warnings list) plus one running counter the strategies write back into.
- Depends on:
SessionizeResponse(the parsed API payload, same group),Event(the aggregate being refreshed), andIUnitOfWork(repository access for strategies that load their own entities). - Concept introduced: a shared context object for a multi-step pipeline. Rather than pass four or five arguments into every strategy method, the orchestrator builds one context and threads it through. Two of its members are deliberately mutable so the strategies can report side information back to the orchestrator without changing the
SyncAsyncreturn contract:Warnings(SessionizeSyncContext.cs:16) is arequired List<string>any strategy can append non-fatal problems to (a session outside the event date range, a question in the reserved id range); the handler folds these into the result DTO.SkippedSoftDeleted(SessionizeSyncContext.cs:19) is a plainint { get; set; }that every strategy increments when it encounters a soft-deleted local row matching an incoming Sessionize id, implementing BR-136 (soft-deleted entities are never resurrected by an import).
- Walkthrough: four
required initmembers set once at construction:Response(:13),Event(:14),UnitOfWork(:15),Warnings(:16); then the single mutable counterSkippedSoftDeleted(:19). Therequiredkeyword forces the handler to supply all four when it news up the context, so a strategy can never see a half-built context. - Why it's built this way: a single context keeps the strategy signature stable (
ISessionizeSyncStrategy.SyncAsynctakes exactly(context, cancellationToken)), and the two mutable fields give strategies a back-channel for warnings and skip counts without a more complex return type. - Where it's used: created once per command in
RefreshFromSessionizeHandlerand passed to each of the five strategies in turn.
ISessionizeSyncStrategy
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.RefreshFromSessionize·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/UseCases/RefreshFromSessionize/ISessionizeSyncStrategy.cs:7· Level 9 · interface
- What it is: the Strategy interface for synchronizing one entity family from a parsed Sessionize response into the domain. Each implementation owns exactly one entity type (categories, rooms, questions, speakers, or sessions).
- Depends on:
SessionizeSyncContext(the input for one run) andSessionizeSyncResult(the return, co-located in the same file at line 21). - Concept introduced: the Strategy pattern for pluggable, ordered sync steps. [Rubric §2, Design Patterns] assesses whether a pattern solves a real structural problem. Sessionize returns one payload covering five interdependent entity types; splitting the sync into one strategy per type keeps each
SyncAsyncsmall and single-purpose, and lets a new entity type be added as a new strategy without touching the ones that exist ([Rubric §1, SOLID], open for extension). [Rubric §16, Maintainability] applies too: a change to how rooms sync cannot break speaker sync because the code paths are physically separate. - Walkthrough: one method:
Task<SessionizeSyncResult> SyncAsync(SessionizeSyncContext context, CancellationToken cancellationToken)(ISessionizeSyncStrategy.cs:15). The strategy reads what it needs from the context, upserts its entity family, and returns aSessionizeSyncResultwith the counts it produced. - Why it's built this way: a record result (not a bare
int) leaves room to add metadata without breaking implementors, and a single context parameter keeps every strategy's signature identical so the orchestrator can loop over them uniformly. - Where it's used: implemented by the five Level-10 strategy classes below; the implementations are held in a static array inside
RefreshFromSessionizeHandlerand executed in dependency order. - Caveats / not-in-source: the strategies are not injected via DI. The handler instantiates them directly in a
static readonly ISessionizeSyncStrategy[]field (RefreshFromSessionizeHandler.cs:25), which is possible because they are stateless (all per-run state lives inSessionizeSyncContext).
CategorySyncStrategy
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.RefreshFromSessionize·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/UseCases/RefreshFromSessionize/CategorySyncStrategy.cs:11· Level 10 · class (internal sealed)
What it is: the sync strategy for categories and their nested category items. This is the fullest of the five strategies, so it also serves as the reference for the shared four-phase shape the others reuse.
Depends on:
ISessionizeSyncStrategy(the interface it implements),SessionizeSyncContext,SessionizeSyncResult,Category(the aggregate it upserts through), andIUnitOfWork(reached via the context).Concept introduced: the shared four-phase upsert every strategy follows:
- Bulk pre-load. Rather than N
GetByIdAsynccalls, the strategy opens its repository viacontext.UnitOfWork.GetRepository<Category, ...>()(CategorySyncStrategy.cs:15) and callsGetByIdsAsynconce with the full set of Sessionize ids (:21). It passesincludes: [nameof(Category.CategoryItems)]to eager-load children,asTracking: trueso updates are tracked, andignoreQueryFilters: trueso soft-deleted rows are visible for the BR-136 skip check. Results become a dictionary keyed by id (:27). This is a deliberate [Rubric §12, Performance & Scalability] choice: a full re-import of hundreds of entities would suffer badly from N+1 queries. - Iterate and discriminate. For each incoming entity (
:31): if it matches a local row thatIsDeleted, incrementcontext.SkippedSoftDeletedandcontinue(:35, BR-136); if it matches an active row, call the aggregate'sUpdate(...)(:41); if there is no match, call the aggregate'sCreate(...)factory (:97), which returnsResult<T>, and skip on failure. - Sync children.
SyncCategoryItems(:61) walks each incoming category's items, applying the same match/skip/update/add logic viaexisting.UpdateCategoryItem/existing.AddCategoryItem(:78,:82), and returns the count. - Batch-add new entities. New aggregates are collected in a local
List<Category>and flushed withcategoryRepo.AddRangeAsync(newCategories, ...)in one call (:55), then aSessionizeSyncResultcarrying both counters is returned (:58).
[Rubric §4, Domain-Driven Design] applies throughout: every mutation goes through a
Categoryfactory or aggregate method, so the domain enforces its own invariants and the strategy never sets fields directly.- Bulk pre-load. Rather than N
Walkthrough:
SyncAsync(:13) runs the four phases above; the private helpersSyncCategoryItems(:61) andCreateNewCategory(:91) keep each phase small.CategorySyncStrategyis the only strategy that reports a secondary count:SecondarySyncedcarries the category-items total (:58).Where it's used: first entry in the handler's
SyncStrategiesarray (RefreshFromSessionizeHandler.cs:27); it runs first because speakers and sessions reference category items.
QuestionSyncStrategy
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.RefreshFromSessionize·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/UseCases/RefreshFromSessionize/QuestionSyncStrategy.cs:11· Level 10 · class (internal sealed)
- What it is: the sync strategy for questions. It follows the same four-phase shape as
CategorySyncStrategy(bulk pre-load atQuestionSyncStrategy.cs:44, discriminate at:53, batch-add at:100) but adds three question-specific concerns. - Depends on:
ISessionizeSyncStrategy,SessionizeSyncContext,SessionizeSyncResult,Question(upserted viaCreate/Update), andQuestionInvariants(the reserved-id constants). - Concept introduced: cross-source invariants enforced at the application layer. Two of this strategy's rules cannot live inside the
Questionentity because they compare external Sessionize data against internal rules:- Reserved-id guard (
:29): incoming ids inside[QuestionInvariants.ManualIdRangeStart, QuestionInvariants.ManualIdRangeEnd]are reserved for manually created questions; a Sessionize question landing there would shadow a user-created one, so it is dropped with a warning (:34) rather than a hard error, and the import continues. [Rubric §11, Security] and [Rubric §16, Maintainability] both touch this: a bad external row cannot corrupt user-owned data, and the rule lives in one place. - Entity-type detection (
:19): the strategy pre-computes which question ids are answered by speakers versus sessions, then tags each question"Session"or"Speaker"(defaulting to"Session"when a question has no answers yet,:66). - Type mapping (
MapSessionizeQuestionType,:109): Sessionize's open type strings (Short_Text,Long_Text,Url,YesNo, and so on) collapse to the three domain-valid values"Rating","Email", and a catch-all"Text". This helper isinternal staticso it is unit-testable without constructing the strategy.
- Reserved-id guard (
- Walkthrough:
SyncAsync(:13) filters the reserved range first (:29), then bulk-loads the surviving ids (:44) and upserts throughQuestion.Update(:79) orQuestion.Create(:83, taggedquestionSource: "Sessionize"). A create failure adds a warning and skips (:90) rather than aborting. - Where it's used: third entry in the handler's
SyncStrategiesarray (RefreshFromSessionizeHandler.cs:29).
RoomSyncStrategy
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.RefreshFromSessionize·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/UseCases/RefreshFromSessionize/RoomSyncStrategy.cs:8· Level 10 · class (internal sealed)
- What it is: the simplest strategy: it syncs rooms, which are children of the
Eventaggregate rather than a top-level entity. - Depends on:
ISessionizeSyncStrategy,SessionizeSyncContext,SessionizeSyncResult; it reaches rooms only throughcontext.Event. - Walkthrough: because rooms are already loaded on
context.Event.Rooms(the handler pre-fetched the event with itsRoomsnavigation), this strategy skips the bulk-load phase entirely and has no async I/O: it returnsTask.FromResult(...)(RoomSyncStrategy.cs:35). It iteratescontext.Event.Rooms(:16), applying the same match/skip/update/add logic through theEventaggregate methodscontext.Event.UpdateRoom(:25) andcontext.Event.AddRoom(:29), with the BR-136 soft-delete skip at:19. - Why it's built this way: delegating to
Event.UpdateRoom/Event.AddRoomkeeps room invariants inside the aggregate ([Rubric §4, Domain-Driven Design]); the synchronousTask.FromResultavoids an unnecessary state machine for a method with no awaits. - Where it's used: second entry in the handler's
SyncStrategiesarray (RefreshFromSessionizeHandler.cs:28); runs after categories and before sessions, which reference rooms.
SessionSyncStrategy
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.RefreshFromSessionize·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/UseCases/RefreshFromSessionize/SessionSyncStrategy.cs:12· Level 10 · class (internal sealed)
- What it is: the richest child-syncing strategy: it upserts sessions and, for each, reconciles three child collections (session speakers, session category items, session question answers).
- Depends on:
ISessionizeSyncStrategy,SessionizeSyncContext,SessionizeSyncResult, andSession(upserted viaCreate/Updateand itsAdd*child methods). - Concept introduced: validate-then-upsert with non-fatal warnings. Before touching a session,
ValidateSessionTimes(SessionSyncStrategy.cs:51) records warnings (never errors) for times outside the event date range (BR-86,:54and:59) and for zero or negative duration (BR-122,:65); the data is still stored as-is. The strategy bulk-loads existing sessions with all three child navigations included (:21), thenResolveOrCreateSession(:71) applies the standard match/skip (BR-136 at:79) /Update(:85) /Create(:95) logic. - Walkthrough: after resolving each session,
SyncSessionChildren(:111) reconciles the three child sets in one pass: it adds any speaker not already linked (AddSessionSpeaker,:117), any category item not already present (AddSessionCategoryItem,:124), and delegates answers toSyncSessionQuestionAnswers(:131), which updates an existing non-deleted answer or adds a new one. Every child comparison uses an identity match plus an!IsDeletedguard so soft-deleted children are not re-added. New sessions are batch-added viaAddRangeAsync(:45). - Why it's built this way: treating out-of-range times as warnings rather than rejections keeps the import resilient to imperfect Sessionize data ([Rubric §17, DevOps & Deployment], graceful degradation), while all mutation still flows through
Sessionaggregate methods. - Where it's used: final entry in the handler's
SyncStrategiesarray (RefreshFromSessionizeHandler.cs:31); it runs last because sessions reference rooms, speakers, categories, and questions synced by the earlier strategies.
SpeakerSyncStrategy
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.RefreshFromSessionize·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/UseCases/RefreshFromSessionize/SpeakerSyncStrategy.cs:12· Level 10 · class (internal sealed)
- What it is: the sync strategy for speakers. It follows the standard four-phase shape (
CategorySyncStrategy) and adds social-link extraction plus event-to-speaker linking. - Depends on:
ISessionizeSyncStrategy,SessionizeSyncContext,SessionizeSyncResult,Speaker(upserted viaCreate/Updateand itsAdd*child methods), andEvent(for theEventSpeakerlink). - Concept introduced: parsing untyped external links into typed fields.
ExtractSocialLinks(SpeakerSyncStrategy.cs:124) iterates the speaker's SessionizeLinksand maps each byLinkType:"Twitter"routes throughExtractTwitterHandle,"LinkedIn"and"Blog"/"Company_Website"fill their url fields, and a url containing"github"is treated as the GitHub link (:151).ExtractTwitterHandle(:158) strips the commontwitter.com/x.comurl prefixes to yield a bare handle and isinternal staticso it is directly unit-testable. - Walkthrough: for each speaker,
CreateOrUpdateSpeaker(:59) applies match/skip (BR-136 at:71) /Update(:77) /Create(:84) logic; becauseSpeaker.Createdoes not accept social links, a freshly created speaker is immediately re-Updated to set them (:90). After upsert, the strategy creates anEventSpeakerlink if one does not already exist (context.Event.AddEventSpeaker,:44), then syncs the speaker's category items (:98) and question answers (:107) with the usual identity-plus-!IsDeletedguards. New speakers are batch-added viaAddRangeAsync(:53). - Where it's used: fourth entry in the handler's
SyncStrategiesarray (RefreshFromSessionizeHandler.cs:30); runs after categories and questions (which speakers reference) and before sessions (which reference speakers).
RefreshFromSessionizeHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.RefreshFromSessionize·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/UseCases/RefreshFromSessionize/RefreshFromSessionizeHandler.cs:16· Level 11 · class (sealed partial, command handler)
- What it is: the command handler for
RefreshFromSessionizeCommand: it loads the event, validates preconditions, calls the Sessionize API, runs the five per-entity strategies in dependency order, and returns a result DTO of per-entity sync counts. It is the orchestration seat of the whole import. - Depends on: first-party:
IUnitOfWork,ICurrentUserService,ISessionizeService(the HTTP client abstraction),Result/Error,SessionizeResponse,SessionizeSyncContext,SessionizeSyncResult,ISessionizeSyncStrategyand its five implementations (CategorySyncStrategy,RoomSyncStrategy,QuestionSyncStrategy,SpeakerSyncStrategy,SessionSyncStrategy),Event,RefreshFromSessionizeResultDTO, and theICommandHandler<in TCommand, TResult>contract it satisfies. Notable externals:TimeProvider(BCL, injected for testable clock reads) andMicrosoft.Extensions.Logging(ILogger<T>plus the[LoggerMessage]source generator). - Concept introduced: the orchestrator that owns sequencing but not sync logic. [Rubric §2, Design Patterns] and [Rubric §6, CQRS & Event-Driven] both apply. The handler holds a
static readonly ISessionizeSyncStrategy[] SyncStrategies(RefreshFromSessionizeHandler.cs:25) with the five strategies in explicit dependency order (categories, rooms, questions, speakers, sessions). Making the arraystaticavoids re-allocating it per request; the strategies are safe to share because they are stateless. The handler knows the order entities must be synced but nothing about how any entity is synced, the clean expression of the Strategy pattern. - Walkthrough: the primary constructor (
:16) takes five dependencies:IUnitOfWork,ISessionizeService,ICurrentUserService,TimeProvider, andILogger<RefreshFromSessionizeHandler>; thesealed partialmodifier pairs with the[LoggerMessage]generator at:150.HandleAsync(:35) runs six phases:- Event load (
:40): fetches theEventwithRoomsandEventSpeakersnavigations,asTracking: true; returnsError.NotFoundwhen absent (:49). - BR-6 precondition (
:54): a missingSessionizeCodereturnsError.Invariantwith code"Event.Sessionize.NoCode". - BR-63 throttle (
:64): ifLastSessionizeRefreshOnis within five minutes oftimeProvider.GetUtcNow().UtcDateTime(:65), returnsError.Invariantcode"Event.Sessionize.Throttled"without calling the API. [Rubric §17, DevOps & Deployment] assesses runtime protections; this throttle keeps the handler within Sessionize's rate limits. - External API call (
:75):sessionizeService.GetAllAsyncruns inside atry/catch (HttpRequestException)(:80); a network failure becomesError.Failurecode"Event.Sessionize.Unavailable", so the Result pattern absorbs the exception into a structured error. - Empty-response short-circuit (
:90): anullresponse is valid (no data yet); the refresh timestamp is stamped, changes saved, and a zero-count DTO returned without running any strategy. - Strategy execution (
:108): a freshSessionizeSyncContextis built, then each strategy isawaited in sequence (:117, not in parallel, because later entities reference earlier ones) with results accumulated in aList<SessionizeSyncResult>. A non-zeroSkippedSoftDeletedis folded intoWarnings(:122), the refresh timestamp is stamped (:128),unitOfWork.RequestIdentityInsert()is called (:132), andSaveChangesAsynccommits the whole batch (:133). The DTO is assembled by reading each result's counters positionally (:139).
- Event load (
- Why it's built this way: keeping only load/check/call/fan-out/commit here (no per-entity merge logic) keeps the method readable despite spanning five entity types, and returning
Resulton every failure path lets the decorator pipeline and controller handle errors uniformly viaMatchrather than exceptions.RequestIdentityInsert()(:132) is a deliberate infrastructure signal: Sessionize preserves its own integer ids, but SQL ServerIDENTITYcolumns reject explicit values, so the unit of work must wrap the save inSET IDENTITY_INSERT ON/OFFper table. The[LoggerMessage]source generator (:150) emits an allocation-free structured log for the success path. - Where it's used: discovered by Scrutor and wrapped by the decorator pipeline (FeatureGate, Logging, Caching, Transactional given the command's markers, see Group 05); invoked by the Conference REST controller's refresh-from-Sessionize endpoint.
- Caveats / not-in-source: the result-DTO assembly reads
results[0]throughresults[4]positionally (:139), so it silently depends onSyncStrategiesstaying in the declared order; reordering the array without updating the indices would swap the reported counts. An enum-keyed result map would be safer but adds complexity the current five-strategy size does not justify.
EventDateRangeRules<T>
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.Validation·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/Validation/EventValidationRules.cs:55· Level 0 · class (sealed, generic)
- What it is - a reusable FluentValidation rule fragment that enforces event date-range integrity: a start date is required, an end date is required, and the end date must fall on or after the start date.
- Depends on -
FluentValidation.AbstractValidator<T>(NuGet);System.Linq.Expressions.Expression<>(BCL). No first-party dependencies. - Concept introduced - the reusable field-rule fragment. This is the first place in this chapter's per-type sections where the Conference module's validation-composition pattern appears, so it is worth teaching from first principles. Each
*Rules<T>class is a tinyAbstractValidator<T>(or aRequiredStringRules<T>subclass) that validates exactly one concern (one field, or one cross-field relationship). The generic parameterTis the owning command or request type, and the constructor takes a property-selectorExpression<Func<T, TProp>>. Because the rule is parameterized onTand the selector, the same fragment can be composed into a create-command validator and an update-command validator via FluentValidation'sInclude(...), with zero copy-paste.[Rubric §1 - SOLID]assesses single-responsibility and open/closed adherence: each fragment owns one rule and new constraints are added by composing another fragment, never by editing an existing one.[Rubric §24 - Forms/Validation/UX Safety]assesses whether validation is centralized and message-consistent: the fragment carries the user-facing message and a stableWithErrorCodestring that the client can key off. - Walkthrough - the constructor (
EventValidationRules.cs:58) takes two selectors,startDateSelectorandendDateSelector. It registers aNotEmptyrule on each (:62,:65) with distinct error codes (Event.StartDate.Required,Event.EndDate.Required). The cross-field check is the notable mechanism: it compiles the start-date selector into a delegate once (var startDateFunc = startDateSelector.Compile();,:68), then adds aMust((instance, endDate) => endDate >= startDateFunc(instance))rule on the end date (:69-71). The two-argumentMustoverload hands the validator both the whole instance and the end-date value, so the compiled start getter reads the sibling property off the same object under validation. - Why it's built this way - compiling the selector once at construction (rather than invoking the expression tree per validation) keeps the cross-property comparison allocation-light on the hot validation path. Splitting each concern into its own fragment means an
UpdateEventCommandvalidator can pull in exactly the rules it needs without inheriting a monolithic validator. - Where it's used - composed via
Include(...)into the event create and update command validators in the Conference Application layer.
GetCategoryDistributionQuery
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.DecisionSupport.GetCategoryDistribution·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/UseCases/DecisionSupport/GetCategoryDistribution/GetCategoryDistributionQuery.cs:5· Level 0 · record (sealed)
- What it is - the CQRS query contract that asks for the distribution of an event's sessions across its category items. A single-line record carrying just the event to analyze.
- Depends on -
EventIdentifierType(the Conference module identifier alias). No other first-party types. - Concept introduced - this is a plain read-side CQRS request; the query/handler split is taught by IQueryHandler<in TQuery, TResult>, so it is cross-referenced rather than re-taught here.
[Rubric §6 - CQRS & Event-Driven]assesses whether reads and writes travel separate paths with explicit contracts: this record is a read intent with no side effects, handled by GetCategoryDistributionHandler. - Walkthrough - one positional parameter,
EventIdof typeEventIdentifierType(:5). No body. - Why it's built this way - keeping the query as a standalone record means it can be dispatched on its own (an organizer opening the category-distribution view) or composed into the larger session-selection dashboard without over-fetching the other analytics dimensions.
- Where it's used - dispatched by the Conference decision-support REST endpoints and resolved by GetCategoryDistributionHandler.
GetContentSimilarityQuery
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.DecisionSupport.GetContentSimilarity·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/UseCases/DecisionSupport/GetContentSimilarity/GetContentSimilarityQuery.cs:6· Level 0 · record (sealed)
- What it is - the query contract that asks for pairs of similar-content sessions within an event, so organizers can spot proposals that overlap and might compete for the same audience.
- Depends on -
EventIdentifierType(module identifier alias). No other first-party types. - Concept introduced - same read-side CQRS shape as GetCategoryDistributionQuery; cross-reference IQueryHandler<in TQuery, TResult> for the pattern.
[Rubric §6 - CQRS & Event-Driven]- a read intent with a tunable parameter, handled by GetContentSimilarityHandler. - Walkthrough - two positional parameters (
:6):EventId(EventIdentifierType) andMinimumSimilarity(double) which defaults to0.3. The default is the only parameter default in the group; it is the floor score a session pair must clear to appear in the result. - Why it's built this way - exposing the threshold as a defaulted parameter lets callers loosen or tighten the similarity floor per request while the common case (organizer opens the view) works with no argument. Caveat: the
0.3default lives in this contract, while the scoring weights that produce the score live in SessionSimilarityCalculator; the two must be read together to reason about what actually surfaces. - Where it's used - dispatched by the Conference decision-support endpoints and resolved by GetContentSimilarityHandler.
RoomCapacityRules<T>
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.Validation·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/Validation/RoomValidationRules.cs:37· Level 0 · class (sealed, generic)
- What it is - a reusable rule fragment enforcing that a room's capacity, when supplied, is strictly positive. Capacity is optional (
int?), so the rule only fires when a value is present. - Depends on -
FluentValidation.AbstractValidator<T>;System.Linq.Expressions.Expression<>. No first-party types. - Concept introduced - same reusable field-rule pattern taught in EventDateRangeRules<T>. The notable wrinkle is the conditional:
.When(x => selector.Compile()(x) is not null)(:43) guards theGreaterThan(0)rule (:42) so a null capacity is silently accepted rather than reported as invalid.[Rubric §24 - Forms/Validation/UX Safety]- an optional numeric field should not raise a validation error simply for being absent. - Walkthrough - the constructor takes an
Expression<Func<T, int?>>selector (:40), chainsGreaterThan(0)with error codeRoom.Capacity.NotPositive, and applies the null-guardWhenclause. - Why it's built this way - separating the presence check (
When) from the value check keeps the "optional but bounded" semantics in one place; a room without a known capacity is valid, a room claiming a non-positive capacity is not. - Where it's used - composed into the room create and update command validators.
RoomSortRules<T>
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.Validation·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/Validation/RoomValidationRules.cs:25· Level 0 · class (sealed, generic)
- What it is - a reusable rule fragment enforcing that a room's sort-order value is non-negative.
- Depends on -
FluentValidation.AbstractValidator<T>;System.Linq.Expressions.Expression<>. No first-party types. - Concept introduced - same pattern as EventDateRangeRules<T>. The simplest variant in the family: one
GreaterThanOrEqualTo(0)rule. - Walkthrough - the constructor takes an
Expression<Func<T, int>>selector (:28) and registersGreaterThanOrEqualTo(0)with error codeRoom.Sort.Negative(:29-30). No conditionals, no cross-field logic. - Why it's built this way - sort order drives deterministic UI ordering of rooms; a negative value has no meaning, so the fragment rejects it at the boundary before it reaches the domain.
- Where it's used - composed into the room create and update command validators alongside RoomCapacityRules<T>.
SessionSimilarityCalculator
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.DecisionSupport.GetContentSimilarity·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/UseCases/DecisionSupport/GetContentSimilarity/SessionSimilarityCalculator.cs:9· Level 0 · class (static, internal)
- What it is - a pure static calculator that scores how similar two sessions are, combining category-item overlap (weighted 0.6) with keyword overlap drawn from titles and descriptions (weighted 0.4). It is the engine behind the content-similarity analysis.
- Depends on -
System.Collections.Frozen.FrozenSet<string>(BCL);CategoryItemIdentifierType(module identifier alias) as a set element type. No other first-party types. - Concept introduced - weighted Jaccard similarity with span-based tokenization. The Jaccard index is the size of a set intersection over the size of its union, a value in
[0.0, 1.0]. Here two independent Jaccard scores are blended: category-item overlap and keyword overlap. Two sessions with identical category tags but no shared keywords score0.6; identical keywords but no shared tags score0.4.[Rubric §12 - Performance & Scalability]assesses allocation and lookup discipline on compute paths: the stop-word set is aFrozenSet<string>for O(1) membership after a one-time build (:14-34), andTokenizeTextscans aReadOnlySpan<char>without allocating a string per character (:41-71). - Walkthrough - members in teaching order:
- Weight constants
CategoryWeight = 0.6andKeywordWeight = 0.4(:11-12). StopWords(:14-34), aFrozenSet<string>of English function words plus conference-generic terms ("SESSION","TALK","WORKSHOP","DEEP","DIVE") that would otherwise inflate false matches; built withStringComparer.Ordinal.TokenizeText(string? text)(:41), returns an empty set for null/whitespace, then walks the span tracking word-start indices, and viaAddTokenIfNotStopWord(:123) uppercases each run of length>= 3(the>= 3filter is at:62) and drops stop words. Uppercasing usesToUpperInvariant(the CA1308 note in the doc comment at:37).CalculateJaccardIndex<T>(HashSet<T>, HashSet<T>)(:80), returns0.0when both sets are empty (:82-83), otherwise iterates the smaller set against the larger for the intersection count (:85-88) and divides bysetA.Count + setB.Count - intersectionCount.CalculateSimilarity(...)(:97), the public composite:CategoryWeight * Jaccard(categoryItems) + KeywordWeight * Jaccard(keywords)(:103-105).GetIntersection<T>(...)(:115), returns the shared elements as aList<T>, used to populate the shared-tags and shared-keywords fields of each result pair.
- Weight constants
- Why it's built this way - a single Jaccard on category items alone would flag unrelated sessions that happen to share a broad track; adding the keyword signal raises the bar, and weighting categories higher reflects that curated tags are a stronger signal than free-text overlap. The static, side-effect-free shape makes it trivially unit-testable in isolation.
- Where it's used - GetContentSimilarityHandler calls
TokenizeText,CalculateSimilarity, andGetIntersectionacross every session pair.
StatusBucket
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.DecisionSupport.GetCategoryDistribution·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/UseCases/DecisionSupport/GetCategoryDistribution/GetCategoryDistributionHandler.cs:94· Level 0 · enum (private)
- What it is - a private enum, nested inside GetCategoryDistributionHandler, that maps a session's raw status string onto one of three counting buckets used to tally the category distribution.
- Depends on - used together with
SessionStatusesstring constants (SessionStatuses) via the handler'sClassifyStatusswitch. - Concept introduced - an internal aggregation vocabulary. The enum is an implementation detail of one handler and is never exposed to callers, who receive DTO counts.
[Rubric §16 - Maintainability]assesses local reasoning: keeping the bucket type private to its handler means the bucketing can evolve without coupling other decision-support handlers. - Walkthrough - three members:
Accepted,AcceptQueue,Pending(:94-99). Note there is noDeclinedmember: declined sessions are excluded upstream byIsDeclined(:114) before bucketing, so the enum only spans the statuses that count toward a category's totals. - Why it's built this way - declined proposals do not contribute to the distribution an organizer is weighing, so filtering them out before the enum stage keeps the three live buckets clean. Keeping the enum private to the handler avoids a false shared dependency with the sibling dashboard handler that keeps its own copy.
- Where it's used - inside GetCategoryDistributionHandler only (
CountSessionsPerCategoryItemandClassifyStatus).
EventNameRules<T>
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.Validation·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/Validation/EventValidationRules.cs:13· Level 5 · class (sealed, generic)
- What it is - a reusable rule fragment for the event name: non-empty and bounded by
EventInvariants.NameMaxLength(500 characters). - Depends on - RequiredStringRules<T> (its base class), EventInvariants.
- Concept introduced - same reusable field-rule pattern as EventDateRangeRules<T>, but this fragment inherits
RequiredStringRules<T>rather thanAbstractValidator<T>directly, delegating theNotEmpty+MaximumLengthwiring to the shared base.[Rubric §4 - DDD]assesses ubiquitous language in code: the class is namedEventNameRules, naming the domain field rather than a generic "NameValidator". - Walkthrough - one constructor, one base call:
base(selector, "Event Name", EventInvariants.NameMaxLength)(:16-17). No body logic; all behavior lives in the parent. - Why it's built this way - the length constant
EventInvariants.NameMaxLength(EventInvariants.cs:13) is the same value the EF configuration uses for the column'sHasMaxLength, so the database constraint and the validation message share one source of truth and cannot drift. - Where it's used - composed into the event create and update command validators.
EventTimeZoneRules<T>
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.Validation·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/Validation/EventValidationRules.cs:25· Level 5 · class (sealed, generic)
- What it is - a rule fragment for an event's time zone: non-empty, bounded by
EventInvariants.TimeZoneMaxLength(100 characters), and a semantic check that the value is a recognizable IANA time-zone identifier (BR-87). - Depends on - EventInvariants;
System.TimeZoneInfo(BCL). InheritsAbstractValidator<T>directly. - Concept introduced - same fragment shape as EventNameRules<T>, but it needs a custom predicate beyond string length, so it extends
AbstractValidator<T>and adds aMust(BeAValidIanaTimeZone)rule (:32).[Rubric §24 - Forms/Validation/UX Safety]- validating that the string actually resolves to a runtime time zone stops an invalid identifier from reaching scheduling logic. - Walkthrough - the constructor chains
NotEmpty(:30),MaximumLength(:31), andMust(BeAValidIanaTimeZone)(:32), each with its own error code.BeAValidIanaTimeZone(:34-48) returnstruefor whitespace (:37) becauseNotEmptyalready covers that branch, then callsTimeZoneInfo.FindSystemTimeZoneByIdinside atryand returnsfalseonly onTimeZoneNotFoundException. - Why it's built this way - returning
truefor the empty case avoids emitting two error messages for the same missing field. Delegating the identifier check toTimeZoneInforeuses the platform's canonical time-zone database instead of hand-maintaining a list. - Caveats / not-in-source -
FindSystemTimeZoneByIdresolves against the host OS time-zone database; whether a given identifier is recognized can differ across operating systems. That runtime dependency is not something the rule itself can guarantee. - Where it's used - composed into the event create and update command validators.
RoomAccessibilityInfoRules<T>
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.Validation·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/Validation/RoomValidationRules.cs:77· Level 5 · class (sealed, generic)
- What it is - a rule fragment bounding a room's optional accessibility-info text to
EventInvariants.RoomAccessibilityInfoMaxLength(500 characters). - Depends on - EventInvariants. Inherits
AbstractValidator<T>. - Concept introduced - same fragment pattern as EventNameRules<T>. The field is nullable (
string?), so the fragment appliesMaximumLengthonly (:82), with noNotEmpty; a null value is silently accepted. - Walkthrough - one constructor, one
MaximumLength(EventInvariants.RoomAccessibilityInfoMaxLength)rule with error codeRoom.AccessibilityInfo.MaxLength(:80-82). - Why it's built this way - the length constant is shared with the EF column configuration, keeping the DB constraint and the UI message aligned.
- Where it's used - composed into the room create and update command validators.
RoomFloorRules<T>
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.Validation·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/Validation/RoomValidationRules.cs:51· Level 5 · class (sealed, generic)
- What it is - a rule fragment bounding a room's optional floor label to
EventInvariants.RoomFloorMaxLength(100 characters). - Depends on - EventInvariants. Inherits
AbstractValidator<T>. - Concept introduced - identical shape to RoomAccessibilityInfoRules<T>: nullable
string?field,MaximumLengthonly, noNotEmpty. - Walkthrough - one
MaximumLength(EventInvariants.RoomFloorMaxLength)rule with error codeRoom.Floor.MaxLength(:55-56). - Where it's used - composed into the room create and update command validators.
RoomLocationRules<T>
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.Validation·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/Validation/RoomValidationRules.cs:64· Level 5 · class (sealed, generic)
- What it is - a rule fragment bounding a room's optional location text to
EventInvariants.RoomLocationMaxLength(255 characters). - Depends on - EventInvariants. Inherits
AbstractValidator<T>. - Concept introduced - identical shape to RoomFloorRules<T>: nullable field,
MaximumLengthonly. - Walkthrough - one
MaximumLength(EventInvariants.RoomLocationMaxLength)rule with error codeRoom.Location.MaxLength(:67-69). - Where it's used - composed into the room create and update command validators.
RoomNameRules<T>
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.Validation·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/Validation/RoomValidationRules.cs:12· Level 5 · class (sealed, generic)
- What it is - a rule fragment for a room's name: non-empty and bounded by
EventInvariants.RoomNameMaxLength(255 characters). Unlike the other room fragments, the name is required. - Depends on - EventInvariants. Inherits
AbstractValidator<T>. - Concept introduced - same fragment pattern as EventNameRules<T>, but implemented directly on
AbstractValidator<T>(notRequiredStringRules<T>), chainingNotEmptythenMaximumLength. - Walkthrough - the constructor chains
NotEmpty(error codeRoom.Name.Required) andMaximumLength(EventInvariants.RoomNameMaxLength)(error codeRoom.Name.MaxLength) on the selector (:16-18). - Why it's built this way - the required name distinguishes this fragment from the nullable room fields; keeping it a separate fragment lets a validator compose exactly the required-vs-optional mix each command needs.
- Where it's used - composed into the room create and update command validators.
GetCategoryDistributionHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.DecisionSupport.GetCategoryDistribution·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/UseCases/DecisionSupport/GetCategoryDistribution/GetCategoryDistributionHandler.cs:14· Level 8 · class (sealed)
- What it is - the query handler that computes, per category item, how many of an event's sessions were submitted, accepted, in the accept-queue, and pending. It powers the organizer's category-distribution view.
- Depends on - IQueryHandler<in TQuery, TResult> (implemented), IUnitOfWork (injected), Session and Category (repository entities), SessionStatuses, and the output DTOs CategoryDistributionDTO, CategoryGroupDistribution, and CategoryItemDistribution. Uses the nested StatusBucket enum.
- Concept introduced - an in-memory analytics read handler. It loads two aggregates untracked, then does all bucketing and grouping in memory rather than pushing aggregation into SQL.
[Rubric §6 - CQRS & Event-Driven]assesses the read path: this is a pure query, returning aResult<CategoryDistributionDTO>and mutating nothing.[Rubric §12 - Performance & Scalability]assesses read efficiency: both repository loads passasTracking: false(:27,:32) so EF skips change-tracking overhead for a read-only projection. - Walkthrough -
HandleAsync(:17) resolves aSessionrepository and aCategoryrepository from the unit of work (:21-22). It loads sessions for the event, includingSessionCategoryItems, filtered toEventId == query.EventId && !s.IsServiceSession(:24-28), and loads all categories with theirCategoryItems(:30-33).CountSessionsPerCategoryItem(:41) flattens each non-declined session into(CategoryItemId, StatusBucket)pairs (excluding soft-deleted category-item links via!sci.IsDeleted,:47), then tallies a running(Total, Accepted, AcceptQueue, Pending)tuple per category item (:50-61).ClassifyStatus(:101) treats a null status orSessionStatuses.AcceptedasAccepted,SessionStatuses.AcceptQueueasAcceptQueue, and everything else asPending, all viaOrdinalIgnoreCasecomparison;IsDeclined(:114) removes declined sessions up front.BuildCategoryGroups(:66) keeps only non-deleted categories that have at least one counted item, orders categories and items by theirSort, and projects each intoCategoryItemDistributionrows (:82-90). The handler wraps the assembled list inResult.Success(new CategoryDistributionDTO { ... })(:38). - Why it's built this way - aggregating in memory keeps the logic engine-agnostic (the same handler runs against any
IUnitOfWork-backed store) and expresses the domain rules (service sessions excluded, declined excluded, soft-deletes excluded) in one readable pass rather than in a SQL statement. The trade-off is that it materializes the event's sessions and categories in full; that is acceptable for a single event's proposal set. - Where it's used - dispatched via GetCategoryDistributionQuery from the Conference decision-support endpoints, and composed into the session-selection dashboard.
GetContentSimilarityHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.DecisionSupport.GetContentSimilarity·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/UseCases/DecisionSupport/GetContentSimilarity/GetContentSimilarityHandler.cs:14· Level 8 · class (sealed)
- What it is - the query handler that finds pairs of similar-content sessions in an event, scoring every candidate pair and returning the strongest matches above a threshold.
- Depends on - IQueryHandler<in TQuery, TResult>, IUnitOfWork, Session and Category, SessionStatuses, SessionSimilarityCalculator, and the DTOs ContentSimilarityDTO and SimilarSessionPair.
- Concept introduced - a pairwise (O(n^2)) analytics handler with a hard result cap.
[Rubric §12 - Performance & Scalability]assesses cost control on a quadratic path: the privateconst int MaxPairs = 50(:17) caps the returned list, and both repository loads useasTracking: false(:32,:38). The scoring math itself is delegated to the pure SessionSimilarityCalculator, keeping the handler focused on orchestration. - Walkthrough -
HandleAsync(:19) loads all non-service, non-declined sessions with their category items (:27-33) and all categories with items (:36-39), then builds aCategoryItemIdentifierType -> namelookup (:41-48). For each session it pre-computes aHashSetof non-deleted category-item ids and a keyword set viaSessionSimilarityCalculator.TokenizeText(s.Title + " " + s.Description)(:51-59). It then double-loops over the session list (:64-79), callingSessionSimilarityCalculator.CalculateSimilarityper pair and keeping only pairs scoring at or abovequery.MinimumSimilarity(:74). Pairs are sorted by score descending (:82) and truncated toMaxPairs(:83-86). Finally each kept pair is projected into aSimilarSessionPair(:89-111): the score is rounded to three digits withMidpointRounding.ToEven(:105), shared category items are resolved to names viaGetIntersection(:94,:106-108), and shared keywords are taken (up to 10) from the keyword intersection (:95,:109). The result is wrapped inResult.Success(new ContentSimilarityDTO { Pairs = result })(:113). - Why it's built this way - pre-computing each session's keyword and category sets once, before the pairwise loop, avoids re-tokenizing inside the O(n^2) comparison. The
MaxPairscap and the score threshold together bound both compute and payload size, so a large proposal set cannot produce an unbounded response. - Where it's used - dispatched via GetContentSimilarityQuery from the Conference decision-support endpoints, and composed into the session-selection dashboard.
ExportEventCalendarQuery
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.ExportCalendar·MMCA.ADC.Conference.Application/Sessions/UseCases/ExportCalendar/ExportEventCalendarQuery.cs:5· Level 0 · record
- What it is: the read request that asks for a published event's whole schedule rendered as an RFC 5545 (
.ics) calendar document. A one-fieldsealed recordcarrying theEventIdto export (ExportEventCalendarQuery.cs:5). - Depends on: the
EventIdentifierTypealias (see identifier aliases). No externals beyond the BCL. - Concept introduced: the request record as a CQRS message. This is the first type in this unit, so the shape is worth stating once: every decision-support and calendar use case in this unit is a positional
sealed recordthat names exactly the inputs its handler needs and nothing else. It is dispatched through the CQRS decorator pipeline (Logging then Caching then handler for queries) and matched to its handler by the generic argument. [Rubric §6, CQRS and Event-Driven] assesses whether reads and writes are modeled as explicit messages: here the intent (export one event's calendar) is a named type, not a method-parameter bag. - Walkthrough: a single
EventIdpositional parameter (:5); the compiler-generated equality andinitimmutability come for free fromrecord. - Why it's built this way: ADR-042 Wave 5 (cited in the doc comment,
:3) adds calendar export as a public-read affordance; keeping the query minimal lets the handler own all the publish/exportability rules. - Where it's used: handled by ExportEventCalendarHandler; reached from the Conference REST calendar endpoint (Group 19).
ExportSessionCalendarQuery
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.ExportCalendar·MMCA.ADC.Conference.Application/Sessions/UseCases/ExportCalendar/ExportSessionCalendarQuery.cs:5· Level 0 · record
- What it is: the single-session variant of the calendar export: it asks for one public session rendered as an
.icsdocument, for the "add to calendar" affordance. A one-fieldsealed recordcarrying theSessionId(ExportSessionCalendarQuery.cs:5). - Depends on: the
SessionIdentifierTypealias. No externals beyond the BCL. - Concept introduced: none new; this is the sibling of ExportEventCalendarQuery, differing only in that it identifies a single session rather than a whole event. [Rubric §6, CQRS and Event-Driven].
- Walkthrough: a single
SessionIdpositional parameter (:5). - Why it's built this way: same ADR-042 Wave 5 lineage (
:3); a separate query keeps the one-session public rules distinct from the whole-event export. - Where it's used: handled by ExportSessionCalendarHandler.
GetSessionSelectionDashboardQuery
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.DecisionSupport.GetSessionSelectionDashboard·MMCA.ADC.Conference.Application/Sessions/UseCases/DecisionSupport/GetSessionSelectionDashboard/GetSessionSelectionDashboardQuery.cs:5· Level 0 · record
- What it is: the read request for the composite session-selection dashboard: given one
EventId, produce category distribution, speaker overlap, speaker locality, and AI scores in a single result. A one-fieldsealed record(GetSessionSelectionDashboardQuery.cs:5). - Depends on: the
EventIdentifierTypealias. No externals. - Concept introduced: none new; same request-record shape as ExportEventCalendarQuery. What makes it interesting is the handler, which fans one query out into four analytics computed off a single data load. [Rubric §6, CQRS and Event-Driven].
- Walkthrough: a single
EventIdpositional parameter (:5). - Why it's built this way: a composite query (one round trip, one message) is the decision-support answer to running four separate analytics queries: see GetSessionSelectionDashboardHandler.
- Where it's used: handled by GetSessionSelectionDashboardHandler; returns a SessionSelectionDashboardDTO.
GetSpeakerSessionOverlapQuery
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.DecisionSupport.GetSpeakerSessionOverlap·MMCA.ADC.Conference.Application/Sessions/UseCases/DecisionSupport/GetSpeakerSessionOverlap/GetSpeakerSessionOverlapQuery.cs:5· Level 0 · record
- What it is: the read request that asks, for one event, which speakers submitted more than one session (and, in practice, every speaker with their submitted sessions so the UI can surface multi-session speakers first). A one-field
sealed recordcarrying theEventId(GetSpeakerSessionOverlapQuery.cs:5). - Depends on: the
EventIdentifierTypealias. No externals. - Concept introduced: none new; same request-record shape as ExportEventCalendarQuery. [Rubric §6, CQRS and Event-Driven].
- Walkthrough: a single
EventIdpositional parameter (:5). - Why it's built this way: the speaker-overlap analysis is one focused slice of the dashboard, exposed on its own query so the UI can request just that view.
- Where it's used: handled by GetSpeakerSessionOverlapHandler; returns a SpeakerSessionOverlapDTO.
ScoreEventSessionsCommand
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.DecisionSupport.ScoreEventSessions·MMCA.ADC.Conference.Application/Sessions/UseCases/DecisionSupport/ScoreEventSessions/ScoreEventSessionsCommand.cs:5· Level 0 · record
- What it is: the write request that triggers AI scoring for every session in an event. A one-field
sealed recordcarrying theEventIdwhose sessions to score (ScoreEventSessionsCommand.cs:5). - Depends on: the
EventIdentifierTypealias. No externals. - Concept introduced: this is the one command among the sibling request records in this unit. It is structurally identical to the query records (a single-field
sealed record), but it dispatches through the command side of the pipeline (ICommandHandler<in TCommand, TResult>), which adds the Validating and Transactional decorators the query side does not carry. [Rubric §6, CQRS and Event-Driven] is precisely the split modeled here: a read (dashboard/overlap) and a write (score) that happen to share a shape are still separate message types on separate pipelines. - Walkthrough: a single
EventIdpositional parameter (:5). - Why it's built this way: scoring mutates persistence (it deletes and rewrites SessionAiScore rows), so it is a command, not a query; keeping it a distinct message makes that read/write asymmetry explicit.
- Where it's used: handled by ScoreEventSessionsHandler; returns a ScoreEventSessionsResultDTO.
SessionScoringResult
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.DecisionSupport.ScoreEventSessions·MMCA.ADC.Conference.Application/Sessions/UseCases/DecisionSupport/ScoreEventSessions/IAiScoringService.cs:40· Level 0 · record
- What it is: the internal result of scoring one session via IAiScoringService. It carries seven numeric sub-scores (each documented
1.0-10.0decimal), a free-textReasoning, theSessionId, and aSuccessflag. It never throws: a failed AI call returns this record withSuccess = false(IAiScoringService.cs:40-71). - Depends on: the
SessionIdentifierTypealias (onSessionId,:43). No first-party types; every property isrequired init. - Concept introduced: never-throw service results. Rather than propagate exceptions from the AI call,
ScoreSessionAsyncreturns this record in every case and the handler branches onSuccess. This is the Result philosophy (see Result) applied to an unreliable network dependency: the scoring loop cannot be aborted by one bad session. [Rubric §29, Resilience and Business Continuity] assesses how failure of a dependency is contained; here it is demoted to a per-item flag rather than an exception that unwinds the whole batch. - Walkthrough:
required initmembers (:43-70):SessionId,OverallScore,TopicRelevanceScore,DescriptionQualityScore,NoveltyScore,ActionableTakeawaysScore,DepthOrInsightQualityScore,CredibilityExperienceScore,Reasoning,Success.requiredon all of them means a scorer implementation cannot forget to populate one. - Why it's built this way: separating this Application-layer result from the external SessionAiScoreDTO lets the scoring contract evolve (add or drop a sub-score) without immediately breaking the API surface.
- Where it's used: returned by
IAiScoringService.ScoreSessionAsyncand consumed by ScoreEventSessionsHandler, which maps a successful one into a SessionAiScore domain row.
SpeakerInfo
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.DecisionSupport.ScoreEventSessions·MMCA.ADC.Conference.Application/Sessions/UseCases/DecisionSupport/ScoreEventSessions/IAiScoringService.cs:23· Level 0 · record
- What it is: the minimal speaker payload the AI scorer needs:
FullName, optionalTagLine, optionalBio. A positionalsealed record(IAiScoringService.cs:23-26). - Depends on: nothing first-party; three
string/string?fields. - Concept introduced: least-privilege data passing. The record deliberately carries only what the model reads (name, tagline, bio) and no ids, contact fields, or other PII. [Rubric §11, Security] and [Rubric §30, Compliance/Privacy] both assess how much data crosses a boundary to a third party: shipping a purpose-built projection to the AI vendor rather than a whole Speaker limits what leaves the trust boundary.
- Walkthrough: three positional parameters (
:23-26); the doc comments note the source-side max lengths (TagLine500,Bio4000) that the domain enforces upstream. - Why it's built this way: a narrow record keeps the scoring prompt small and avoids leaking speaker identity to the external model.
- Where it's used: nested inside SessionScoringInput; populated by ScoreEventSessionsHandler from each Speaker's
FullName/TagLine/Bio.
StatusBucket
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.DecisionSupport.GetSessionSelectionDashboard·MMCA.ADC.Conference.Application/Sessions/UseCases/DecisionSupport/GetSessionSelectionDashboard/GetSessionSelectionDashboardHandler.cs:308· Level 0 · enum
- What it is: a private three-member enum (
Accepted,AcceptQueue,Pending) that classifies a session's status string into the buckets the category-distribution aggregation counts (GetSessionSelectionDashboardHandler.cs:308-313). - Depends on: conceptually on SessionStatuses, the string constants the classifier compares against.
- Concept introduced: a handler-private classification vocabulary. The enum is declared
privateinside the handler and never leaves it; callers receive aggregated counts on the DTO, never bucket values. Modeling the three-way status collapse as a named enum keeps the counting code readable versus repeatingstring.Equals(..., Accepted)comparisons inline. [Rubric §16, Maintainability]. - Walkthrough:
ClassifyStatus(Session)(:315-326) maps a null-or-Acceptedstatus toStatusBucket.Accepted,AcceptQueuetoStatusBucket.AcceptQueue, and everything else toStatusBucket.Pending; the enum is then consumed byCountCategoryItems(:127-150) to tally each category item. - Why it's built this way: keeping the bucket private to this handler means its bucketing can diverge from any other dashboard's without coupling the two use cases (a sibling copy historically lived in the retired
GetCategoryDistributionhandler). - Where it's used: inside GetSessionSelectionDashboardHandler only.
SessionScoringInput
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.DecisionSupport.ScoreEventSessions·MMCA.ADC.Conference.Application/Sessions/UseCases/DecisionSupport/ScoreEventSessions/IAiScoringService.cs:33· Level 1 · record
- What it is: the full input for scoring one session:
SessionId,Title, optionalDescription, and the list of SpeakerInfo records for the session's speakers. A positionalsealed record(IAiScoringService.cs:33-37). - Depends on: SpeakerInfo (Level 0, same file
:23), viaIReadOnlyList<SpeakerInfo>; theSessionIdentifierTypealias. Referencing a Level-0 first-party type is what puts this record at Level 1. - Concept introduced: none new; it is the request DTO for IAiScoringService, assembling exactly what the model prompt needs (title, description, speaker bios) and nothing more, extending the least-privilege discipline SpeakerInfo sets. [Rubric §6, CQRS and Event-Driven].
- Walkthrough: four positional parameters (
:33-37);Speakersmay be empty (documented at:32), so a speaker-less session still scores. - Why it's built this way: a purpose-built input record keeps the port contract stable and testable with a fake scorer.
- Where it's used: constructed by ScoreEventSessionsHandler per session and passed to
IAiScoringService.ScoreSessionAsync.
IAiScoringService
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.DecisionSupport.ScoreEventSessions·MMCA.ADC.Conference.Application/Sessions/UseCases/DecisionSupport/ScoreEventSessions/IAiScoringService.cs:6· Level 2 · interface
- What it is: the application-layer port for scoring a single conference session with an AI model. Its contract guarantees it never throws: failure is reported through the returned SessionScoringResult's
Successflag (IAiScoringService.cs:6-17). - Depends on: SessionScoringInput (Level 1,
:33) as the argument and SessionScoringResult (Level 0,:40) as the return; BCLTask/CancellationTokenotherwise. - Concept introduced: a port/adapter boundary for an external AI capability. The Application layer declares the interface; the Anthropic HTTP and JSON details live in an Infrastructure adapter (
AnthropicScoringService), so the vendor SDK never reaches Application. [Rubric §3, Clean Architecture] assesses whether outward dependencies are inverted behind an abstraction, which this does exactly, and [Rubric §1, SOLID] (the Dependency Inversion Principle) is the same story: the handler depends on this port, not a concrete API client. The "never throws" clause (documented at:9) also ties to [Rubric §29, Resilience and Business Continuity]. - Walkthrough:
ScoreSessionAsync(SessionScoringInput, CancellationToken)(:11-13) returns the per-session result;ModelId(:16) exposes which model produced the score, so persisted rows can record the model used for auditability. - Why it's built this way: defining the port in Application lets the scoring use case be unit-tested with a fake scorer and lets the AI vendor be swapped without touching ScoreEventSessionsHandler.
- Where it's used: injected into ScoreEventSessionsHandler; implemented by the Infrastructure
AnthropicScoringServiceadapter (Group 20).
CalendarExportMapper
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.ExportCalendar·MMCA.ADC.Conference.Application/Sessions/UseCases/ExportCalendar/CalendarExportMapper.cs:13· Level 7 · class
- What it is: an
internal statichelper that decides whether a session can appear in a public calendar and maps an exportable one to an IcsEvent, converting the event-zone wall-clock times to UTC with explicit DST discipline (CalendarExportMapper.cs:13). - Depends on: Session and Event (Conference domain), IcsEvent (the calendar builder's entry type in
MMCA.Common.Shared.Calendars), and BCLTimeZoneInfo/DateTimeOffset. - Concept introduced: wall-clock to UTC conversion at the layer boundary. The calendar builder is UTC-only by contract, but sessions store times as wall-clock local to the event's IANA time zone.
ToUtc(:41-50) resolves that gap the same way the reminder planner does: it specifies the kind asUnspecified, and ifTimeZoneInfo.IsInvalidTimereports the instant falls in a spring-forward gap (:44), it shifts ahead one hour before computing the offset. The deterministic DST handling is a correctness safeguard [Rubric §16, Maintainability]. - Walkthrough:
ProductId(:16): the RFC 5545PRODIDconstant-//MMCA//AtlDevCon//ENstamped on every ADC calendar.IsExportable(Session)(:19-22): true only when the session has bothStartsAtandEndsAt, is not a service session, and is notDeclined/Cancelled(case-insensitive): the single source of truth for public exportability, reused by both handlers.ToIcsEvent(Session, Event, TimeZoneInfo, string?)(:25-38): builds a stable UID (session-{Id}@atldevcon), joins the room name and venue address into a location string, and converts start/end viaToUtc.ToUtc(DateTime, TimeZoneInfo)(:41-50): the DST-aware conversion described above.
- Why it's built this way: centralizing the exportability rule and the time conversion in one internal helper keeps the two calendar handlers thin and guarantees they agree on what "public" and "UTC" mean (ADR-042 Wave 5).
- Where it's used: by ExportEventCalendarHandler and ExportSessionCalendarHandler; the resulting
IcsEventlist is handed to IcsCalendarBuilder.
ExportEventCalendarHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.ExportCalendar·MMCA.ADC.Conference.Application/Sessions/UseCases/ExportCalendar/ExportEventCalendarHandler.cs:15· Level 8 · class
- What it is: the query handler that produces a whole-schedule
.icsstring for a published event: every exportable session becomes one VEVENT with its room in the location. Unpublished or unknown events return NotFound (ExportEventCalendarHandler.cs:15). - Depends on: IUnitOfWork (for the Event and Session repositories), CalendarExportMapper, IcsCalendarBuilder, and the Result/Error types. Implements IQueryHandler<in TQuery, TResult> of ExportEventCalendarQuery to
Result<string>. - Concept introduced: public-read handlers leak nothing. The handler treats "unpublished" and "not found" identically: if the event is missing or
!IsPublishedit returnsError.NotFound(:28-32), so a caller cannot distinguish an unpublished event from a nonexistent one. [Rubric §11, Security] assesses exactly this kind of existence-hiding on public endpoints. - Walkthrough:
- Load the event with its
Roomschild collection eagerly included (:25-27); rooms are children of the Event aggregate and have no repository of their own (note at:24). - Guard: null or unpublished then NotFound (
:28-32). - Load all non-deleted sessions for the event via a filtered
GetAllAsync(:34-36) and build a room-id to name dictionary (:37). - Resolve the event's IANA zone via
TimeZoneInfo.FindSystemTimeZoneById(@event.TimeZone)(:39). - Filter to
CalendarExportMapper.IsExportable, order byStartsAt, and map each to anIcsEvent, resolving each session's room name from the dictionary (:40-48). - Hand the entries to
IcsCalendarBuilder.Buildwith theProductIdand current UTC timestamp, and return the string asResult.Success(:50-51).
- Load the event with its
- Why it's built this way: ADR-042 Wave 5: rendering the schedule server-side keeps the RFC 5545 formatting in one shared builder and lets the read endpoint be output-cached like the other Conference public reads.
- Where it's used: invoked from the Conference calendar REST endpoint (Group 19); the
stringpayload is served as an.icsdownload.
ExportSessionCalendarHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.ExportCalendar·MMCA.ADC.Conference.Application/Sessions/UseCases/ExportCalendar/ExportSessionCalendarHandler.cs:16· Level 8 · class
- What it is: the single-session sibling of ExportEventCalendarHandler: it produces a one-VEVENT
.icsdocument for the public add-to-calendar affordance, enforcing the same public-read rules (ExportSessionCalendarHandler.cs:16). - Depends on: IUnitOfWork, CalendarExportMapper, IcsCalendarBuilder, Result/Error. Implements IQueryHandler<in TQuery, TResult> of ExportSessionCalendarQuery to
Result<string>. - Concept introduced: none new; it applies the same existence-hiding discipline ExportEventCalendarHandler introduces, just with two guards instead of one. [Rubric §11, Security].
- Walkthrough:
- Load the session by id; if null or not
CalendarExportMapper.IsExportable, return NotFound targetingSession(:25-31). - Load the owning event with
Roomsincluded; if null or unpublished, return NotFound targetingEvent(:34-41): a session on an unpublished event stays hidden. - Resolve the session's room name from the event's rooms (
:43-45) and the IANA time zone (:47). - Build a one-entry calendar via
IcsCalendarBuilder.Buildand returnResult.Success(:48-53).
- Load the session by id; if null or not
- Why it's built this way: a dedicated one-session path (rather than filtering the whole-event export) keeps the add-to-calendar button cheap and the leak-prevention guards explicit (ADR-042 Wave 5).
- Where it's used: the Conference session detail page's add-to-calendar affordance (Group 19/21).
GetSessionSelectionDashboardHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.DecisionSupport.GetSessionSelectionDashboard·MMCA.ADC.Conference.Application/Sessions/UseCases/DecisionSupport/GetSessionSelectionDashboard/GetSessionSelectionDashboardHandler.cs:16· Level 8 · class
- What it is: the composite decision-support handler: it loads an event's sessions, categories, speakers, and AI scores once, then computes summary counts, category distribution, speaker overlap, speaker locality, and the AI-score table into a single SessionSelectionDashboardDTO (
GetSessionSelectionDashboardHandler.cs:16). - Depends on: IUnitOfWork and the Event/Session/Speaker/Category/SessionAiScore repositories;
SpeakerLocalityHelperand SessionStatuses; the dashboard DTO family (CategoryDistributionDTO, CategoryGroupDistribution, CategoryItemDistribution, SpeakerSessionOverlapDTO, MultiSessionSpeaker, SpeakerSessionSummary, SpeakerLocalitySummary, SessionAiScoreDTO). Implements IQueryHandler<in TQuery, TResult> of GetSessionSelectionDashboardQuery toResult<SessionSelectionDashboardDTO>. - Concept introduced: load-once, compute-many for read performance. The handler deliberately issues a small fixed set of reads (event validation, then sessions, categories, speakers by id, AI scores) and derives all four analytics in memory from those materialized collections, rather than running a separate query per panel. The comment at
:33calls out that the loads are sequential for EF single-context safety (aDbContextis not concurrency-safe). [Rubric §12, Performance and Scalability] assesses read-path efficiency: one bounded batch of no-tracking reads (asTracking: false, e.g.:37,:42,:55) beats N per-panel round trips. - Walkthrough:
- Resolve the four repositories, then validate the event exists, else NotFound (
:23-31). - Load non-service sessions with
SessionSpeakers/SessionCategoryItemsincluded, all categories withCategoryItems, and the distinct speakers viaGetByIdsAsyncwithSpeakerCategoryItems(:34-57). - Build category-item name and locality lookups (
SpeakerLocalityHelper.FindLocalityCategory/BuildLocalityLookup,:60-70). - Compute summary counts (total, accepted, accept-queue, declined, and pending as the remainder) using SessionStatuses comparisons (
:73-80). - Compute
categoryDistribution(ComputeCategoryDistribution,:118-178, which classifies each session via the private StatusBucket enum),speakerOverlap(ComputeSpeakerOverlap,:180-247), andspeakerLocality(ComputeSpeakerLocality,:249-306). - Load the
SessionAiScorerows for these sessions and map them toSessionAiScoreDTOs, splitting out the "Level" category and each session's speaker localities (BuildAiScoreDtos,:331-354). - Assemble and return the composite DTO (
:102-115).
- Resolve the four repositories, then validate the event exists, else NotFound (
- Why it's built this way: a single composite endpoint backs an organizer dashboard that shows all four analytics at once; computing them from one shared load avoids re-reading the same sessions four times.
- Where it's used: the Conference session-selection dashboard page (Group 21); the same overlap computation is also exposed standalone by GetSpeakerSessionOverlapHandler.
- Caveats / not-in-source: the private StatusBucket enum and the private compute methods are implementation details of this handler and are not exposed to callers.
GetSpeakerSessionOverlapHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.DecisionSupport.GetSpeakerSessionOverlap·MMCA.ADC.Conference.Application/Sessions/UseCases/DecisionSupport/GetSpeakerSessionOverlap/GetSpeakerSessionOverlapHandler.cs:18· Level 8 · class
- What it is: the standalone speaker-overlap handler: it returns every speaker who submitted at least one session for an event, with their sessions, sorted so multi-session speakers surface first (session count descending, then accepted-session presence, then name) (
GetSpeakerSessionOverlapHandler.cs:18). - Depends on: IUnitOfWork and the Session/Speaker/Category repositories;
SpeakerLocalityHelper, SessionStatuses, SpeakerSessionOverlapDTO/MultiSessionSpeaker/SpeakerSessionSummary. Implements IQueryHandler<in TQuery, TResult> of GetSpeakerSessionOverlapQuery toResult<SpeakerSessionOverlapDTO>. - Concept introduced: none new; it reuses the same load-once-compute pattern and the same MultiSessionSpeaker projection that the dashboard's
ComputeSpeakerOverlapbuilds, exposed as its own focused query. [Rubric §6, CQRS and Event-Driven] and [Rubric §12, Performance and Scalability]. - Walkthrough:
- Load non-service sessions with
SessionSpeakers/SessionCategoryItemsincluded (:29-33). - Group sessions by speaker id (
GroupSessionsBySpeaker,:61-82), skipping soft-deleted join rows; short-circuit to an empty result when no speakers (:38-39). - Load those speakers by id (with
SpeakerCategoryItems) and all categories, then build the locality lookup and category-item name lookup (:41-54). - Build the MultiSessionSpeaker list (
BuildMultiSessionSpeakers,:99-140), stamping each speaker's locality tier viaSpeakerLocalityHelper.GetLocalityTierand whether any session is accepted, then sort by session count, accepted presence, and name (:127-137).
- Load non-service sessions with
- Why it's built this way: organizers sometimes want just the overlap view without paying for the whole composite dashboard, so it is its own use case reusing the shared helper and DTOs.
- Where it's used: the Conference speaker-overlap page (Group 21); returns the same DTO shape the dashboard embeds.
ScoreEventSessionsHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.DecisionSupport.ScoreEventSessions·MMCA.ADC.Conference.Application/Sessions/UseCases/DecisionSupport/ScoreEventSessions/ScoreEventSessionsHandler.cs:15· Level 8 · class
- What it is: the command handler that scores every session in an event via IAiScoringService, persisting each score immediately so the UI can show real-time progress (
ScoreEventSessionsHandler.cs:15). Asealed partial class(partial for the source-generated log methods). - Depends on: IUnitOfWork and the Session/SessionAiScore/Speaker repositories; IAiScoringService;
ILogger<ScoreEventSessionsHandler>; SpeakerInfo/SessionScoringInput/SessionScoringResult; ScoreEventSessionsResultDTO. Implements ICommandHandler<in TCommand, TResult> of ScoreEventSessionsCommand toResult<ScoreEventSessionsResultDTO>. - Concept introduced: per-item save with contained failure, plus source-generated logging. Two patterns meet here. First, resilience: the loop scores one session at a time and saves it individually (
:99-111); a scorer failure, a domain-factory failure, or a save exception incrementsfailedandcontinues rather than aborting the batch (:79-97), and only an all-failed run returnsError.Failure(:116-122). This is the never-throw contract of SessionScoringResult carried up into batch orchestration. [Rubric §29, Resilience and Business Continuity]. Second, observability: every log call is a[LoggerMessage]static partialmethod (:127-143), the compiler-generated high-performance logging pattern that avoids boxing and template re-parsing. [Rubric §13, Observability and Operability]. - Walkthrough:
- Load non-service sessions (with
SessionSpeakers) for the event; short-circuit to a zero-count success when none (:27-34). ExecuteDeleteAsyncall existing scores for these sessions so the dashboard resets to zero and progress is visible from scratch (:37-45).- Batch-load the distinct speakers and build a lookup (
:48-59). - For each session: project its non-deleted speakers into SpeakerInfo, build a SessionScoringInput, call
aiScoringService.ScoreSessionAsync(:66-77); on a failed result or a failedSessionAiScore.Create, count it as failed and continue (:79-97); otherwiseAddAsyncandSaveChangesAsyncper session, catching non-cancellation exceptions as a per-session failure (:99-111). - Log completion and return the count DTO, or
Error.Failure(AiScoring.AllFailed) when nothing scored and something failed (:114-124).
- Load non-service sessions (with
- Why it's built this way: saving per session gives the organizer live progress and means a mid-run failure keeps the scores already computed; the all-failed guard surfaces a misconfiguration (a missing Anthropic API key) as a single actionable error rather than a silent empty result.
- Where it's used: invoked from the Conference AI-scoring endpoint (Group 19); the resulting scores feed GetSessionSelectionDashboardHandler's AI-score panel.
- Caveats / not-in-source: the actual AI call and model are supplied by the Infrastructure
AnthropicScoringServiceadapter (Group 20); this handler only orchestrates the port.
GetNowNextQuery
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.NowNext·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/UseCases/NowNext/GetNowNextQuery.cs:11· Level 0 · record
- What it is: the read request for the conference "happening now / up next" snapshot. Its single optional field,
EventId, names the target event; passingnulltells the handler to auto-select the current-or-next published event (GetNowNextQuery.cs:11). - Depends on: the
EventIdentifierTypealias (Conference's event id type, declared in the module's Shared project). No other first-party types. - Concept introduced, the CQRS query record.
[Rubric §6, CQRS & Event-Driven]assesses whether reads and writes are genuinely separated, with each read expressed as its own request object routed to its own handler. A query here is an immutable request-for-data with no behavior: it names the read and carries its parameters, nothing more. The whole type is one line,public sealed record GetNowNextQuery(EventIdentifierType? EventId);. It is dispatched through the shared decorator pipeline to itsIQueryHandler<in TQuery, TResult>implementation,GetNowNextHandler, which answers with aResult.[Rubric §5, Vertical Slice]assesses whether a feature's pieces live together: query, handler, and result shape for this feature all sit under oneUseCases/NowNextfolder instead of layer-wide "Queries" and "Handlers" buckets. - Walkthrough: a positional
recordwith one nullable member,EventId(GetNowNextQuery.cs:11). Thenullsentinel is load-bearing: the doc comment (GetNowNextQuery.cs:3-10) records that the home-screen widget has no event id of its own, so it passesnulland the handler features the live-or-next published event, the same rule the other home surfaces use. - Why it's built this way: a
recordgives value equality and immutability for free, so the query is safe to use as a cache or log key in the pipeline; making the event id optional lets one query serve both the event-scoped page and the global home widget without a second type. - Where it's used: handled by
GetNowNextHandler; constructed in two places byEventsController, once with the route id (MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.API/Controllers/EventsController.cs:182) and once withEventId: nullfor the id-less home form (EventsController.cs:196). Both actions are[AllowAnonymous]and short-TTL output-cached under theNowNextCachepolicy (EventsController.cs:175-177, 190-192).
GetPublicSessionFilterQuery
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.GetPublicSessionFilter·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/UseCases/GetPublicSessionFilter/GetPublicSessionFilterQuery.cs:9· Level 0 · record
- What it is: a parameterless marker query that asks for the specification describing "publicly visible sessions": non-declined, non-cancelled sessions whose parent event is published (BR-132 / BR-49).
- Depends on: nothing first-party. It is an empty record with no positional parameters.
- Concept, the marker query.
[Rubric §6, CQRS & Event-Driven]. Not every query carries parameters. This one is literallypublic sealed record GetPublicSessionFilterQuery;(GetPublicSessionFilterQuery.cs:9): it exists to select a handler through the pipeline. What comes back is not rows but a reusableSpecification<TEntity, TIdentifierType>that other read paths compose into their own queries, so "what counts as a public session" is defined in exactly one place. - Concept, cross-store filtering.
[Rubric §8, Data Architecture]assesses how a rule spanning two physical stores is expressed without an illegal cross-database join. The doc comment (GetPublicSessionFilterQuery.cs:3-8) states the constraint plainly:Sessionlives in Cosmos DB whileEventlives in SQL Server, so the "parent event is published" check cannot be a navigation join.GetPublicSessionFilterHandlerresolves it through the framework's cross-source helper instead. - Walkthrough: no members. The type is the request; every line of behavior is in
GetPublicSessionFilterHandler. - Why it's built this way: a marker record keeps the read strongly named and pipeline-routable even with no arguments, and returning a specification rather than materialized rows lets the paged public list and any other public-facing read share one definition of visibility.
- Where it's used: handled by
GetPublicSessionFilterHandler; constructed bySessionsControllerin its privateBuildPublicSessionSpecificationAsynchelper (MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.API/Controllers/SessionsController.cs:69), which returnsnullfor organizers (they see everything) and the specification for everyone else (SessionsController.cs:64-73).
GetSessionBookmarkCountQuery
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Speakers.UseCases.GetSessionBookmarkCount·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Speakers/UseCases/GetSessionBookmarkCount/GetSessionBookmarkCountQuery.cs:6· Level 0 · record
- What it is: a speaker-facing query asking "how many attendees bookmarked my session" (BR-210). It carries the requesting
SpeakerIdand the targetSessionId(GetSessionBookmarkCountQuery.cs:6). - Depends on: the
SpeakerIdentifierTypeandSessionIdentifierTypealiases. No other first-party types (it is a request record in the same mould asGetNowNextQuery). - Concept, ownership carried on the query.
[Rubric §11, Security]assesses whether authorization data travels with the request instead of being assumed by the code that serves it. The query deliberately carriesSpeakerIdso the handler can verify the caller actually presents the session before revealing a count; that check lives inGetSessionBookmarkCountHandler, not in the query. - Walkthrough: a positional
recordwith two members in order,SpeakerIdthenSessionId(GetSessionBookmarkCountQuery.cs:6). The doc comment documents each parameter and cites BR-210 (GetSessionBookmarkCountQuery.cs:3-5). - Why it's built this way: passing the speaker identity as part of the read, rather than reaching into ambient user context inside the handler, keeps the handler pure and unit-testable and makes the authorization dependency explicit in the contract.
- Where it's used: handled by
GetSessionBookmarkCountHandler; constructed bySpeakersControlleronGET {speakerId}/sessions/{sessionId}/bookmarks/count(MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.API/Controllers/SpeakersController.cs:271), an[AllowAnonymous]action output-cached underConferencePublicCache(SpeakersController.cs:262-264).
SessionEventIdRules<T>
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.Validation·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/Validation/SessionValidationRules.cs:24· Level 0 · class (sealed, generic)
- What it is: a reusable FluentValidation rule asserting that a session request carries a non-empty event id (
SessionValidationRules.cs:24-30). - Depends on:
FluentValidation.AbstractValidator<T>(NuGet); theEventIdentifierTypealias. - Concept introduced, the composable rule fragment.
[Rubric §24, Forms/Validation/UX Safety]assesses whether validation is factored into reusable single-purpose units rather than restated per request. Unlike its file-mates, which inherit the shared string rules, this one derives straight fromAbstractValidator<T>(SessionValidationRules.cs:24-25) because it validates an identifier, not a length-bounded string. It is generic overT, the request type carrying the property, and takes anExpression<Func<T, EventIdentifierType>>selector (SessionValidationRules.cs:27) so any session request canIncludeit against whichever property holds the event id. - Walkthrough: an expression-bodied constructor:
RuleFor(selector).NotEmpty()with the message "You must specify an Event for the Session" and the explicit error code"Session.EventId.Required"(SessionValidationRules.cs:27-29). TheWithErrorCodecall is what keeps the failure machine-classifiable once it flows back as anErrorthrough theResultpipeline. - Why it's built this way: a single-rule generic validator can be shared by every session request through FluentValidation's
Include, so "an event is mandatory" is authored once instead of copy-pasted. - Where it's used:
Included bySessionCreateRequestValidator(MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/UseCases/Create/SessionCreateRequestValidator.cs:12). Note it is deliberately absent fromSessionUpdateRequestValidator(.../Sessions/UseCases/Update/SessionUpdateRequestValidator.cs:11-17): a session's owning event is set at creation and is not part of the update payload.
SessionAccessibilityInfoRules<T>
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.Validation·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/Validation/SessionValidationRules.cs:87· Level 5 · class (sealed, generic)
- What it is: the reusable rule for the optional accessibility-info text on a session, enforcing only the max length defined on the domain invariants (
SessionValidationRules.cs:87-92). - Depends on:
OptionalStringRules<T>(its base),SessionInvariants(forAccessibilityInfoMaxLength, which is 500 atMMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Domain/Sessions/SessionInvariants.cs:22). - Concept introduced, the optional-string rule family.
[Rubric §24, Forms/Validation/UX Safety]and[Rubric §16, Maintainability](which assesses whether a constant governing a field lives in one place). Six of the session field rules share one shape: a one-line sealed generic class whose constructor forwards(selector, label, SessionInvariants.<Field>MaxLength)toOptionalStringRules<T>. The base does all the work, a singleRuleFor(selector).MaximumLength(maxLength)with an invariant-culture message (MMCA.Common/Source/Core/MMCA.Common.Application/Validation/CommonValidationRules.cs:25-29); it deliberately has noNotEmpty, which is what makes the field optional. Because the length comes from the domainSessionInvariantsrather than a literal, the same constant governs the EF column, the domain guard, and this request-level check. The five siblings below repeat this shape with a different label and constant; only their arguments differ. - Walkthrough: constructor forwards
(selector, "Accessibility Info", SessionInvariants.AccessibilityInfoMaxLength)to the base (SessionValidationRules.cs:90-91). - Why it's built this way: subclassing the shared optional-string rule means every optional text field in the codebase fails the same way with the same message shape, and pulling the bound from invariants stops the validator and the persistence layer from drifting apart.
- Where it's used:
Included by bothSessionCreateRequestValidator(SessionCreateRequestValidator.cs:17) andSessionUpdateRequestValidator(SessionUpdateRequestValidator.cs:16).
SessionDescriptionRules<T>
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.Validation·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/Validation/SessionValidationRules.cs:37· Level 5 · class (sealed, generic)
- What it is: the reusable rule for a session's optional description, enforcing the domain max length (
SessionValidationRules.cs:37-42). - Depends on:
OptionalStringRules<T>,SessionInvariants(DescriptionMaxLengthis 4000,SessionInvariants.cs:16). - Concept: identical shape to
SessionAccessibilityInfoRules<T>; see that section for the family rationale and the[Rubric §24]/[Rubric §16]explanation. - Walkthrough: forwards
(selector, "Session Description", SessionInvariants.DescriptionMaxLength)to the base (SessionValidationRules.cs:40-41). - Why it's built this way: the description is the largest free-text field on a session (4000 characters), so a bound is enforced at the edge as well as in the domain, and it comes from the same constant in both places.
- Where it's used:
Included bySessionCreateRequestValidator(SessionCreateRequestValidator.cs:13) andSessionUpdateRequestValidator(SessionUpdateRequestValidator.cs:12).
SessionLiveUrlRules<T>
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.Validation·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/Validation/SessionValidationRules.cs:62· Level 5 · class (sealed, generic)
- What it is: the reusable rule for a session's optional live-stream URL. It is length-only: the value is never parsed or format-checked (
SessionValidationRules.cs:62-67). - Depends on:
OptionalStringRules<T>,SessionInvariants(LiveUrlMaxLengthis 2000,SessionInvariants.cs:28). - Concept, the deliberate non-validation.
[Rubric §9, API & Contract Design]assesses whether a contract's tolerance is a decision rather than an oversight. The doc comment (SessionValidationRules.cs:56-61) says explicitly that the check is length-only because the value is stored as an opaque string for Sessionize compatibility: the upstream import is the authority on the URL's shape, so a stricter format rule here would reject data the conference actually publishes. Shape otherwise identical toSessionAccessibilityInfoRules<T>. - Walkthrough: forwards
(selector, "Live URL", SessionInvariants.LiveUrlMaxLength)to the base (SessionValidationRules.cs:65-66). - Why it's built this way: accepting whatever Sessionize supplies keeps the import path lossless; the length cap still protects the column and the payload size.
- Where it's used:
Included bySessionCreateRequestValidator(SessionCreateRequestValidator.cs:15) andSessionUpdateRequestValidator(SessionUpdateRequestValidator.cs:14). - Caveats / not-in-source: because no format rule runs here, a malformed live URL is only detectable downstream (in the UI or by the viewer). Nothing in this file validates the scheme or host.
SessionRecordingUrlRules<T>
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.Validation·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/Validation/SessionValidationRules.cs:75· Level 5 · class (sealed, generic)
- What it is: the reusable rule for a session's optional post-event recording URL, again length-only (
SessionValidationRules.cs:75-80). - Depends on:
OptionalStringRules<T>,SessionInvariants(RecordingUrlMaxLengthis 2000,SessionInvariants.cs:31). - Concept: the same opaque-string decision as
SessionLiveUrlRules<T>, stated in its own doc comment atSessionValidationRules.cs:69-74. - Walkthrough: forwards
(selector, "Recording URL", SessionInvariants.RecordingUrlMaxLength)to the base (SessionValidationRules.cs:78-79). - Why it's built this way: recording links arrive from the same external pipeline as live links, so both are treated identically.
- Where it's used:
Included bySessionCreateRequestValidator(SessionCreateRequestValidator.cs:16) andSessionUpdateRequestValidator(SessionUpdateRequestValidator.cs:15).
SessionResourceLinksRules<T>
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.Validation·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/Validation/SessionValidationRules.cs:99· Level 5 · class (sealed, generic)
- What it is: the reusable rule for a session's optional resource-links blob (slides, repos, handouts), enforcing the domain max length (
SessionValidationRules.cs:99-104). - Depends on:
OptionalStringRules<T>,SessionInvariants(ResourceLinksMaxLengthis 2000,SessionInvariants.cs:25). - Concept: family member of
SessionAccessibilityInfoRules<T>; nothing structurally new. - Walkthrough: forwards
(selector, "Resource Links", SessionInvariants.ResourceLinksMaxLength)to the base (SessionValidationRules.cs:102-103). - Why it's built this way: consistency with the other optional text fields; the single length constant is the only thing that varies.
- Where it's used:
Included bySessionCreateRequestValidator(SessionCreateRequestValidator.cs:18) andSessionUpdateRequestValidator(SessionUpdateRequestValidator.cs:17).
SessionStatusRules<T>
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.Validation·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/Validation/SessionValidationRules.cs:49· Level 5 · class (sealed, generic)
- What it is: the reusable rule for a session's optional status string ("Accepted", "Declined", and so on), enforcing the domain max length (
SessionValidationRules.cs:49-54). - Depends on:
OptionalStringRules<T>,SessionInvariants(StatusMaxLengthis 100,SessionInvariants.cs:19). - Concept: same family as
SessionAccessibilityInfoRules<T>. Worth noting what this rule does not do: it validates length only, never membership inSessionStatuses. Status values originate in Sessionize, so the field stays an open string here and the meaningful statuses are compared by constant downstream (seeGetPublicSessionFilterHandler). - Walkthrough: forwards
(selector, "Session Status", SessionInvariants.StatusMaxLength)to the base (SessionValidationRules.cs:52-53). - Why it's built this way: keeping status open-ended lets the import accept new upstream vocabulary without a code change; the read paths that care about a specific status compare it explicitly.
- Where it's used:
Included bySessionCreateRequestValidator(SessionCreateRequestValidator.cs:14) andSessionUpdateRequestValidator(SessionUpdateRequestValidator.cs:13).
SessionTitleRules<T>
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.Validation·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/Validation/SessionValidationRules.cs:13· Level 5 · class (sealed, generic)
- What it is: the reusable rule for a session title, enforcing both non-empty and the domain max length (
SessionValidationRules.cs:13-18). - Depends on:
RequiredStringRules<T>(its base),SessionInvariants(TitleMaxLengthis 500,SessionInvariants.cs:13). - Concept, required versus optional string rules.
[Rubric §24, Forms/Validation/UX Safety]. This is the one mandatory text field among the session rules, so it inheritsRequiredStringRules<T>rather thanOptionalStringRules<T>. The base pairsNotEmpty()withMaximumLength(maxLength)and formats both messages withCultureInfo.InvariantCulture(MMCA.Common/Source/Core/MMCA.Common.Application/Validation/CommonValidationRules.cs:13-18); the optional base drops theNotEmptyand keeps the rest. Choosing a base is therefore the entire "is this field required" decision, expressed once per field. - Walkthrough: a one-line class whose constructor forwards
(selector, "Session Title", SessionInvariants.TitleMaxLength)to the base (SessionValidationRules.cs:16-17). AllNotEmpty/MaximumLengthmechanics live upstream in Common. - Why it's built this way: subclassing the shared required-string rule keeps every "required, length-bounded text field" in the workspace validated identically, and reading the length from invariants prevents the validator and the EF column from diverging.
- Where it's used:
Included bySessionCreateRequestValidator(SessionCreateRequestValidator.cs:11) andSessionUpdateRequestValidator(SessionUpdateRequestValidator.cs:11), the only rule in the file included by both under the same field name.
SessionRoomScheduling
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.Validation·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/Validation/SessionRoomScheduling.cs:14· Level 7 · class (static)
- What it is: the shared room double-booking guard for the session create and update paths. It supplies the overlap predicate used for a server-side existence check and the conflict
Errorreturned when the check trips (SessionRoomScheduling.cs:14). - Depends on:
Session, theRoomIdentifierType/SessionIdentifierTypealiases,ErrorfromMMCA.Common.Shared.Abstractions, andSystem.Linq.Expressions.Expression<T>(BCL). - Concept introduced, validation that needs the database.
[Rubric §24, Forms/Validation/UX Safety]and[Rubric §8, Data Architecture]. FluentValidation rules like the ones above are pure functions over the request; a double-booking rule is not, because it depends on other rows. Rather than dragging a repository into a validator, the rule is expressed as a SQL-translatable predicate factory that a handler passes torepository.ExistsAsync(...), so the check runs as one server-side existence query instead of loading sessions into memory.[Rubric §1, SOLID]: the predicate and the error message are the only two things the two call sites need to share, so those two things (and nothing else) are what the static class exposes. - Concept, half-open intervals. The overlap test uses
[StartsAt, EndsAt)semantics: two sessions conflict only whens.StartsAt < endsAt && s.EndsAt > startsAt(SessionRoomScheduling.cs:39). Back-to-back sessions, where one ends exactly when the next begins, do not conflict. That is the standard way to avoid a spurious conflict on every adjacent slot. - Walkthrough
BuildOverlapPredicate(SessionRoomScheduling.cs:26-40): takes the room, the requested[startsAt, endsAt)window, and an optionalexcludeSessionIdused by the update path so a session does not conflict with itself.- The exclusion sentinel (
SessionRoomScheduling.cs:32-34): instead of branching into two predicate shapes, anullexclusion collapses toint.MinValue. The inline comment explains why that is safe: session ids are always positive (Sessionize-assigned or the reserved manual range), so the sentinel excludes nothing. One predicate shape also means one query plan. - The predicate body (
SessionRoomScheduling.cs:36-39): same room, not the excluded id, both endpoints non-null, and the half-open overlap test. DoubleBookedError(SessionRoomScheduling.cs:49-54): buildsError.Conflictwith code"Session.Room.DoubleBooked"and a human message, parameterized bysource(the calling handler name) andtarget(the property name), so the same error surfaces with correct tracing from either call site.
- Why it's built this way: the doc comment (
SessionRoomScheduling.cs:7-13) is candid that rejecting the overlap is a policy choice, it guards against accidental double-booking, and deliberate co-location such as lightning talks sharing a slot would need this relaxed to a warning. Keeping the policy in one static class means that change would be made in exactly one place rather than in two handlers. - Where it's used:
CreateSessionHandlercalls it only when room and both times are supplied, passing no exclusion (MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/UseCases/Create/CreateSessionHandler.cs:95-103);UpdateSessionHandlercalls it withexcludeSessionId: command.Idso a session may keep or shrink its own slot (.../Sessions/UseCases/Update/UpdateSessionHandler.cs:120-128).
GetNowNextHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.NowNext·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/UseCases/NowNext/GetNowNextHandler.cs:20· Level 8 · class (sealed)
- What it is: the query handler that builds the now-next snapshot: sessions running at the query instant plus the next starting batch, for one published
Event(GetNowNextHandler.cs:20). - Depends on:
GetNowNextQuery,IUnitOfWork,TimeProvider(BCL),CurrentEventSelector,CalendarExportMapper,EventandSession, and theNowNextDTO/NowNextSessionDTOresult shapes. - Concept introduced, the injectable clock.
[Rubric §14, Testability]assesses whether time-dependent logic is driven by an abstraction instead ofDateTime.UtcNow. The handler takesTimeProviderthrough its primary constructor (GetNowNextHandler.cs:21) and readstimeProvider.GetUtcNow()once at the top (GetNowNextHandler.cs:29), so "now" is a single deterministic value for the whole computation and can be pinned in a test.[Rubric §12, Performance & Scalability]: the now and next partitions are computed in memory over one session load rather than issuing several round-trips. - Concept, read-side projection.
[Rubric §6, CQRS & Event-Driven]. This is decision-support output the domain never stores: it reshapes aggregate state into a live-schedule view that only exists on the read side. - Walkthrough
- Event selection (
GetNowNextHandler.cs:31,90-113):SelectEventAsynceither loads the explicitEventIdwith itsRooms(GetNowNextHandler.cs:97-101) or, when the id isnull, loads all published events and delegates toCurrentEventSelector.SelectCurrentOrNext(GetNowNextHandler.cs:103-112). A missing or unpublished event returnsError.NotFoundtagged with the handler andEvent(GetNowNextHandler.cs:32-36). - Session load plus eligibility (
GetNowNextHandler.cs:38-56): loads the event's sessions and its room-name lookup, then filters withCalendarExportMapper.IsExportable, which requires both timestamps present, a non-service session, and a status that is neither "Declined" nor "Cancelled" (.../Sessions/UseCases/ExportCalendar/CalendarExportMapper.cs:20-23). The now-next surface therefore cannot show a session the calendar export would hide. - Time-zone conversion (
GetNowNextHandler.cs:43-51,80-88): resolves the event's zone with atry/catchonTimeZoneNotFoundExceptionfalling back toTimeZoneInfo.Utc, so an unknown zone id degrades instead of throwing; each row carries both the stored wall-clock times and their UTC conversions viaCalendarExportMapper.ToUtc. - Now and next partitioning (
GetNowNextHandler.cs:58-72): "now" is every row whose interval straddlesutcNow, ordered by start then room name withStringComparer.OrdinalIgnoreCase; "next" is the batch sharing the earliest future start, so parallel tracks appear together rather than one arbitrary session (the intent is stated in the inline comment atGetNowNextHandler.cs:64). - Live flag and result (
GetNowNextHandler.cs:74-77): asksCurrentEventSelector.GetLiveWindowUtcfor the event's window and setsisLiveby containment, then returnsResult.Success(new NowNextDTO(...)).
- Event selection (
- Why it's built this way: delegating event selection and the live window to
CurrentEventSelector, and eligibility plus UTC conversion toCalendarExportMapper, keeps the now-next widget, the home surfaces, and the calendar export consistent by construction rather than by three copies of the same rules. The doc comment attributes the feature to ADR-042 Wave 8 (GetNowNextHandler.cs:12-19). - Where it's used: invoked by both now-next actions on
EventsController(EventsController.cs:182, 196). - Caveats / not-in-source: the "ADR-042 Wave 8" attribution comes from the source doc comment; this walkthrough describes only what the method bodies do.
GetSessionBookmarkCountHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Speakers.UseCases.GetSessionBookmarkCount·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Speakers/UseCases/GetSessionBookmarkCount/GetSessionBookmarkCountHandler.cs:14· Level 8 · class (sealed)
- What it is: the handler for
GetSessionBookmarkCountQuery. It verifies the calling speaker is assigned to the session, then reads the bookmark count from the Engagement bounded context (GetSessionBookmarkCountHandler.cs:14). - Depends on:
IUnitOfWork,IBookmarkCountService(the cross-module port),Session,Error/Result. - Concept introduced, the cross-module read over a port.
[Rubric §7, Microservices Readiness]assesses whether a module reaches another bounded context through an interface rather than a direct type or table reference. Bookmarks belong to Engagement, so this Conference handler depends on theIBookmarkCountServiceabstraction injected through its primary constructor (GetSessionBookmarkCountHandler.cs:14-16) and callsGetBookmarkCountForSessionAsync(GetSessionBookmarkCountHandler.cs:43). In the extracted topology that port is satisfied by a gRPC client (or a disabled-module stub); the handler cannot tell which, and does not change either way. - Concept, authorize before you answer.
[Rubric §11, Security]. The ownership gate runs before the cross-module call, so an unauthorized caller never causes a downstream read and never learns a number. - Walkthrough
- Load with membership (
GetSessionBookmarkCountHandler.cs:23-30): resolves theSessionrepository and loads the session includingSessionSpeakerswithasTracking: false(this is a read, so no change tracking). A missing session returnsError.NotFoundtagged with the handler andSession. - Ownership check (
GetSessionBookmarkCountHandler.cs:32-40): if no non-deletedSessionSpeakermatches the query'sSpeakerId, it returnsError.Forbiddenwith code"Speaker.NotAssigned", targeted atSpeakerId. The explicit!ss.IsDeletedterm matters: a speaker removed from a session is soft-deleted, and a removed speaker must not keep reading the session's numbers. - Cross-module read (
GetSessionBookmarkCountHandler.cs:42-45): calls the port and wraps theintinResult.Success.
- Load with membership (
- Why it's built this way: keeping bookmark counting behind an interface preserves the Engagement / Conference boundary (ADR-007's extraction path), so the same handler runs in-process in a monolith or over gRPC in the extracted topology with no code change; doing the ownership check locally, on data Conference already owns, means the authorization decision needs no remote call.
- Where it's used: invoked by
SpeakersControlleron the per-session bookmark-count endpoint (SpeakersController.cs:270-272).
GetPublicSessionFilterHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.GetPublicSessionFilter·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/UseCases/GetPublicSessionFilter/GetPublicSessionFilterHandler.cs:17· Level 9 · class (sealed)
- What it is: the handler for
GetPublicSessionFilterQuery. It returns aSpecification<TEntity, TIdentifierType>selecting public sessions: non-declined, non-cancelled sessions whose parent event is published (BR-132 / BR-49) (GetPublicSessionFilterHandler.cs:17-19). - Depends on:
IUnitOfWork,CrossSourceSpecification,Event,Session,SessionStatuses. - Concept, the cross-source specification helper.
[Rubric §8, Data Architecture]and[Rubric §7, Microservices Readiness]. When the principal entity and the dependent entity live in different stores, the "belongs to a published event" filter cannot be a join. The handler hands that problem to the framework'sCrossSourceSpecification.BuildAsync(GetPublicSessionFilterHandler.cs:26-33), which resolves the publishedEventids from their own source and returns aSession.EventId IN (...)criteria ANDed with a local predicate, translatable whereverSessionis stored.[Rubric §12, Performance & Scalability]: the id set is resolved once per request and embedded in the filter, so the session read stays a single query. - Walkthrough
- The
BuildAsynccall (GetPublicSessionFilterHandler.cs:26-33): generic over<Session, SessionIdentifierType, Event, EventIdentifierType>, withprincipalPredicate: e => e.IsPublished,dependentForeignKey: s => s.EventId, andlocalPredicate: s => s.Status != SessionStatuses.Declined && s.Status != "Cancelled". - Return (
GetPublicSessionFilterHandler.cs:35): wraps the built specification inResult.Success. The handler has no failure path of its own; anything it cannot do surfaces from the helper.
- The
- Why it's built this way: routing the two-store filter through one reusable helper solves the "no cross-database join" rule in the framework once, and keeping the status test as a local predicate leaves the visibility rule readable at the call site. The doc comments on both the query and the handler (
GetPublicSessionFilterQuery.cs:3-8,GetPublicSessionFilterHandler.cs:11-16) record thatSessionis Cosmos-stored andEventSQL-stored, which is the reason the helper exists. - Where it's used:
SessionsControllercalls it for every non-organizer session read and applies the returned specification to the listing query (SessionsController.cs:64-73); organizers skip it entirely and see declined sessions. - Caveats / not-in-source:
"Cancelled"is a bare string literal atGetPublicSessionFilterHandler.cs:31whileDeclineduses theSessionStatusesconstant. Both are checked, but only one is symbolic.
GetSessionBookmarkCountsQuery
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Speakers.UseCases.GetSessionBookmarkCounts·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Speakers/UseCases/GetSessionBookmarkCounts/GetSessionBookmarkCountsQuery.cs:6· Level 0 · record
- What it is: the query message a speaker sends to learn how many attendees have bookmarked each of their sessions, asked for a whole set of sessions in one call (BR-210). It is a two-member positional
sealed recordand nothing more. - Depends on: the
SpeakerIdentifierTypeandSessionIdentifierTypealiases (module-wideglobal usingdeclarations, see the primer's identifier-alias section in00-primer.md);IReadOnlyCollection<T>(BCL). - Concept introduced: the batched query message. Most read slices in this chapter carry a single id. This one carries a collection (
SessionIds, line 8) so the handler can answer for N sessions with a bounded number of round trips instead of one call per session. The shape of the message is what makes the batching possible: an API that only accepted a single id would force the caller into an N+1 loop no matter how efficient the handler was.[Rubric §12, Performance & Scalability](assesses whether the design avoids avoidable round trips under realistic load): the contract itself is the optimization, and the downstream IBookmarkCountService has a matching batch method so the saving survives the cross-module hop. - Walkthrough:
SpeakerId(line 7) identifies the caller and exists purely so the handler can re-verify ownership server side;SessionIds(line 8) is the requested set. There are no methods, no validation, and no defaults: the record is an immutable request envelope. - Why it's built this way: CQRS query messages in this codebase are plain records dispatched to an IQueryHandler<in TQuery, TResult>; keeping them behavior-free means the decorator pipeline (logging, caching) can wrap any of them uniformly.
- Where it's used: constructed by
SpeakersController(MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.API/Controllers/SpeakersController.cs:291, which passessessionIds ?? []) and handled by GetSessionBookmarkCountsHandler.
GetSessionFeedbackQuery
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Speakers.UseCases.GetSessionFeedback·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Speakers/UseCases/GetSessionFeedback/GetSessionFeedbackQuery.cs:6· Level 0 · record
- What it is: the query asking for the aggregated attendee feedback (ratings plus free-text answers) on one session, on behalf of one speaker (BR-210).
- Depends on: the
SpeakerIdentifierTypeandSessionIdentifierTypealiases. - Concept introduced: none new. Like GetSessionBookmarkCountsQuery it is a behavior-free record; the difference is scope (one session, line 6).
[Rubric §11, Security](assesses whether authorization data travels with the request and is checked server side): carryingSpeakerIdin the message, rather than trusting a client assertion that "these are my sessions", is what lets GetSessionFeedbackHandler reject a speaker asking about someone else's session. - Walkthrough: two positional members on a single line:
SpeakerId(the caller) andSessionId(the target). No members beyond those. - Why it's built this way: the vertical-slice convention pairs one folder with one query record, one handler, and one result DTO, so the whole read use case is legible in one directory.
[Rubric §5, Vertical Slice]. - Where it's used: created by
SpeakersController(MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.API/Controllers/SpeakersController.cs:252) and answered with a SessionFeedbackDTO.
GetSpeakersByEventFilterQuery
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Speakers.UseCases.GetSpeakersByEventFilter·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Speakers/UseCases/GetSpeakersByEventFilter/GetSpeakersByEventFilterQuery.cs:12· Level 0 · record
- What it is: a one-member query (
EventId, line 12) whose answer is not data but a filter: the handler returns a Specification<TEntity, TIdentifierType> that selects the speakers belonging to an event. - Depends on: the
EventIdentifierTypealias. - Concept introduced: a query that returns a specification instead of rows. Normally a query handler returns DTOs. Here the handler returns a reusable predicate object, so the caller can compose it with paging, sorting, and its own criteria before a single row is fetched. The XML comment on the record (lines 3-10) states the domain reason this indirection exists: a
Speakerbelongs to an event either directly, through theEventSpeakerjoin written by the Sessionize sync, or transitively through any session of that event, through theSessionSpeakerjoin written by organizer session management. The two link paths are populated by different flows, so "speakers of this event" is a union, not a column.[Rubric §2, Design Patterns]: this is the Specification pattern used as the return type of a use case, which keeps the union rule in one place instead of duplicating it into every controller that lists speakers.[Rubric §8, Data Architecture]: the comment notes the criteria stays engine-portable because the joins are resolved as ID-list projections rather than navigation joins. - Walkthrough: a single positional member,
EventId. All of the interesting work lives in GetSpeakersByEventFilterHandler; the record only names the input. - Why it's built this way:
Speakerhas noEventIdcolumn, so the relationship cannot be expressed as a simple property filter. Modeling the answer as a specification lets the read endpoint keep its generic list-and-page machinery and just plug in an extra criterion. - Where it's used:
SpeakersControllerinjects the handler asIQueryHandler<GetSpeakersByEventFilterQuery, Result<Specification<Speaker, SpeakerIdentifierType>>>(MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.API/Controllers/SpeakersController.cs:52) and dispatches the query at line 99 of that file.
ConferenceCategoryUpdateRequest
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Categories.UseCases.Update·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Categories/UseCases/Update/ConferenceCategoryUpdateRequest.cs:6· Level 1 · record
- What it is: the inbound payload the API binds when a client edits an existing conference category: a
record classofinit-only properties describing the new field values. - Depends on: IConcurrencyAware (Level 0, implemented at line 6).
- Concept introduced: the update request that carries an EF rowversion token. The record implements IConcurrencyAware with a nullable
byte[]? RowVersion { get; init; }(line 9). The client echoes back the rowversion it last read; UpdateConferenceCategoryHandler stamps it as the entity's original token, so a concurrent edit fails loudly rather than silently overwriting.[Rubric §9, API & Contract Design](assesses whether request contracts are explicit and version-safe):required string Title(line 12) forces the caller to supply the one field the aggregate cannot default, so a malformed request is rejected at model binding rather than deep inside the handler. - Walkthrough:
RowVersion(line 9, nullable, a first-time create or a client that opts out passes null);Title(line 12,required);Sort(line 15, anintdisplay order that defaults to 0);Type(line 18, an optional discriminator whose doc comment gives "session" and "speaker" as examples). Every property isget; init;, so the request is immutable once bound. - Why it's built this way: keeping the update payload separate from the create payload lets the two evolve independently: an update can demand a concurrency token, a create has nothing to concur with.
- Where it's used: bound by
ConferenceCategoriesController, wrapped into UpdateConferenceCategoryCommand (MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.API/Controllers/ConferenceCategoriesController.cs:110), validated by ConferenceCategoryUpdateRequestValidator, and unpacked field by field into the domainUpdate(...)call.
EventUpdateRequest
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.Update·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/UseCases/Update/EventUpdateRequest.cs:7· Level 1 · record
- What it is: the inbound payload for editing an existing conference event: the widest of the Conference update requests, covering identity, dates, venue, and the live-layer moderation default.
- Depends on: IConcurrencyAware (line 7); the QuestionModerationDefault enum (line 40);
DateOnly(BCL). - Concept introduced: none new: the rowversion round trip is described under ConferenceCategoryUpdateRequest and applies verbatim here (
RowVersion, line 10). What this record adds is the split betweenrequiredand optional fields.[Rubric §9, API & Contract Design]:Name(line 13),StartDate(line 19),EndDate(line 22), andTimeZone(line 25) arerequired, so the compiler and the model binder together guarantee an event can never be updated into a state with no name or no schedule; everything else is genuinely nullable. - Walkthrough
- Concurrency:
RowVersion(line 10). - Identity:
Name(line 13, required),Description(line 16, optional). - Schedule:
StartDate/EndDateasDateOnly(lines 19, 22) andTimeZone(line 25), documented as an IANA time zone identifier. Storing the zone as a string next to date-only values is what lets the handler detect a zone change without reinterpreting stored times. - Integration and venue:
SessionizeCode(line 28, the code the Sessionize import uses),VenueAddress(line 31),VenueMapUrl(line 34),WiFiInfo(line 37), all optional strings. - Live layer:
QuestionModerationDefault(line 40), the BR-233 default applied to session questions.
- Concurrency:
- Why it's built this way: a record gives value equality and a compact immutable contract at no cost, and the flat property list maps one-to-one onto the ten arguments the
Event.Update(...)domain method takes, so the handler stays a straight-line translation with nothing to interpret. - Where it's used: bound by the Events controller, carried by UpdateEventCommand, checked by EventUpdateRequestValidator, and applied by UpdateEventHandler.
UpdateEventResult
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.Update·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/UseCases/Update/UpdateEventCommand.cs:19· Level 3 · record
- What it is: a two-member
sealed recorddeclared alongside its command in the same file, pairing the updated EventDTO with a single advisory flag,HasTimeZoneWarning. - Depends on: EventDTO.
- Concept introduced: the warning-carrying result versus a hard failure. Most Conference commands return
Result<TDTO>directly (see UpdateConferenceCategoryHandler). The event update returnsResult<UpdateEventResult>instead, so the Result still models success versus failure while the wrapped record adds a non-fatal signal: the event's timezone was changed while sessions already exist (BR-131, documented at line 18).[Rubric §9, API & Contract Design]: separating a hard error (a failedResult) from a soft advisory (aboolon a successful payload) lets the UI warn ("this event has sessions; changing the timezone may misplace them") without blocking the save.[Rubric §16, Maintainability]: a dedicated result type leaves room for a second advisory flag later without changing the handler signature. - Walkthrough: positional members
EventandHasTimeZoneWarning(line 19). No behavior; it is a pure carrier, which is why it lives in the command file rather than earning its own. - Why it's built this way: overloading the DTO with a transient UI concern would leak a request-scoped fact into a persisted shape, and an out parameter does not survive an async
Resultpipeline. - Where it's used: constructed by UpdateEventHandler at
.../Events/UseCases/Update/UpdateEventHandler.cs:67and unwrapped by the Events controller.
EventUpdateRequestValidator
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.Update·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/UseCases/Update/EventUpdateRequestValidator.cs:7· Level 6 · class
- What it is: the FluentValidation validator for EventUpdateRequest. It composes three reusable field-rule sets and adds one bespoke rule, so the create and update paths share a single definition of "a valid name / timezone / date range".
- Depends on:
AbstractValidator<T>(FluentValidation, NuGet); the shared rule sets EventNameRules<T>, EventTimeZoneRules<T>, and EventDateRangeRules<T>; the QuestionModerationDefault enum. - Concept introduced: rule composition via FluentValidation
Include.Include(ruleSet)folds an entire rule set into this validator. Each set is generic over the request type and is constructed with property selectors, so the same rules bind to whichever record is being validated:new EventNameRules<EventUpdateRequest>(p => p.Name)(line 11),new EventTimeZoneRules<EventUpdateRequest>(p => p.TimeZone)(line 12), andnew EventDateRangeRules<EventUpdateRequest>(p => p.StartDate, p => p.EndDate)(line 13, taking two selectors because the rule spans two properties).[Rubric §1, SOLID]: a name-length or timezone rule changes in exactly one place and both the create and update slices follow.[Rubric §15, Best Practices & Code Quality]: the one inline rule,RuleFor(x => x.QuestionModerationDefault).IsInEnum()(lines 15-18), attaches both a human message and a stable machine-readableWithErrorCode("Event.QuestionModerationDefault.Invalid"), so clients can branch on the code rather than on prose. - Walkthrough: a
sealed class : AbstractValidator<EventUpdateRequest>(line 7) whose parameterless constructor (line 9) does all the wiring: threeIncludecalls, then the enum guard. Nothing else is declared. - Why it's built this way: validators are discovered by assembly scanning and executed by the validating decorator in the CQRS pipeline before the transaction opens (see group-05), so no handler carries validation boilerplate and no command can skip the gate.
- Where it's used: resolved and run by the validating decorator ahead of UpdateEventHandler.
UpdateConferenceCategoryCommand
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Categories.UseCases.Update·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Categories/UseCases/Update/UpdateConferenceCategoryCommand.cs:9· Level 6 · record
- What it is: the CQRS command message for the category update slice: a positional
sealed recordpairing the targetIdwith the updateRequest, which also declares what cache it invalidates. - Depends on: ICommandWithRequest<out TRequest> and ICacheInvalidating (both Level 0, both implemented at line 9); ConferenceCategoryUpdateRequest; the
ConferenceCategoryIdentifierTypealias; the Category domain entity, referenced only insideCachePrefix. - Concept introduced:
ICacheInvalidatingon a write command. Implementing ICacheInvalidating tells the caching decorator to evict every cached read whose key starts with the returned prefix once the command succeeds, so a stale category is not served after an edit. The prefix is derived from the type itself,$"{typeof(Category).FullName}:"(line 12), rather than hand-typed, so a namespace rename cannot silently break invalidation. Note the deliberate naming split: the command and request are namedConferenceCategory...(the module-level vocabulary, disambiguating from other modules' categories) while the domain entity is plainCategory.[Rubric §6, CQRS & Event-Driven]: the command is a thin intent message with no logic, keeping the write side declarative.[Rubric §12, Performance & Scalability]: read caching stays correct because every mutation self-declares what it invalidates instead of relying on a TTL to expire. - Walkthrough: the record header carries the two positional members and the two interfaces; the body is a single expression-bodied
CachePrefixproperty (lines 11-12). No other members. - Why it's built this way: pairing an id with a request DTO is the standard
ICommandWithRequest<TRequest>shape used across every module, so the command plugs into the shared handler pipeline with zero special casing. - Where it's used: constructed by
ConferenceCategoriesController(MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.API/Controllers/ConferenceCategoriesController.cs:110) and dispatched to UpdateConferenceCategoryHandler through the decorator pipeline.
UpdateEventCommand
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.Update·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/UseCases/Update/UpdateEventCommand.cs:10· Level 6 · record
- What it is: the CQRS command for the event update slice, structurally identical to UpdateConferenceCategoryCommand:
sealed record UpdateEventCommand(EventIdentifierType Id, EventUpdateRequest Request)implementing ICommandWithRequest<out TRequest> and ICacheInvalidating (line 10). - Depends on: EventUpdateRequest; the
EventIdentifierTypealias; the Event domain entity (only for the cache prefix). - Concept introduced: none new: see the
ICacheInvalidatingdiscussion under UpdateConferenceCategoryCommand.CachePrefixreturns$"{typeof(Event).FullName}:"(line 13). - Walkthrough: two positional members plus the expression-bodied
CachePrefix(lines 12-13). Note that UpdateEventResult is declared in the same file at line 19; the two live together because the result exists only to serve this command. - Why it's built this way: the command carries no logic so that the pipeline (logging, caching, validating, transactional decorators) is the only thing between the controller and the handler, and every write slice behaves the same way.
- Where it's used: created by the Events controller and dispatched to UpdateEventHandler.
ConferenceCategoryUpdateRequestValidator
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Categories.UseCases.Update·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Categories/UseCases/Update/ConferenceCategoryUpdateRequestValidator.cs:7· Level 7 · class
- What it is: the smallest validator in this unit: it validates ConferenceCategoryUpdateRequest by including one shared rule set and nothing else.
- Depends on:
AbstractValidator<T>(FluentValidation); ConferenceCategoryTitleRules<T>. - Concept introduced: none new: rule composition via
Includeis taught under EventUpdateRequestValidator. This class is the minimal expression of it, and it shows why the pattern pays off: the entire body is=> Include(new ConferenceCategoryTitleRules<ConferenceCategoryUpdateRequest>(p => p.Title));(lines 9-10), an expression-bodied constructor. The title rules themselves live once and are shared with the create request. - Walkthrough:
sealed class : AbstractValidator<ConferenceCategoryUpdateRequest>(line 7) with a single expression-bodied constructor (line 9).SortandTypecarry no validation rules here:Sortis anintwith no constrained range, andTypeis a free-form optional string. - Why it's built this way: even a one-rule validator is worth its own class, because the pipeline discovers validators by type and a slice with no validator would silently skip the gate. Making the file trivial is the point: nothing here can drift from the create path.
- Where it's used: run by the validating decorator ahead of UpdateConferenceCategoryHandler.
- Caveats / not-in-source: whether
SortorTypeare constrained further downstream is not visible from this file; the domainCategory.Updateonly re-checks the title (MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Domain/Categories/Category.cs:86-87).
GetSessionBookmarkCountsHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Speakers.UseCases.GetSessionBookmarkCounts·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Speakers/UseCases/GetSessionBookmarkCounts/GetSessionBookmarkCountsHandler.cs:17· Level 8 · class
- What it is: the read handler for GetSessionBookmarkCountsQuery. It re-verifies server side that each requested session really belongs to the calling speaker, then delegates the actual counting to the Engagement module in one batched call, returning a map of session id to count.
- Depends on: IQueryHandler<in TQuery, TResult> (line 20); IUnitOfWork (line 18); IBookmarkCountService (line 19); the Session and SessionSpeaker domain entities; Result.
- Concept introduced: never trust the client's id list, and cross-module reads through an interface. Two ideas meet in this handler.
- Server-side re-authorization. The query arrives with a list of session ids the client claims are the speaker's. The handler loads those sessions with their
SessionSpeakersnavigation included (lines 34-38) and keeps only the ones wheres.SessionSpeakers.Any(ss => ss.SpeakerId == query.SpeakerId && !ss.IsDeleted)(lines 43-46). A foreign or stale id is silently dropped rather than failing the batch (comment at lines 40-42), so one bad id never denies a speaker the counts for their legitimate sessions.[Rubric §11, Security](assesses whether authorization is enforced at the server on data the client cannot forge): the ownership check reads the join table, and the!ss.IsDeletedclause means a soft-deleted assignment no longer confers access. - Cross-module call through a
Sharedinterface. Bookmarks are owned by the Engagement module, so Conference never touches Engagement entities. It depends on IBookmarkCountService, declared inMMCA.ADC/Source/Modules/Engagement/MMCA.ADC.Engagement.Shared/UserSessionBookmarks/IBookmarkCountService.cs:8, and callsGetBookmarkCountsForSessionsAsync(lines 54-56), the batch method whose contract promises every requested id is present in the result with 0 for sessions that have none (IBookmarkCountService.cs:19-27). In the deployed topology that interface is satisfied by a gRPC client, so the batching directly reduces network hops.[Rubric §7, Microservices Readiness]: the module boundary is an interface in aSharedproject, which is exactly what makes the in-process and the gRPC implementations interchangeable.
- Server-side re-authorization. The query arrives with a list of session ids the client claims are the speaker's. The handler loads those sessions with their
- Walkthrough
- Empty-input short circuit (lines 27-31): an empty
SessionIdsreturns an empty dictionary without touching the database. - Load with the join (lines 33-38):
unitOfWork.GetReadRepository<Session, SessionIdentifierType>()thenGetAllAsyncwithincludes: [nameof(Session.SessionSpeakers)], awhereofquery.SessionIds.Contains(s.Id)(oneINquery, not N lookups) andasTracking: false. The read repository expresses query-only intent and keeps the change tracker clean. - Filter to owned sessions (lines 43-46), then a second short circuit if nothing survived (lines 48-52), which avoids a pointless cross-module round trip.
- Delegate and return (lines 54-58): the batched count call, wrapped in
Result.Success(counts).
- Empty-input short circuit (lines 27-31): an empty
- Why it's built this way: counting bookmarks inside Conference would require Conference to read Engagement's tables, breaking database-per-service (ADR-006). Routing through the interface keeps each module the sole owner of its data while still letting a speaker see one merged view.
- Where it's used: injected into
SpeakersControllerasIQueryHandler<GetSessionBookmarkCountsQuery, Result<IReadOnlyDictionary<SessionIdentifierType, int>>>(.../MMCA.ADC.Conference.API/Controllers/SpeakersController.cs:51) and invoked at line 291 of that file.
GetSessionFeedbackHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Speakers.UseCases.GetSessionFeedback·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Speakers/UseCases/GetSessionFeedback/GetSessionFeedbackHandler.cs:15· Level 8 · class
- What it is: the read handler that turns raw session question answers into a speaker-facing feedback summary: numeric questions become an average plus a response count, everything else becomes a list of text responses. It refuses the request outright if the caller is not assigned to the session.
- Depends on: IQueryHandler<in TQuery, TResult> (line 16); IUnitOfWork (line 16); the Session, SessionQuestionAnswer, and Question domain entities; SessionFeedbackDTO with RatingQuestionSummary and TextQuestionResponses; Error and Result;
CultureInfo/NumberStyles(BCL). - Concept introduced: in-memory aggregation over an eagerly loaded aggregate, and
Error.Forbiddenas a first-class outcome.- Authorization as a typed error. If the session exists but the caller is not among its non-deleted speakers (line 33), the handler returns
Error.Forbidden(code: "Speaker.NotAssigned", ...)(lines 35-40) rather than throwing or returning an empty payload. A missing session returnsError.NotFoundinstead (lines 29-30).[Rubric §11, Security]: the two distinct outcomes let the API answer 403 versus 404 precisely;[Rubric §9, API & Contract Design]: the stablecodestring is what a client branches on. - Aggregation in the application layer. The answers are already attached to the loaded session (
includesat line 26) and, as the comment at line 42 notes, EF's global query filters have already excluded soft-deleted rows, so the grouping runs in memory over a bounded set rather than as extra SQL.[Rubric §12, Performance & Scalability]: the second query is deliberately narrowed to just the questions that actually have answers (lines 56-62), so an event with hundreds of questions does not drag them all into memory to summarize one session.
- Authorization as a typed error. If the session exists but the caller is not among its non-deleted speakers (line 33), the handler returns
- Walkthrough
- Load the session with what it needs (lines 23-28):
GetRepository<Session, SessionIdentifierType>()thenGetByIdAsyncwithincludes: [nameof(Session.SessionSpeakers), nameof(Session.SessionQuestionAnswers)]andasTracking: false. Null givesError.NotFound(lines 29-30). - Ownership check (lines 33-40), described above.
- Empty-answer fast path (lines 44-53): with no answers it returns a well-formed
SessionFeedbackDTOcarrying the session id and title and two empty collections, so the caller never has to special-case null. - Question lookup (lines 56-63): the distinct
QuestionIdvalues become aHashSet, oneGetAllAsyncfetches exactly those questions, andToDictionarybuilds the lookup. - Group and summarize (lines 68-101): answers are grouped by
QuestionId; a group whose question is missing from the lookup is skipped (lines 70-71). Whenquestion.QuestionType == "Rating"(line 73) the answer values are parsed withint.TryParse(..., NumberStyles.Integer, CultureInfo.InvariantCulture, ...)(line 76), unparseable values are dropped, and a RatingQuestionSummary withAverageRatingandResponseCountis emitted only if at least one value parsed (lines 81-90). Every other question type becomes a TextQuestionResponses carrying the raw strings (lines 94-99). - Return (lines 103-109): a
SessionFeedbackDTOwith the two lists.
- Load the session with what it needs (lines 23-28):
- Why it's built this way: parsing with
CultureInfo.InvariantCulturematters because the answer value is stored as a free-form string; a culture-sensitive parse would behave differently per server locale. Tolerating unparseable ratings instead of failing keeps one malformed historical answer from blanking a speaker's whole feedback page. - Where it's used: injected into
SpeakersController(.../MMCA.ADC.Conference.API/Controllers/SpeakersController.cs:49) and invoked at line 252 of that file. - Caveats / not-in-source: the rating question type is matched against the literal string
"Rating"(line 73).Question.QuestionTypeis astring, so the set of valid type values is not enforced by this file; a typo in stored data would be summarized as a text question instead.
GetSpeakersByEventFilterHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Speakers.UseCases.GetSpeakersByEventFilter·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Speakers/UseCases/GetSpeakersByEventFilter/GetSpeakersByEventFilterHandler.cs:19· Level 8 · class
- What it is: the handler that resolves "which speakers belong to this event" into a reusable filter. It collects the speaker ids linked directly to the event and those reachable through the event's sessions, unions them, and returns a
Speaker.Id IN (...)specification. - Depends on: IQueryHandler<in TQuery, TResult> (line 21); IUnitOfWork (line 20); the EventSpeaker, Session, SessionSpeaker, and Speaker domain entities; Specification<TEntity, TIdentifierType> and InlineSpecification<TEntity, TIdentifierType>; Result.
- Concept introduced: ID-list projection instead of a navigation join. Rather than writing one LINQ expression that walks
Speaker -> SessionSpeaker -> Session -> Event, the handler issues separate projection queries and combines their results in memory. Each usesGetProjectedAsync(selector, predicate, asTracking: false, cancellationToken)on a read repository, which returns just the selected column instead of whole entities. The class comment (lines 11-18) gives the rationale: the ID-list shape mirrors the framework's cross-source specification helper (BR-132 precedent), no navigation join is required, so the criteria stays translatable on any engine, and the Speaker aggregate keeps a by-ID boundary to the Event and Session aggregates.[Rubric §4, DDD]: aggregates reference each other by identifier, never by object graph, and this handler honors that even while answering a cross-aggregate question.[Rubric §8, Data Architecture]: engine-portable criteria matter because the framework supports more than one provider (ADR-018), and a provider-specific join expression would not survive the move. - Walkthrough
- Direct links (lines 28-31): project
SpeakerIdfromEventSpeakerwhereEventIdmatches. These rows are written by the Sessionize sync. - The event's sessions (lines 33-36): project
IdfromSessionwhereEventIdmatches. - Transitive links (lines 38-48): only if there are sessions. The session id collection is materialized once into a list (line 42) with the explicit comment that this embeds a stable collection EF can translate into an
INclause; thenSessionSpeaker.SpeakerIdis projected wheresessionIdList.Contains(ss.SessionId). These rows come from organizer session management, a different flow from the Sessionize sync, which is exactly why both paths must be checked. - Union (line 50):
eventSpeakerIds.Concat(sessionSpeakerIds).Distinct()collected into anIReadOnlyList<SpeakerIdentifierType>. - Build the filter (lines 52-53):
Result.Success<Specification<Speaker, SpeakerIdentifierType>>(new InlineSpecification<Speaker, SpeakerIdentifierType>(s => speakerIds.Contains(s.Id))). The captured local list becomes theINlist when the caller composes and executes the specification.
- Direct links (lines 28-31): project
- Why it's built this way:
Speakerhas noEventIdcolumn, so membership is a union of two join tables. Returning a specification instead of a page of speakers lets the calling endpoint keep its generic paging, sorting, and projection machinery and simply add this criterion. The cost is three small projection queries up front, paid once per request. - Where it's used: injected into
SpeakersController(.../MMCA.ADC.Conference.API/Controllers/SpeakersController.cs:52) and invoked at line 99, where the returned specification is composed into the speaker list query. - Caveats / not-in-source: the handler does not verify that the event exists; a nonexistent
EventIdsimply yields an empty id list and therefore a specification that matches no speakers.
UpdateConferenceCategoryHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Categories.UseCases.Update·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Categories/UseCases/Update/UpdateConferenceCategoryHandler.cs:15· Level 8 · class (sealed partial)
- What it is: the command handler for UpdateConferenceCategoryCommand. It is the clearest example of the canonical update shape in this module: load, stamp the concurrency token, delegate to the domain, save, log, map.
- Depends on: ICommandHandler<in TCommand, TResult> (line 18); IUnitOfWork (line 16); ConferenceCategoryDTOMapper (line 17);
ILogger<T>(Microsoft.Extensions.Logging); the Category domain entity; Error / Result; ConferenceCategoryDTO. - Concept introduced: optimistic concurrency stamping, and delegation to the domain.
- Concurrency stamp. After loading, the handler calls
repository.SetOriginalRowVersion(entity, command.Request.RowVersion)(line 32). The comment (lines 30-31) states the intent exactly: this stamps the client's last-seen token so a concurrent edit surfaces as aDbUpdateConcurrencyExceptionmapped to HTTP 409, instead of silent last-write-wins.[Rubric §8, Data Architecture]: correctness under concurrent edits is enforced at the persistence boundary rather than by hopeful convention. - Domain delegation. The handler does not assign properties. It calls
entity.Update(Title, Sort, Type)(lines 34-37), which returns a Result; on failure the errors are propagated unchanged (lines 39-40). The entity, not the handler, owns its invariants:Category.Updatere-checks the title throughCategoryInvariants.EnsureTitleIsValidand raises aCategoryChangeddomain event (MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Domain/Categories/Category.cs:84-95).[Rubric §3, Clean Architecture]: the application layer orchestrates persistence while the domain layer decides what a valid category is. - Structured logging without allocation. The class is
partialso the source generator can emitLogConferenceCategoryUpdatedfrom the[LoggerMessage]declaration (lines 49-50), invoked at line 44.[Rubric §13, Observability & Operability]: every successful update is recorded with a strongly typedCategoryIdproperty that a log query can filter on, with no interpolated-string cost when the level is disabled.
- Concurrency stamp. After loading, the handler calls
- Walkthrough: primary-constructor dependencies (lines 15-18);
GetRepository<Category, ConferenceCategoryIdentifierType>()andGetByIdAsync(lines 25-26); null givesError.NotFound.WithSource(nameof(UpdateConferenceCategoryHandler)).WithTarget(nameof(Category))(lines 27-28); concurrency stamp (line 32); domainUpdateplus failure propagation (lines 34-40);await unitOfWork.SaveChangesAsync(cancellationToken).ConfigureAwait(false)(line 42); log (line 44);Result.Success(dtoMapper.MapToDTO(entity))(line 46). Note that validation is absent from this method by design: it already ran in the pipeline decorator. - Why it's built this way: the handler is deliberately thin. Everything that is not orchestration (validation, caching, transactions, logging of the request) is a decorator concern, and everything that is a business rule belongs to the entity. What remains is the six-step skeleton every update handler in the module shares.
- Where it's used: registered by assembly scanning and resolved by
ConferenceCategoriesControllerthrough the command decorator pipeline.
UpdateEventHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.Update·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/UseCases/Update/UpdateEventHandler.cs:16· Level 8 · class (sealed partial)
- What it is: the handler for UpdateEventCommand. It follows the same skeleton as UpdateConferenceCategoryHandler and adds one thing: a cross-aggregate probe that detects a timezone change on an event that already has sessions (BR-131), reported as an advisory flag on UpdateEventResult rather than as a failure.
- Depends on: ICommandHandler<in TCommand, TResult> (line 19); IUnitOfWork (line 17); EventDTOMapper (line 18);
ILogger<T>; the Event and Session domain entities; Error / Result. - Concept introduced: the soft business rule computed across aggregates. BR-131 is not an invariant; changing an event's timezone is legal. But if sessions already exist, their displayed times shift, and the organizer deserves a warning. The
Eventaggregate cannot see its sessions, so the check lives here: the handler first compares the stored zone to the requested one with!string.Equals(entity.TimeZone, command.Request.TimeZone, StringComparison.Ordinal)(line 36) and only if it actually changed does it ask aSessionrepositoryExistsAsync(s => s.EventId == command.Id, ...)(lines 41-44). The guard order matters: the common case (no zone change) costs zero extra queries, andExistsAsynccompiles to an existence check rather than materializing sessions.[Rubric §4, DDD]: a rule that spans two aggregates is enforced in the application layer, which is the only place both are visible.[Rubric §12, Performance & Scalability]: the conditional existence probe keeps the added cost off the normal path. - Walkthrough
- Load and guard (lines 26-29):
GetRepository<Event, EventIdentifierType>(),GetByIdAsync, thenError.NotFound.WithSource(nameof(UpdateEventHandler)).WithTarget(nameof(Event))when the event is missing. - Concurrency stamp (line 33), identical in intent to the category handler.
- BR-131 detection (lines 35-46): the ordinal comparison, the conditional existence query, and the resulting
hasTimeZoneWarningbool, which is advisory and never turns into a failure. - Domain delegation (lines 48-58):
entity.Update(Name, Description, StartDate, EndDate, TimeZone, SessionizeCode, VenueAddress, VenueMapUrl, WiFiInfo, QuestionModerationDefault), ten arguments mapping one-to-one onto EventUpdateRequest. A failedResultis propagated withResult.Failure<UpdateEventResult>(result.Errors)(lines 60-61). - Persist, log, wrap (lines 63-67):
SaveChangesAsync, the generatedLogEventUpdated(declared at lines 70-71), thenResult.Success(new UpdateEventResult(dtoMapper.MapToDTO(entity), hasTimeZoneWarning)).
- Load and guard (lines 26-29):
- Why it's built this way: the handler orchestrates the lookups the entity cannot perform (does this event have sessions?) while the entity keeps sole responsibility for validating and mutating its own state. Returning the warning inside a successful
Resultrather than as an error keeps the decision with the organizer: the API can prompt for confirmation without the save having been rejected. Ordinal string comparison is used because a timezone identifier is an opaque key, not human text to be compared culturally. - Where it's used: registered by assembly scanning and resolved by the Events controller through the command decorator pipeline; its result is unwrapped into the HTTP response along with the timezone warning.
QuestionUpdateRequest
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Questions.UseCases.Update·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Questions/UseCases/Update/QuestionUpdateRequest.cs:6· Level 1 · record
- What it is - the request DTO the API binds a question-edit form into before it becomes an
UpdateQuestionCommand. A plainrecord classof immutableinitproperties describing every editable question field plus the optimistic-concurrency token. - Depends on -
IConcurrencyAware(the one-property contract that surfaces theRowVersiontoken). No first-party field types beyond BCL primitives. - Concept introduced - the request/command split. A request is the wire-shaped payload a controller model-binds and a validator checks; a command is the internal CQRS message a handler runs. Keeping them separate means the transport contract (
QuestionUpdateRequest) can carry presentation concerns likeRowVersionand nullable optionals, whileUpdateQuestionCommandstays a thin envelope pairing the targetIdwith this request.[Rubric §9 - API & Contract Design](§9 assesses whether public contracts are explicit and versioned; this DTO is the public edit contract, immutable and documented per-field).[Rubric §8 - Data Architecture](§8 assesses persistence and concurrency strategy; theRowVersionbyte array round-trips the EF rowversion token so a stale edit is caught rather than silently overwriting). - Walkthrough -
RowVersion(QuestionUpdateRequest.cs:9) is the nullablebyte[]?optimistic-concurrency token the client echoes back from its last read, satisfyingIConcurrencyAware.QuestionText,QuestionEntity, andQuestionType(:12,:15,:18) arerequired string, so the model binder cannot construct the request without them, which is whyQuestionUpdateRequestValidatoronly needs to police length, not presence.QuestionEntitynames the target aggregate ("Session" or "Event") andQuestionTypethe input kind ("Rating", "Text", "Email"); both are immutable once answers exist (BR-137), a rule the handler enforces rather than the request.Sort(:21) is the display orderintandIsRequired(:24) the answer-required flag. - Why it's built this way -
required initgives an immutable, fully-constructed payload with no partially-populated intermediate state; the per-field XML docs double as OpenAPI descriptions. Manual DTO shaping over reflection-magic mapping follows ADR-001. - Where it's used - validated by
QuestionUpdateRequestValidator, wrapped byUpdateQuestionCommand, and unpacked field-by-field byUpdateQuestionHandler.
SessionUpdateRequest
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.Update·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/UseCases/Update/SessionUpdateRequest.cs:6· Level 1 · record
- What it is - the request DTO for editing a conference session: the widest of the three update requests in this unit, carrying scheduling, status, streaming, and room-assignment fields plus the concurrency token.
- Depends on -
IConcurrencyAware, and the module aliasesEventIdentifierType(the required parent-event id) andRoomIdentifierType?(the optional assigned room). - Concept reinforced - the request/command split. Same shape introduced by
QuestionUpdateRequest: an immutablerecord class : IConcurrencyAwarethat a controller binds and a validator checks before it is wrapped inUpdateSessionCommand.[Rubric §9 - API & Contract Design]and[Rubric §8 - Data Architecture]as with the question request. - Walkthrough -
RowVersion(SessionUpdateRequest.cs:9) is the concurrency token.EventId(:12) isrequiredand its doc records BR-140: it must equal the session's currentEventIdbecause a session cannot move between events (the handler rejects a mismatch).Title(:15) is the onlyrequired string; the rest are optionals.Description(:18),StartsAt/EndsAt(:21,:24, nullableDateTime),Status(:27),LiveUrl/RecordingUrl(:42,:45),AccessibilityInfo(:48), andResourceLinks(:51) are nullable strings and times. Four booleans (:30-:39) capture the session lifecycle and kind:IsInformed,IsConfirmed,IsServiceSession,IsPlenumSession.RoomId(:54) is the nullable room assignment the handler cross-checks against the parent event (BR-130). - Why it's built this way - the wide optional surface lets one request serve every partial edit an organizer makes; keeping
EventIdrequiredbut immutable means the request still round-trips the value for the handler's guard without inviting a re-parent. - Where it's used - validated by
SessionUpdateRequestValidator, wrapped byUpdateSessionCommand, and consumed field-by-field byUpdateSessionHandler.
SpeakerUpdateRequest
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Speakers.UseCases.Update·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Speakers/UseCases/Update/SpeakerUpdateRequest.cs:6· Level 1 · record
- What it is - the request DTO the API binds a speaker-edit form into before it becomes an
UpdateSpeakerCommand: the editable speaker fields (name, bio, social links) plus the concurrency token and an optional linked-user id. - Depends on -
IConcurrencyAware, and the module aliasUserIdentifierType?(the optional linked-user id). - Concept reinforced - the request/command split. Same immutable
record class : IConcurrencyAwareshape asQuestionUpdateRequest; the transport DTO carriesRowVersionand nullable optionals whileUpdateSpeakerCommandstays a thin envelope.[Rubric §9 - API & Contract Design]and[Rubric §8 - Data Architecture]. - Walkthrough -
RowVersion(SpeakerUpdateRequest.cs:9) is the concurrency token.FirstName/LastName(:12,:15) arerequired string, soSpeakerUpdateRequestValidatoronly checks length.Email,Bio,TagLine,ProfilePicture,TwitterHandle(:18,:21,:24,:27,:33) are nullable optionals, as are the three social URLsLinkedInUrl/GitHubUrl/WebsiteUrl(:36,:39,:42) kept as plainstring?because the Sessionize import stores them as raw strings.IsTopSpeaker(:30) is the featured-speaker curation flag andLinkedUserId(:45) optionally ties the speaker to an IdentityUser; both are organizer-only fields thatUpdateSpeakerHandlerignores on a self-edit. - Why it's built this way -
required initgives an immutable, fully-constructed payload; the per-field XML docs double as OpenAPI descriptions. The URLs staystringdeliberately so the import's raw values round-trip without aUriparse. Manual DTO shaping follows ADR-001. - Caveats / not-in-source - the current file carries no
[SuppressMessage]analyzer attributes on thebyte[]or URL properties; an earlier edition of this section described such suppressions, which no longer exist in source. - Where it's used - validated by
SpeakerUpdateRequestValidator, wrapped byUpdateSpeakerCommand, and unpacked field-by-field byUpdateSpeakerHandler.
RoomChangedHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.DomainEventHandlers·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/DomainEventHandlers/RoomChangedHandler.cs:11· Level 3 · class
- What it is - an
IDomainEventHandler<RoomChanged>that reacts to a room being added, updated, or deleted by writing a single structured log line. The lightest possible domain-event handler: no state check, no side effect beyond observability. - Depends on -
IDomainEventHandler<in TDomainEvent>,RoomChanged,DomainEntityState, andILogger<T>(BCL). - Concept introduced - the in-process domain-event handler. When an aggregate calls
AddDomainEvent(...), the event sits in the aggregate's list untilSaveChangesAsynccommits, then theDomainEventDispatcher(see Group 04) fans it out to every registeredIDomainEventHandler<T>. The aggregate never knows who listens; Scrutor discovers handlers by scanning the Application assembly.[Rubric §6 - CQRS & Event-Driven](§6 assesses whether state changes are announced as events and consumed by decoupled handlers rather than leaked as inline side effects, which is exactly this shape).[Rubric §13 - Observability](§13 assesses operability signals; the handler exists purely to emit a compile-time-generated, allocation-light log record). - Walkthrough - the primary constructor (
RoomChangedHandler.cs:11) injects onlyILogger<RoomChangedHandler>.HandleAsync(:15) forwards the event'sState,EventId,RoomId, andRoomNameto the source-generatedLogRoomChanged(:17) and returnsTask.CompletedTask(:18) since there is no async work.LogRoomChanged(:21-:22) is a[LoggerMessage]static partial voidwhose template embeds{State}so one log line covers add/update/delete. - Why it's built this way - a
sealed partial classplus[LoggerMessage]gives a strongly-typed, zero-boxing log path; keeping the handler side-effect-free means it can never fail the surrounding transaction. - Where it's used - auto-registered by Scrutor's Application-assembly scan; fired by the
DomainEventDispatcherwhenever anEventaggregate raisesRoomChanged.
UpdateSessionResult
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.Update·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/UseCases/Update/UpdateSessionCommand.cs:19· Level 3 · record
- What it is - a small result wrapper that pairs the updated
SessionDTOwith abool HasDateRangeWarningflag, so the session update can succeed while still telling the caller the new times fall outside the event's dates (BR-86). - Depends on -
SessionDTO. - Concept introduced - the soft-warning result. A command normally returns just the mapped DTO, but some business rules are advisory rather than blocking: BR-86 says a session scheduled outside its event's date range is allowed but flagged. Rather than fail the command or invent an out-of-band channel,
UpdateSessionHandlerreturns this two-field record so the API can surface the warning in the response body without a non-2xx status.[Rubric §9 - API & Contract Design](§9 assesses contract expressiveness; a typed result carries the advisory signal instead of overloading the status code).[Rubric §24 - Forms/Validation/UX Safety](§24 assesses validation UX; a non-blocking warning lets the organizer confirm an intentional out-of-range time). - Walkthrough - the positional record (
UpdateSessionCommand.cs:19) declaresSessionDTO Sessionandbool HasDateRangeWarning. It lives in the same file asUpdateSessionCommandbecause the two are the input/output pair of one use case. - Where it's used - constructed only by
UpdateSessionHandlerat the end of a successful update and returned inside aResult<UpdateSessionResult>.
SessionCreatedHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.DomainEventHandlers·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/DomainEventHandlers/SessionCreatedHandler.cs:11· Level 4 · class
- What it is - an
IDomainEventHandler<SessionChanged>that logs session creation only. It shows the common pattern where one event type carries aStateand a handler routes on it. - Depends on -
IDomainEventHandler<in TDomainEvent>,SessionChanged,DomainEntityState, andILogger<T>(BCL). - Concept reinforced - state-routed event handling.
SessionChangedis a single event raised for adds, updates, and deletes; a handler that only cares about one transition guards onStateand returns early otherwise. This keeps the event vocabulary small (one event per aggregate) while still letting handlers specialize.[Rubric §6 - CQRS & Event-Driven]and[Rubric §13 - Observability]as withRoomChangedHandler. - Walkthrough -
HandleAsync(SessionCreatedHandler.cs:15) first checksdomainEvent.State != DomainEntityState.Addedand bails withTask.CompletedTask(:17-:18) for updates and deletes. Only for anAddedtransition does it callLogSessionCreatedwithSessionId,Title,EventId(:20).LogSessionCreated(:24-:25) is the[LoggerMessage]generated method. - Why it's built this way - routing on
Stateinside one handler avoids proliferating near-identical handler types while keeping each concern's logic isolated to its branch. - Where it's used - discovered by the Scrutor scan; invoked by the
DomainEventDispatcheron everySessionChanged, acting only when the session was just created.
SpeakerDeletedHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Speakers.DomainEventHandlers·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Speakers/DomainEventHandlers/SpeakerDeletedHandler.cs:20· Level 4 · class
- What it is - the one handler in this unit that does real work: on a speaker soft-delete it logs the deletion and publishes a
SpeakerUnlinkedFromUserintegration event so the Identity module can clear the previously linked user'sLinkedSpeakerId(BR-70). This is a domain event translating into a cross-service integration event. - Depends on -
IDomainEventHandler<in TDomainEvent>,SpeakerChanged,SpeakerUnlinkedFromUser,IIntegrationEventPublisher,DomainEntityState, plusIServiceScopeFactoryandILogger<T>(BCL). - Concept introduced - bridging a domain event to an integration event across a service boundary. A domain event is in-process and lives inside one aggregate's transaction; an integration event is the durable, broker-carried message other services consume. This handler is the bridge: the Conference-side
Speaker.Delete()already cleared its ownLinkedUserId, and the handler tells Identity to do the symmetric cleanup asynchronously.[Rubric §7 - Microservices Readiness](§7 assesses whether cross-module coupling is broker-mediated rather than a direct call; the class doc,SpeakerDeletedHandler.cs:14-:18, records replacing a former in-processIUserSpeakerLinkService.ClearLinkedSpeakerAsynccall with an eventually-consistent message).[Rubric §6 - CQRS & Event-Driven]and[Rubric §29 - Resilience](§29 assesses continuity; the publication rides the outbox, so a broker hiccup does not lose the unlink; ADR-003). - Walkthrough - the primary constructor (
:20-:22) injectsIServiceScopeFactoryandILogger.HandleAsync(:25) null-guards the event (:27), then returns early unlessState == DomainEntityState.Deleted(:29-:30). It logs viaLogSpeakerDeleted(:32, generated at:48-:49). The load-bearing detail is the scope handling: domain-event handlers are registered as singletons, butIIntegrationEventPublisheris scoped, so the handler opensscopeFactory.CreateAsyncScope()(:40) and resolves the publisher from that scope (:41) before callingPublishAsyncwith a newSpeakerUnlinkedFromUser(PreviousLinkedUserId, SpeakerId)(:42-:44). The whole publish path is guarded byif (domainEvent.PreviousLinkedUserId.HasValue)(:38), so a speaker who was never linked raises nothing. - Why it's built this way - the singleton-handler / scoped-dependency mismatch is a standard DI hazard;
CreateAsyncScopeis the correct fix rather than capturing a scoped service in a singleton. Routing the cleanup through the broker keeps Conference from taking a hard dependency on Identity's write path. - Where it's used - Scrutor registers it; the
DomainEventDispatcherfires it whenever aSpeakeris soft-deleted.
QuestionUpdateRequestValidator
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Questions.UseCases.Update·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Questions/UseCases/Update/QuestionUpdateRequestValidator.cs:7· Level 6 · class
- What it is - the FluentValidation validator for
QuestionUpdateRequest, assembled by composing the reusable question field rules. - Depends on -
QuestionTextRules<T>,QuestionUpdateRequest, and FluentValidation'sAbstractValidator<T>. - Concept introduced - validators as rule compositions. A concrete validator's job is to say which fields it has and to pull in the shared rule for each via
Include(...), so the actual constraints live once in the field rules. TheValidatingCommandDecoratorruns this validator before the transaction opens (see the CQRS pipeline in Group 05).[Rubric §24 - Forms/Validation/UX Safety](§24 assesses that request validation happens before any state change, which this composition guarantees). - Walkthrough - the sealed validator (
QuestionUpdateRequestValidator.cs:7) has a constructor (:9) whose single statementInclude(new QuestionTextRules<QuestionUpdateRequest>(p => p.QuestionText))(:10) pulls the shared text rule in against the request'sQuestionText. No inline rules; every constraint lives inQuestionTextRules<T>. - Where it's used - resolved by the validating decorator when an
UpdateQuestionCommandcarries this request.
SessionUpdateRequestValidator
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.Update·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/UseCases/Update/SessionUpdateRequestValidator.cs:7· Level 6 · class
- What it is - the FluentValidation validator for
SessionUpdateRequest, the widest composition in this unit: oneIncludeper validated session field. - Depends on -
SessionTitleRules<T>,SessionDescriptionRules<T>,SessionStatusRules<T>,SessionLiveUrlRules<T>,SessionRecordingUrlRules<T>,SessionAccessibilityInfoRules<T>,SessionResourceLinksRules<T>,SessionUpdateRequest, andAbstractValidator<T>. - Concept reinforced - validator by composition. Same shape as
QuestionUpdateRequestValidator, just with seven field rules; the count ofIncludecalls tracks the number of length/format-constrained fields. Scheduling and cross-entity rules (BR-86 date range, BR-130 room, BR-140 immutable event) are not here: they are handler-side because they need loaded aggregate state, which a stateless request validator cannot see.[Rubric §24 - Forms/Validation/UX Safety]. - Walkthrough - the constructor (
SessionUpdateRequestValidator.cs:9) issues sevenInclude(...)calls (:11-:17), each binding a reusable rule to its property:Title,Description,Status,LiveUrl,RecordingUrl,AccessibilityInfo,ResourceLinks. - Where it's used - run by the validating decorator ahead of
UpdateSessionHandler.
SpeakerUpdateRequestValidator
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Speakers.UseCases.Update·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Speakers/UseCases/Update/SpeakerUpdateRequestValidator.cs:7· Level 6 · class
- What it is - the FluentValidation validator for
SpeakerUpdateRequest, composed from the reusable speaker name rules. - Depends on -
SpeakerFirstNameRules<T>,SpeakerLastNameRules<T>,SpeakerUpdateRequest, andAbstractValidator<T>. - Concept reinforced - validator by composition. Same shape as
QuestionUpdateRequestValidator; only the tworequiredname fields carry constraints, so the many optional social/bio fields need no rule.[Rubric §24 - Forms/Validation/UX Safety]. - Walkthrough - the constructor (
SpeakerUpdateRequestValidator.cs:9) callsInclude(new SpeakerFirstNameRules<SpeakerUpdateRequest>(p => p.FirstName))(:11) and the matching last-name include (:12). No inline rules; every constraint is inherited from the composed field rules. - Where it's used - resolved by the validating decorator when an
UpdateSpeakerCommandcarries this request.
UpdateQuestionCommand
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Questions.UseCases.Update·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Questions/UseCases/Update/UpdateQuestionCommand.cs:9· Level 6 · record
- What it is - the CQRS command that pairs a target question
Idwith itsQuestionUpdateRequestpayload, invalidating the question cache on success. - Depends on -
ICommandWithRequest<out TRequest>,ICacheInvalidating,QuestionUpdateRequest,Question(for the cache prefix), and the module aliasQuestionIdentifierType. - Concept introduced - the request-carrying, cache-invalidating command. Implementing
ICommandWithRequest<TRequest>advertises that this command wraps a validated request DTO, which is how the validating decorator knows to runQuestionUpdateRequestValidatoragainstRequestrather than the command itself. ImplementingICacheInvalidatingdeclares aCachePrefixthat the caching decorator reads after the handler succeeds to evict stale reads.[Rubric §6 - CQRS & Event-Driven](§6 assesses that the write side declares its read-side invalidation contract).[Rubric §12 - Performance & Scalability](§12 assesses caching strategy; output-cached question reads stay fresh without a blanket flush). - Walkthrough - the positional record (
UpdateQuestionCommand.cs:9) carriesQuestionIdentifierType IdandQuestionUpdateRequest Request;CachePrefix(:12) is$"{typeof(Question).FullName}:", so a successful update evicts cached question reads. - Where it's used - dispatched by the question update controller endpoint and executed by
UpdateQuestionHandler.
UpdateSpeakerCommand
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Speakers.UseCases.Update·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Speakers/UseCases/Update/UpdateSpeakerCommand.cs:13· Level 6 · record
- What it is - the CQRS command that pairs a target speaker
Idwith itsSpeakerUpdateRequestpayload and aCallerIsOrganizerauthorization flag, invalidating the speaker cache on success. - Depends on -
ICommandWithRequest<out TRequest>,ICacheInvalidating,SpeakerUpdateRequest,Speaker(for the cache prefix), and the module aliasSpeakerIdentifierType. - Concept introduced - carrying the caller's authority in the command, not the request body. Same request-carrying / cache-invalidating shape as
UpdateQuestionCommand, with one extra field worth studying:CallerIsOrganizeris bound at the API edge from the caller's role claim, never from the request body. This lets one command serve both an organizer edit and a BR-214 speaker self-edit while keeping the authority decision on the trusted side of the boundary.[Rubric §11 - Security](§11 assesses authorization placement; the privilege flag is derived server-side so a crafted body cannot elevate).[Rubric §6 - CQRS & Event-Driven]. - Walkthrough - the positional record (
UpdateSpeakerCommand.cs:13-:16) carriesSpeakerIdentifierType Id,SpeakerUpdateRequest Request, andbool CallerIsOrganizer; the XML doc (:9-:12) records that when the flag isfalsethe handler ignores the organizer-only request fields (IsTopSpeaker,LinkedUserId).CachePrefix(:19) is$"{typeof(Speaker).FullName}:", so a successful update evicts cached speaker reads. - Where it's used - dispatched by the speaker update controller endpoint (which binds
CallerIsOrganizerfrom the role claim) and executed byUpdateSpeakerHandler.
UpdateSessionCommand
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.Update·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/UseCases/Update/UpdateSessionCommand.cs:10· Level 7 · record
- What it is - the CQRS command that pairs a target session
Idwith itsSessionUpdateRequestpayload, invalidating the session cache on success. Unlike its siblings it produces anUpdateSessionResultrather than a bare DTO. - Depends on -
ICommandWithRequest<out TRequest>,ICacheInvalidating,SessionUpdateRequest,Session(for the cache prefix), and the module aliasSessionIdentifierType. - Concept reinforced - request-carrying, cache-invalidating command. Same two-interface shape as
UpdateQuestionCommand. It sits one Level higher than the question/speaker commands because its handler returns the richerUpdateSessionResult, which itself depends onSessionDTO.[Rubric §6 - CQRS & Event-Driven]. - Walkthrough - the positional record (
UpdateSessionCommand.cs:10) carriesSessionIdentifierType IdandSessionUpdateRequest Request;CachePrefix(:13) is$"{typeof(Session).FullName}:". Its input/output twinUpdateSessionResultis declared in the same file (:19). - Where it's used - dispatched by the session update controller endpoint and executed by
UpdateSessionHandler.
UpdateQuestionHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Questions.UseCases.Update·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Questions/UseCases/Update/UpdateQuestionHandler.cs:18· Level 8 · class
- What it is - the command handler for
UpdateQuestionCommand: it loads the question, stamps the client's concurrency token, enforces BR-137 (type/entity immutable once answers exist), delegates the field changes to the domainUpdate, persists, and returns the updated DTO. - Depends on -
ICommandHandler<in TCommand, TResult>,IUnitOfWork,QuestionDTOMapper,Question,EventQuestionAnswer,SessionQuestionAnswer,Result,Error, andILogger<T>. - Concept introduced - the load / guard / delegate / save / map handler shape. The canonical write handler fetches the aggregate through the
IUnitOfWorkrepository, applies cross-cutting checks (concurrency, cross-aggregate invariants), calls a domain method that enforces its own invariants and raises events,SaveChangesAsync(which commits, stamps audit, and dispatches domain events through the outbox), and maps the result to a DTO. Business rules live in the aggregate; only rules needing other aggregates' state (here, whether answers exist) sit in the handler.[Rubric §4 - DDD](§4 assesses that the aggregate owns its invariants;Question.Updatedoes, the handler only orchestrates plus the cross-aggregate check).[Rubric §8 - Data Architecture](optimistic concurrency via the rowversion token).[Rubric §3 - Clean Architecture](the handler depends only on abstractions). - Walkthrough -
HandleAsync(UpdateQuestionHandler.cs:24) gets theQuestionrepository (:28) and loads bycommand.Id(:29); a null entity returnsError.NotFound(:30-:31). It then callsrepository.SetOriginalRowVersion(entity, command.Request.RowVersion)(:35) so a stale edit throwsDbUpdateConcurrencyException, which the pipeline maps to 409. BR-137 (:37-:62) fires only ifQuestionTypeorQuestionEntitychanged: it probes two read repositories viaExistsAsyncfor anyEventQuestionAnswer(:41-:44) orSessionQuestionAnswer(:48-:51) tied to the question, and if answers exist returns anError.ValidationcodedQuestion.ImmutableAfterAnswers(:56-:60). Otherwise it delegates toentity.Update(...)(:64-:69), propagates failures (:71-:72), awaitsSaveChangesAsync(:74), logs viaLogQuestionUpdated(:76, defined:81-:82), and returnsResult.Success(dtoMapper.MapToDTO(entity))(:78). - Why it's built this way - the two
GetReadRepositoryprobes are short-circuited (:46) so the second query runs only when the first finds nothing; routing the text/sort changes throughQuestion.Updatekeeps validation and event-raising inside the aggregate while the answer-existence guard, which no single aggregate can answer, stays in the handler. - Where it's used - invoked by the question update controller endpoint through the CQRS decorator pipeline (feature-gate, logging, caching, validating, transactional wrappers).
UpdateSpeakerHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Speakers.UseCases.Update·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Speakers/UseCases/Update/UpdateSpeakerHandler.cs:15· Level 8 · class
- What it is - the command handler for
UpdateSpeakerCommand: it loads the speaker, applies the client's optimistic-concurrency token, masks organizer-only fields on a self-edit (BR-214), delegates the field changes to the domainUpdate, persists, and returns the updated DTO. - Depends on -
ICommandHandler<in TCommand, TResult>,IUnitOfWork,SpeakerDTOMapper,Speaker,SpeakerDTO,Result,Error, andILogger<T>. - Concept reinforced - the load/guard/delegate/save/map handler with an authorization mask. Same four-step shape as
UpdateQuestionHandler, with the concurrency stamp plus one field-level authorization step worth studying: before applying changes it chooses whether to honor the request's organizer-only fields or keep the entity's current values, based on the command'sCallerIsOrganizerflag.[Rubric §11 - Security](§11 assesses privilege enforcement; a self-service caller cannot re-link or feature a speaker even by crafting the body).[Rubric §8 - Data Architecture](optimistic concurrency) and[Rubric §4 - DDD](the entity's ownUpdateenforces the invariants). - Walkthrough -
HandleAsync(UpdateSpeakerHandler.cs:21) gets theSpeakerrepository (:25) and loads bycommand.Id(:26); a null entity returnsError.NotFound(:27-:28). It callsrepository.SetOriginalRowVersion(entity, command.Request.RowVersion)(:32) to arm the 409-on-stale check. The BR-214 mask (:34-:39) computesisTopSpeakerandlinkedUserIdascommand.CallerIsOrganizer ? command.Request.<field> : entity.<field>, so a non-organizer keeps the stored values forIsTopSpeaker(curation flag) andLinkedUserId(changed only via the governed/linkendpoint with its BR-208 uniqueness check). It delegates every editable field, passing the masked values, toentity.Update(...)(:41-:53), propagates failures (:55-:56), awaitsSaveChangesAsync(:58), logs viaLogSpeakerUpdated(:60, defined:65-:66), and returnsResult.Success(dtoMapper.MapToDTO(entity))(:62). - Why it's built this way - routing all mutation through
Speaker.Updatekeeps validation and event-raising inside the aggregate; deriving the privilege fromCallerIsOrganizer(bound server-side, perUpdateSpeakerCommand) rather than trusting the request body is what stops a self-edit from silently re-linking or featuring a speaker. - Where it's used - invoked by the speaker update controller endpoint through the CQRS decorator pipeline (its request is validated by
SpeakerUpdateRequestValidatorbefore the transaction opens).
UpdateSessionHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.Update·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/UseCases/Update/UpdateSessionHandler.cs:17· Level 9 · class
- What it is - the richest handler in this unit and the command handler for
UpdateSessionCommand: it loads the session, stamps concurrency, enforces the immutable event (BR-140), validates the room against the parent event and its schedule (BR-130 + double-booking), delegates to the domainUpdate, computes the BR-86 date-range warning, persists, and returns anUpdateSessionResult. - Depends on -
ICommandHandler<in TCommand, TResult>,IUnitOfWork,IRepository<TEntity, TIdentifierType>,SessionDTOMapper,Session,Event,SessionRoomScheduling,UpdateSessionResult,Result,Error, andILogger<T>. - Concept reinforced - the load/guard/delegate/save/map handler with cross-aggregate validation. Same shape as
UpdateQuestionHandler, taken to its fullest: this handler loads a second aggregate (the parentEventwith its rooms) because two of its rules span aggregates (a room must belong to the event, and a time slot must not double-book a room).[Rubric §4 - DDD](single-aggregate invariants stay inSession.Update; genuinely cross-aggregate rules sit in the handler with the loadedEvent).[Rubric §8 - Data Architecture](concurrency plus a schedule-overlapExistsAsyncguard).[Rubric §24 - Forms/Validation/UX Safety](BR-86 returns a non-blocking warning rather than a failure). - Walkthrough -
HandleAsync(UpdateSessionHandler.cs:23) loads theSession(:27-:28), returnsError.NotFoundon null (:29-:30), and stamps the concurrency token (:34). BR-140 (:37-:44) rejects any change toEventIdwith anError.UnprocessableEntitycodedSession.EventId.Immutable. It then loads the parentEventwith itsRoomsuntracked (:47-:52), returningNotFoundif the parent is gone (:53-:54). Room validation is factored into the privateValidateRoomAssignmentAsync(:57, defined:97-:130): when aRoomIdis supplied it checks the room belongs to the event and is not soft-deleted (BR-130, elseSession.RoomId.CrossEvent), and when both times are set it callsrepository.ExistsAsync(SessionRoomScheduling.BuildOverlapPredicate(...), excludeSessionId: command.Id)(:119-:125) so an overlap with another session in the same room returnsSessionRoomScheduling.DoubleBookedError(:128). On success it delegates all editable fields toentity.Update(...)(:61-:75), propagates failures (:77-:78), computeshasDateRangeWarningvia the privateIsOutsideEventDateRangeagainst the event'sStartDate/EndDate(:81-:83, defined:136-:145), awaitsSaveChangesAsync(:85), logs (:87), and returnsResult.Success(new UpdateSessionResult(dtoMapper.MapToDTO(entity), hasDateRangeWarning))(:89). - Why it's built this way - loading the
Eventuntracked (asTracking: false) is correct because the handler only reads its rooms and dates, never mutates it; the overlap query lives inSessionRoomSchedulingso the same predicate is shared with the create path; BR-86 is advisory, so it rides out onUpdateSessionResultinstead of failing an otherwise valid edit. - Where it's used - invoked by the session update controller endpoint through the CQRS decorator pipeline; the endpoint reads
HasDateRangeWarningoff theUpdateSessionResultto surface the advisory.
QuestionTextRules<T>
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Questions.Validation·MMCA.ADC.Conference.Application/Questions/Validation/QuestionValidationRules.cs:12· Level 5 · class
- What it is: a reusable FluentValidation rule set for the text of a conference question. It packages the two constraints that any question-text field must satisfy (non-empty, bounded length) so several validators can share one definition.
- Depends on:
AbstractValidator<T>(FluentValidation,QuestionValidationRules.cs:13),System.Linq.Expressions.Expression<Func<T, string>>for the property selector (:1,15), andQuestionInvariantsfor the max-length constant (:3,18). - Concept introduced: the parameterized rule set. Instead of writing the same
RuleFor(x => x.Text).NotEmpty().MaximumLength(...)chain inside every command validator that carries question text, the constraints live once in a smallAbstractValidator<T>subclass whose constructor takes a propertyselector. A concrete validator then folds it in with FluentValidation'sInclude(...). Because the rule set is generic inTand receives the selector, the exact same object validates a create command, an update command, or an import request, each pointing the selector at its own text property. This is the same reuse device the framework'sRequiredStringRules<T>provides generically;QuestionTextRules<T>is a domain-specific hand-rolled variant that also pins the error codes.[Rubric §24, Forms/Validation/UX Safety]assesses whether input constraints are declared once and consistently applied: centralizing the question-text contract here keeps every entry path in agreement.[Rubric §1, SOLID]: one rule set, one responsibility, reused rather than copied. - Walkthrough: the class is
sealedand generic inT(:12). The expression-bodied constructor (:15) bindsRuleFor(selector)and chains two clauses:NotEmpty()with the message "You must enter a Question Text" and error codeQuestion.QuestionText.Required(:17), thenMaximumLength(QuestionInvariants.QuestionTextMaxLength)with a length message and error codeQuestion.QuestionText.MaxLength(:18). The stableWithErrorCode(...)strings are what the API error-mapping layer keys on, so they are part of the contract, not just prose. - Why it's built this way: sourcing the bound from
QuestionInvariants(rather than a literal here) keeps the application-layer validator and the domain aggregate's own guard citing one constant, so a limit change never drifts between layers. - Where it's used:
Included by the question command validators in this module (create/update question, and the answer-text validators that follow the same shape).
SpeakerFirstNameRules<T>
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Speakers.Validation·MMCA.ADC.Conference.Application/Speakers/Validation/SpeakerValidationRules.cs:11· Level 5 · class
- What it is: the reusable rule set for a speaker's first name, wrapping the "required, bounded length" contract so speaker command validators can share one definition.
- Depends on:
RequiredStringRules<T>(the framework base it extends,SpeakerValidationRules.cs:3,12),System.Linq.Expressions.Expression<Func<T, string>>for the selector (:1,14), andSpeakerInvariantsfor the max-length constant (:2,15). - Concept introduced: reusing the framework rule base instead of re-declaring clauses. Where
QuestionTextRules<T>hand-writes theNotEmpty().MaximumLength(...)chain,SpeakerFirstNameRules<T>derives from the sharedRequiredStringRules<T>and simply passes the selector, a human label, and the length bound to its base constructor. The generic base already encodes the non-empty-plus-max-length pattern, so the subclass is three lines.[Rubric §1, SOLID]and[Rubric §24, Forms/Validation/UX Safety]: the field contract is defined once in Common and specialized per field with a singlebase(...)call, the strongest form of the DRY validation approach in this codebase. - Walkthrough:
sealed class SpeakerFirstNameRules<T> : RequiredStringRules<T>(:11-12); the constructor (:14) forwards tobase(selector, "First Name", SpeakerInvariants.FirstNameMaxLength)(:15). The label "First Name" is what surfaces in the generated required/length messages; the bound comes from the domain invariants type. - Where it's used:
Included by the speaker create/update command validators (and the Sessionize import mapping validators) so a speaker's first name is checked identically everywhere.
SpeakerLastNameRules<T>
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Speakers.Validation·MMCA.ADC.Conference.Application/Speakers/Validation/SpeakerValidationRules.cs:22· Level 5 · class
- What it is: the last-name counterpart to
SpeakerFirstNameRules<T>, the reusable "required, bounded length" rule set for a speaker's last name. - Depends on:
RequiredStringRules<T>(SpeakerValidationRules.cs:23), the selector expression (:25), andSpeakerInvariantsfor the length bound (:26). - Concept introduced: none new; structurally identical to
SpeakerFirstNameRules<T>. The only differences are the label and the constant: the constructor (:25) callsbase(selector, "Last Name", SpeakerInvariants.LastNameMaxLength)(:26). It lives in the same file so the two speaker-name rule sets are read and changed together. - Where it's used: paired with
SpeakerFirstNameRules<T>in the speaker command validators.
AddCategoryItemCommand
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Categories.UseCases.AddCategoryItem·MMCA.ADC.Conference.Application/Categories/UseCases/AddCategoryItem/AddCategoryItemCommand.cs:14· Level 6 · record
- What it is: the command to add one child item to an existing conference
Categoryaggregate, carrying the owning category id plus the new item's optional id, name, and sort order. - Depends on:
ICacheInvalidating(the marker that opts the command into cache eviction,AddCategoryItemCommand.cs:2,18) and theCategorytype for the cache prefix (:1,21).ConferenceCategoryIdentifierTypeandCategoryItemIdentifierTypeare the module identifier aliases (see primer). - Concept introduced: the child-add command shape. A positional
recordis the write intent; its behavior lives entirely in the handler. Two details are worth calling out. First,CategoryItemIdis nullable (CategoryItemIdentifierType?,:16): the Sessionize import supplies the source-assigned id, while a manual add leaves itnullfor database-generated identity. Second, implementingICacheInvalidatinghooks the command into theCachingCommandDecoratorso a successful add evicts the category read cache.[Rubric §6, CQRS & Event-Driven]assesses whether writes are explicit intents flowing through a uniform pipeline: this record is the intent, and the marker interface is how the cross-cutting cache concern attaches declaratively. - Walkthrough: the positional parameters are
CategoryId(:15), the nullableCategoryItemId(:16),Name(:17), andSort(:18); the record implementsICacheInvalidating(:18).CachePrefix(:21) returns"{typeof(Category).FullName}:", the key namespace the caching decorator wipes on success. - Where it's used: validated by
AddCategoryItemCommandValidatorand handled byAddCategoryItemHandler.
CategoryItemDTOMapper
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Categories.DTOs·MMCA.ADC.Conference.Application/Categories/DTOs/CategoryItemDTOMapper.cs:12· Level 6 · class
- What it is: the Mapperly-generated mapper that turns a
CategoryItemdomain entity into its wire-facingCategoryItemDTO. - Depends on:
IEntityDTOMapper<TEntity, TEntityDTO, TIdentifierType>(the mapper contract,CategoryItemDTOMapper.cs:3,13),CategoryItem,CategoryItemDTO, and the Mapperly source generator (Riok.Mapperly.Abstractions,:4). - Concept introduced: source-generated DTO mapping (ADR-001). The class is
sealed partialand carries[Mapper](:11-12); Mapperly generates the body of thepartial CategoryItemDTO MapToDTO(...)(:16) at compile time by name-matching properties, so there is no runtime reflection and no hand-written field copy. This is the manual-mapping-versus-Mapperly split described in ADR-001: property-name-parallel entity/DTO pairs get generated mappers; only mismatches need hand-written code.[Rubric §9, API & Contract Design]assesses whether the domain model is shielded from the wire contract: this mapper keeps the entity and DTO as two separate shapes rather than serializing entities directly. - Walkthrough: the generated
MapToDTO(:16) does the single-entity conversion.MapToDTOs(:19) is hand-written: it null-guards the collection withArgumentNullException.ThrowIfNull(:21) then projects each element throughMapToDTOinto a materialized array via the collection expression[.. entityCollection.Select(MapToDTO)](:22). The mapper is registered by assembly scanning (Scrutor), so it is resolved by DI, notnew-ed. - Where it's used: composed as a child mapper by
ConferenceCategoryDTOMapperand injected intoAddCategoryItemHandlerto return the newly added item.
ConferenceCategoryCreateRequest
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Categories.UseCases.Create·MMCA.ADC.Conference.Application/Categories/UseCases/Create/ConferenceCategoryCreateRequest.cs:10· Level 6 · record
- What it is: the request DTO a client posts to create a conference
Category. It carries the four inbound fields (Id,Title,Sort,Type) and doubles as the command that flows through the CQRS pipeline; there is no separate command wrapper for create in this module. - Depends on:
ICreateRequestandICacheInvalidating(bothMMCA.Common.Application,ConferenceCategoryCreateRequest.cs:2,10), and theCategoryentity only for itsFullNamein the cache prefix (:1,13).ConferenceCategoryIdentifierTypeis the module identifier alias (see primer). - Concept introduced: the request-is-the-command idiom. Rather than a
CreateXCommandrecord wrapping aCreateXRequest, the create path lets onerecord classbe both the wire contract and the handler input. ImplementingICreateRequest(a marker) lets the generic entity request-mapper pipeline recognize it, andICacheInvalidatingopts it into theCachingCommandDecoratorso a successful create evicts the read cache.[Rubric §6, CQRS & Event-Driven]: writes are modeled as explicit intents flowing through a uniform pipeline, and the marker interfaces are how cross-cutting behavior attaches to them declaratively. - Walkthrough:
CachePrefix(:13) returns"{typeof(Category).FullName}:", the key namespace the caching decorator wipes on success.Id(:16) isinit-only and optional (auto-generated when unset).Title(:19) isrequired string, the one field the create validator guards.Sort(:22) is anintdisplay order defaulting to zero.Type(:25) is an optional discriminator string ("session", "speaker"). - Why it's built this way:
required/initgives an immutable request whose one mandatory field cannot be omitted at the compiler level, matching the codebase-wide immutability convention. Cache invalidation keyed off the entity'sFullNamekeeps producer (this request) and consumer (the query cache) agreed on one prefix string. - Where it's used: validated by
ConferenceCategoryCreateRequestValidator, translated byConferenceCategoryCreateRequestMapper, and handled byCreateConferenceCategoryHandler.
RemoveCategoryItemCommand
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Categories.UseCases.RemoveCategoryItem·MMCA.ADC.Conference.Application/Categories/UseCases/RemoveCategoryItem/RemoveCategoryItemCommand.cs:12· Level 6 · record
- What it is: the command to remove one item from a conference
Categoryaggregate, identified by the owningCategoryIdand the targetCategoryItemId. - Depends on:
ICacheInvalidatingand theCategorytype for the cache prefix (RemoveCategoryItemCommand.cs:1-2,17).ConferenceCategoryIdentifierType/CategoryItemIdentifierTypeare the module aliases. - Concept introduced: none new; this is the child-removal shape of the CQRS command pattern (parent id + child id + cache invalidation). The positional
record(:12-14) carries the two ids, andCachePrefix(:17) evicts the category read cache on success, exactly asAddCategoryItemCommanddoes.[Rubric §6, CQRS & Event-Driven]: the mutation is an explicit intent, and child mutations route through the aggregate root rather than deleting a child row directly. - Walkthrough:
CategoryIdandCategoryItemId(:13-14) are the two positional parameters; the record implementsICacheInvalidating(:14) andCachePrefix(:17) returns theCategoryfull-name prefix. There is nothing else: the record is pure intent, the behavior lives in its handler. - Where it's used: handled by
RemoveCategoryItemHandler, which tolerates a default (unset)CategoryId.
AddCategoryItemCommandValidator
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Categories.UseCases.AddCategoryItem·MMCA.ADC.Conference.Application/Categories/UseCases/AddCategoryItem/AddCategoryItemCommandValidator.cs:7· Level 7 · class
- What it is: the FluentValidation validator for
AddCategoryItemCommand, enforced by the pipeline before the add handler runs. It checks the new item'sNameandSort. - Depends on:
AbstractValidator<T>(FluentValidation,AddCategoryItemCommandValidator.cs:1,7),AddCategoryItemCommand, and two shared rule sets,CategoryItemNameRules<T>andCategoryItemSortRules<T>(from the module'sCategories.Validationnamespace,:2). - Concept introduced: rule-set composition via
Include. Rather than re-declaring the item-field rules inline, the constructor (:9-13) folds two reusable rule objects into the validator:Include(new CategoryItemNameRules<AddCategoryItemCommand>(p => p.Name))(:11) andInclude(new CategoryItemSortRules<AddCategoryItemCommand>(p => p.Sort))(:12). Each rule set is parameterized by a property selector, so the same name/sort constraints are reused by any other command that edits the same fields (the update-category-item validator uses the identical pair).[Rubric §24, Forms/Validation/UX Safety]and[Rubric §1, SOLID]: the item's field rules live in exactly one place, so a length or range change updates every path at once. TheValidatingCommandDecoratorruns this before the transaction opens. - Walkthrough: the constructor body (
:9-13) is twoIncludecalls, one per validated field. Nothing validates the two id fields (CategoryId,CategoryItemId): the owning category's existence is checked by the handler, which returnsNotFoundwhen it is absent. - Where it's used: discovered by Scrutor and invoked by the validating decorator ahead of
AddCategoryItemHandler.
ConferenceCategoryCreateRequestMapper
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Categories.UseCases.Create·MMCA.ADC.Conference.Application/Categories/UseCases/Create/ConferenceCategoryCreateRequestMapper.cs:11· Level 7 · class
- What it is: the request-to-entity mapper that turns a validated
ConferenceCategoryCreateRequestinto aCategoryby calling the domain factory. - Depends on:
IEntityRequestMapper<TEntity, TCreateRequest, TIdentifierType>(the contract,ConferenceCategoryCreateRequestMapper.cs:2,12),ConferenceCategoryCreateRequest,Category, andResult<T>(MMCA.Common.Shared.Abstractions,:3,15). - Concept introduced: the request mapper as the factory gateway. Unlike a DTO mapper (pure property copy), a request mapper is allowed to fail:
CreateEntityAsync(:15) returnsTask<Result<Category>>because it delegates toCategory.Create(...)(:19-23), the domain factory that enforces invariants and returns a failureResulton invalid input. This keeps entity construction (and its invariants) inside the domain while the application layer only wires the fields across.[Rubric §4, Domain-Driven Design]: invalid entities cannot be constructed because the only path in is theResult-returning factory; the mapper nevernews aCategory. - Walkthrough:
CreateEntityAsync(:15) null-guards the request (:17) then returnsTask.FromResult(Category.Create(request.Id, request.Title, request.Sort, request.Type))(:19-23), passing the four request fields positionally to the factory. It is synchronous work wrapped inTask.FromResultbecause no I/O is involved. - Why it's built this way: the generic create pipeline (
CreateConferenceCategoryHandler) is entity-agnostic; per-entity construction lives behind this small mapper so the handler can stay reusable across aggregates. - Where it's used: injected into
CreateConferenceCategoryHandleras theIEntityRequestMapperforCategory.
ConferenceCategoryCreateRequestValidator
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Categories.UseCases.Create·MMCA.ADC.Conference.Application/Categories/UseCases/Create/ConferenceCategoryCreateRequestValidator.cs:7· Level 7 · class
- What it is: the FluentValidation validator for
ConferenceCategoryCreateRequest, enforced by the pipeline before the create handler runs. - Depends on:
AbstractValidator<T>(FluentValidation,ConferenceCategoryCreateRequestValidator.cs:1,7),ConferenceCategoryCreateRequest, and the sharedConferenceCategoryTitleRules<T>rule set (fromCategories.Validation,:2). - Concept introduced: none new; the same
Include-composition idiom taught forAddCategoryItemCommandValidator, folding a single rule set. The expression-bodied constructor (:9-10) callsInclude(new ConferenceCategoryTitleRules<ConferenceCategoryCreateRequest>(p => p.Title)), reusing the title constraints (length, non-empty) that the update-category validator also includes, so they live in exactly one place.[Rubric §24, Forms/Validation/UX Safety]: validation is single-sourced across every command that edits a category title. - Walkthrough: the whole body is the expression-bodied constructor (
:9-10): a singleIncludeof the title rule set bound top => p.Title. Nothing else is validated at the request level (theId/Sort/Typefields carry no create-time constraints). - Where it's used: discovered by Scrutor and invoked by the validating decorator ahead of
CreateConferenceCategoryHandler.
ConferenceCategoryDTOMapper
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Categories.DTOs·MMCA.ADC.Conference.Application/Categories/DTOs/ConferenceCategoryDTOMapper.cs:13· Level 7 · class
- What it is: the Mapperly mapper from the
Categoryaggregate to itsConferenceCategoryDTO, including its childCategoryItemscollection. - Depends on:
IEntityDTOMapper<TEntity, TEntityDTO, TIdentifierType>(ConferenceCategoryDTOMapper.cs:3,15), the Mapperly generator (:4), and one injected child mapper,CategoryItemDTOMapper(:13-14). - Concept introduced: mapper composition with
[UseMapper]. This is the parent side of the two-mapper pair. The class issealed partialwith a primary constructor that takes theCategoryItemDTOMapperand stores it in a[UseMapper]-tagged field (:17-18); that attribute tells Mapperly to reuse the child mapper for the nestedCategoryItemscollection instead of regenerating that logic. Everything else is name-matched, so there is no hand-written junction here (unlike the more complex event mapper elsewhere in this group that must patch an un-generatable field).[Rubric §9, API & Contract Design]: the aggregate's wire shape is assembled from composable per-child mappers, keeping each entity/DTO pair mapped in one place. - Walkthrough: the primary constructor (
:13-14) receivescategoryItemDTOMapper; the[UseMapper] private readonlyfield (:17-18) exposes it to the generator.MapToDTO(:21) is the generated single-entity conversion (which routes child items through the reused mapper).MapToDTOs(:24) is the standard hand-written projection: null-guard (:26) then[.. entityCollection.Select(MapToDTO)](:27). - Where it's used: the primary DTO mapper for Conference
Categoryreads, resolved (with its child auto-injected) by Scrutor/DI, and consumed byCreateConferenceCategoryHandlerto shape its response.
AddCategoryItemHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Categories.UseCases.AddCategoryItem·MMCA.ADC.Conference.Application/Categories/UseCases/AddCategoryItem/AddCategoryItemHandler.cs:15· Level 8 · class
- What it is: the handler for
AddCategoryItemCommand: load the owningCategoryaggregate, delegate the add to the root, persist, log, and return the new item's DTO. - Depends on:
ICommandHandler<in TCommand, TResult>(AddCategoryItemHandler.cs:6,18),IUnitOfWork(:5,16),CategoryItemDTOMapper(:2,17),Result/Error(:7), andILogger<T>(:1,18). - Concept introduced: routing child mutation through the aggregate root. The handler never inserts a
CategoryItemrow directly; it loads theCategoryand callscategory.AddCategoryItem(...)(:30) so the aggregate enforces its own consistency (the DDD boundary rule from Group 02). It is deliberately thin because the cross-cutting concerns (validation, caching, transaction) are applied by the decorator pipeline around it via the command's marker interfaces (see primer and Group 05).[Rubric §4, Domain-Driven Design](mutations go through the root) and[Rubric §13, Observability & Operability]: the[LoggerMessage]source-generated log (:41-42) is the compile-time, allocation-free logging pattern. - Walkthrough:
HandleAsync(:21) gets the typed repository from the unit of work (:25) and loads the category by id with the simpleGetByIdAsync(:26), no eager include, because an add does not need existing items materialized. Anullcategory returnsError.NotFoundtagged with source and target for diagnostics (:27-28). It then callscategory.AddCategoryItem(command.CategoryItemId, command.Name, command.Sort)(:30) and short-circuits with the aggregate's errors on failure (:31-32). On success itSaveChangesAsynces (:34), emits the source-generatedLogCategoryItemAdded(:36), and returnsResult.Successwrapping the DTO fromdtoMapper.MapToDTO(result.Value!)(:38), mapping the item the aggregate just created. The[LoggerMessage]partial declares the template (:41-42). - Why it's built this way: the handler opens no transaction and invalidates no cache itself; the transactional and caching decorators do that, driven by
AddCategoryItemCommand'sICacheInvalidatingmarker. This keeps the handler focused and the cross-cutting behavior uniform across every command. - Where it's used: invoked by the Categories REST controller (via the decorated command dispatch) when a new item is added to a category.
CreateConferenceCategoryHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Categories.UseCases.Create·MMCA.ADC.Conference.Application/Categories/UseCases/Create/CreateConferenceCategoryHandler.cs:16· Level 8 · class
- What it is: the command handler that creates a conference
Category: map the request to an entity via the domain factory, persist it, log, and return the mapped DTO. - Depends on:
ICommandHandler<in TCommand, TResult>(CreateConferenceCategoryHandler.cs:7,20),IUnitOfWork(:6,17), theIEntityRequestMapperforCategory(satisfied byConferenceCategoryCreateRequestMapper,:5,18),ConferenceCategoryDTOMapper(:2,19), andILogger<T>(:1,20). - Concept introduced: the write-handler shape and high-performance logging. The handler is a
sealed partialclass using a primary constructor for DI (:16-20) and implementsICommandHandler<ConferenceCategoryCreateRequest, Result<ConferenceCategoryDTO>>(:20), one of the create paths where the request is also the command. It is deliberately thin because the cross-cutting concerns (validation, caching, transaction, logging) are added by the decorator pipeline around it (see primer and Group 05); the handler only orchestrates the happy path.[Rubric §6, CQRS & Event-Driven](one intent, one handler) and[Rubric §13, Observability & Operability]: the[LoggerMessage]source-generated log (:42-43) is compile-time and allocation-free, no boxing, no runtime format parsing. - Walkthrough:
HandleAsync(:23) first calls the request mapper (:27) and short-circuits withResult.Failureif the domain factory rejected the input (:28-29), propagating the errors. On success it takes the entity (:31), gets the typed repository from the unit of work (:32),AddAsynces it (:34), andSaveChangesAsynces (:35), the single save that persists the row (and would flush any domain events through the outbox). It then emits the source-generatedLogConferenceCategoryCreated(:37) and returnsResult.Successwrapping the DTO fromdtoMapper.MapToDTO(entity)(:39). The[LoggerMessage]partial declares the structured template (:42-43). - Why it's built this way: the handler never opens a transaction or invalidates cache itself; those are the transactional and caching decorators' jobs, driven by the marker interfaces the request implements. This keeps the handler focused and the cross-cutting behavior uniform across every command.
- Where it's used: invoked by the Categories REST controller (via the decorated command dispatch) on
POSTof a new category.
RemoveCategoryItemHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Categories.UseCases.RemoveCategoryItem·MMCA.ADC.Conference.Application/Categories/UseCases/RemoveCategoryItem/RemoveCategoryItemHandler.cs:13· Level 8 · class
- What it is: the handler for
RemoveCategoryItemCommand: load the owningCategoryaggregate with its items and delegate the removal to the aggregate root. - Depends on:
ICommandHandler<in TCommand, TResult>(RemoveCategoryItemHandler.cs:4,15),IUnitOfWork(:3,14),Result/Error(:5), andILogger<T>(:1,15). - Concept introduced: routing child mutation through the aggregate root, plus a defensive id resolution. The handler never deletes a child row directly; it loads the
Category(eager-includingCategoryItems, tracked) and callsentity.RemoveCategoryItem(...)(:49) so the aggregate enforces its own consistency (the DDD boundary rule from Group 02). The load path is dual: because the DELETE endpoint takes the category id as an optional query parameter and the UI's generic delete sends only the item id (arriving as the default0), aCategoryId == default(:28) triggers a reverse lookup that finds the owning category byCategoryItems.Any(ci => ci.Id == command.CategoryItemId)(:30-34); otherwise it loads by id directly (:39-43).[Rubric §4, Domain-Driven Design](mutations go through the root) and[Rubric §9, API & Contract Design](a contract quirk handled explicitly, not silently mis-deleting). - Walkthrough:
HandleAsync(:18) gets the repository (:22), then branches on the unset-CategoryIdcase (:28): either the reverseGetAllAsync(:30-34) taking the first match (:35), or the directGetByIdAsync(:39-43), both eager-loadingCategoryItemsand tracking. Anullentity returnsError.NotFoundsourced/targeted for diagnostics (:46-47). On a successfulRemoveCategoryItem(:49-50) itSaveChangesAsynces (:52) and logs (:54); the domainResultis returned as-is (:57) so an invariant failure from the aggregate surfaces unchanged. - Why it's built this way: the reverse lookup exists because of a real client contract gap (the category-item DELETE case where an unbound query id defaulted to
0); resolving the parent from the child keeps the generic UI delete working without leaking existence. Loading with tracking is required so EF sees the child removal. - Where it's used: invoked by the Categories controller on item-delete; returns 404 when neither branch finds an owner.
AddEventQuestionAnswerCommand
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.AddEventQuestionAnswer·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/UseCases/AddEventQuestionAnswer/AddEventQuestionAnswerCommand.cs:11· Level 6 · record
- What it is the write-side message that asks the system to record an answer to a conference
Eventquestion (for example a per-attendee dietary or accessibility answer). It is asealed recordwith four positional fields: the targetEventId, an optional explicitEventQuestionAnswerId, theQuestionIdbeing answered, and the rawAnswerValuestring (AddEventQuestionAnswerCommand.cs:11-15). - Depends on the identifier aliases
EventIdentifierType,EventQuestionAnswerIdentifierType, andQuestionIdentifierType(see primer section 2); the framework markerICacheInvalidating; and the domainEventaggregate, referenced only to compute a cache prefix. - Concept introduced the cache-invalidating command idiom, taught once here for the whole unit. A command is an immutable request record: it carries data, never behavior. By implementing
ICacheInvalidatingand exposingCachePrefix => $"{typeof(Event).FullName}:"(AddEventQuestionAnswerCommand.cs:18), the record tells the caching decorator in the CQRS pipeline to evict every cached read whose key starts with theEventtype name after the handler succeeds. The command never touches the cache itself; the cross-cutting decorator reads this property and does the eviction, so the write and the cache concern stay separate. [Rubric section 6, CQRS and Event-Driven] assesses whether writes and reads are modeled as distinct, explicit messages: the mutation is a named record routed to a single handler. [Rubric section 10, Cross-Cutting] assesses whether concerns like caching are centralized rather than sprinkled into business code:CachePrefixis a one-line declaration that hands the whole eviction concern to the pipeline. - Walkthrough the four constructor parameters (
AddEventQuestionAnswerCommand.cs:12-15) are the entire payload.EventQuestionAnswerIdis nullable because the caller usually lets the aggregate assign it during the upsert handled downstream.CachePrefix(:18) is the only member with logic, and it is a pure expression. - Why it's built this way modeling each mutation as its own record keeps the vertical slice self-describing; records give value equality and
with-based copies for free, which matters for test assertions. - Where it's used validated first by
AddEventQuestionAnswerCommandValidator, then dispatched intoAddEventQuestionAnswerHandler.
AddEventSpeakerCommand
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.AddEventSpeaker·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/UseCases/AddEventSpeaker/AddEventSpeakerCommand.cs:10· Level 6 · record
- What it is the request to associate an existing
Speakerwith an existingEvent. Asealed recordwith three fields:EventId, an optionalEventSpeakerId, and theSpeakerIdto attach (AddEventSpeakerCommand.cs:10-13). - Depends on the aliases
EventIdentifierType,EventSpeakerIdentifierType,SpeakerIdentifierType;ICacheInvalidating; andEventfor the cache prefix. - Concept introduced none new. It is the same cache-invalidating command shape introduced by
AddEventQuestionAnswerCommand:CachePrefix => $"{typeof(Event).FullName}:"(AddEventSpeakerCommand.cs:16) so a successful add evicts the event read cache. [Rubric section 6, CQRS and Event-Driven] applies for the same reason. - Walkthrough three positional parameters (
:11-13);EventSpeakerIdis nullable so the caller can omit it and let the aggregate assign the association id. - Where it's used validated by
AddEventSpeakerCommandValidator, then handled byAddEventSpeakerHandler.
EventQuestionAnswerDTOMapper
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.DTOs·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/DTOs/EventQuestionAnswerDTOMapper.cs:12· Level 6 · class
- What it is the mapper that projects an
EventQuestionAnswerdomain entity into its read-sideEventQuestionAnswerDTO. It implementsIEntityDTOMapper<EventQuestionAnswer, EventQuestionAnswerDTO, EventQuestionAnswerIdentifierType>(EventQuestionAnswerDTOMapper.cs:12-13). - Depends on the
IEntityDTOMappercontract; the domainEventQuestionAnswerand its DTO; and Mapperly's[Mapper]attribute (Riok.Mapperly, NuGet). - Concept introduced source-generated DTO mapping. The class is
partialand carries[Mapper](:11-12), so at compile time Mapperly writes the body of thepartial EventQuestionAnswerDTO MapToDTO(EventQuestionAnswer entity)declaration (:16) by matching property names between entity and DTO. No reflection runs at runtime, which is the difference from a general-purpose object mapper. The hand-writtenMapToDTOs(:19-23) guards against a null collection (:21) and projects each element with a collection expression. [Rubric section 3, Clean Architecture] assesses whether the application layer, not the domain, owns the DTO translation: the mapper sits in the Application project and depends inward on the domain type. [Rubric section 9, API and Contract Design] assesses whether the wire shape is distinct from the aggregate: this mapper is the single translation point that keepsEventQuestionAnswerfree of serialization concerns (ADR-001 chooses compile-time mapping over runtime reflection). - Walkthrough two members: the Mapperly-generated
MapToDTOsingle-entity projection (:16) and the manual collection overload (:19-23). Both come from theIEntityDTOMappercontract. - Where it's used injected into
AddEventQuestionAnswerHandlerand, as a child mapper, intoEventDTOMapper.
EventSpeakerDTOMapper
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.DTOs·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/DTOs/EventSpeakerDTOMapper.cs:12· Level 6 · class
- What it is the Mapperly mapper from an
EventSpeakerdomain entity to anEventSpeakerDTO, implementingIEntityDTOMapper<EventSpeaker, EventSpeakerDTO, EventSpeakerIdentifierType>(EventSpeakerDTOMapper.cs:12-13). - Depends on the
IEntityDTOMappercontract, the domainEventSpeakerand its DTO, and Mapperly's[Mapper]. - Concept introduced none new; it is structurally identical to
EventQuestionAnswerDTOMapper: a[Mapper]-attributedpartialclass with a generatedMapToDTO(:16) and a null-guarded collection overload (:19-23). [Rubric section 3, Clean Architecture] and [Rubric section 9, API and Contract Design] apply for the same reasons. - Where it's used injected into
AddEventSpeakerHandlerand, as a child mapper, intoEventDTOMapper.
OwnEventQuestionAnswerSpecification
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.Specifications·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/Specifications/OwnEventQuestionAnswerSpecification.cs:11· Level 6 · class
- What it is a query specification (BR-8) that filters event question answers down to those created by a given user, so an attendee sees only their own answers while an organizer bypasses it by passing no specification (
OwnEventQuestionAnswerSpecification.cs:7-16). - Depends on the framework base
Specification<EventQuestionAnswer, EventQuestionAnswerIdentifierType>; the domainEventQuestionAnswer; theUserIdentifierTypealias; andSystem.Linq.Expressions.Expression<T>(BCL). - Concept introduced the specification as a reusable, testable query predicate (the pattern is taught in group 3). The user id arrives through the primary constructor
(UserIdentifierType userId)(:11), and the only override is theCriteriaexpressiona => a.CreatedBy == userId(:15-16). BecauseCriteriais anExpression, not a compiled delegate, EF Core translates it into a SQLWHEREclause rather than filtering in memory. [Rubric section 11, Security] assesses whether row-level access is enforced authoritatively: the filter keys off the entity's auditCreatedByfield, so ownership is decided by stamped data, not by a client-supplied argument. [Rubric section 4, Domain-Driven Design] assesses whether a business rule (BR-8, own-answer visibility) is captured as a first-class, named artifact rather than an ad hocWheresprinkled in a controller. - Walkthrough one expression-bodied member: the
Criteriaoverride (:15-16). Everything else (paging, includes, ordering) is inherited from the base specification. - Where it's used applied by the Conference read side for question-answer list endpoints when the caller is a non-organizer; organizers pass
nullto see all answers.
PublishedEventSpecification
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.Specifications·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/Specifications/PublishedEventSpecification.cs:11· Level 6 · class
- What it is a specification (BR-108) that restricts an event query to published events only, applied for non-organizer users on public read endpoints (
PublishedEventSpecification.cs:7-15). - Depends on the base
Specification<Event, EventIdentifierType>; the domainEvent; andExpression<T>(BCL). - Concept introduced none new; it is the simplest form of the specification introduced by
OwnEventQuestionAnswerSpecification. It takes no constructor arguments and itsCriteriais the constant predicatee => e.IsPublished(:14), which EF Core folds into the SQL query. [Rubric section 8, Data Architecture] assesses whether visibility rules are pushed to the database instead of over-fetching then filtering: unpublished events never leave SQL Server for anonymous readers. - Where it's used applied to public event browse and detail reads for non-organizer callers.
RoomDTOMapper
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.DTOs·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/DTOs/RoomDTOMapper.cs:12· Level 6 · class
- What it is the Mapperly mapper from a
Roomdomain entity to aRoomDTO, implementingIEntityDTOMapper<Room, RoomDTO, RoomIdentifierType>(RoomDTOMapper.cs:12-13). - Depends on the
IEntityDTOMappercontract, the domainRoomand its DTO, and Mapperly's[Mapper]. - Concept introduced none new; identical shape to
EventQuestionAnswerDTOMapper: a generatedMapToDTO(:16) plus the null-guarded collection overload (:19-23). [Rubric section 3, Clean Architecture] and [Rubric section 9, API and Contract Design] apply. - Where it's used injected into the room command handlers and, as a child mapper, into
EventDTOMapper.
UpdateCategoryItemCommand
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Categories.UseCases.UpdateCategoryItem·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Categories/UseCases/UpdateCategoryItem/UpdateCategoryItemCommand.cs:14· Level 6 · record
- What it is the request to rename or reorder an existing item inside a conference
Category. Asealed recordwith four positional fields: the owningCategoryId, the targetCategoryItemId, the newName, and the newSortorder (UpdateCategoryItemCommand.cs:14-18). - Depends on the aliases
ConferenceCategoryIdentifierTypeandCategoryItemIdentifierType;ICacheInvalidating; and the domainCategoryfor the cache prefix. - Concept introduced none new; it is the cache-invalidating command shape from
AddEventQuestionAnswerCommand, but keyed to the category read cache:CachePrefix => $"{typeof(Category).FullName}:"(UpdateCategoryItemCommand.cs:21) so a successful update evicts cached category reads. [Rubric section 6, CQRS and Event-Driven] and [Rubric section 10, Cross-Cutting] apply for the same reasons as the event commands. - Walkthrough four positional parameters (
:15-18); unlike theAdd*commands,CategoryItemIdis required (non-nullable), because an update always targets a known item.CachePrefix(:21) is the only computed member. - Where it's used validated by
UpdateCategoryItemCommandValidator, then handled byUpdateCategoryItemHandler.
AddEventQuestionAnswerCommandValidator
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.AddEventQuestionAnswer·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/UseCases/AddEventQuestionAnswer/AddEventQuestionAnswerCommandValidator.cs:8· Level 7 · class
- What it is the FluentValidation validator that screens an
AddEventQuestionAnswerCommandbefore it reaches the handler. It enforces one rule:AnswerValuemust not be empty (AddEventQuestionAnswerCommandValidator.cs:10-14). - Depends on FluentValidation's
AbstractValidator<T>(NuGet) and the command it validates. - Concept introduced input validation as a pipeline stage. Validators are auto-registered by assembly scanning and invoked by the validating decorator before the transaction opens (see the CQRS pipeline), so a malformed command is rejected without ever loading an aggregate. The rule attaches both a human message and a stable
WithErrorCode("EventQuestionAnswer.AnswerValue.Required")(:14), so clients can branch on a code rather than parse prose. [Rubric section 24, Forms/Validation/UX Safety] assesses whether inputs are validated with actionable, stable errors at the boundary: the error code is the machine-readable contract that lets the UI show the right field message. - Walkthrough a single expression-bodied constructor (
:10-14) registers theAnswerValuerule. Deeper business rules (question must targetEvent, answer must match the question type) are enforced later in the handler, not here, because they need database lookups. - Where it's used consumed by the validating decorator ahead of
AddEventQuestionAnswerHandler.
AddEventSpeakerCommandValidator
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.AddEventSpeaker·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/UseCases/AddEventSpeaker/AddEventSpeakerCommandValidator.cs:8· Level 7 · class
- What it is the validator for
AddEventSpeakerCommand. It enforces thatSpeakerIdis not the default value (AddEventSpeakerCommandValidator.cs:10-13). - Depends on
AbstractValidator<T>(FluentValidation); the command; theSpeakerIdentifierTypealias. - Concept introduced none new; the same boundary-validation role as
AddEventQuestionAnswerCommandValidator. The one rule usesNotEqual(default(SpeakerIdentifierType))(:12) rather thanNotEmpty, the correct guard for a value-type id whose zero or empty value is meaningless. [Rubric section 24, Forms/Validation/UX Safety] applies. - Where it's used ahead of
AddEventSpeakerHandler.
EventDTOMapper
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.DTOs·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/DTOs/EventDTOMapper.cs:14· Level 7 · class
- What it is the composite mapper from an
Eventaggregate to its fullEventDTO, including its child collections. It implementsIEntityDTOMapper<Event, EventDTO, EventIdentifierType>and delegates child mapping to the three leaf mappers (EventDTOMapper.cs:14-18). - Depends on the
IEntityDTOMappercontract; the child mappersRoomDTOMapper,EventSpeakerDTOMapper, andEventQuestionAnswerDTOMapper; the domainEventandEventDTO; Mapperly's[Mapper],[UseMapper], and[MapperIgnoreTarget]; andSystem.Globalization.CultureInfo(BCL). - Concept introduced mapper composition, and the escape hatch for a projection Mapperly cannot express. The three child mappers arrive through the primary constructor (
:14-17) and are exposed as[UseMapper]private fields (:20-27), which tells Mapperly to call them when it maps the matching child collections onEvent.MapToDTO(:30-41) calls the generatedMapToDTOGenerated(:51), then applies a manualwithpost-step (:36-40): the audit fieldLastSessionizeRefreshByis aUserIdentifierType?on the entity but astring?on the DTO, and that nullable-value-to-string conversion is not expressible in Mapperly, so it runs by hand after the generated pass. The generated method carries[MapperIgnoreTarget(nameof(EventDTO.LastSessionizeRefreshBy))](:50) so Mapperly does not try (and fail) to map that member itself. [Rubric section 3, Clean Architecture] assesses whether translation stays in the application layer: the aggregate and its children never learn about their DTOs. [Rubric section 9, API and Contract Design] assesses whether a rich aggregate projects to one coherent contract: this mapper assembles the full event tree in a single call. - Walkthrough
MapToDTO(:30) null-guards the entity (:32), runs the generated map (:33), then patches the one non-mappable field (:36-40).MapToDTOs(:44-48) is the null-guarded collection overload.MapToDTOGenerated(:51) is the Mapperly-written private core. - Why it's built this way delegating to child mappers keeps each leaf projection defined once and reused, so a change to
RoomDTOmapping propagates through every parent that includes rooms (ADR-001 for the manual-mapping stance). - Where it's used injected into the Conference event create and read handlers to project the full event tree.
UpdateCategoryItemCommandValidator
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Categories.UseCases.UpdateCategoryItem·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Categories/UseCases/UpdateCategoryItem/UpdateCategoryItemCommandValidator.cs:7· Level 7 · class
- What it is the validator for
UpdateCategoryItemCommand. Rather than inlining rules, it composes two reusable category-item field rule sets (UpdateCategoryItemCommandValidator.cs:11-12). - Depends on
AbstractValidator<T>(FluentValidation) and the shared rule typesCategoryItemNameRules<T>andCategoryItemSortRules<T>. - Concept introduced rule composition via
Include. EachInclude(new CategoryItemNameRules<UpdateCategoryItemCommand>(p => p.Name))(:11) folds a generic, property-selector-parameterized rule set into this validator. The sameCategoryItemNameRules<T>is reused by the add-category-item validator, so the field constraints have one source of truth and cannot drift between add and update. [Rubric section 1, SOLID] assesses single-responsibility and reuse: each rule set is one small, testable unit; the validator only wires them to properties. [Rubric section 16, Maintainability] assesses whether shared logic is factored rather than duplicated: a length change inCategoryItemNameRulespropagates everywhere at once. - Walkthrough the constructor (
:9-13) is twoIncludecalls, each passing a lambda that points the generic rule at the matching command property (Name,Sort). - Where it's used ahead of
UpdateCategoryItemHandler.
AddEventQuestionAnswerHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.AddEventQuestionAnswer·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/UseCases/AddEventQuestionAnswer/AddEventQuestionAnswerHandler.cs:17· Level 8 · class
- What it is the command handler that records a question answer against an event. It is the most business-rule-dense handler in the unit, enforcing several conference rules (BR-108, BR-128, BR-124, BR-107) around a load-validate-mutate-save flow (
AddEventQuestionAnswerHandler.cs:17-113). - Depends on
IUnitOfWork(repositories plus save),ICurrentUserService(the acting user), theEventQuestionAnswerDTOMapper, anILogger, and theICommandHandlercontract; the domainEvent,Question,EventInvariants, andQuestionInvariants; andResult/Error. - Concept introduced the full anatomy of a command handler, and the point where business rules that need I/O live.
HandleAsync(:24) loads theEventaggregate throughIUnitOfWork.GetRepository, eagerly includingEventQuestionAnswersand requesting change tracking (:28-33) because it intends to mutate. A missing event short-circuits to anError.NotFound(:34-35). It then layers checks that a boundary validator could not do because they require data: BR-108 asserts the event is published viaEventInvariants.EnsureEventIsPublished(:38); BR-128 and BR-124 run inValidateQuestionAsync(:58), which loads theQuestion, rejects it ifQuestionEntityis not"Event"(:65), and delegates the answer-shape check toQuestionInvariants.EnsureAnswerValueMatchesQuestionType(:75-76). BR-107 is an upsert: it looks for an existing non-deleted answer keyed by(QuestionId, CreatedBy)for the current user (:48-50) and routes to update or create accordingly. [Rubric section 6, CQRS and Event-Driven] assesses the single-handler-per-command shape. [Rubric section 4, Domain-Driven Design] assesses whether invariants live in the domain: the handler orchestrates but delegates each rule to an invariants helper or an aggregate method. [Rubric section 11, Security] assesses whether the acting identity is authoritative: the upsert key usescurrentUserService.UserId(:48), not a client-supplied owner, so a user can only overwrite their own answer. - Walkthrough after the guards,
UpdateExistingAnswerAsync(:79) callsentity.UpdateEventQuestionAnswerand saves;CreateNewAnswerAsync(:94) callsentity.AddEventQuestionAnswerand saves. Both persist throughunitOfWork.SaveChangesAsync(:89,:106), where audit stamping, domain-event dispatch, and the outbox fire, then map the child to a DTO. Logging uses a source-generatedLoggerMessagepartial (:111-112), which is allocation-free and the reason the class ispartial. - Why it's built this way rules that need the database (published state, question targeting, answer typing, ownership) cannot run in the stateless validator, so they sit in the handler as explicit
Resultchecks that fail fast before any mutation. [Rubric section 13, Observability and Operability] is served by the structuredLoggerMessage. - Where it's used dispatched from the Conference questions/answers REST controller through the decorator pipeline.
AddEventSpeakerHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.AddEventSpeaker·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/UseCases/AddEventSpeaker/AddEventSpeakerHandler.cs:15· Level 8 · class
- What it is the handler that attaches a speaker to an event. It is the canonical thin load-delegate-save handler (
AddEventSpeakerHandler.cs:15-45). - Depends on
IUnitOfWork, theEventSpeakerDTOMapper, anILogger,ICommandHandler; theEventaggregate and itsAddEventSpeakermethod;Result/Error. - Concept introduced none new; this is the minimal form of the pattern
AddEventQuestionAnswerHandlershows in full.HandleAsync(:21) loads theEventby id (:26), returnsError.NotFoundif absent (:27-28), delegates toentity.AddEventSpeaker(:30-32), and only saves and maps on success (:33-40). All the association invariants live inside the aggregate method, so the handler stays a thin orchestrator. [Rubric section 5, Vertical Slice] assesses whether a feature is a self-contained slice: command, validator, and handler sit in one folder and read top to bottom. - Walkthrough the aggregate method returns a
Result; the handler checksIsFailure(:33) before persisting, so a rejected association never reaches the database.SaveChangesAsync(:36) triggers the shared persistence machinery; aLoggerMessagepartial records the add (:43-44). - Where it's used dispatched from the Conference event-speakers REST controller.
UpdateCategoryItemHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Categories.UseCases.UpdateCategoryItem·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Categories/UseCases/UpdateCategoryItem/UpdateCategoryItemHandler.cs:13· Level 8 · class
- What it is the handler for
UpdateCategoryItemCommand: load the owningCategoryaggregate with its items, ask it to update the named item, save if it agreed. Its return type is a bareResult(no DTO), because the update returns no payload (UpdateCategoryItemHandler.cs:13-44). - Depends on
IUnitOfWork, anILogger,ICommandHandler; theCategoryaggregate and itsUpdateCategoryItemmethod;Result/Error. - Concept introduced none new; it is the child-mutation variant of the load-delegate-save shape.
HandleAsync(:18) loads theCategorywith itsCategoryItemseagerly included and change tracking on (:22-27), because it mutates a child of the aggregate. A missing category returnsError.NotFound(:28-29). It delegates toentity.UpdateCategoryItem(:31) so the item-level invariants live inside the aggregate, and only when that resultIsSuccessdoes it save and log (:32-37). It returns the domain method'sResultdirectly (:39), so a rejected update flows straight back to the caller and nothing persists. [Rubric section 4, Domain-Driven Design] assesses whether the aggregate owns its children: the item update runs throughCategory, never as a direct write to aCategoryItemrow. [Rubric section 6, CQRS and Event-Driven] assesses the single-handler-per-command shape. - Walkthrough the save is inside the
IsSuccessbranch (:32-37), not unconditional, the same discipline the state-transition handlers use: a rejected update must not open a transaction. TheLoggerMessagepartial (:42-43) records the updated item and category ids. - Where it's used dispatched from the Conference categories REST controller update-item endpoint.
AddRoomCommand
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.AddRoom·MMCA.ADC.Conference.Application/Events/UseCases/AddRoom/AddRoomCommand.cs:15· Level 6 · record
- What it is - the write-side request to add a room to an existing conference event. It is the input contract for AddRoomHandler: eight positional parameters carrying the target event, an optional explicit room id, and the room's descriptive fields.
- Depends on - the module identifier aliases
EventIdentifierTypeandRoomIdentifierType(from ConferenceShared, resolved throughMMCA.ADC.Conference.Domain.Events,AddRoomCommand.cs:1), the domain Event type (used only to build the cache prefix,AddRoomCommand.cs:26), and the cross-cutting marker ICacheInvalidating fromMMCA.Common.Application.UseCases(AddRoomCommand.cs:2,23). - Concept introduced - cache-invalidating command. Where a query request opts into caching, a mutation opts into cache eviction by implementing ICacheInvalidating. The record supplies
CachePrefix => $"{typeof(Event).FullName}:"(AddRoomCommand.cs:26), and the caching decorator in the CQRS pipeline (introduced in Group 05) purges every cached read under that key prefix after the command succeeds. Because a room is a child of an event, adding one must evict the event read cache, hence theEvent-typed prefix rather than a room-typed one.[Rubric §10 - Cross-Cutting]assesses whether caching is handled uniformly instead of hand-rolled per handler: here the command declares intent and the pipeline enforces it, so no handler touches the cache directly. - Walkthrough - the
sealed recorddeclaresEventId, then a nullableRoomId(AddRoomCommand.cs:16-17), then the room fieldsNameandSort(required by position) plus the four optional fieldsCapacity,Floor,Location,AccessibilityInfo(AddRoomCommand.cs:18-22). The nullableRoomIdis the load-bearing detail: a null id tells the handler to auto-assign one in the reserved manual range, a non-null id is respected as an explicit Sessionize id. - Why it's built this way - a positional record gives value equality and immutability for free, and keeping the room id optional lets one command serve both organizer-created rooms (no id) and Sessionize-imported rooms (explicit id). Cache invalidation as a marker interface follows the decorator-pipeline pattern (ADR-001 family / CQRS pipeline).
- Where it's used - dispatched by the Conference REST API layer (rooms controller) and handled by AddRoomHandler.
EventCreateRequest
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.Create·MMCA.ADC.Conference.Application/Events/UseCases/Create/EventCreateRequest.cs:10· Level 6 · record
- What it is - the request DTO for creating a new conference event. Unlike the positional command records in this group, it is an init-only property bag that carries every field the Event.Create factory needs.
- Depends on - ICreateRequest from
MMCA.Common.Application.Interfaces(EventCreateRequest.cs:2,10), ICacheInvalidating (EventCreateRequest.cs:10), the Event domain type for the cache prefix, and theEventIdentifierTypealias forId(EventCreateRequest.cs:16). - Concept introduced - the generic create-request pipeline. Implementing ICreateRequest is what lets this DTO flow through the shared entity-request-mapper machinery (IEntityRequestMapper) rather than each create handler hand-mapping fields.
[Rubric §9 - API & Contract Design]assesses whether inbound contracts are explicit and validated: this record makes the create shape a first-class type withrequiredmarkers onName,StartDate,EndDate, andTimeZone. - Walkthrough -
CachePrefixis the sameEvent.FullName-based key as the other event mutations (EventCreateRequest.cs:13).Idisinitand auto-generated when omitted (EventCreateRequest.cs:16). Four fields arerequired:Name(:19),StartDate/EndDateasDateOnly(:25,28), and the IANATimeZonestring (:31). The remaining fields (Description,SessionizeCode,VenueAddress,VenueMapUrl,WiFiInfo) are optional nullable strings (EventCreateRequest.cs:22,34-43). - Why it's built this way - using
required/initproperties instead of a constructor lets the DTO be deserialized directly from JSON while still failing fast at construction if a mandatory field is missing.DateOnly(notDateTime) models a calendar event window with no spurious time-of-day component. - Where it's used - validated by EventCreateRequestValidator, translated to a domain entity by EventCreateRequestMapper, and handled by CreateEventHandler.
PublishEventCommand
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.Publish·MMCA.ADC.Conference.Application/Events/UseCases/Publish/PublishEventCommand.cs:8· Level 6 · record
- What it is - a one-field command that publishes an event, making it visible to attendees.
- Depends on - the
EventIdentifierTypealias forId, Event for the cache prefix, and ICacheInvalidating (PublishEventCommand.cs:1-2,8). - Concept introduced - none new; this is the minimal shape of a state-transition command. It carries only the aggregate id (
PublishEventCommand.cs:8) because the state change itself lives in the domain method Event.Publish, not in the command. - Walkthrough - the
sealed record PublishEventCommand(EventIdentifierType Id)declares the id positionally and supplies the same event-scopedCachePrefix(PublishEventCommand.cs:11) so publishing evicts the cached event reads. - Why it's built this way - a publish is a verb with no payload beyond the target, so the command stays a single-parameter record; all invariants (for example, refusing to publish an already-published event) belong to the aggregate.
- Where it's used - handled by PublishEventHandler; an
Unpublishsibling (not in this unit) mirrors it.
RemoveEventQuestionAnswerCommand
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.RemoveEventQuestionAnswer·MMCA.ADC.Conference.Application/Events/UseCases/RemoveEventQuestionAnswer/RemoveEventQuestionAnswerCommand.cs:9· Level 6 · record
- What it is - the request to remove a single question answer from an event. It carries the owning
EventIdand theEventQuestionAnswerIdto delete. - Depends on - the
EventIdentifierTypeandEventQuestionAnswerIdentifierTypealiases, Event for the cache prefix, and ICacheInvalidating (RemoveEventQuestionAnswerCommand.cs:1-2,11). - Concept introduced - none new; this is the standard remove-child command shape shared with RemoveEventSpeakerCommand and RemoveRoomCommand: the aggregate id plus the child id, and the event-scoped
CachePrefix(RemoveEventQuestionAnswerCommand.cs:14). - Walkthrough - two positional parameters,
EventIdthenEventQuestionAnswerId(RemoveEventQuestionAnswerCommand.cs:10-11); no other payload, because the ownership rule that governs deletion is enforced in the handler, not the command. - Why it's built this way - keeping the command a pure identifier pair keeps the authorization decision (BR-52/BR-53) in one place, RemoveEventQuestionAnswerHandler, rather than trusting a caller-supplied owner field.
- Where it's used - handled by RemoveEventQuestionAnswerHandler.
RemoveEventSpeakerCommand
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.RemoveEventSpeaker·MMCA.ADC.Conference.Application/Events/UseCases/RemoveEventSpeaker/RemoveEventSpeakerCommand.cs:9· Level 6 · record
- What it is - the request to remove a speaker association from an event, carrying
EventIdand theEventSpeakerId(the association row, not the speaker itself). - Depends on - the
EventIdentifierTypeandEventSpeakerIdentifierTypealiases, Event for the cache prefix, and ICacheInvalidating (RemoveEventSpeakerCommand.cs:1-2,11). - Concept introduced - none new; identical remove-child shape to RemoveEventQuestionAnswerCommand and RemoveRoomCommand. It targets an EventSpeaker association and evicts the event cache via
CachePrefix(RemoveEventSpeakerCommand.cs:14). - Walkthrough - the two positional parameters
EventIdandEventSpeakerId(RemoveEventSpeakerCommand.cs:10-11). - Why it's built this way - removing the association (not the speaker aggregate) keeps the operation inside the event boundary; the speaker continues to exist independently.
- Where it's used - handled by RemoveEventSpeakerHandler.
RemoveRoomCommand
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.RemoveRoom·MMCA.ADC.Conference.Application/Events/UseCases/RemoveRoom/RemoveRoomCommand.cs:9· Level 6 · record
- What it is - the request to remove a room from an event, carrying
EventIdandRoomId. - Depends on - the
EventIdentifierTypeandRoomIdentifierTypealiases, Event for the cache prefix, and ICacheInvalidating (RemoveRoomCommand.cs:1-2,11). - Concept introduced - none new; the same remove-child shape as RemoveEventQuestionAnswerCommand and RemoveEventSpeakerCommand, with the event-scoped
CachePrefix(RemoveRoomCommand.cs:14). - Walkthrough - two positional parameters,
EventIdthenRoomId(RemoveRoomCommand.cs:10-11). - Why it's built this way - the room is a child of the event aggregate, so the command names both the aggregate and the child, and the aggregate method Event.RemoveRoom owns the removal invariants.
- Where it's used - handled by RemoveRoomHandler.
AddRoomCommandValidator
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.AddRoom·MMCA.ADC.Conference.Application/Events/UseCases/AddRoom/AddRoomCommandValidator.cs:7· Level 7 · class
- What it is - the FluentValidation validator for AddRoomCommand. It runs in the validation decorator of the CQRS pipeline before the handler executes.
- Depends on -
AbstractValidator<AddRoomCommand>from FluentValidation (AddRoomCommandValidator.cs:1,7) and the reusable room rule sets RoomNameRules<T>, RoomSortRules<T>,RoomCapacityRules<T>,RoomFloorRules<T>,RoomLocationRules<T>, andRoomAccessibilityInfoRules<T>from theEvents.Validationnamespace (AddRoomCommandValidator.cs:2,11-16). - Concept introduced - rule composition via
Include. Rather than restating each room-field rule inline, the validator composes generic, property-selector-parameterized rule objects with FluentValidation'sInclude(...)(AddRoomCommandValidator.cs:11-16). Each rule set is constructed with a lambda selecting the property onAddRoomCommand(for examplep => p.Name), so one rule definition serves every command that has a room name.[Rubric §15 - Best Practices & Code Quality]assesses duplication: composed rule sets keep validation DRY across the Add and (Sessionize import) room-writing paths. - Walkthrough - the constructor includes six rule sets, one per validated room field, in field order (
AddRoomCommandValidator.cs:9-17). There is no bespoke rule here; all logic lives in the included rule types. - Why it's built this way - centralizing per-field rules in shared generic rule classes means a room-name constraint change is made once and applies to every command that reuses
RoomNameRules<T>. - Where it's used - resolved and invoked by the pipeline's validation decorator (Group 05) for AddRoomCommand.
EventCreateRequestMapper
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.Create·MMCA.ADC.Conference.Application/Events/UseCases/Create/EventCreateRequestMapper.cs:11· Level 7 · class
- What it is - the adapter that turns an EventCreateRequest DTO into an Event domain entity by calling the domain factory. It is the request-side complement to the DTO mappers.
- Depends on - IEntityRequestMapper<TEntity, TCreateRequest, TIdentifierType> from
MMCA.Common.Application.Interfaces(EventCreateRequestMapper.cs:2,11-13), Event and itsCreatefactory (EventCreateRequestMapper.cs:1,19), and Result<T> fromMMCA.Common.Shared.Abstractions(EventCreateRequestMapper.cs:3,15). - Concept introduced - request-to-entity mapping as a first-class port. Implementing the generic IEntityRequestMapper lets a generic create handler stay entity-agnostic: the handler asks the mapper for the entity and never references the concrete constructor. Because the factory returns Result<T>, any domain-invariant failure (for example an invalid date range) surfaces as a failed result, not an exception.
[Rubric §4 - Domain-Driven Design]assesses whether entity construction is guarded: here the mapper never news up anEvent, it delegates toEvent.Create. - Walkthrough -
CreateEntityAsyncguards the argument withArgumentNullException.ThrowIfNull(request)(EventCreateRequestMapper.cs:17), then wraps the synchronousEvent.Create(...)call inTask.FromResult, forwarding all ten request fields in factory order (EventCreateRequestMapper.cs:19-29). No persistence happens here; it returns only the validated (or failed) entity. - Why it's built this way - keeping mapping in a dedicated class satisfies the manual-mapping convention (ADR-001, no reflection-based mapper for entity construction) and isolates the one place that knows the
Event.Createparameter order. - Where it's used - injected into CreateEventHandler as
IEntityRequestMapper<Event, EventCreateRequest, EventIdentifierType>.
EventCreateRequestValidator
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.Create·MMCA.ADC.Conference.Application/Events/UseCases/Create/EventCreateRequestValidator.cs:7· Level 7 · class
- What it is - the FluentValidation validator for EventCreateRequest, enforcing the create-time field rules before the handler runs.
- Depends on -
AbstractValidator<EventCreateRequest>(EventCreateRequestValidator.cs:1,7) and the reusable event rule sets EventNameRules<T>, EventTimeZoneRules<T>, and EventDateRangeRules<T> fromEvents.Validation(EventCreateRequestValidator.cs:2,11-13). - Concept introduced - none new; it uses the same
Include-based rule composition as AddRoomCommandValidator. Notably EventDateRangeRules<T> takes two selectors (p => p.StartDate, p => p.EndDate,EventCreateRequestValidator.cs:13) so it can validate the pair relationally (start before end), a check no single-field rule could express. - Walkthrough - three
Includecalls in the constructor: name, time zone, then the two-selector date-range rule (EventCreateRequestValidator.cs:9-14). - Why it's built this way - composing rules keeps event validation consistent across create and update paths and keeps the relational date rule in one reusable place.
- Where it's used - invoked by the pipeline validation decorator (Group 05) ahead of CreateEventHandler.
AddRoomHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.AddRoom·MMCA.ADC.Conference.Application/Events/UseCases/AddRoom/AddRoomHandler.cs:15· Level 8 · class
- What it is - the command handler that adds a room to an existing event, including the app-side room-id auto-assignment logic that keeps organizer-created rooms from colliding with Sessionize-assigned ids.
- Depends on - IUnitOfWork (
AddRoomHandler.cs:16), RoomDTOMapper (:17),ILogger<AddRoomHandler>(:18), the ICommandHandler<in TCommand, TResult> contract (:18), the Event and Room aggregates, EventInvariants for the manual-id range constants (AddRoomHandler.cs:39,45,47), and Result/Error (:28,48). - Concept introduced - reserved-range id allocation in the handler. Room ids are application-assigned (the int PK is the Sessionize id). When the caller supplies no
RoomId, the handler queries a read repository for existing rooms whose id falls in the reserved manual range (EventInvariants.RoomManualIdRangeStart..RoomManualIdRangeEnd,AddRoomHandler.cs:39) withignoreQueryFilters: trueso soft-deleted rows still count, then takesMax(id) + 1or the range start (AddRoomHandler.cs:43-45), returning a failure if the range is exhausted (:47-48). This is the one handler in the group that assigns a primary key itself rather than delegating to the database.[Rubric §8 - Data Architecture]assesses id strategy: a reserved manual range guarantees organizer-created rooms never overwrite an id the Sessionize import will later claim. - Walkthrough - resolve the event repository and load the event, failing with
Error.NotFoundif absent (AddRoomHandler.cs:25-28). ComputeroomId: honor an explicitcommand.RoomId, otherwise auto-assign from the reserved range via the read repository (AddRoomHandler.cs:33-51). Delegate creation to the aggregate methodentity.AddRoom(...)(:53-60), propagating any domain failure (:61-62). Persist withSaveChangesAsync(:64), log via the source-generatedLogRoomAdded(:66,71-72), and return the mapped RoomDTO (:68). - Why it's built this way - the aggregate owns room creation invariants while the handler owns only the cross-aggregate id-allocation concern that no single event can decide alone (the range is global across events,
AddRoomHandler.cs:38-39). The[LoggerMessage]source generator (AddRoomHandler.cs:71) gives allocation-free structured logging ([Rubric §13 - Observability & Operability]). - Where it's used - dispatched from the Conference rooms REST controller through the CQRS pipeline for AddRoomCommand.
CreateEventHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.Create·MMCA.ADC.Conference.Application/Events/UseCases/Create/CreateEventHandler.cs:16· Level 8 · class
- What it is - the handler that creates a new event: it maps the request to an entity, persists it, and returns the event DTO.
- Depends on - IUnitOfWork (
CreateEventHandler.cs:17), IEntityRequestMapper<Event, EventCreateRequest, EventIdentifierType> (:18),EventDTOMapper(:19, see EventDTOMapper),ILogger<CreateEventHandler>(:20), the ICommandHandler contract handling EventCreateRequest (:20), and Result/EventDTO. - Concept introduced - the create flow via a request mapper. Unlike the other handlers here that load an existing aggregate, this one builds a new one indirectly: it calls
requestMapper.CreateEntityAsync(command, ...)(CreateEventHandler.cs:27) and short-circuits on failure (:28-29) so a domain-invariant violation fromEvent.Createreturns a failed Result rather than throwing. This keeps the handler decoupled from the concreteEventconstructor.[Rubric §5 - Vertical Slice]assesses whether a use case is self-contained: the create slice pairs its own request, validator, mapper, and handler in one folder. - Walkthrough - map request to entity and bail on failure (
CreateEventHandler.cs:27-29); unwrap the entity (:31); get the write repository andAddAsyncthe entity (:32-34);SaveChangesAsync(:35); log with the source-generatedLogEventCreated(:37,42-43); return the mapped EventDTO (:39). - Why it's built this way - delegating construction to the mapper/factory keeps the create handler generic in spirit and consistent with the other create handlers in the module;
ConfigureAwait(false)on the awaited infrastructure calls (CreateEventHandler.cs:34-35) follows the library-code convention (ADR-049). - Where it's used - dispatched by the Conference events REST controller for EventCreateRequest.
PublishEventHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.Publish·MMCA.ADC.Conference.Application/Events/UseCases/Publish/PublishEventHandler.cs:13· Level 8 · class
- What it is - the handler that transitions an event to the published state by loading the aggregate and calling its
Publishmethod. - Depends on - IUnitOfWork (
PublishEventHandler.cs:14),ILogger<PublishEventHandler>(:15), the ICommandHandler contract returning a non-generic Result (:15), the Event aggregate, and Error (:25). - Concept introduced - state-transition handler returning bare
Result. This handler produces no DTO; it returns Result (success or failure only) because publishing is a status change, not a data read. The decision to allow or reject the transition lives entirely inentity.Publish()(PublishEventHandler.cs:27). - Walkthrough - load the event, failing with
Error.NotFoundif missing (PublishEventHandler.cs:22-25); callentity.Publish()(:27); only on success persist and log viaLogEventPublished(:28-32); return the domain result unchanged (:34), so a failed transition propagates the aggregate's own error. - Why it's built this way - persisting only when the transition succeeds (
PublishEventHandler.cs:28) avoids an emptySaveChangesAsyncand keeps the aggregate the single source of publish invariants. Returning the domainresultverbatim preserves its error detail. - Where it's used - dispatched for PublishEventCommand from the Conference events REST controller; the
Unpublishhandler (not in this unit) mirrors it.
RemoveEventQuestionAnswerHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.RemoveEventQuestionAnswer·MMCA.ADC.Conference.Application/Events/UseCases/RemoveEventQuestionAnswer/RemoveEventQuestionAnswerHandler.cs:14· Level 8 · class
- What it is - the handler that removes a question answer from an event, and the only remove handler in this unit that enforces per-record ownership authorization (BR-52/BR-53).
- Depends on - IUnitOfWork (
RemoveEventQuestionAnswerHandler.cs:15), ICurrentUserService (:16),ILogger<...>(:17), the ICommandHandler contract (:17), the Event aggregate and its EventQuestionAnswer children, RoleNames (:6,35), and Result/Error. - Concept introduced - owner-or-admin authorization inside a command handler. After loading the event with its answers included (
RemoveEventQuestionAnswerHandler.cs:25-29), the handler finds the target answer among non-deleted children (:34) and rejects the delete withError.Forbiddenwhen the caller is neither in theOrganizerrole nor the answer's creator:!currentUserService.IsInRole(RoleNames.Organizer) && answer.CreatedBy != currentUserService.UserId!.Value(RemoveEventQuestionAnswerHandler.cs:35-42). This implements BR-52/BR-53 (attendees delete only their own answers, organizers delete any).[Rubric §11 - Security]assesses authorization placement: putting the ownership check in the handler keeps it enforced regardless of which controller or client invokes the command. - Walkthrough - resolve repository and load the event with
includes: [nameof(Event.EventQuestionAnswers)]andasTracking: true(RemoveEventQuestionAnswerHandler.cs:24-29), failingNotFoundif absent (:30-31). Locate the active answer (:34); if found and the caller is not an organizer and not the owner, returnError.Forbiddenwith codeEventQuestionAnswer.NotOwner(:35-42). Otherwise delegate toentity.RemoveEventQuestionAnswer(...)(:44); on success persist and log (:45-49); return the domain result (:51). - Why it's built this way - loading
asTracking: trueis required because the removal mutates the aggregate's child collection; the ownership check runs before the domain call so an unauthorized delete never reaches the aggregate. ReturningForbidden(403 semantics) rather thanNotFoundis deliberate here because the caller already proved the answer exists by targeting a loaded event. - Where it's used - dispatched for RemoveEventQuestionAnswerCommand from the Conference REST layer.
- Caveats / not-in-source - the check at
RemoveEventQuestionAnswerHandler.cs:34-35only forbids when a matching activeansweris found; if the answer id does not match a loaded child, the guard is skipped and the domain method decides the outcome.
RemoveEventSpeakerHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.RemoveEventSpeaker·MMCA.ADC.Conference.Application/Events/UseCases/RemoveEventSpeaker/RemoveEventSpeakerHandler.cs:13· Level 8 · class
- What it is - the handler that removes a speaker association from an event by loading the aggregate with its speakers and delegating to the domain method.
- Depends on - IUnitOfWork (
RemoveEventSpeakerHandler.cs:14),ILogger<...>(:15), the ICommandHandler contract returning Result (:15), the Event aggregate and its EventSpeaker children, and Error (:29). - Concept introduced - none new; this is the plain remove-child handler shape (no ownership check), shared with RemoveRoomHandler.
- Walkthrough - load the event with
includes: [nameof(Event.EventSpeakers)]andasTracking: true(RemoveEventSpeakerHandler.cs:23-27), failingNotFoundif missing (:28-29); callentity.RemoveEventSpeaker(command.EventSpeakerId)(:31); on success persist and log viaLogSpeakerRemovedFromEvent(:32-36); return the domain result (:38). - Why it's built this way - tracking is enabled because the child collection is mutated; the aggregate method owns the removal invariants, so the handler is a thin load-delegate-save shell.
- Where it's used - dispatched for RemoveEventSpeakerCommand from the Conference REST layer.
RemoveRoomHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.RemoveRoom·MMCA.ADC.Conference.Application/Events/UseCases/RemoveRoom/RemoveRoomHandler.cs:13· Level 8 · class
- What it is - the handler that removes a room from an event, structurally identical to RemoveEventSpeakerHandler but targeting the Room child collection.
- Depends on - IUnitOfWork (
RemoveRoomHandler.cs:14),ILogger<...>(:15), the ICommandHandler contract returning Result (:15), the Event aggregate and its Room children, and Error (:29). - Concept introduced - none new; identical remove-child flow to RemoveEventSpeakerHandler.
- Walkthrough - load the event with
includes: [nameof(Event.Rooms)]andasTracking: true(RemoveRoomHandler.cs:23-27), failingNotFoundif missing (:28-29); callentity.RemoveRoom(command.RoomId)(:31); on success persist and log viaLogRoomRemoved(:32-36); return the domain result (:38). - Why it's built this way - same rationale as the speaker remover: aggregate-owned invariants, thin handler, tracking on because the collection mutates.
- Where it's used - dispatched for RemoveRoomCommand from the Conference rooms REST controller.
QuestionCreateRequest
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Questions.UseCases.Create·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Questions/UseCases/Create/QuestionCreateRequest.cs:10· Level 6 · record
- What it is: the input contract for creating a conference
Question(the survey/registration questions attendees answer against events and sessions). It carries the question text, an optional target entity and input type, a sort order, and an "is required" flag. - Depends on:
ICreateRequestandICacheInvalidating(both marker interfaces from the CQRS layer), and theQuestionIdentifierTypealias plusQuestionfor the cache-prefix type name. - Concept: the request record is both the transport DTO and the CQRS command in this slice. A
QuestionCreateRequestis dispatched directly toCreateQuestionHandler(its handler isICommandHandler<QuestionCreateRequest, Result<QuestionDTO>>), so there is no separateCreateQuestionCommandtype. ImplementingICreateRequestlets the generic request-mapper pipeline recognize it, andICacheInvalidatingmakes the caching decorator evict on success.[Rubric §6, CQRS & Event-Driven]assesses whether writes are modeled as explicit intents: here the intent, its validation, and its cache side effect are all declared on one immutable record. - Walkthrough:
CachePrefix => $"{typeof(Question).FullName}:"(QuestionCreateRequest.cs:13) names the cache region to purge.Idis aninitproperty whose XML doc states it is auto-generated by the handler and caller-provided values are ignored (QuestionCreateRequest.cs:16); the server actually overwrites it (seeCreateQuestionHandler).QuestionTextisrequired(QuestionCreateRequest.cs:19);QuestionEntityandQuestionTypeare nullable strings (QuestionCreateRequest.cs:22-25);SortandIsRequireddefault to0/false(QuestionCreateRequest.cs:28-31). - Why it's built this way:
required/initgives immutability with compile-time enforcement of the mandatory field, and folding the command into the request keeps the create slice to one type. The cache prefix keyed ontypeof(Question).FullNamecouples eviction to the entity, not to a hand-written string. - Where it's used: constructed by the Questions controller and handed to
CreateQuestionHandler, which routes it throughQuestionCreateRequestMapperto theQuestion.Createfactory. Validated byQuestionCreateRequestValidator.
QuestionDTOMapper
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Questions.DTOs·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Questions/DTOs/QuestionDTOMapper.cs:12· Level 6 · class (sealed partial)
- What it is: the read-side mapper that turns a
Questiondomain entity into aQuestionDTOfor API responses. - Depends on:
IEntityDTOMapper<TEntity, TEntityDTO, TIdentifierType>(the mapper contract),Question,QuestionDTO, and the Mapperly source generator (Riok.Mapperly.Abstractions, NuGet). - Concept introduced, compile-time DTO mapping with Mapperly.
[Rubric §1, SOLID]and[Rubric §15, Best Practices & Code Quality]. The class issealed partialand carries the[Mapper]attribute (QuestionDTOMapper.cs:11-12); thepartial QuestionDTO MapToDTO(Question entity)declaration (QuestionDTOMapper.cs:16) has no body, and the Mapperly generator emits the property-by-property copy at build time. There is no reflection and no runtime mapping engine: the generated method is ordinary code the analyzers can see, so a shape mismatch fails the build rather than a request (ADR-001, manual/Mapperly DTO mapping). This is the shared shape for the family ofXDTOMappertypes in the Conference module; seeCategoryItemDTOMapperfor the sibling that also carries this pattern. - Walkthrough:
MapToDTOis the generated single-entity map (QuestionDTOMapper.cs:16).MapToDTOs(QuestionDTOMapper.cs:19-23) is hand-written: it null-guards the collection withArgumentNullException.ThrowIfNulland returns a collection expression[.. entityCollection.Select(MapToDTO)]. - Why it's built this way: generating the map keeps it fast and verifiable; the tiny hand-written collection wrapper exists because Mapperly generates the element map and the batch loop is trivial and null-guarded once.
- Where it's used: injected into the Questions read/query handlers and into
CreateQuestionHandler, which callsdtoMapper.MapToDTO(entity)to shape its success payload.
SpeakerCategoryItemDTOMapper
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Speakers.DTOs·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Speakers/DTOs/SpeakerCategoryItemDTOMapper.cs:12· Level 6 · class (sealed partial)
- What it is: the Mapperly mapper from a
SpeakerCategoryItemjoin entity (a speaker's assignment to a category item, for example a locality tag) to itsSpeakerCategoryItemDTO. - Depends on:
IEntityDTOMapper<TEntity, TEntityDTO, TIdentifierType>,SpeakerCategoryItem,SpeakerCategoryItemDTO, Mapperly (NuGet). - Concept: identical in shape to
QuestionDTOMapper: a[Mapper]sealed partialclass with a generatedpartial MapToDTO(SpeakerCategoryItemDTOMapper.cs:16) and a hand-written null-guardedMapToDTOs(SpeakerCategoryItemDTOMapper.cs:19-23). SeeQuestionDTOMapperfor the Mapperly explanation; only the entity/DTO pair differs. - Where it's used: registered as a
[UseMapper]child ofSpeakerDTOMapper, so a speaker's category-item collection is mapped without the parent mapper duplicating the per-item logic.
SpeakerLocalityHelper
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.DecisionSupport·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/UseCases/DecisionSupport/SpeakerLocalityHelper.cs:10· Level 6 · class (internal static)
- What it is: a stateless helper for the session-selection decision-support analytics that answers "where is this speaker traveling from" and "does that make them a local speaker", reading the answer out of the category system rather than a dedicated field on
Speaker. - Depends on:
CategoryandSpeaker(Conference domain), plus theCategoryItemIdentifierTypealias and BCL collections. - Concept introduced, locality-as-category (no dedicated speaker field).
[Rubric §4, Domain-Driven Design]and[Rubric §8, Data Architecture]. A speaker's origin is not a scalar column; it is aSpeakerCategoryItemassignment inside a "Where are you traveling from" category (Sessionize category id121854). This helper is the single place that lookup is encoded, so the rest of the analytics code deals in plain tier names ("Atlanta and Suburbs", "Georgia", "Surrounding State", "North America", "Not North America") rather than category ids. - Walkthrough: four static methods form a small pipeline.
FindLocalityCategory(SpeakerLocalityHelper.cs:57-74) scans the loadedCategorylist, skips soft-deleted rows, returns the first whoseTitlecontains "traveling" (case-insensitive), and otherwise falls back to the category whoseId == 121854(SpeakerLocalityHelper.cs:66-70).BuildLocalityLookup(SpeakerLocalityHelper.cs:81-89) projects that category's non-deleted items into anid → namedictionary (empty dictionary when the category is null).GetLocalityTier(SpeakerLocalityHelper.cs:19-33) walks a speaker'sSpeakerCategoryItems, skips deleted assignments, and returns the tier name for the first assignment found in that lookup, elsenull.IsLocalSpeaker(SpeakerLocalityHelper.cs:41-49) treats a tier as local when its name contains "Atlanta", "Georgia", or "Surrounding" (case-insensitive), and returnsfalsefor a null tier. - Why it's built this way: modeling locality through the existing category machinery avoids a schema change and keeps the Sessionize import as the single source of that data (speaker location lives on
CategoryItemcategory121854, not onSpeaker). The title-match-with-id-fallback makes the lookup resilient to a renamed category while still working against the raw imported id. The string-contains local test is deliberately loose so tier labels can be reworded without touching this code. - Where it's used: the session decision-support handlers that rank or annotate sessions by speaker locality (the
DecisionSupportuse-case folder it lives in). - Caveats / not-in-source:
IsLocalSpeakermatches by substring, so a future tier name that coincidentally contains one of those words would be classified local; that risk is not guarded in source.
SpeakerQuestionAnswerDTOMapper
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Speakers.DTOs·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Speakers/DTOs/SpeakerQuestionAnswerDTOMapper.cs:12· Level 6 · class (sealed partial)
- What it is: the Mapperly mapper from a
SpeakerQuestionAnswerentity (a speaker's answer to a conference question) to itsSpeakerQuestionAnswerDTO. - Depends on:
IEntityDTOMapper<TEntity, TEntityDTO, TIdentifierType>,SpeakerQuestionAnswer,SpeakerQuestionAnswerDTO, Mapperly (NuGet). - Concept: structurally identical to
QuestionDTOMapper:[Mapper]sealed partial, generatedpartial MapToDTO(SpeakerQuestionAnswerDTOMapper.cs:16), hand-written null-guardedMapToDTOs(SpeakerQuestionAnswerDTOMapper.cs:19-23). SeeQuestionDTOMapperfor the shared explanation. - Where it's used: registered as a
[UseMapper]child ofSpeakerDTOMapperso speaker responses map alongside the parent.
UnpublishEventCommand
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.Unpublish·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/UseCases/Unpublish/UnpublishEventCommand.cs:8· Level 6 · record
- What it is: the command to unpublish an
Event, hiding it from attendees by transitioning it back to draft. - Depends on:
ICacheInvalidatingandEvent(for the cache-prefix type name), withEventIdentifierType. - Concept: a minimal state-transition command carrying only the target id.
CachePrefix => $"{typeof(Event).FullName}:"(UnpublishEventCommand.cs:11) evicts the event cache region on success. It is the mirror of thePublishEventCommandin this module; the actual state rule lives on the aggregate, not here.[Rubric §6, CQRS & Event-Driven]: a lifecycle change is a first-class command rather than a flag set on an update DTO. - Walkthrough: a
sealed record UnpublishEventCommand(EventIdentifierType Id)(UnpublishEventCommand.cs:8) with the single computedCachePrefixmember (UnpublishEventCommand.cs:11). - Where it's used: dispatched to
UnpublishEventHandler, which loads the event and callsEvent.Unpublish().
UpdateEventQuestionAnswerCommand
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.UpdateEventQuestionAnswer·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/UseCases/UpdateEventQuestionAnswer/UpdateEventQuestionAnswerCommand.cs:10· Level 6 · record
- What it is: the command to change the value of an existing answer to a question inside an event.
- Depends on:
ICacheInvalidating,Event(cache prefix), with theEventIdentifierTypeandEventQuestionAnswerIdentifierTypealiases. - Concept: a child-mutation command that names both the owning aggregate and the child.
[Rubric §4, DDD]: the child answer is mutated through itsEventaggregate root, so the command carriesEventId(the aggregate) andEventQuestionAnswerId(the child), not a free-floating answer id. - Walkthrough:
sealed record UpdateEventQuestionAnswerCommand(EventIdentifierType EventId, EventQuestionAnswerIdentifierType EventQuestionAnswerId, string AnswerValue)(UpdateEventQuestionAnswerCommand.cs:10-13), implementingICacheInvalidatingwithCachePrefix => $"{typeof(Event).FullName}:"(UpdateEventQuestionAnswerCommand.cs:16). - Where it's used: dispatched to
UpdateEventQuestionAnswerHandler, which additionally enforces the answer-ownership rule.
UpdateRoomCommand
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.UpdateRoom·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/UseCases/UpdateRoom/UpdateRoomCommand.cs:15· Level 6 · record
- What it is: the command to update a
Roomthat belongs to an event: its name, sort order, and optional capacity, floor, location, and accessibility info. - Depends on:
ICacheInvalidating,Event(cache prefix), with theEventIdentifierTypeandRoomIdentifierTypealiases. - Concept: the richest child-update command in this unit, but the same shape as
UpdateEventQuestionAnswerCommand: identify the aggregate (EventId) and child (RoomId), then carry the new field values. Nullable parameters (Capacity,Floor,Location,AccessibilityInfo) model genuinely optional room metadata. - Walkthrough:
sealed record UpdateRoomCommand(EventIdentifierType EventId, RoomIdentifierType RoomId, string Name, int Sort, int? Capacity, string? Floor, string? Location, string? AccessibilityInfo)(UpdateRoomCommand.cs:15-23), withCachePrefix => $"{typeof(Event).FullName}:"(UpdateRoomCommand.cs:26). - Where it's used: validated by
UpdateRoomCommandValidatorand handled byUpdateRoomHandler, which delegates toEvent.UpdateRoom(...).
QuestionCreateRequestMapper
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Questions.UseCases.Create·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Questions/UseCases/Create/QuestionCreateRequestMapper.cs:11· Level 7 · class (sealed)
- What it is: the write-side translator that turns a validated
QuestionCreateRequestinto aQuestiondomain entity by calling the aggregate'sCreatefactory. - Depends on:
IEntityRequestMapper<TEntity, TCreateRequest, TIdentifierType>,Question,QuestionCreateRequest, andResult<T>. - Concept introduced, request-to-entity mapping through a factory returning
Result<T>.[Rubric §4, DDD]: unlike the read-side Mapperly mappers, an entity cannot be blindly copied from a DTO, because construction must pass the aggregate's invariants. So this mapper does not property-copy; it callsQuestion.Create(...), which returns aResult<Question>that is a failure if any invariant is violated. It is the counterpart to the DTO mappers: DTO mappers leave the domain, request mappers enter it. - Walkthrough:
CreateEntityAsync(QuestionCreateRequestMapper.cs:15-27) null-guards the request, then returnsTask.FromResult(Question.Create(...)), forwardingId,QuestionText, the null-forgivenQuestionEntity!andQuestionType!,Sort,IsRequired, and a fixedquestionSource: "User"(QuestionCreateRequestMapper.cs:19-26). The literal"User"marks these as manually authored questions, distinct from Sessionize-imported ones. The!forgiveness relies on the validator having already enforced those fields. - Why it's built this way: keeping construction inside the domain factory means the Application layer never builds a half-valid entity; the mapper is a thin adapter from transport shape to factory call. It is synchronous work wrapped in
Task.FromResultto satisfy the async contract without a needless state machine. - Where it's used: injected into
CreateQuestionHandlerasIEntityRequestMapper<Question, QuestionCreateRequest, QuestionIdentifierType>and invoked after the handler has reserved an id.
QuestionCreateRequestValidator
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Questions.UseCases.Create·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Questions/UseCases/Create/QuestionCreateRequestValidator.cs:7· Level 7 · class (sealed)
- What it is: the FluentValidation validator for
QuestionCreateRequest. - Depends on:
AbstractValidator<T>(FluentValidation, NuGet) and the reusableQuestionTextRules<T>rule set from this module. - Concept: rule composition via
Include.[Rubric §24, Forms/Validation/UX Safety]and[Rubric §16, Maintainability]: rather than restating field rules, the validator pulls in a shared, per-field rule set so the create and update paths validateQuestionTextidentically. The sameInclude(...)idiom appears inUpdateRoomCommandValidator. - Walkthrough: the constructor is a single expression-bodied
Include(new QuestionTextRules<QuestionCreateRequest>(p => p.QuestionText))(QuestionCreateRequestValidator.cs:9-10); the property selector binds the shared rule set to this request'sQuestionText. - Where it's used: resolved by the validation decorator in the CQRS pipeline before
QuestionCreateRequestreachesCreateQuestionHandler.
SpeakerDTOMapper
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Speakers.DTOs·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Speakers/DTOs/SpeakerDTOMapper.cs:17· Level 7 · class (sealed partial)
- What it is: the Mapperly mapper from a
Speakeraggregate to aSpeakerDTO. Unlike the plain mappers in this unit, it composes child mappers and redacts PII based on the caller's role. - Depends on:
IEntityDTOMapper<TEntity, TEntityDTO, TIdentifierType>, the child mappersSpeakerCategoryItemDTOMapperandSpeakerQuestionAnswerDTOMapper,ICurrentUserService,RoleNames, theEmailvalue object, and Mapperly (NuGet). - Concept introduced, PII redaction inside the mapper.
[Rubric §11, Security]and[Rubric §5, Vertical Slice]. BR-66 states that a speaker's email is PII and must not appear in public API responses. The mapper enforces this at the mapping boundary: it runs the generated map, then returns the DTO unchanged only when the current user is anOrganizer, otherwise it strips the email. It also demonstrates Mapperly's[UseMapper]composition, where a parent mapper delegates child-collection mapping to injected sibling mappers instead of re-declaring those maps. - Walkthrough: the primary constructor injects the two child mappers and
ICurrentUserService(SpeakerDTOMapper.cs:17-21); the child mappers are stored in[UseMapper]fields so Mapperly wires them into the generated map (SpeakerDTOMapper.cs:23-27). The generated map itself is the privateMapToDTOGenerated(SpeakerDTOMapper.cs:46). The publicMapToDTO(SpeakerDTOMapper.cs:30-37) null-guards, calls the generated map, then applies BR-66:currentUserService.IsInRole(RoleNames.Organizer) ? dto : dto with { Email = null }(SpeakerDTOMapper.cs:36).MapToDTOs(SpeakerDTOMapper.cs:40-44) is the standard null-guarded batch. A privateNullableEmailToString(Email? email)helper (SpeakerDTOMapper.cs:49) tells Mapperly how to flatten theEmailvalue object to a string on the wire. - Why it's built this way: putting the redaction in the mapper means every read path that returns a speaker gets BR-66 for free, rather than each controller remembering to strip email. Delegating to child mappers via
[UseMapper]keeps the per-entity map defined once (inSpeakerCategoryItemDTOMapperandSpeakerQuestionAnswerDTOMapper) and reused here. - Where it's used: the Speaker query/read handlers and any handler returning a
SpeakerDTO. - Caveats / not-in-source: redaction keys off
RoleNames.Organizeronly; whether other roles should ever see the email is a policy question not expressed here.
UpdateRoomCommandValidator
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.UpdateRoom·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/UseCases/UpdateRoom/UpdateRoomCommandValidator.cs:7· Level 7 · class (sealed)
- What it is: the FluentValidation validator for
UpdateRoomCommand, composed from six reusable room field rule sets. - Depends on:
AbstractValidator<T>(FluentValidation, NuGet) and the module'sRoomNameRules<T>,RoomSortRules<T>,RoomCapacityRules<T>,RoomFloorRules<T>,RoomLocationRules<T>, andRoomAccessibilityInfoRules<T>rule sets. - Concept: the same
Include-composition idiom asQuestionCreateRequestValidator, scaled to six fields. Each field's rules live in one rule-set class shared between the add-room and update-room commands, so validation stays consistent across the two paths.[Rubric §16, Maintainability]. - Walkthrough: the constructor
Includes one rule set per field, each bound with a property selector:RoomNameRulesonName,RoomSortRulesonSort,RoomCapacityRulesonCapacity,RoomFloorRulesonFloor,RoomLocationRulesonLocation, andRoomAccessibilityInfoRulesonAccessibilityInfo(UpdateRoomCommandValidator.cs:11-16). - Where it's used: run by the validation decorator before
UpdateRoomCommandreachesUpdateRoomHandler.
CreateQuestionHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Questions.UseCases.Create·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Questions/UseCases/Create/CreateQuestionHandler.cs:16· Level 8 · class (sealed partial)
- What it is: the create handler for conference questions. It differs from a generic create in one way: it allocates the new id itself, from a reserved range, so it will not collide with ids that the Sessionize import assigns.
- Depends on:
IUnitOfWork,IEntityRequestMapper<TEntity, TCreateRequest, TIdentifierType>(satisfied byQuestionCreateRequestMapper),QuestionDTOMapper,ILogger,Question,QuestionInvariants, andResult/Error. - Concept introduced, application-enforced key allocation.
[Rubric §8, Data Architecture]. The question id space is shared with an external system (Sessionize), so the database identity column cannot own it. Instead, the handler reserves a "manual" range (QuestionInvariants.ManualIdRangeStart..ManualIdRangeEnd) and picks the next free id inside it. This is the one create path in the unit that overrides the caller-supplied id rather than trusting or generating it downstream. The handler's command type is the request: it implementsICommandHandler<QuestionCreateRequest, Result<QuestionDTO>>(CreateQuestionHandler.cs:20). - Walkthrough:
HandleAsync(CreateQuestionHandler.cs:23-59) gets theQuestionrepository (CreateQuestionHandler.cs:27), then loads all existing questions in the manual range withignoreQueryFilters: trueso soft-deleted rows still reserve their ids (CreateQuestionHandler.cs:31-35).nextIdismax + 1or the range start when empty (CreateQuestionHandler.cs:37-39); exceedingManualIdRangeEndreturns anError.Failure"Manual question ID range exhausted." (CreateQuestionHandler.cs:41-42). It then rewrites the command viacommand = command with { Id = nextId }(CreateQuestionHandler.cs:45), maps it to an entity through the request mapper (bailing on failure,CreateQuestionHandler.cs:47-49), adds and saves through theIUnitOfWork(CreateQuestionHandler.cs:53-54), logs via a source-generatedLogQuestionCreated(CreateQuestionHandler.cs:56,61-62), and returnsResult.Success(dtoMapper.MapToDTO(entity))(CreateQuestionHandler.cs:58). - Why it's built this way: caller-provided ids are ignored so the manual and imported id ranges stay disjoint; counting soft-deleted rows into the reservation prevents an id being handed out twice after a delete. Everything else (audit stamping, domain-event capture, outbox writes) is the single
SaveChangesAsynccall the handler stays thin over. - Where it's used: dispatched from the Questions controller via the CQRS pipeline (logging, caching, then this handler).
UnpublishEventHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.Unpublish·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/UseCases/Unpublish/UnpublishEventHandler.cs:13· Level 8 · class (sealed partial)
- What it is: the handler for
UnpublishEventCommand. It is the canonical "load aggregate, call a domain state-transition method, save on success" shape. - Depends on:
IUnitOfWork,ILogger,Event, andResult/Error. - Concept: the thinnest handler in the unit.
[Rubric §4, DDD]and[Rubric §6, CQRS]: the decision to unpublish (its invariants, its domain event) lives on theEventaggregate; the handler only orchestrates load, call, save. - Walkthrough:
HandleAsync(UnpublishEventHandler.cs:18-35) gets theEventrepository, loads by id, and returnsError.NotFound(sourced/targeted to the handler andEvent) when the row is missing (UnpublishEventHandler.cs:22-25). It callsentity.Unpublish()(UnpublishEventHandler.cs:27); only onIsSuccessdoes itSaveChangesAsyncand log viaLogEventUnpublished(UnpublishEventHandler.cs:28-32). It returns the domainResultunchanged (UnpublishEventHandler.cs:34). - Why it's built this way: guarding the save on
result.IsSuccessmeans a domain-rule rejection never persists a partial change; returning the domain result verbatim preserves the aggregate's error to the caller. - Where it's used: dispatched from the Events controller's unpublish endpoint.
UpdateEventQuestionAnswerHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.UpdateEventQuestionAnswer·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/UseCases/UpdateEventQuestionAnswer/UpdateEventQuestionAnswerHandler.cs:14· Level 8 · class (sealed partial)
- What it is: the handler for
UpdateEventQuestionAnswerCommand. It is the "load aggregate, mutate child, save" shape plus an ownership check (BR-52/BR-53): attendees may edit only their own answers, organizers any. - Depends on:
IUnitOfWork,ICurrentUserService,ILogger,Event(and itsEventQuestionAnswerchild),RoleNames, andResult/Error. - Concept introduced, ownership authorization in the handler.
[Rubric §11, Security]. Whether a user may mutate a given answer depends on "who is the current user", which is an application concern, not a domain invariant, so the check lives here rather than on the aggregate. This is the discriminator that separates a plain child-update handler (likeUpdateRoomHandler) from an owned-child handler. - Walkthrough:
HandleAsync(UpdateEventQuestionAnswerHandler.cs:20-54) loads theEventwithincludes: [nameof(Event.EventQuestionAnswers)]andasTracking: true(UpdateEventQuestionAnswerHandler.cs:24-29) so EF has the child collection loaded and tracked for the domain mutation; a missing event returnsError.NotFound(UpdateEventQuestionAnswerHandler.cs:30-31). It finds the non-deleted answer by id (UpdateEventQuestionAnswerHandler.cs:34), and if the caller is not anOrganizerandanswer.CreatedBy != currentUserService.UserId!.Value, returns anError.ForbiddencodedEventQuestionAnswer.NotOwner(UpdateEventQuestionAnswerHandler.cs:35-42). Otherwise it delegates toentity.UpdateEventQuestionAnswer(id, value)(UpdateEventQuestionAnswerHandler.cs:44-46), and on success saves and logs (UpdateEventQuestionAnswerHandler.cs:47-51). - Why it's built this way: the include-and-track pair is required because the domain method walks the answer collection and EF must be tracking it to persist the change. Returning
Forbiddenbefore touching the domain keeps authorization and business rules cleanly separated. - Where it's used: dispatched from the event question-answer controller endpoint.
UpdateRoomHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events.UseCases.UpdateRoom·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/UseCases/UpdateRoom/UpdateRoomHandler.cs:13· Level 8 · class (sealed partial)
- What it is: the handler for
UpdateRoomCommand. It loads the owningEventwith its rooms and delegates the mutation toEvent.UpdateRoom(...). - Depends on:
IUnitOfWork,ILogger,Event(and itsRoomchild), andResult/Error. - Concept: the plain child-update sibling of
UpdateEventQuestionAnswerHandler, without the ownership check (a room is organizer-managed metadata, not user-owned content). Same include-and-track requirement, since the domain method walks theRoomscollection. - Walkthrough:
HandleAsync(UpdateRoomHandler.cs:18-46) loads theEventwithincludes: [nameof(Event.Rooms)]andasTracking: true(UpdateRoomHandler.cs:23-27), returnsError.NotFoundwhen missing (UpdateRoomHandler.cs:28-29), then callsentity.UpdateRoom(command.RoomId, command.Name, command.Sort, command.Capacity, command.Floor, command.Location, command.AccessibilityInfo)(UpdateRoomHandler.cs:31-38). On success it saves and logs viaLogRoomUpdated(UpdateRoomHandler.cs:39-43), returning the domainResult(UpdateRoomHandler.cs:45). - Why it's built this way: uniform load-mutate-save keeps the handler a five-line orchestration over the aggregate; the validity of each field was already enforced by
UpdateRoomCommandValidator, so the handler trusts the command shape and defers business rules toEvent.UpdateRoom. - Where it's used: dispatched from the event rooms controller endpoint.
AddSpeakerCategoryItemCommand, RemoveSpeakerCategoryItemCommand
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Speakers.UseCases.AddSpeakerCategoryItem/.RemoveSpeakerCategoryItem· Level 6 · record (sealed)
| Type | File:Line | Notes (what differs) |
|---|---|---|
AddSpeakerCategoryItemCommand |
MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Speakers/UseCases/AddSpeakerCategoryItem/AddSpeakerCategoryItemCommand.cs:13 |
Carries an optional SpeakerCategoryItemId (explicit join-entity id or null for a database-generated identity) plus the CategoryItemId to associate. |
RemoveSpeakerCategoryItemCommand |
MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Speakers/UseCases/RemoveSpeakerCategoryItem/RemoveSpeakerCategoryItemCommand.cs:12 |
Carries only the SpeakerCategoryItemId of the join entity to remove (no CategoryItemId). |
- What they are: the two child-collection commands that add or remove a category-item association on an existing
Speakeraggregate. They are plain immutable message records; all behavior lives in the handlers below. - Depends on: the
SpeakerIdentifierType/SpeakerCategoryItemIdentifierType/CategoryItemIdentifierTypemodule aliases (see the primer on identifier-type aliases),Speaker(only to compute the cache prefix), andICacheInvalidating. - Concept introduced: cache invalidation carried by the command. Both records implement
ICacheInvalidatingand exposeCachePrefix => $"{typeof(Speaker).FullName}:"(AddSpeakerCategoryItemCommand.cs:19,RemoveSpeakerCategoryItemCommand.cs:17). That property is what the caching decorator in the CQRS pipeline reads to evict every speaker-scoped cache entry when the command succeeds. The prefix is computed at the type level, not per instance, so mutating any speaker (or its child associations) evicts the whole speaker cache uniformly rather than one key.[Rubric §6: CQRS & Event-Driven](a distinct command type per operation, each carrying its own cross-cutting contract).[Rubric §12: Performance & Scalability](cache eviction is co-located with the command, not scattered into handler code). - Walkthrough:
AddSpeakerCategoryItemCommand(:13-16) takesSpeakerId, a nullableSpeakerCategoryItemId, andCategoryItemId; the nullable id lets a caller supply an explicit join key (used by the Sessionize import for idempotent upserts) or leave itnullfor database identity.RemoveSpeakerCategoryItemCommand(:12-14) takes onlySpeakerIdand theSpeakerCategoryItemIdto drop. - Why it's built this way: the command is the smallest possible contract: it names the aggregate and the child, and declares its cache-eviction intent. Everything else (loading, invariant checks, saving) is the handler's job.
- Where they're used: dispatched from
SpeakerCategoryItemsControllerthrough the CQRS pipeline toAddSpeakerCategoryItemHandler/RemoveSpeakerCategoryItemHandler.
LinkUserToSpeakerCommand, UnlinkUserFromSpeakerCommand
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Speakers.UseCases.LinkUser/.UnlinkUser· Level 6 · record (sealed)
| Type | File:Line | Notes (what differs) |
|---|---|---|
LinkUserToSpeakerCommand |
MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Speakers/UseCases/LinkUser/LinkUserToSpeakerCommand.cs:13 |
Carries both SpeakerId and the UserId to link (BR-209). |
UnlinkUserFromSpeakerCommand |
MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Speakers/UseCases/UnlinkUser/UnlinkUserFromSpeakerCommand.cs:12 |
Carries only SpeakerId; the previously linked user is recovered from the aggregate. |
- What they are: the commands that establish or tear down the bidirectional User to Speaker association (BR-209). When a conference speaker also holds an application account, linking connects the Conference-side
Speaker.LinkedUserIdto the Identity-sideUser.LinkedSpeakerId. - Depends on: the
SpeakerIdentifierType/UserIdentifierTypemodule aliases,Speaker(for the cache prefix),ICacheInvalidating, andITransactional. - Concept reinforced: the transactional command marker. Beyond the cache prefix (
LinkUserToSpeakerCommand.cs:16,UnlinkUserFromSpeakerCommand.cs:15), both records also implementITransactional(:13/:12). That marker tells the transactional decorator in the CQRS pipeline to wrap the handler in a database transaction, which matters here because the handler mutates the Speaker aggregate and, in the sameSaveChangesAsync, writes an outbox row carrying the cross-context event. The transaction guarantees the local link change and the outbox capture commit or roll back together.[Rubric §6: CQRS & Event-Driven].[Rubric §7: Microservices Readiness](the transaction boundary is what keeps the eventual cross-service update from being lost on a crash). - Walkthrough:
LinkUserToSpeakerCommand(:13) is a two-field record (SpeakerId,UserId).UnlinkUserFromSpeakerCommand(:12) is a one-field record (SpeakerId); it deliberately does not name a user, because the handler reads the currently linked user off the aggregate before clearing it so the emitted event can name whom it unlinked. - Why it's built this way: the two modules own separate databases (ADR-006), so there is no foreign key between
SpeakerandUser. The link is kept consistent through an integration event, and theITransactionalmarker is what makes that event durable (see the handlers below). - Where they're used: dispatched from
SpeakersController's link/unlink actions toLinkUserToSpeakerHandler/UnlinkUserFromSpeakerHandler.
SpeakerCreateRequest
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Speakers.UseCases.Create·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Speakers/UseCases/Create/SpeakerCreateRequest.cs:10· Level 6 · record
- What it is: the request DTO for creating a new conference speaker. Unlike the terse child-collection commands above, this is a wide record carrying every writable speaker field.
- Depends on: the
SpeakerIdentifierType/UserIdentifierTypemodule aliases,Speaker(for the cache prefix),ICreateRequest, andICacheInvalidating. - Concept introduced: the create-request DTO as a pipeline hook. By implementing
ICreateRequest(:10), the record opts into the generic create pipeline: the request itself is dispatched as the command (there is no separateCreateSpeakerCommand), and a request mapper turns it into a domain entity.required/initmembers (FirstName,LastName,FullNameat:19-25) enforce that the mandatory fields are present at construction, while the remaining nullableinitfields (Email,Bio,TagLine,ProfilePicture,TwitterHandle,LinkedInUrl,GitHubUrl,WebsiteUrl,LinkedUserIdat:28-55) are optional.[Rubric §9: API & Contract Design](an explicit, immutable request contract withrequiredguarantees).[Rubric §5: Vertical Slice](the request, mapper, validator, and handler for Create sit in one folder). - Walkthrough:
CachePrefix(:13) evicts the speaker cache on success like the commands above.Id(:16) is aSpeakerIdentifierType(a Sessionize-assigned GUID), so the caller supplies the key rather than the database generating it.IsTopSpeaker(:40) is a plainboolflag;LinkedUserId(:55) is a nullableUserIdentifierTypeallowing a speaker to be created already linked to an account. - Why it's built this way: a client-supplied
Idplusrequiredstring fields lets the Sessionize import upsert speakers idempotently while the analyzers guarantee no mandatory field is silently defaulted. - Where it's used: validated by
SpeakerCreateRequestValidator, mapped bySpeakerCreateRequestMapper, and handled byCreateSpeakerHandler.
AddSpeakerCategoryItemCommandValidator
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Speakers.UseCases.AddSpeakerCategoryItem·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Speakers/UseCases/AddSpeakerCategoryItem/AddSpeakerCategoryItemCommandValidator.cs:8· Level 7 · class (sealed)
- What it is: the FluentValidation validator for
AddSpeakerCategoryItemCommand, run by the validation decorator before the handler. - Depends on:
FluentValidation.AbstractValidator<T>(NuGet) and theCategoryItemIdentifierTypemodule alias. - Concept reinforced: validation as a pipeline stage. The validator subclasses
AbstractValidator<AddSpeakerCategoryItemCommand>(:8); the CQRS pipeline resolves it and rejects the command with a validation error before the handler ever loads the aggregate. This one carries a single inline rule rather than composed rule objects (contrastSpeakerCreateRequestValidatorbelow).[Rubric §24: Forms, Validation & UX Safety](input is rejected at the boundary with a clear message).[Rubric §15: Best Practices & Code Quality]. - Walkthrough: the constructor (
:10-13) is an expression body:RuleFor(x => x.CategoryItemId).NotEqual(default(CategoryItemIdentifierType)).WithMessage("Category item ID is required."). It guards against a caller passing the zero/empty identifier, which would otherwise fall through to a not-found at the handler. - Why it's built this way: the only field worth validating on this command is the required
CategoryItemId; a full rule-composition class would be overkill for one constraint. - Where it's used: resolved automatically by the validation decorator wrapping
AddSpeakerCategoryItemHandler.
SpeakerCreateRequestMapper
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Speakers.UseCases.Create·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Speakers/UseCases/Create/SpeakerCreateRequestMapper.cs:11· Level 7 · class (sealed)
- What it is: the mapper that turns a
SpeakerCreateRequestinto aSpeakerdomain entity by delegating to the entity'sCreate(...)factory. - Depends on:
IEntityRequestMapper<TEntity, TCreateRequest, TIdentifierType>,Speaker, andResult. - Concept reinforced: the Application layer bridges the HTTP request to the domain factory. The mapper implements
IEntityRequestMapper<Speaker, SpeakerCreateRequest, SpeakerIdentifierType>(:11-13). Neither the controller nor the domain factory knows about the other: the handler calls the mapper, the mapper calls the one canonical construction path. It is a plainsealed classwith no[Mapper]attribute, so this is hand-written mapping, not Mapperly source generation (contrast the DTO mappers).[Rubric §3: Clean Architecture].[Rubric §4: DDD](factory methods are the sole entity construction path). - Walkthrough:
CreateEntityAsync(:15-28) first guardsArgumentNullException.ThrowIfNull(request)(:17), then callsSpeaker.Create(request.Id, request.FirstName, request.LastName, request.Email, request.Bio, request.TagLine, request.ProfilePicture, request.IsTopSpeaker)(:19-27) and wraps the returnedResult<Speaker>inTask.FromResult(...). Note that not every request field flows to the factory: the social/URL fields (TwitterHandle,LinkedInUrl, and so on) andLinkedUserIdare not passed toSpeaker.Createhere. If the factory returns a failure (invalid data), that failure propagates up as aResulterror rather than an exception. - Why it's built this way: keeping construction in the domain factory and out of the controller/handler means invariant enforcement lives in one place, and the generic create pipeline can drive any aggregate through the same
IEntityRequestMappercontract. - Where it's used: injected into
CreateSpeakerHandlerasIEntityRequestMapper<Speaker, SpeakerCreateRequest, SpeakerIdentifierType>. - Caveats / not-in-source: whether the URL/social fields are meant to be persisted at create time is Not determinable from source: the mapper simply does not forward them to the factory, and no later assignment is visible in this file.
SpeakerCreateRequestValidator
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Speakers.UseCases.Create·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Speakers/UseCases/Create/SpeakerCreateRequestValidator.cs:7· Level 7 · class (sealed)
- What it is: the FluentValidation validator for
SpeakerCreateRequest, composed from reusable field-rule objects rather than inline rules. - Depends on:
FluentValidation.AbstractValidator<T>(NuGet),SpeakerFirstNameRules<T>, andSpeakerLastNameRules<T>. - Concept reinforced: rule composition via
Include. The constructor (:9-13) callsInclude(new SpeakerFirstNameRules<SpeakerCreateRequest>(p => p.FirstName))andInclude(new SpeakerLastNameRules<SpeakerCreateRequest>(p => p.LastName)). FluentValidation'sInclude(...)copies every rule from the included validator into this one. Because the field rules are generic classes parameterized on the containing type and given a property selector, the same first-name and last-name constraints are shared between the speaker create and update validators without duplication: add a constraint once and both validators pick it up.[Rubric §15: Best Practices & Code Quality].[Rubric §24: Forms, Validation & UX Safety](consistent validation feedback across create and update). - Why it's built this way: extracting field rules into shared classes is the module-wide convention for validators; it keeps field-level logic in one place across the create/update slices.
- Where it's used: resolved by the validation decorator in front of
CreateSpeakerHandler.
AddSpeakerCategoryItemHandler, RemoveSpeakerCategoryItemHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Speakers.UseCases.AddSpeakerCategoryItem/.RemoveSpeakerCategoryItem· Level 8 · class (sealed partial)
| Type | File:Line | Notes (what differs) |
|---|---|---|
AddSpeakerCategoryItemHandler |
MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Speakers/UseCases/AddSpeakerCategoryItem/AddSpeakerCategoryItemHandler.cs:15 |
Returns Result<SpeakerCategoryItemDTO>; injects a DTO mapper; loads the speaker without an explicit include. |
RemoveSpeakerCategoryItemHandler |
MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Speakers/UseCases/RemoveSpeakerCategoryItem/RemoveSpeakerCategoryItemHandler.cs:13 |
Returns a bare Result; loads the speaker with an explicit includes array and asTracking: true. |
- What they are: the two CQRS command handlers that mutate a
Speakeraggregate's category-item collection through its own domain methods. - Depends on:
IUnitOfWork,Speaker,ILogger(BCL, source-generated[LoggerMessage]),Result/Error; the Add handler also injectsSpeakerCategoryItemDTOMapperand returnsSpeakerCategoryItemDTO. - Concept reinforced: the aggregate as the unit of consistency, and
SaveChangesAsyncas the boundary to the framework. Both handlers follow the canonical body: get a repository viaunitOfWork.GetRepository<Speaker, SpeakerIdentifierType>(), load by id, fail withError.NotFoundif missing, call a domain method, and save only on success.[Rubric §4: DDD].[Rubric §6: CQRS & Event-Driven]. - Walkthrough:
AddSpeakerCategoryItemHandler.HandleAsync(:21-39) loads the speaker (:26), returnsResult.Failure<SpeakerCategoryItemDTO>(Error.NotFound.WithSource(...).WithTarget(nameof(Speaker)))when null (:28), then callsspeaker.AddSpeakerCategoryItem(command.SpeakerCategoryItemId, command.CategoryItemId)(:30). On failure it returns the domain errors; on success it awaitsunitOfWork.SaveChangesAsync(...)(:34), logs via the generatedLogCategoryItemAdded(:36,:41-42), and returnsdtoMapper.MapToDTO(result.Value!)(:38).RemoveSpeakerCategoryItemHandler.HandleAsync(:18-40) differs in loading: it passesincludes: [nameof(Speaker.SpeakerCategoryItems)]andasTracking: true(:23-27) because the domain methodRemoveSpeakerCategoryItem(:31) walks the child collection, so EF must have materialized it and be tracking changes. It returns a bareResultand saves only when the removal succeeds (:32-34). - Why it's built this way: the include/tracking difference is the load-bearing distinction between add and remove handlers across the module: an add appends and needs no child collection loaded, whereas a remove/update must have the collection tracked. The single
SaveChangesAsynccall is the one boundary that triggers audit stamping, domain-event capture, and outbox writes; the handler never touches the database directly. - Where they're used: dispatched from
SpeakerCategoryItemsControllerfor the speaker's category-item add/remove endpoints.
CreateSpeakerHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Speakers.UseCases.Create·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Speakers/UseCases/Create/CreateSpeakerHandler.cs:16· Level 8 · class (sealed partial)
- What it is: the handler that creates a
Speaker: it maps the validated request to a domain entity, adds it to the repository, saves, and returns the mapped DTO. - Depends on:
IUnitOfWork,IEntityRequestMapper<TEntity, TCreateRequest, TIdentifierType>(satisfied bySpeakerCreateRequestMapper),SpeakerDTOMapper,ILogger, andResult. - Concept reinforced: the generic create slice, end to end. The request itself is the command: the class implements
ICommandHandler<SpeakerCreateRequest, Result<SpeakerDTO>>(:20).[Rubric §5: Vertical Slice].[Rubric §3: Clean Architecture](the handler orchestrates mapper, repository, and DTO mapper without embedding domain construction). - Walkthrough:
HandleAsync(:23-40) callsrequestMapper.CreateEntityAsync(command, ...)(:27), returning the mapper's errors on failure (:28-29). On success it takesresult.Value!(:31), gets the speaker repository (:32), awaitsrepository.AddAsync(entity, ...)thenunitOfWork.SaveChangesAsync(...)(:34-35), logs via the generatedLogSpeakerCreated(:37,:42-43), and returnsResult.Success(dtoMapper.MapToDTO(entity))(:39). - Why it's built this way: validation, cache invalidation, and transaction concerns are handled by the pipeline decorators around this handler, so the create logic reduces to map, add, save, and return a DTO. The
SpeakerDTOMapperkeeps the domain entity from leaking across the API boundary. - Where it's used: dispatched from
SpeakersController's create endpoint via the CQRS pipeline.
LinkUserToSpeakerHandler, UnlinkUserFromSpeakerHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Speakers.UseCases.LinkUser/.UnlinkUser· Level 8 · class (sealed partial)
| Type | File:Line | Notes (what differs) |
|---|---|---|
LinkUserToSpeakerHandler |
MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Speakers/UseCases/LinkUser/LinkUserToSpeakerHandler.cs:20 |
Enforces BR-208 (no other speaker already linked to the user) and raises SpeakerLinkedToUser. |
UnlinkUserFromSpeakerHandler |
MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Speakers/UseCases/UnlinkUser/UnlinkUserFromSpeakerHandler.cs:19 |
Captures the previously linked user before clearing it and raises SpeakerUnlinkedFromUser. |
- What they are: the handlers that mutate the Conference side of the bidirectional User to Speaker link (
Speaker.LinkedUserId) and emit an integration event so the Identity side (User.LinkedSpeakerId) updates independently. - Depends on:
IUnitOfWork,Speaker,ILogger,Result/Error, and theSpeakerLinkedToUser/SpeakerUnlinkedFromUserintegration-event records. Note there is no injected event-publisher service: the event is raised on the aggregate. - Concept introduced: cross-context coordination through the outbox, captured in the same transaction. The two modules own separate databases (ADR-006), so there is no foreign key between
SpeakerandUser; consistency flows through events. Rather than publish after saving, each handler callsspeaker.AddDomainEvent(new SpeakerLinkedToUser(...))(LinkUserToSpeakerHandler.cs:54) orSpeakerUnlinkedFromUser(UnlinkUserFromSpeakerHandler.cs:42) on the aggregate BEFORE the singleSaveChangesAsync, so the outbox row is written in the same transaction as the link change (ADR-003). The in-source comment (LinkUserToSpeakerHandler.cs:51-53) spells out why: a crash can no longer commit the Conference-side link while losing the event that updates the Identity side. TheOutboxProcessorlater routes the event to the registeredIMessageBustransport. This is why the commands carryITransactional.[Rubric §6: CQRS & Event-Driven].[Rubric §7: Microservices Readiness](the update crosses a service boundary with no shared reference, making Conference and Identity independently extractable). - Walkthrough:
LinkUserToSpeakerHandler.HandleAsync(:25-62) loads the speaker (:30), returnsError.NotFoundif missing (:32), then enforces BR-208 by querying all speakers whoseLinkedUserIdequals the target user and failing withError.Invariant(code: "Speaker.UserAlreadyLinked", ...)if any other speaker already holds that link (:35-46). It callsspeaker.LinkUser(command.UserId)(:48); on success it raises the event (:54), saves (:56), and logs (:58).UnlinkUserFromSpeakerHandler.HandleAsync(:24-51) loads the speaker, capturesspeaker.LinkedUserIdintopreviousUserId(:33) before callingspeaker.UnlinkUser()(:34), then, only when a previous user existed (:40), raisesSpeakerUnlinkedFromUser(previousUserId.Value, command.SpeakerId)(:42) so the event can name the user that was unlinked, saves (:45), and logs (:47). - Why it's built this way: raising the event on the aggregate pre-save (instead of a post-save publisher call) is a deliberate durability fix: it removes the second commit that could drop the cross-context event. It is also what keeps the two modules decoupled, since neither references the other's Application or Domain.
- Where they're used:
SpeakersController's link/unlink actions; the emitted events are consumed on the Identity side to set or clearUser.LinkedSpeakerId.
AddSessionCategoryItemCommand
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.AddSessionCategoryItem·MMCA.ADC.Conference.Application/Sessions/UseCases/AddSessionCategoryItem/AddSessionCategoryItemCommand.cs:10· Level 7 · record
- What it is: the command that associates a category item (a
SessionCategoryItemjoin row) with an existing session. It carries the target session, an optional explicit id for the new join entity, and the category item to attach. - Depends on:
ICacheInvalidating(marker);Session(used only to build the cache prefix); the module identifier aliasesSessionIdentifierType,SessionCategoryItemIdentifierType, andCategoryItemIdentifierType(see primer §2). - Concept introduced, the cache-invalidating child-add command shape.
[Rubric §6, CQRS & Event-Driven]assesses whether writes are modeled as distinct, single-purpose messages; this record is a pure write intent with no behavior beyond declaring which cached reads it should evict.[Rubric §12, Performance & Scalability]assesses caching discipline: implementingICacheInvalidatingwithCachePrefix => $"{typeof(Session).FullName}:"(AddSessionCategoryItemCommand.cs:16) is what lets the caching decorator in the CQRS pipeline drop every cached read keyed under theSessiontype once the write commits. Every session child-add command in this unit shares that identical one-line prefix. - Walkthrough: a
sealed recordwith a three-parameter positional constructor (AddSessionCategoryItemCommand.cs:10-13):SessionId, a nullableSessionCategoryItemId(:12, wherenullmeans the database generates the join-row identity), andCategoryItemId. Its single read-only member isCachePrefix(:16). - Why it's built this way: records give value equality and immutability for free, and keeping the cache tag on the message rather than in the handler means the pipeline decorator, not the handler, owns eviction (see the decorator pipeline in primer §2). The nullable identity parameter lets a Sessionize import supply the source system's id while an interactive create lets the database assign one.
- Where it's used: dispatched by
SessionCategoryItemsController(Group 20); validated byAddSessionCategoryItemCommandValidator; handled byAddSessionCategoryItemHandler.
AddSessionQuestionAnswerCommand
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.AddSessionQuestionAnswer·MMCA.ADC.Conference.Application/Sessions/UseCases/AddSessionQuestionAnswer/AddSessionQuestionAnswerCommand.cs:11· Level 7 · record
- What it is: the command that adds (or upserts) a question answer on an existing session. It carries the target session, an optional explicit answer id, the question being answered, and the answer text.
- Depends on:
ICacheInvalidating;Session(cache prefix); the aliasesSessionIdentifierType,SessionQuestionAnswerIdentifierType,QuestionIdentifierType; BCLstring. - Concept reinforced, cache-invalidating command. Same shape as
AddSessionCategoryItemCommand: a write message tagged with the sharedSessioncache prefix (AddSessionQuestionAnswerCommand.cs:18). The one structural difference is a fourth parameter, the free-textAnswerValue.[Rubric §6, CQRS & Event-Driven]. - Walkthrough: a
sealed recordwith a four-parameter positional constructor (AddSessionQuestionAnswerCommand.cs:11-15):SessionId, a nullableSessionQuestionAnswerId(:13,nullfor database-generated identity),QuestionId, and thestring AnswerValue.CachePrefixat:18. - Where it's used: dispatched by
SessionQuestionAnswersController(Group 20); validated byAddSessionQuestionAnswerCommandValidator; handled byAddSessionQuestionAnswerHandler.
AddSessionSpeakerCommand
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.AddSessionSpeaker·MMCA.ADC.Conference.Application/Sessions/UseCases/AddSessionSpeaker/AddSessionSpeakerCommand.cs:10· Level 7 · record
- What it is: the command that associates a speaker with an existing session by creating a
SessionSpeakerjoin row. - Depends on:
ICacheInvalidating;Session(cache prefix); the aliasesSessionIdentifierType,SessionSpeakerIdentifierType,SpeakerIdentifierType. - Concept reinforced, cache-invalidating command. Structurally identical to
AddSessionCategoryItemCommand: a three-parameter write message carrying the owning session, a nullable join-row id, and the related entity id, tagged with the sharedSessioncache prefix.[Rubric §6, CQRS & Event-Driven]. - Walkthrough: a
sealed recordwith three parameters (AddSessionSpeakerCommand.cs:10-13):SessionId, a nullableSessionSpeakerId(:12,nullfor database-generated identity), andSpeakerId.CachePrefixat:16. - Where it's used: dispatched by
SessionSpeakersController(Group 20); validated byAddSessionSpeakerCommandValidator; handled byAddSessionSpeakerHandler.
OwnSessionQuestionAnswerSpecification
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.Specifications·MMCA.ADC.Conference.Application/Sessions/Specifications/OwnSessionQuestionAnswerSpecification.cs:11· Level 7 · class
- What it is: a query specification that filters session question answers down to those created by one specific user (BR-9). Attendees see only their own answers; organizers bypass the filter (the callers pass
nullrather than this specification). - Depends on:
Specification<TEntity, TIdentifierType>(the reusable base);SessionQuestionAnswerand itsSessionQuestionAnswerIdentifierTypealias; theUserIdentifierTypealias; BCLSystem.Linq.Expressions. - Concept reinforced, the specification pattern for row-level read filtering. The specification pattern itself is introduced in Group 3; this is a concrete instance of it in the Conference module.
[Rubric §11, Security]assesses whether access is scoped per record and not just per route: the specification encodes ownership as data (CreatedBy == userId) so the repository translates it into aWHEREclause, keeping other attendees' answers out of the result set at the database rather than filtering in memory.[Rubric §3, Clean Architecture]assesses whether query intent lives in a first-class, testable object instead of an inline LINQ predicate scattered across handlers. - Walkthrough: a
sealed classwith a primary constructor taking aUserIdentifierType userId(OwnSessionQuestionAnswerSpecification.cs:11), deriving fromSpecification<SessionQuestionAnswer, SessionQuestionAnswerIdentifierType>(:12). It overrides the single abstract memberCriteria(:15-16) to return the expressiona => a.CreatedBy == userId. - Why it's built this way: expressing the ownership filter as a reusable specification means the "attendees see only their own" rule is one object that both the read query and its tests can compose, and the organizer bypass stays an explicit caller decision (pass the specification or pass
null) rather than a branch buried inside a handler. - Where it's used: consumed by the session-question-answer read/list query handlers (outside this unit) when the current user is a non-organizer.
- Caveats / not-in-source: the organizer-bypass convention ("pass
null") is documented in this file's XML summary (OwnSessionQuestionAnswerSpecification.cs:9) but is enforced by the query handlers that call it, which are not in this unit.
SessionCategoryItemDTOMapper
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.DTOs·MMCA.ADC.Conference.Application/Sessions/DTOs/SessionCategoryItemDTOMapper.cs:12· Level 7 · class
- What it is: a Mapperly source-generated mapper that turns a
SessionCategoryItemdomain entity into aSessionCategoryItemDTOfor the API boundary. - Depends on:
IEntityDTOMapper<TEntity, TEntityDTO, TIdentifierType>(the framework contract);SessionCategoryItemandSessionCategoryItemDTO; theSessionCategoryItemIdentifierTypealias; Riok.Mapperly's[Mapper]attribute (NuGet, source generator). - Concept introduced, compile-time DTO mapping via Mapperly.
[Rubric §9, API & Contract Design]assesses whether the domain-to-contract translation is explicit and stable.[Rubric §12, Performance & Scalability]and[Rubric §15, Best Practices & Code Quality]also apply: Mapperly emits the field-copy code at compile time, so there is no runtime reflection and the mapping is analyzer-visible. The class is asealed partial classdecorated with[Mapper](SessionCategoryItemDTOMapper.cs:11-13); the generator fills in the body of the single-entitypartial SessionCategoryItemDTO MapToDTO(SessionCategoryItem entity)(:16). The collection overloadMapToDTOs(:19-23) is hand-written: it null-guards withArgumentNullException.ThrowIfNulland projects with a collection expression ([.. entityCollection.Select(MapToDTO)]). Manual DTO mapping (here via a generator) is the framework norm per ADR-001. - Why it's built this way: a dedicated generated mapper per entity keeps the projection close to the type it maps, stays free of reflection cost, and lets the analyzer flag any unmapped member at build time rather than silently dropping fields.
- Where it's used: injected into
AddSessionCategoryItemHandlerto project the result, and composed intoSessionDTOMapper(via[UseMapper]) so the parent session mapping reuses it for the nested collection.
SessionQuestionAnswerDTOMapper
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.DTOs·MMCA.ADC.Conference.Application/Sessions/DTOs/SessionQuestionAnswerDTOMapper.cs:12· Level 7 · class
- What it is: the Mapperly mapper for
SessionQuestionAnswerentities toSessionQuestionAnswerDTO. - Depends on:
IEntityDTOMapper<TEntity, TEntityDTO, TIdentifierType>;SessionQuestionAnswerandSessionQuestionAnswerDTO; theSessionQuestionAnswerIdentifierTypealias; Riok.Mapperly. - Concept reinforced, generated DTO mapping. Byte-for-byte the same shape as
SessionCategoryItemDTOMapper:[Mapper] sealed partial class(SessionQuestionAnswerDTOMapper.cs:11-13), generatedpartial MapToDTO(:16), and the hand-written null-guardedMapToDTOsprojection (:19-23).[Rubric §9, API & Contract Design]. - Where it's used: injected into
AddSessionQuestionAnswerHandlerand composed intoSessionDTOMappervia[UseMapper].
SessionSpeakerDTOMapper
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.DTOs·MMCA.ADC.Conference.Application/Sessions/DTOs/SessionSpeakerDTOMapper.cs:12· Level 7 · class
- What it is: the Mapperly mapper for
SessionSpeakerentities toSessionSpeakerDTO. - Depends on:
IEntityDTOMapper<TEntity, TEntityDTO, TIdentifierType>;SessionSpeakerandSessionSpeakerDTO; theSessionSpeakerIdentifierTypealias; Riok.Mapperly. - Concept reinforced, generated DTO mapping. Identical structure to
SessionCategoryItemDTOMapper:[Mapper] sealed partial class(SessionSpeakerDTOMapper.cs:11-13), generatedpartial MapToDTO(:16), and the hand-writtenMapToDTOs(:19-23).[Rubric §9, API & Contract Design]. - Where it's used: injected into
AddSessionSpeakerHandlerand composed intoSessionDTOMappervia[UseMapper].
AddSessionCategoryItemCommandValidator
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.AddSessionCategoryItem·MMCA.ADC.Conference.Application/Sessions/UseCases/AddSessionCategoryItem/AddSessionCategoryItemCommandValidator.cs:8· Level 8 · class
- What it is: a single-rule FluentValidation validator that guards
AddSessionCategoryItemCommandbefore its handler runs. - Depends on: FluentValidation's
AbstractValidator<TCommand>(NuGet); theCategoryItemIdentifierTypealias. - Concept introduced, input validation at the command boundary (the pipeline's first business stage).
[Rubric §24, Forms, Validation & UX Safety]assesses whether input is checked before business logic executes. Thissealed classhas oneRuleFor(AddSessionCategoryItemCommandValidator.cs:10-13):RuleFor(x => x.CategoryItemId).NotEqual(default(CategoryItemIdentifierType)).WithMessage("Category item ID is required."). It is auto-discovered by the framework's assembly scan and invoked by the validating decorator in the CQRS pipeline ahead of the matching handler, so the handler can assume the id is present. Only the cheap shape check lives here; the deeper rule (does that category item actually exist and apply) is enforced later in the domain aggregate. - Why it's built this way: separating "is the request well-formed?" (declarative validator) from "is the operation allowed?" (handler and domain invariants) keeps each concern in one place, and the convention scan wires in a new command simply by the presence of a validator class.
- Where it's used: run by the validating command decorator ahead of
AddSessionCategoryItemHandler.
AddSessionCategoryItemHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.AddSessionCategoryItem·MMCA.ADC.Conference.Application/Sessions/UseCases/AddSessionCategoryItem/AddSessionCategoryItemHandler.cs:16· Level 8 · class
- What it is: the handler that adds a category-item association to a session. It is the canonical "load aggregate, call domain method, save, map" template with no extra guards.
- Depends on:
ICommandHandler<in TCommand, TResult>;IUnitOfWork;ResultandError;SessionandSessionCategoryItem;SessionCategoryItemDTOMapper;Microsoft.Extensions.Logging. - Concept introduced, the load-aggregate then delegate-mutation template plus source-generated logging.
[Rubric §5, Vertical Slice]assesses whether each write is a thin, self-contained slice; this file is that slice end to end.[Rubric §6, CQRS & Event-Driven]assesses the command handler contract: it implementsICommandHandler<AddSessionCategoryItemCommand, Result<SessionCategoryItemDTO>>(AddSessionCategoryItemHandler.cs:19).[Rubric §13, Observability & Operability]assesses structured logging: thesealed partial classplus[LoggerMessage](:42-43) emit a zero-allocation, compile-checked log helper, the pattern every handler in this unit uses. - Walkthrough
- Primary constructor (
:16-19):IUnitOfWork, aSessionCategoryItemDTOMapper, andILogger<AddSessionCategoryItemHandler>. HandleAsync(:22) resolves the repository (:26) and loads the session by id (:27). Note this handler calls the plainGetByIdAsync(command.SessionId, cancellationToken)with no include list and no tracking flag, unlikeAddSessionSpeakerHandlerandAddSessionQuestionAnswerHandler, which load the child navigation withasTracking: true.- A missing session returns
Error.NotFoundwith source and target set (:28-29). - It delegates to
session.AddSessionCategoryItem(command.SessionCategoryItemId, command.CategoryItemId)(:31); a failedResultis passed straight through (:32-33). - On success it saves through the unit of work with
ConfigureAwait(false)(:35), logs via the generated helper (:37), and returnsResult.Success(dtoMapper.MapToDTO(result.Value!))(:39).
- Primary constructor (
- Why it's built this way: pushing the "may this category item be added?" decision into
Session.AddSessionCategoryItemkeeps the handler as pure orchestration, so the interesting invariant stays unit-testable on the aggregate and this same shape recurs across the module. - Where it's used: registered by the Conference application DI scan; invoked by the CQRS pipeline for
SessionCategoryItemsControllerposts (Group 20).
AddSessionQuestionAnswerCommandValidator
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.AddSessionQuestionAnswer·MMCA.ADC.Conference.Application/Sessions/UseCases/AddSessionQuestionAnswer/AddSessionQuestionAnswerCommandValidator.cs:8· Level 8 · class
- What it is: a single-rule FluentValidation validator for
AddSessionQuestionAnswerCommand. - Depends on: FluentValidation's
AbstractValidator<TCommand>. - Concept reinforced, command-boundary validation. Same shape as
AddSessionCategoryItemCommandValidator. Its one rule (AddSessionQuestionAnswerCommandValidator.cs:10-13) isRuleFor(x => x.AnswerValue).NotEmpty().WithMessage("Answer value is required."), a check that the answer text is non-blank. The deeper rules (the question exists, targets sessions, and the value matches the question type) need the loaded aggregate and therefore live inAddSessionQuestionAnswerHandlerinstead.[Rubric §24, Forms, Validation & UX Safety]. - Where it's used: run by the validating decorator ahead of
AddSessionQuestionAnswerHandler.
AddSessionQuestionAnswerHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.AddSessionQuestionAnswer·MMCA.ADC.Conference.Application/Sessions/UseCases/AddSessionQuestionAnswer/AddSessionQuestionAnswerHandler.cs:19· Level 8 · class
- What it is: the richest handler in this unit. It adds a question answer to a session, but first enforces a chain of business rules across three aggregates and then performs an upsert (update the caller's existing answer, or create a new one).
- Depends on:
ICommandHandler<in TCommand, TResult>;IUnitOfWork;ICurrentUserService;ResultandError; the domain typesSession,SessionQuestionAnswer,Question,Eventand the invariant helpersSessionInvariants,EventInvariants,QuestionInvariants; theSessionQuestionAnswerDTOMapper;Microsoft.Extensions.Logging. - Concept introduced, the guarded multi-aggregate handler.
[Rubric §4, DDD]assesses whether business rules run through named domain invariants rather than being scattered ad hoc; this handler routes every check into an invariant helper or an aggregate method and re-implements none of them.[Rubric §13, Observability & Operability]applies through thesealed partial class(AddSessionQuestionAnswerHandler.cs:19) plus[LoggerMessage](:133-134). - Walkthrough
- Primary constructor (
:19-23):IUnitOfWork,ICurrentUserService, aSessionQuestionAnswerDTOMapper, and the logger. HandleAsync(:26) loads the session with itsSessionQuestionAnswersnavigation included andasTracking: true(:30-35); a missing session returnsError.NotFound(:36-37).ValidateSessionEligibilityAsync(:60) enforces three rules in order: BR-91 (service sessions cannot receive feedback, viaSessionInvariants.EnsureNotServiceSession,:65), BR-49 (only accepted/null-status sessions are eligible, viaSessionInvariants.EnsureStatusIsEligible,:70), and BR-108 (the parent event must be published: it loads theEventthrough a second repository and callsEventInvariants.EnsureEventIsPublished,:75-80).ValidateQuestionAsync(:83) enforces BR-128 (the question must exist and itsQuestionEntitymust equal"Session", elseError.Validationwith code"Question.NotFoundOrWrongEntity",:88-97) and BR-124 (the answer value must match the question type, viaQuestionInvariants.EnsureAnswerValueMatchesQuestionType,:100-101).- The upsert (BR-107,
:49-57): it looks for an existing non-deleted answer with the sameQuestionIdcreated by the current user (a => !a.IsDeleted && a.QuestionId == command.QuestionId && a.CreatedBy == userId,:51-52). If found,UpdateExistingAnswerAsync(:104) delegates tosession.UpdateSessionQuestionAnswer(...)(:110); otherwiseCreateNewAnswerAsync(:119) delegates tosession.AddSessionQuestionAnswer(...)(:124). Either branch saves through the unit of work, logs, and returns the mappedSessionQuestionAnswerDTO.
- Primary constructor (
- Why it's built this way: the handler orchestrates but never re-implements a rule, so each check stays testable in isolation and reusable across handlers. Loading the parent
Eventinside the handler (rather than reaching across an aggregate boundary in the domain) keepsSessionandEventas separate aggregates. - Where it's used: registered by the Conference application DI scan; invoked by the CQRS pipeline for
SessionQuestionAnswersControllerposts (Group 20).
AddSessionSpeakerCommandValidator
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.AddSessionSpeaker·MMCA.ADC.Conference.Application/Sessions/UseCases/AddSessionSpeaker/AddSessionSpeakerCommandValidator.cs:8· Level 8 · class
- What it is: a single-rule FluentValidation validator for
AddSessionSpeakerCommand. - Depends on: FluentValidation's
AbstractValidator<TCommand>; theSpeakerIdentifierTypealias. - Concept reinforced, command-boundary validation. Same shape as
AddSessionCategoryItemCommandValidator. Its one rule (AddSessionSpeakerCommandValidator.cs:10-13) isRuleFor(x => x.SpeakerId).NotEqual(default(SpeakerIdentifierType)).WithMessage("Speaker ID is required."), guarding that a speaker id was supplied.[Rubric §24, Forms, Validation & UX Safety]. - Where it's used: run by the validating decorator ahead of
AddSessionSpeakerHandler.
AddSessionSpeakerHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.AddSessionSpeaker·MMCA.ADC.Conference.Application/Sessions/UseCases/AddSessionSpeaker/AddSessionSpeakerHandler.cs:16· Level 8 · class
- What it is: the handler that adds a speaker association to a session. Like
AddSessionCategoryItemHandler, it is the clean "load aggregate, call domain method, save, map" template without the extra business guards thatAddSessionQuestionAnswerHandlercarries. - Depends on:
ICommandHandler<in TCommand, TResult>;IUnitOfWork;ResultandError;SessionandSessionSpeaker;SessionSpeakerDTOMapper;Microsoft.Extensions.Logging. - Concept reinforced, the load-aggregate then delegate-mutation template.
[Rubric §5, Vertical Slice]. The primary constructor (AddSessionSpeakerHandler.cs:16-19) takes the unit of work, theSessionSpeakerDTOMapper, and the logger, implementingICommandHandler<AddSessionSpeakerCommand, Result<SessionSpeakerDTO>>.HandleAsync(:22) loads the session with itsSessionSpeakersnavigation included andasTracking: true(:27), returnsError.NotFoundwhen absent (:28-29), delegates tosession.AddSessionSpeaker(command.SessionSpeakerId, command.SpeakerId)(:31), passes any failure straight through (:32-33), then saves withConfigureAwait(false)(:35), logs via the generated[LoggerMessage]helper (:42-43), and returns the mappedSessionSpeakerDTO(:39). The aggregate, not the handler, owns whether the speaker may be added. - Why it's built this way: keeping the domain decision inside
Session.AddSessionSpeakerkeeps the handler as pure orchestration, so the same shape recurs across the module and the interesting rules stay unit-testable on the aggregate. - Where it's used: invoked by the CQRS pipeline for
SessionSpeakersControllerposts (Group 20).
SessionDTOMapper
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.DTOs·MMCA.ADC.Conference.Application/Sessions/DTOs/SessionDTOMapper.cs:14· Level 8 · class
- What it is: the aggregate-root mapper that projects a
Sessiondomain entity to aSessionDTO, composing the three child mappers for the session's nested collections. - Depends on:
IEntityDTOMapper<TEntity, TEntityDTO, TIdentifierType>;SessionandSessionDTO; the three child mappersSessionSpeakerDTOMapper,SessionQuestionAnswerDTOMapper, andSessionCategoryItemDTOMapper; Riok.Mapperly's[Mapper]and[UseMapper]. - Concept introduced, composing Mapperly mappers with
[UseMapper].[Rubric §9, API & Contract Design]assesses whether nested contracts map through their own dedicated, testable mappers rather than duplicated inline projections. Thissealed partial class(SessionDTOMapper.cs:14) takes the three child mappers through its primary constructor (:14-17) and stores each as a[UseMapper]-annotated private field (:20-27). When the generator fills inpartial SessionDTO MapToDTO(Session entity)(:30), Mapperly routes each nested collection (speakers, question answers, category items) through the matching child mapper it found via[UseMapper], so the session projection reuses the exact same per-child mapping the child endpoints use. The collection overloadMapToDTOs(:33-37) is again hand-written with a null guard and a collection-expression projection. This manual/generated mapping approach is the ADR-001 convention. - Why it's built this way: delegating child mapping keeps one source of truth per entity type: a change to how a
SessionSpeakerbecomes a DTO lands inSessionSpeakerDTOMapperalone and the session mapping inherits it, with no reflection and full compile-time checking. - Where it's used: the primary
Sessionprojection, injected into the session create and query handlers (the create handler is in a sibling unit of this group; query handlers elsewhere in the module) and returned fromSessionsControllerreads (Group 20).
GetNowNextQuery
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.NowNext·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/UseCases/NowNext/GetNowNextQuery.cs:23· Level 7 · record
- What it is: the query object for the "happening now / up next" snapshot behind the home-screen widget (ADR-042 Wave 8). It carries one nullable parameter,
EventId(GetNowNextQuery.cs:23); a value targets a specific published event, andnulltells the handler to auto-select the current-or-next published event, which is what the home widget passes because it has no event id of its own. - Depends on:
IQueryCacheable(the marker it implements), theSessiondomain type (used only for itsFullNamewhen building the cache key),EventIdentifierType(the module identifier alias), and the BCLCultureInfo/TimeSpan. - Concept introduced, query-level read caching: this is the first type in this unit to implement
IQueryCacheable, the read side of the caching decorator taught in the CQRS pipeline. A query that implements it is intercepted by the caching decorator, which reads or writes a cache entry keyed onCacheKeyforCacheDurationbefore the handler runs.[Rubric §12, Performance and Scalability]assesses whether hot reads avoid recomputation and database round-trips: here a public, non-user-specific read that the home surface hits on every load is memoized instead of recomputed.[Rubric §10, Cross-Cutting]: caching is a pipeline concern the query only declares, never implements. - Walkthrough:
CacheKey(GetNowNextQuery.cs:26-35) composes"{Session.FullName}:NowNext:{scope}", wherescopeis the invariant-culture event id or the literal"current"whenEventIdis null. Placing the key under theSessionfull-name prefix is deliberate: the session write commands in this unit invalidate on that same prefix (seeRemoveSessionCategoryItemCommand), so any session mutation evicts this snapshot once anIConnectionMultiplexeris registered.CacheDuration(GetNowNextQuery.cs:38) is a deliberately short 30 seconds; it bounds staleness both from event-level edits and from the continuous now/next time-bucket transitions, and it is the sole backstop when Redis prefix eviction is unavailable. - Why it's built this way: keying under the aggregate prefix lets one prefix eviction cover every derived read of that aggregate, and the short TTL keeps a time-sensitive widget honest even without a cache server. See the caching-decorator rationale in the CQRS pipeline.
- Where it's used: handled by
GetNowNextHandler; dispatched from the Conference now-next read endpoint and the home-screen widget.
RemoveSessionCategoryItemCommand
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.RemoveSessionCategoryItem·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/UseCases/RemoveSessionCategoryItem/RemoveSessionCategoryItemCommand.cs:9· Level 7 · record
- What it is: the command to detach a category-item association from a session. It is a two-field record,
SessionIdplus the join-entity idSessionCategoryItemId(RemoveSessionCategoryItemCommand.cs:9-11). - Depends on:
ICacheInvalidating, theSessiontype (for the prefix), and theSessionIdentifierType/SessionCategoryItemIdentifierTypealiases. - Concept introduced, write-side cache invalidation: this is the first type here to implement
ICacheInvalidating, the counterpart toIQueryCacheable. A command that implements it declares aCachePrefix; after the command succeeds, the invalidation decorator evicts every cache entry whose key starts with that prefix.CachePrefix => "{Session.FullName}:"(RemoveSessionCategoryItemCommand.cs:14) is the same prefix the session reads (includingGetNowNextQuery) key under, so removing a category item flushes all cached session projections in one stroke.[Rubric §12, Performance and Scalability]and[Rubric §10, Cross-Cutting]: read caching and its invalidation are declared by the messages and applied uniformly by the pipeline, never hand-wired per handler. - Walkthrough: the record body is only the
CachePrefixexpression-bodied property (RemoveSessionCategoryItemCommand.cs:13-14); the ids are positional parameters. - Where it's used: handled by
RemoveSessionCategoryItemHandler; posted from the session-category management endpoint.
RemoveSessionQuestionAnswerCommand
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.RemoveSessionQuestionAnswer·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/UseCases/RemoveSessionQuestionAnswer/RemoveSessionQuestionAnswerCommand.cs:9· Level 7 · record
- What it is: the command to remove a question answer from a session. Structurally identical to
RemoveSessionCategoryItemCommand:SessionIdplusSessionQuestionAnswerId(RemoveSessionQuestionAnswerCommand.cs:9-11), with the sameCachePrefix => "{Session.FullName}:"(RemoveSessionQuestionAnswerCommand.cs:14). - Depends on:
ICacheInvalidating,Session, and theSessionIdentifierType/SessionQuestionAnswerIdentifierTypealiases. - Concept introduced: none new; see the cache-invalidation walkthrough on
RemoveSessionCategoryItemCommand. The behavioral difference is entirely in its handler, which adds an ownership check. - Where it's used: handled by
RemoveSessionQuestionAnswerHandler.
RemoveSessionSpeakerCommand
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.RemoveSessionSpeaker·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/UseCases/RemoveSessionSpeaker/RemoveSessionSpeakerCommand.cs:9· Level 7 · record
- What it is: the command to remove a speaker association from a session. Same shape again:
SessionIdplus the join-entity idSessionSpeakerId(RemoveSessionSpeakerCommand.cs:9-11) andCachePrefix => "{Session.FullName}:"(RemoveSessionSpeakerCommand.cs:14). - Depends on:
ICacheInvalidating,Session, and theSessionIdentifierType/SessionSpeakerIdentifierTypealiases. - Concept introduced: none new; see
RemoveSessionCategoryItemCommand. Note that its handler tolerates a defaultedSessionIdand resolves the owning session from the join id. - Where it's used: handled by
RemoveSessionSpeakerHandler.
SessionCreateRequest
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.Create·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/UseCases/Create/SessionCreateRequest.cs:10· Level 7 · record
- What it is: the create request DTO for a conference session. It doubles as the create command itself:
CreateSessionHandleris registered as the handler forSessionCreateRequest, so the request travels the CQRS pipeline unchanged. - Depends on:
ICreateRequest(so the generic create-entity mapper pipeline can process it) andICacheInvalidating(so a successful create evicts the session cache,SessionCreateRequest.cs:13); theSessiontype for the prefix; and theSessionIdentifierType/EventIdentifierType/RoomIdentifierTypealiases. - Walkthrough: a
record class(SessionCreateRequest.cs:10) carrying the session's writable fields asinit-only properties.Title(SessionCreateRequest.cs:19) andEventId(SessionCreateRequest.cs:61) arerequired; everything else is optional, including the schedule (StartsAt/EndsAt,SessionCreateRequest.cs:25-28), status and lifecycle flags (Status,IsInformed,IsConfirmed,IsServiceSession,IsPlenumSession,SessionCreateRequest.cs:31-43), the URL and info fields (LiveUrl,RecordingUrl,AccessibilityInfo,ResourceLinks,SessionCreateRequest.cs:46-55),Duration(SessionCreateRequest.cs:58), and the assignedRoomId(SessionCreateRequest.cs:64).Id(SessionCreateRequest.cs:16) is caller-supplied but auto-generated when left at its default (seeCreateSessionHandlerfor the manual-id logic).CachePrefixreturns theSessionfull-name prefix (SessionCreateRequest.cs:13), matching the read caches and the other session write commands. - Why it's built this way: sharing one immutable request type for both the API contract and the internal command keeps the create slice thin.
requiredmarks the genuinely mandatory inputs at the type level so a malformed request cannot even be constructed.[Rubric §9, API and Contract Design]: the contract is a small, explicit, immutable shape. - Where it's used: validated by
SessionCreateRequestValidator, mapped to a domain entity bySessionCreateRequestMapper, and handled byCreateSessionHandler. - Caveats / not-in-source: the current source carries no per-property
SuppressMessageattributes onLiveUrl/RecordingUrl(a prior edition described CA1056 suppressions there; they are not present atSessionCreateRequest.cs:46-51).
UpdateSessionQuestionAnswerCommand
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.UpdateSessionQuestionAnswer·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/UseCases/UpdateSessionQuestionAnswer/UpdateSessionQuestionAnswerCommand.cs:10· Level 7 · record
- What it is: the command to edit an existing question answer on a session. It follows the same cache-invalidating command shape as the Remove commands above, adding one payload field:
SessionId,SessionQuestionAnswerId, and the newAnswerValuetext (UpdateSessionQuestionAnswerCommand.cs:10-13). - Depends on:
ICacheInvalidating,Session, and theSessionIdentifierType/SessionQuestionAnswerIdentifierTypealiases. - Concept introduced: none new;
CachePrefix => "{Session.FullName}:"(UpdateSessionQuestionAnswerCommand.cs:16) works exactly as described onRemoveSessionCategoryItemCommand. The only structural difference from the Remove commands is the extraAnswerValuestring carried to the handler. - Where it's used: handled by
UpdateSessionQuestionAnswerHandler.
EventLiveValidationService
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/EventLiveValidationService.cs:18· Level 8 · class
- What it is: the Conference-side implementation of
IEventLiveValidationService, the contract the Engagement module's live layer calls (in-process during tests, over gRPC in production) to learn whether an event or session is live and who may act on it. - Depends on:
IEventLiveValidationService(the interface),IUnitOfWork(repository access), theEventandSessionaggregates,SessionInvariants(eligibility rules), theEventLiveInfoandSessionLiveInfoDTOs,Result, and the BCLTimeZoneInfo/DateTime/TimeOnly. - Concept introduced, a cross-service read contract satisfied in the owning module: Engagement owns the live experience but not the source of truth for events and sessions, so it asks Conference through this narrow interface.
[Rubric §7, Microservices Readiness]assesses whether cross-module dependencies flow through interfaces a process boundary can later intercept: this service is the in-process implementation behind a gRPC adapter (ADR-007), so Engagement's code does not change when the two modules split into separate hosts.[Rubric §3, Clean Architecture]: the live-window math lives in the Application layer over repository abstractions, with no EF or transport types leaking in. - Walkthrough:
GetEventLiveInfoAsync(EventLiveValidationService.cs:21-41) loads the event untracked, returnsError.NotFoundwhen absent (EventLiveValidationService.cs:32-36), computes the UTC live window, and returns anEventLiveInfocarrying the published flag plus the window (EventLiveValidationService.cs:38-40).GetSessionLiveInfoAsync(EventLiveValidationService.cs:44-97) loads the session with itsSessionSpeakers, then enforces the bookmark eligibility rules by reusingSessionInvariants:EnsureNotServiceSession(BR-91,EventLiveValidationService.cs:63) andEnsureStatusIsEligible(BR-49, no declined or cancelled sessions,EventLiveValidationService.cs:67). It then loads the parent event, computes the window, and projects the active (non-soft-deleted) speaker ids (BR-236,EventLiveValidationService.cs:86-87), returning aSessionLiveInfothat also carries the plenum flag and the event'sQuestionModerationDefault(BR-233,EventLiveValidationService.cs:89-96).ComputeLiveWindowUtc(EventLiveValidationService.cs:99-115) is the shared time math: the window opens atStartDate00:00 local and closes (exclusive) atEndDate + 1 day00:00 local, converted to UTC through the event's IANATimeZone(EventLiveValidationService.cs:101-107). This mirrors the home-page countdown's Live-phase rule. ATimeZoneNotFoundExceptionis caught and the stored local times are treated as UTC rather than failing the call (EventLiveValidationService.cs:109-114), a defensive path for legacy rows even though writes are guarded byEventInvariants.EnsureTimeZoneIsValid.
- Why it's built this way: centralizing the live-window definition here (and reusing the same eligibility invariants the bookmark rules use) keeps one authoritative answer to "is this live and eligible" that both modules trust. See ADR-007 for the gRPC extraction rationale.
- Where it's used: consumed by the Engagement live layer (LivePolls and SessionQuestions), reached through the Conference
.ContractsgRPC adapter in production.
GetNowNextHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.NowNext·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/UseCases/NowNext/GetNowNextHandler.cs:20· Level 8 · class
- What it is: the handler that builds the now-next snapshot: the sessions running at the query instant plus the next starting batch, for one published event or the auto-selected current-or-next event.
- Depends on:
IQueryHandler<in TQuery, TResult>,GetNowNextQuery,IUnitOfWork, theEventandSessionaggregates,CurrentEventSelector(event auto-selection and live-window math),CalendarExportMapper(eligibility plus wall-clock-to-UTC conversion), theNowNextDTOandNowNextSessionDTOshapes,Result, and the BCLTimeProvider. - Concept introduced,
TimeProvider-injected clock for a time-bucketed read: the handler takesTimeProvider timeProviderin its primary constructor (GetNowNextHandler.cs:20-22) and readsGetUtcNow()once (GetNowNextHandler.cs:29) so "now" is a single injected, testable instant rather than an ambientDateTime.UtcNow.[Rubric §14, Testability]: a fakeTimeProviderlets a test place the wall clock precisely inside or across a session boundary.[Rubric §12, Performance and Scalability]: pairing this handler with the 30-second cache on its query keeps a per-load widget cheap. - Walkthrough:
- Selects the event via
SelectEventAsync(GetNowNextHandler.cs:90-113): an explicit id loads that event with itsRooms; a null id loads all published events and defers toCurrentEventSelector.SelectCurrentOrNext(GetNowNextHandler.cs:107-112). A missing or unpublished event returnsError.NotFound(GetNowNextHandler.cs:32-36). - Loads the event's sessions, builds a room-id to name map, and resolves the IANA time zone, falling back to
TimeZoneInfo.UtconTimeZoneNotFoundException(GetNowNextHandler.cs:38-51). - Filters sessions through
CalendarExportMapper.IsExportable(scheduled, non-service, not declined or cancelled) and projects each to a row with both local and UTC start/end viaCalendarExportMapper.ToUtc(GetNowNextHandler.cs:53-56,GetNowNextHandler.cs:80-88). - "Now" is rows whose
[StartsAtUtc, EndsAtUtc)contains the instant, ordered by start then room name (GetNowNextHandler.cs:58-62). "Next" is the batch sharing the earliest future start, so parallel tracks surface together (GetNowNextHandler.cs:64-72). - Computes the event's live flag from
CurrentEventSelector.GetLiveWindowUtcand returns aNowNextDTO(GetNowNextHandler.cs:74-77).
- Selects the event via
- Why it's built this way: reusing the calendar-export eligibility and DST conversion keeps the now-next view consistent with the exported schedule (one rule, two readers), and the earliest-future-start batching matches how attendees think about "up next" across concurrent tracks (ADR-042 Wave 8).
- Where it's used: dispatched behind the Conference now-next endpoint and the home-screen widget.
RemoveSessionCategoryItemHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.RemoveSessionCategoryItem·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/UseCases/RemoveSessionCategoryItem/RemoveSessionCategoryItemHandler.cs:13· Level 8 · class
- What it is: the handler for
RemoveSessionCategoryItemCommand. It is the canonical "remove a child from the session aggregate" shape that the sibling handlers below vary from. - Depends on:
ICommandHandler<in TCommand, TResult>,IUnitOfWork, theSessionaggregate (and itsSessionCategoryItemchild),Result, andMicrosoft.Extensions.Logging. - Concept introduced, load-tracked-then-mutate-through-the-aggregate: the recurring pattern for child removal.
HandleAsync(RemoveSessionCategoryItemHandler.cs:18-39) loads the session with itsSessionCategoryItemsandasTracking: true(RemoveSessionCategoryItemHandler.cs:22-27), returnsError.NotFoundwhen absent (RemoveSessionCategoryItemHandler.cs:28-29), then delegates the actual removal to the domain methodentity.RemoveSessionCategoryItem(...)(RemoveSessionCategoryItemHandler.cs:31) so the aggregate enforces its own invariants. Only on success does itSaveChangesAsyncand log (RemoveSessionCategoryItemHandler.cs:32-36).[Rubric §4, Domain-Driven Design]: the handler never mutates child state directly; it asks the aggregate root.[Rubric §13, Observability and Operability]: logging uses the source-generated[LoggerMessage]partial (RemoveSessionCategoryItemHandler.cs:41-42), the zero-allocation, compile-checked logging pattern used on every handler in this codebase. - Walkthrough: the
sealed partial classprimary constructor takesIUnitOfWorkand a typedILogger(RemoveSessionCategoryItemHandler.cs:13-15); the generatedLogCategoryItemRemovedFromSessionmethod is declared atRemoveSessionCategoryItemHandler.cs:41-42. - Why it's built this way: keeping removal logic in the aggregate and cache eviction on the command (via
ICacheInvalidating) leaves the handler as pure orchestration: load, delegate, save, log. - Where it's used: registered by the Conference Application module scan; invoked from the session-category management endpoint.
RemoveSessionQuestionAnswerHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.RemoveSessionQuestionAnswer·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/UseCases/RemoveSessionQuestionAnswer/RemoveSessionQuestionAnswerHandler.cs:14· Level 8 · class
- What it is: the handler for
RemoveSessionQuestionAnswerCommand. Same load-delegate-save shape asRemoveSessionCategoryItemHandler, with an ownership guard added before the removal. - Depends on: everything the sibling handler depends on, plus
ICurrentUserServiceandRoleNames. - Concept introduced, per-record ownership authorization in the handler (BR-52 / BR-53): after loading the session with its
SessionQuestionAnswers(RemoveSessionQuestionAnswerHandler.cs:24-31), it finds the target answer and, if the caller is not anOrganizerand did not create it, returnsError.Forbiddenwith codeSessionQuestionAnswer.NotOwner(RemoveSessionQuestionAnswerHandler.cs:33-42). Only then does it delegate toentity.RemoveSessionQuestionAnswer(...)(RemoveSessionQuestionAnswerHandler.cs:44).[Rubric §11, Security]: attendees may delete only their own answers, checked against the record'sCreatedByand the current user id, with organizers exempt.[Rubric §6, CQRS and Event-Driven]: the authorization decision lives inside the command slice, not scattered in the controller. - Walkthrough: primary constructor adds
ICurrentUserService(RemoveSessionQuestionAnswerHandler.cs:14-17); the success path saves and logs via the generatedLogQuestionAnswerRemovedFromSession(RemoveSessionQuestionAnswerHandler.cs:45-49,RemoveSessionQuestionAnswerHandler.cs:54-55). - Where it's used: invoked from the session question-answer endpoint.
RemoveSessionSpeakerHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.RemoveSessionSpeaker·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/UseCases/RemoveSessionSpeaker/RemoveSessionSpeakerHandler.cs:13· Level 8 · class
- What it is: the handler for
RemoveSessionSpeakerCommand. The same load-delegate-save shape asRemoveSessionCategoryItemHandler, plus a fallback for resolving the owning session when the command omitsSessionId. - Depends on:
ICommandHandler<in TCommand, TResult>,IUnitOfWork, theSessionaggregate (and itsSessionSpeakerchild),Result, and logging. - Concept introduced, resolving the parent aggregate from a join id: the DELETE endpoint takes the session id as an optional query parameter, but the UI's generic delete sends only the join-entity id, so
SessionIdarrives as the default0(RemoveSessionSpeakerHandler.cs:24-26). WhenSessionId == defaultthe handler queries for the session whoseSessionSpeakerscontains the join id (RemoveSessionSpeakerHandler.cs:28-36); otherwise it loads by id directly (RemoveSessionSpeakerHandler.cs:37-44). Both branches loadasTracking: true.[Rubric §9, API and Contract Design]: the handler adapts to a real client quirk without weakening the domain call, which still targets one aggregate.[Rubric §12, Performance and Scalability]: the fallback is anAny(...)-predicate scan used only when the id is missing. - Walkthrough: after resolving the session (or
Error.NotFound,RemoveSessionSpeakerHandler.cs:46-47), it delegates toentity.RemoveSessionSpeaker(...)(RemoveSessionSpeakerHandler.cs:49), then saves and logs on success via the generatedLogSpeakerRemovedFromSession(RemoveSessionSpeakerHandler.cs:50-54,RemoveSessionSpeakerHandler.cs:59-60). - Where it's used: invoked from the session-speaker management endpoint.
SessionCreateRequestMapper
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.Create·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/UseCases/Create/SessionCreateRequestMapper.cs:11· Level 8 · class
- What it is: the adapter that turns a validated
SessionCreateRequestinto aSessiondomain entity by calling the aggregate'sCreatefactory. - Depends on:
IEntityRequestMapper<TEntity, TCreateRequest, TIdentifierType>(the framework contract it implements, closed overSession/SessionCreateRequest/SessionIdentifierType), theSessionfactory, andResult. - Concept introduced, request-to-entity mapping as its own step: the create pipeline separates "shape the input" (this mapper) from "orchestrate the use case" (the handler).
CreateEntityAsync(SessionCreateRequestMapper.cs:15-34) guards the null request (SessionCreateRequestMapper.cs:17) then forwards each request field positionally intoSession.Create(...)(SessionCreateRequestMapper.cs:19-33), returning itsResult<Session>wrapped in a completedTask.[Rubric §1, SOLID]: single responsibility, the mapper knows the factory's argument order and nothing else.[Rubric §2, Design Patterns]: the mapper decouples the request DTO from the domain constructor so the handler depends only on the interface. Manual mapping over reflection follows ADR-001. - Walkthrough: a
sealed classwith the one interface method; note thatIsInformed,IsConfirmed, andDurationfrom the request are not passed toSession.Createhere, so the factory sets its own defaults for those (SessionCreateRequestMapper.cs:19-33). - Where it's used: injected into
CreateSessionHandlerasIEntityRequestMapper<Session, SessionCreateRequest, SessionIdentifierType>.
SessionCreateRequestValidator
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.Create·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/UseCases/Create/SessionCreateRequestValidator.cs:7· Level 8 · class
- What it is: the FluentValidation validator for
SessionCreateRequest. It composes reusable per-field rule sets rather than restating each rule inline. - Depends on:
FluentValidation(AbstractValidator<T>,Include) and the sharedSession*Rules<T>rule classes fromMMCA.ADC.Conference.Application.Sessions.Validation. - Concept introduced, composed validation via reusable rule includes: the constructor (
SessionCreateRequestValidator.cs:9-19) callsInclude(...)for each field rule set (SessionTitleRules,SessionEventIdRules,SessionDescriptionRules,SessionStatusRules,SessionLiveUrlRules,SessionRecordingUrlRules,SessionAccessibilityInfoRules,SessionResourceLinksRules), each generic over the request type and given a property selector.[Rubric §24, Forms/Validation/UX Safety]and[Rubric §15, Best Practices]: field rules are defined once and shared across create and update validators, so a rule like title length or URL format cannot drift between the two request paths. The validator runs in the CQRS pipeline before the handler executes. - Walkthrough:
sealed class SessionCreateRequestValidator : AbstractValidator<SessionCreateRequest>(SessionCreateRequestValidator.cs:7); eightIncludecalls, each wiring aSession<Field>Rules<SessionCreateRequest>to the matching property lambda (SessionCreateRequestValidator.cs:11-18). - Where it's used: resolved by the validation stage of the create pipeline for
SessionCreateRequest. - Caveats / not-in-source: the composed
Session*Rulesrule classes live in theSessions.Validationnamespace and are documented in their own unit; this section only covers how the create validator assembles them.
UpdateSessionQuestionAnswerHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.UpdateSessionQuestionAnswer·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/UseCases/UpdateSessionQuestionAnswer/UpdateSessionQuestionAnswerHandler.cs:14· Level 8 · class
- What it is: the handler for
UpdateSessionQuestionAnswerCommand. It mirrorsRemoveSessionQuestionAnswerHandlerexactly, enforcing the same BR-52 / BR-53 ownership rule before applying an edit instead of a removal. - Depends on:
ICommandHandler<in TCommand, TResult>,IUnitOfWork,ICurrentUserService,RoleNames, theSessionaggregate,Result, and logging. - Concept introduced: none new; the ownership guard is identical to
RemoveSessionQuestionAnswerHandler: load the session with its answers (UpdateSessionQuestionAnswerHandler.cs:24-31), block a non-organizer editing another user's answer withError.ForbiddencodeSessionQuestionAnswer.NotOwner(UpdateSessionQuestionAnswerHandler.cs:33-42), then delegate toentity.UpdateSessionQuestionAnswer(command.SessionQuestionAnswerId, command.AnswerValue)(UpdateSessionQuestionAnswerHandler.cs:44). - Walkthrough: the success path saves and logs via the generated
LogSessionQuestionAnswerUpdated(UpdateSessionQuestionAnswerHandler.cs:45-49,UpdateSessionQuestionAnswerHandler.cs:54-55). - Where it's used: invoked from the session question-answer edit endpoint.
CreateSessionHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions.UseCases.Create·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/UseCases/Create/CreateSessionHandler.cs:20· Level 9 · class
- What it is: the command handler for creating a session. It is the richest write path in this unit: it assigns manual ids in a reserved range, guards against room double-booking, delegates entity construction to the mapper, persists, and retries on a concurrent id collision.
- Depends on:
ICommandHandler<in TCommand, TResult>,IUnitOfWork,IEntityRequestMapper<TEntity, TCreateRequest, TIdentifierType>(viaSessionCreateRequestMapper),SessionDTOMapper, theSessionaggregate,SessionInvariants(the manual-id range),SessionRoomScheduling(the overlap predicate), theSessionDTOresult shape,Result, plusIServiceScopeFactoryand logging. - Concept introduced, app-assigned ids with a bounded retry on collision: session ids are application-assigned because the int primary key IS the Sessionize id. When the caller supplies no id (organizer create, request
Iddefaults to0),CreateCoreAsyncreads the current maximum in the reserved manual range and takes the next one, ignoring query filters so soft-deleted rows still reserve their id (CreateSessionHandler.cs:77-92); an exhausted range returns a failure (CreateSessionHandler.cs:89-90). Because two concurrent creates can compute the same next id,HandleAsyncwraps the attempt in a bounded loop ofMaxManualIdAttempts= 3 (CreateSessionHandler.cs:28,CreateSessionHandler.cs:40-60); a duplicate-key failure recomputes the id in a fresh DI scope (scopeFactory.CreateAsyncScope()) so a cleanDbContextis used, since the ambient one still tracks the failed insert (CreateSessionHandler.cs:49-53).[Rubric §8, Data Architecture]: id allocation is an explicit, range-partitioned concern rather than a database identity column.[Rubric §12, Performance and Scalability]: the collision path is exceptional and capped, not a lock on the hot path. - Walkthrough:
- An explicit caller id (for example a Sessionize import) is respected as-is and gets a single attempt with no id recomputation, because a collision there is a genuine caller error (
CreateSessionHandler.cs:35-38). CreateCoreAsync(CreateSessionHandler.cs:67-117) resolves the manual id when unset, then applies the room double-booking guard when a room and both times are present, rejecting an overlapping[StartsAt, EndsAt)in the same room viaSessionRoomScheduling.BuildOverlapPredicateandDoubleBookedError(CreateSessionHandler.cs:95-103).- It maps via
requestMapper.CreateEntityAsync(early-return on failure), adds, saves, logs, and returnsResult.Success(dtoMapper.MapToDTO(entity))(CreateSessionHandler.cs:105-116). IsUniqueKeyViolation(CreateSessionHandler.cs:124-133) walks the exception chain looking for the message "duplicate key"; detection is message-based because the Application layer cannot reference EF Core types (both SQL errors 2601 and 2627 report it).
- An explicit caller id (for example a Sessionize import) is respected as-is and gets a single attempt with no id recomputation, because a collision there is a genuine caller error (
- Why it's built this way: keeping the id in a reserved manual range prevents organizer-created sessions from colliding with future Sessionize-assigned ids, and the fresh-scope retry is the only reliable way to recover from a duplicate-key race without leaking EF types into the Application layer. Structured logging uses the source-generated
[LoggerMessage]partialsLogSessionCreatedandLogManualIdCollision(CreateSessionHandler.cs:135-139). - Where it's used: registered as the handler for
SessionCreateRequest; invoked from the session create endpoint and from the Sessionize import path.
EventNavigationPopulator
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Events·MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Events/EventNavigationPopulator.cs:11· Level 10 · class
- What it is: the declarative navigation populator for the
Eventaggregate. It loads the child collections EF Core cannot materialize through.Include()on this model. - Depends on:
DeclarativeNavigationPopulator<TEntity>(the base it extends),ChildNavigationDescriptor<TEntity, TParentId, TChild, TChildId>(the per-collection descriptors),IUnitOfWork, and theEvent,Room,EventSpeaker, andEventQuestionAnswerentities. - Concept introduced, declarative child loading over manual joins: rather than writing imperative load-and-assign code, this populator declares a list of
ChildNavigationDescriptors to its base constructor (EventNavigationPopulator.cs:11-36), each naming the property, the parent-key selector, the child foreign-key selector, and an assign action. The base then loads and assigns each collection uniformly (the mechanism is taught in Group 11, ADR-002).[Rubric §8, Data Architecture]: aggregate hydration is described declaratively, so adding a child collection is one descriptor, not a new query method.[Rubric §2, Design Patterns]: the descriptor list is a small internal DSL over the shared populator. - Walkthrough: three descriptors, one per collection, each keyed on
Event.Idversus the child'sEventId:Roomsassigned viaSetRooms(EventNavigationPopulator.cs:15-21),EventSpeakersviaSetEventSpeakers(EventNavigationPopulator.cs:22-28), andEventQuestionAnswersviaSetEventQuestionAnswers(EventNavigationPopulator.cs:29-35). The class body is empty; all behavior comes from the base and the descriptor list. - Why it's built this way: the aggregate exposes
Set*methods so the populator assigns child collections through the domain's own guarded mutators rather than back-door setters, keeping hydration consistent with the aggregate's rules (ADR-002). - Where it's used: resolved by the navigation-population pipeline whenever an
Eventis read with these navigations requested.
SessionBookmarkValidationService
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions·MMCA.ADC.Conference.Application/Sessions/SessionBookmarkValidationService.cs:12· Level 8 · class (sealed)
- What it is : the Conference-side implementation of
ISessionBookmarkValidationService, the contract Engagement calls before it lets an attendee bookmark a session. It answers two questions: "is this session eligible to be bookmarked?" (BR-49 and BR-91) and "what session ids belong to this event?" (used by the event-scoped bookmark filter). - Depends on :
IUnitOfWork(repository access), theSessionaggregate, its domain-sideSessionInvariantshelper, and theResult/Errorpair for the outcome.SessionIdentifierTypeandEventIdentifierTypeare the module identifier aliases (see primer §2). - Concept introduced : the cross-module validation service that keeps a business rule on the owning side of a boundary.
[Rubric §7 : Microservices Readiness](assesses whether a module can be extracted without dragging another module's rules along): the "can this session be bookmarked" rule is Conference's knowledge (session status and the service-session flag live on theSessionaggregate), so Engagement does not read Conference tables directly, it calls this contract.[Rubric §6 : CQRS & Event-Driven]: this is a read-side collaborator, not a command handler, so it returns a bareResultverdict rather than mutating anything. When both modules run in-process the call is a direct method invocation; when Conference is extracted it is satisfied by a gRPC adapter, so the interface is the extraction boundary (ADR-007). - Walkthrough : the primary-constructor parameter
unitOfWorkis the only dependency (SessionBookmarkValidationService.cs:12).ValidateSessionForBookmarkAsync(SessionBookmarkValidationService.cs:15-39) resolves theSessionrepository, loads the session untracked with no includes (asTracking: false,SessionBookmarkValidationService.cs:19-24, a read-only lookup), and returnsError.NotFoundstamped with source and target when the id does not resolve (SessionBookmarkValidationService.cs:28-29). It then delegates both rules to the domain:SessionInvariants.EnsureNotServiceSession(BR-91, service sessions like breaks and lunch cannot be bookmarked,SessionBookmarkValidationService.cs:33) and, if that passes,SessionInvariants.EnsureStatusIsEligible(BR-49, only Accepted or status-null sessions qualify,SessionBookmarkValidationService.cs:38). Keeping the rule text inSessionInvariantsmeans the same invariant is reused wherever a session is validated, not re-implemented here.GetSessionIdsByEventAsync(SessionBookmarkValidationService.cs:42-54) fetches every session for an event untracked, filtered bys.EventId == eventId(SessionBookmarkValidationService.cs:47-51), and projects to aSessionIdentifierTypecollection with a collection-expression spread (SessionBookmarkValidationService.cs:53). The global soft-delete query filter excludes deleted sessions automatically (see primer §2).
- Why it's built this way : the rule belongs to the aggregate that owns the data, so the check lives with Conference and Engagement depends on the interface, not the schema (ADR-006 database-per-service, ADR-007 gRPC extraction). Returning
Resultrather than throwing lets the calling command fold the verdict into its own pipeline. - Where it's used : Engagement's
CreateBookmarkHandlercallsValidateSessionForBookmarkAsyncbefore creating or reactivating a bookmark, and the event-filtered bookmark query callsGetSessionIdsByEventAsync. This is the reciprocal of Conference calling Engagement'sIBookmarkCountService, the bidirectional gRPC pair the AppHost deliberately wires without a reciprocalWaitFor.
UserRegisteredHandler
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Users.IntegrationEventHandlers·MMCA.ADC.Conference.Application/Users/IntegrationEventHandlers/UserRegisteredHandler.cs:38· Level 8 · class (sealed partial)
- What it is : the Conference-side subscriber to Identity's
UserRegisteredintegration event. When a new user registers, it tries to auto-link them to an existing speaker record so that person's talks show up as theirs (BR-207). - Depends on :
IServiceScopeFactory(per-event DI scope),IUnitOfWork,IIntegrationEventPublisher, theSpeakeraggregate, theEmailvalue object, andILogger. It implementsIIntegrationEventHandler<in TIntegrationEvent>closed overUserRegistered, and publishesSpeakerLinkedToUserback to Identity. - Concept introduced : the integration-event consumer: scoped DI inside a singleton handler, and idempotent eventual consistency.
[Rubric §6 : CQRS & Event-Driven](assesses reliable, idempotent consumers that carry enough context to act): the handler runs as a singleton per framework convention, so it cannot hold a scopedIUnitOfWorkas a field, it opensscopeFactory.CreateAsyncScope()per event and resolves the scoped services inside (UserRegisteredHandler.cs:52-54).[Rubric §29 : Resilience](assesses graceful handling of partial failure): the whole body is wrapped in a best-efforttry/catchthat swallows everything exceptOperationCanceledException(UserRegisteredHandler.cs:108-113), because the user-registration transaction has already committed and a throw here would push a poison message back through the broker. At-least-once delivery (ADR-003) means the handler may re-run, and the "already linked to this user" early return (UserRegisteredHandler.cs:81-87) plus the unique index onSpeaker.LinkedUserIdmake re-delivery idempotent. - Walkthrough : the constructor takes
scopeFactoryandlogger(UserRegisteredHandler.cs:38-40); two match-mode constants label the outcome for logging (UserRegisteredHandler.cs:42-43).HandleAsync(UserRegisteredHandler.cs:46-114) null-checks the event, opens the scope, and resolves aSpeakerrepository plus theIIntegrationEventPublisher(UserRegisteredHandler.cs:52-56). It attempts an email match first, then falls back to a name match, tracking which strategy won (UserRegisteredHandler.cs:58-64). No match logs and returns (UserRegisteredHandler.cs:66-70). A speaker already linked to a different user is left alone (UserRegisteredHandler.cs:73-77); a speaker already linked to this user re-publishesSpeakerLinkedToUserfor Identity to re-sync and returns without re-linking (UserRegisteredHandler.cs:81-87). Otherwise it callsspeaker.LinkUser(...)(a domain method returningResult,UserRegisteredHandler.cs:89-94), saves, publishes the back-link event so Identity can setUser.LinkedSpeakerId(UserRegisteredHandler.cs:99-104), and logs the auto-link.TryMatchByEmailAsync(UserRegisteredHandler.cs:116-138) is the original BR-207 path: it builds anEmailvalue object (which normalizes to lowercase, so theHasConversion-stored value compares case-insensitively) and returns the first speaker whose non-null email matches (UserRegisteredHandler.cs:131-137).TryMatchByNameAsync(UserRegisteredHandler.cs:140-176) is the Sessionize fallback: Sessionize-imported speakers have a nullEmailbecause the publicview/Allendpoint omits PII, so the handler matches on exactFirstName + LastNameamong unlinked candidates (UserRegisteredHandler.cs:156-162). It links only when exactly one candidate matches, logging and skipping an ambiguous name collision of two or more (UserRegisteredHandler.cs:164-175), so it never guesses.- The six
[LoggerMessage]source-generated log methods (UserRegisteredHandler.cs:178-197) are why the class ispartial.
- Why it's built this way : ADR-003 (outbox + at-least-once) requires idempotent, crash-tolerant consumers; the scope-per-event pattern is the standard way a singleton event handler borrows scoped EF state. The name fallback exists specifically because the Sessionize public feed omits PII, so there is no email to match on. This is a deliberate divergence from Store, which handles the same "link on registration" concept with an in-process domain event: do not "align" them.
- Where it's used : registered as a broker consumer in the Conference service host; the producer is Identity's registration path (its
UserRegisteredoutbox message over the MassTransit broker). The link is eventually consistent: the new user's first token does not yet carry thespeaker_idclaim, it appears on the next token refresh after this handler completes.
SpeakerEntityQueryService
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Speakers·MMCA.ADC.Conference.Application/Speakers/SpeakerEntityQueryService.cs:15· Level 9 · class (sealed)
- What it is : a thin concrete subclass of
EntityQueryService<TEntity, TEntityDTO, TIdentifierType>closed overSpeaker/SpeakerDTO/SpeakerIdentifierType. It adds exactly one thing: a property map that lets API consumers sort and filter on the computedFullNamefield even though no such column exists on the entity. - Depends on : the base
EntityQueryService<TEntity, TEntityDTO, TIdentifierType>and the five services it forwards to the base constructor:IUnitOfWork,INavigationMetadataProvider,IEntityQueryPipeline,SpeakerDTOMapper, andINavigationPopulator<in TEntity>overSpeaker(its concrete populator isSpeakerNavigationPopulator). - Concept introduced : the
DTOToEntityPropertyMapoverride for a computed sort/filter key.[Rubric §12 : Performance & Scalability](assesses server-side sort/filter that translates to SQL rather than materializing and sorting in memory): the base exposes aprotected virtual IReadOnlyDictionary<string, string> DTOToEntityPropertyMap, and this subclass overrides it (SpeakerEntityQueryService.cs:34) to mapFullNameto the EF-translatable expression(FirstName + " " + LastName)(SpeakerEntityQueryService.cs:28-31). The query pipeline renders that into the correctORDER BY/WHEREfragment; without the map, sorting byFullNamewould fail because the entity has noFullNameproperty.[Rubric §9 : API & Contract Design]: the DTO field name is the stable public sort key, decoupled from the internal entity shape. - Walkthrough : the primary constructor takes all five services and forwards them to the base (
SpeakerEntityQueryService.cs:15-22).PropertyMapis astatic readonlydictionary evaluated once at class load, holding the singleFullName-> compound-expression entry (SpeakerEntityQueryService.cs:28-31). The overrideDTOToEntityPropertyMap => PropertyMap(SpeakerEntityQueryService.cs:34) hands it to the base. There are no other members: all query, page, and project logic lives inEntityQueryService. - Why it's built this way : a
static readonlymap is computed once per appdomain, not per request. Making this a thin concrete subclass rather than registering the base directly is the DI hook: the container bindsIEntityQueryService<TEntity, TEntityDTO, TIdentifierType>for speakers to this type, so the non-default map is picked up automatically. - Where it's used : registered in the Conference Application DI as the speaker query service and injected into the
SpeakersController, which serves the read endpoints behind theSpeakersCacheoutput-cache policy.
ConferenceCategoryNavigationPopulator
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Categories·MMCA.ADC.Conference.Application/Categories/ConferenceCategoryNavigationPopulator.cs:11· Level 10 · class (sealed)
- What it is : the entity-specific navigation populator for the
Categoryaggregate. It declares which child collections EF Core cannot eager-load via.Include()and how to fetch and assign them, then lets the base class do the work. - Depends on :
IUnitOfWork(repository access), the baseDeclarativeNavigationPopulator<TEntity>overCategory, and oneChildNavigationDescriptor<TEntity, TParentId, TChild, TChildId>for theCategoryItemscollection. - Concept reinforced : declarative child-navigation loading (ADR-002). The mechanism itself (bulk-fetch children with an
IUnitOfWorkrepository, group them by parent key, assign through a domain setter) is taught onDeclarativeNavigationPopulator<TEntity>.[Rubric §2 : Design Patterns]: this is the Template Method pattern with the twist that the "subclass" only supplies data through a descriptor array, it overrides no methods.[Rubric §3 : Clean Architecture]: the application layer stays free of any EFInclude, all loading goes through repository queries. - Walkthrough : the primary constructor passes
unitOfWorkand a single-element descriptor array to the base (ConferenceCategoryNavigationPopulator.cs:11-22). The loneChildNavigationDescriptorsupplies four members (ConferenceCategoryNavigationPopulator.cs:15-20):PropertyName = nameof(Category.CategoryItems)(which property the base marks populated),ParentKeySelector = e => e.Id(group children by parent),ChildForeignKeySelector = child => child.CategoryId(match child rows to parent), andAssignAction = (e, categoryItems) => e.SetCategoryItems(categoryItems)(hand the loaded children back through the aggregate's own setter, so the domain, not infrastructure, owns the assignment). The class body is empty. - Why it's built this way : ADR-002 requires the application layer to control eager loading without an EF dependency; the generic machinery lives in the base and each aggregate gets a small, locatable configuration file in its own feature folder.
- Where it's used : registered by convention via
INavigationPopulator<in TEntity>scanning in the Conference module DI, and resolved by the category query service when loading categories that need their items. - Caveats / not-in-source :
SetCategoryItemsis the aggregate's own child-assignment method; if the child collection is renamed or added to onCategory, this descriptor must be updated to match.
SessionNavigationPopulator
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Sessions·MMCA.ADC.Conference.Application/Sessions/SessionNavigationPopulator.cs:12· Level 10 · class (sealed)
- What it is : the same declarative navigation populator pattern as
ConferenceCategoryNavigationPopulator, but for theSessionaggregate, which has three child collections rather than one. - Depends on :
IUnitOfWork, the baseDeclarativeNavigationPopulator<TEntity>overSession, and threeChildNavigationDescriptorentries. - Concept reinforced : declarative child loading (ADR-002), taught on
DeclarativeNavigationPopulator<TEntity>. SeeConferenceCategoryNavigationPopulatorfor the per-member breakdown; this class only differs in the number and identity of its children. - Walkthrough : the constructor passes
unitOfWorkand a three-element descriptor array to the base (SessionNavigationPopulator.cs:12-37), one descriptor each forSessionSpeakers(child FKSessionId, setterSetSessionSpeakers,SessionNavigationPopulator.cs:16-22),SessionQuestionAnswers(SetSessionQuestionAnswers,SessionNavigationPopulator.cs:23-29), andSessionCategoryItems(SetSessionCategoryItems,SessionNavigationPopulator.cs:30-36). Each descriptor shares theParentKeySelector = e => e.Idshape; only the child type, foreign-key selector, and assign action change. - Why it's built this way : one populator file per aggregate keeps the configuration small and co-located with the session use cases (ADR-002).
- Where it's used : registered via
INavigationPopulator<in TEntity>scanning and resolved by the session query path when loading sessions with their speakers, Q&A, and category items.
SpeakerNavigationPopulator
MMCA.ADC.Conference.Application ·
MMCA.ADC.Conference.Application.Speakers·MMCA.ADC.Conference.Application/Speakers/SpeakerNavigationPopulator.cs:11· Level 10 · class (sealed)
- What it is : the declarative navigation populator for the
Speakeraggregate, loading its two child collections. Same shape asConferenceCategoryNavigationPopulator. - Depends on :
IUnitOfWork, the baseDeclarativeNavigationPopulator<TEntity>overSpeaker, and twoChildNavigationDescriptorentries. - Concept reinforced : declarative child loading (ADR-002); see
ConferenceCategoryNavigationPopulatorfor the mechanism. - Walkthrough : the constructor passes
unitOfWorkand a two-element descriptor array to the base (SpeakerNavigationPopulator.cs:11-30):SpeakerCategoryItems(child FKSpeakerId, setterSetSpeakerCategoryItems,SpeakerNavigationPopulator.cs:15-21) andSpeakerQuestionAnswers(SetSpeakerQuestionAnswers,SpeakerNavigationPopulator.cs:22-28). This is the populatorSpeakerEntityQueryServicereceives as itsINavigationPopulator<in TEntity>dependency. - Why it's built this way : one small populator per aggregate, co-located with the speaker feature (ADR-002).
- Where it's used : registered via
INavigationPopulator<in TEntity>scanning and resolved bySpeakerEntityQueryServicewhen a speaker read needs its category items and Q&A.
⬅ ADC Conference - Domain Model & Module Contracts • Index • ADC Conference - Infrastructure & Persistence ➡