MesseApp Developer Documentation
Table of Contents
- Mail Processing
- Application Events
- [Special DTO Variants]
- [Event Import Process (ChefsInspiration Integration)]
- Testing and Database Seeding
- Technical Debt
Mail Processing
The mailing system follows a builder → enqueue → worker → delivery → callback flow:
Build
A new mail is created using theIMailBuilderobtained fromIMailService.GetNewMailBuilder().
Example:var mail = mailService.GetNewMailBuilder() .From("noreply@example.com") .To("user@example.com") .SetSubjectTemplate("SubjectTemplate") .SetBodyTemplate("BodyTemplate") .AddVariable("Key", "Value") .AddCallback(new CallbackCommand { ... }) .Build();Enqueue
The created mail is queued viamailService.EnqueueAsync(mail). This only stores the mail in the database/queue. It is not sent immediately.Worker picks up batch
A background worker (MailQueueWorker) wakes up and requests a batch of mails withGetBatchForProcessingAsync(int buffer). This ensures mails are processed outside of request scope.Delivery
For each mail, the worker:- Creates a
MimeMessagewithmailProcessorService.ProcessMail(mail). - Sends it through
SendMailAsync(IMailClientService, MimeMessage, CancellationToken). The mail’s status (DeliverState,SentAt,RetryCount, etc.) is updated in the repository.
- Creates a
Callback Dispatch
After successful delivery,DispatchCallbacksAsync(mail)is invoked to trigger any follow-up commands (e.g., setting reminders, updating related domain state).
Key principles
- Never send mails directly from request scope; always enqueue.
- Background workers are responsible for sending and updating mail states.
- Callbacks ensure post-delivery actions are handled in a consistent worker-managed environment.
Application Events
The Application Events system is an append-only event log.
It stores events in the database which can then be consumed by specialized background workers.
This mechanism is used for cross-cutting concerns such as:
- Broadcast emails
- Live notifications
- Webhook delivery
- Other asynchronous integrations
How it works
- Events are written to the
DomainEventstable via theIApplicationEventDispatcher. - Each event is immutable and never updated or deleted.
- Background workers read new events and fan them out to the appropriate channels (email, in-app, etc.).
- Events carry an
IdempotencyKeyto prevent duplicates. - Application services only publish events – they never call other services directly.
Usage
You can publish a new application event by calling the dispatcher:
public class HallController(IApplicationEventDispatcher applicationEvents)
{
public async Task<IActionResult> UploadMedia(int hallId, [FromBody] RequestMediaMetadataDto metadataDto)
{
...
await applicationEvents.PublishAsync(
new MediaChanged(nameof(Hall), hall.Id, metadata.Id), ct);
...
}
}
The event is then appended to the database and picked up asynchronously by the responsible workers.
Key principles
- Push-only: events are never modified after being written.
- Decoupled: services remain independent; only the dispatcher is called.
- Durable: the database serves as the single source of truth for events.
- Eventually consistent: workers process events asynchronously, so effects are not immediate.
Special DTO Variants: Up vs. Down
V2 endpoints no longer use graph-like DTOs.
Instead, responses are composed of use-case-specific slices or other **context-specific, flattened response models
**.
This ensures:
- explicit response contracts
- predictable payload shapes
- no implicit or accidental data exposure
AutoMapper is still used, but instead of just mapping, it is applied within a dedicated Assembler layer.
Assemblers are responsible for composing and aggregating Domain Models into the final response model.
To avoid circular references in serialization and automated mapping (e.g. with AutoMapper), some model entities are represented by two complementary DTO variants: Up and Down.
Down DTOs
- Purpose: Represent a potentially full downward object graph.
- Structure:
- Contain navigation properties that enumerate child collections (e.g.
EventDtoDown.Halls,HallDtoDown.Booths). - Do not contain any back-reference to parent entities.
- Contain navigation properties that enumerate child collections (e.g.
- Use Case:
- Suitable when the client needs to traverse an entity hierarchy downwards without risk of circular JSON serialization.
- Loading an
EventDtoDowncan recursively resolve itsHallsand theirBooths, but will not reference back to theEvent.
Up DTOs
- Purpose: Represent an entity with an upward reference to its parent.
- Structure:
- Contain only a single reference to their parent entity (e.g.
BoothDtoUp.Hall,HallDtoUp.Event). - Do not contain enumerations of children to avoid recursion downwards.
- Contain only a single reference to their parent entity (e.g.
- Use Case:
- Suitable when the client needs to understand the context or parent relationship of an entity without traversing the entire tree.
- A
HallDtoUpcontains itsEvent, but no list ofBoothsor other child entities.
Mapping & Serialization Considerations
Circular Dependency Prevention:
- Mixing
UpandDownDTOs within the same graph could create cycles. - Therefore, always use either
UporDownvariants consistently within a given API response or ensure that you have automated tests in place that verify the correctness of your chosen approach.
- Mixing
AutoMapper Configuration:
- Configure separate mapping profiles for
UpandDownvariants.
- Configure separate mapping profiles for
Serialization Impact:
- Down variants may produce large payloads when expanded, due to deep child enumerations.
- Up variants produce compact payloads, but are limited to representing upward context only.
🧩 Event Import Process (ChefsInspiration Integration)
The Event Import Process handles the synchronization of external event data from the ChefsInspiration API into the
MesseApp database.
This process is designed as a multi-layered workflow, allowing clear separation of concerns between data fetching,
transformation, and persistence.
🔁 Execution Flow
The import can be triggered either manually via an API endpoint or automatically by a scheduled job.
Trigger Endpoint:
[HttpPost("EventImportJob")]
[NeedsApiKey]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> EventImportJob()
{
await eventImportJob.ExecuteAsync();
return Ok("EventImportJob executed.");
}
When executed, the job coordinates the following steps:
🧠 Step-by-Step Logic
Data Fetching (Fetcher + RequestHelper)
TheEventImportFetcherretrieves the latest event data from the external ChefsInspiration API.
It uses theRequestHelper, which performs two HTTP requests:GET /api/auth– authenticates using the configured API key and retrieves a bearer token.GET /api/events/sync?since={timestamp}&cutoffYears={int}– fetches event updates since the last known update.
The endpoint returns anEventSyncResponseDtocontaining:existingIds→ all IDs still present in the source systemmodifiedEvents→ all new or updated eventscutoffTimestamp→ the reference time for synchronization
Data Processing (Fetcher)
Once the data is received, theFetchermaps the external DTOs into MesseApp domain models (EventandMediaMetadata)
and prepares them for persisting.
This step ensures:- Conversion of date formats to UTC (
yyyy-MM-dd'T'HH:mm:ssZ) - Linking of media assets to their corresponding event entities
- Identification of outdated or removed events based on missing IDs in
existingIds
- Conversion of date formats to UTC (
Persistence Layer (Job Execution)
The job then executes the import through theEventServiceandMediaService:EventService.UpdateModifiedEventsAsync()
Upserts all modified or newly created events into the database.MediaService.UpdateEventThumbnailsAsync()
Synchronizes related media metadata (e.g., banners, images) for all updated events.
public async Task ExecuteAsync() { var ev = await eventImportFetcher.GetEventsAsync(); IReadOnlyList<Event> updatedEventList = await eventService.UpdateModifiedEventsAsync(ev); List<MediaMetadata> updatedMetadata = ev.ToBeUpsertedEvents .Where(x => x.Metadata is not null && updatedEventList.Any(y => y.Id == x.Event.Id)) .Select(x => { x.Metadata!.EntityId = x.Event.Id; return x.Metadata; }) .ToList(); await mediaService.UpdateEventThumbnailsAsync(updatedMetadata); }
🧩 Responsibilities Overview
| Component | Responsibility |
|---|---|
| RequestHelper | Handles HTTP calls to the external API and converts responses into EventSyncResponseDto. |
| EventImportFetcher | Coordinates data fetching and preprocessing (mapping, validation, filtering). |
| EventImportJob | Main orchestration layer; triggers fetch, service updates, and persistence. |
| EventService | Upserts or updates event entities in the local database. |
| MediaService | Updates or attaches media metadata related to imported events. |
⚙️ Key Design Principles
- Separation of concerns: Networking, transformation, and persistence are cleanly separated.
- Safe failure handling: Network or parsing errors are caught, logged, and return
nullwithout crashing the job. - Idempotent execution: Running the import multiple times yields consistent results — only changed or new data is applied.
- Data integrity: Media metadata is only updated if its corresponding event record was successfully persisted.
🧾 Summary
The event import process provides a robust and maintainable integration layer between MesseApp and the external
ChefsInspiration API.
It ensures that all external event updates, additions, and deletions are accurately reflected in the local MesseApp
database through a controlled, asynchronous job execution pipeline.
Testing and Database Seeding
For integration tests, the DatabaseSeedingFactory is the current standard approach for creating test data.
The factory evolved organically over time as the test suite and domain grew. It exists to provide:
- reusable and readable test data setup
- guided, domain-aware seeding through layered scopes
- fine-grained configuration via
With*methods without inflatingAdd*APIs
Some older tests still contain legacy, ad-hoc seeding logic from earlier development phases. These tests were never migrated because they still serve their original purpose, but they should not be used as a reference when writing new tests.
When adding new integration tests or extending existing ones, always prefer the
DatabaseSeedingFactory over manual DbContext seeding.
If you encounter a missing capability while writing tests, feel free to extend the factory. It is intended to grow together with the requirements of the test suite, as long as new additions follow the existing layering and design principles.
Technical Debt
This section documents known technical debts within the MesseApp backend. These topics represent intentional trade-offs where refactoring or redesign was consciously deferred due to risk, effort, or missing immediate business value.
The goal of this section is to:
- preserve architectural context
- explain non-obvious domain decisions
- avoid repeated re-evaluation of already assessed trade-offs
- provide a clear starting point for future refactorings
Order Media Association (CI-640)
The MesseApp currently uses a historically grown and non-intuitive model for associating exhibitor media with orders.
From a domain perspective, exhibitor media would logically belong to the Order entity.
Historically, however, these media objects are attached to the Booth entity, in the same way as booth media uploaded
by the Messe team.
The differentiation between booth-related and order-related media is handled exclusively via the MediaRole.
This structure is deeply embedded in:
- domain logic
- query patterns
- test setup and fixtures
Refactoring this model would require:
- extensive domain refactoring
- database migrations
- updates to existing tests and assumptions
For this reason, the model has been intentionally preserved so far.
A refactoring would be user-transparent from a functional perspective,
but the required effort and associated risk are currently not justified.
All new features interacting with order-related media are therefore explicitly adapted to the existing historical
model,
instead of introducing parallel or partially refactored structures.
Related Jira: CI-640