MesseApp Developer Documentation

Table of Contents


Mail Processing

The mailing system follows a builder → enqueue → worker → delivery → callback flow:

  1. Build
    A new mail is created using the IMailBuilder obtained from IMailService.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();
    
  2. Enqueue
    The created mail is queued via mailService.EnqueueAsync(mail). This only stores the mail in the database/queue. It is not sent immediately.

  3. Worker picks up batch
    A background worker (MailQueueWorker) wakes up and requests a batch of mails with GetBatchForProcessingAsync(int buffer). This ensures mails are processed outside of request scope.

  4. Delivery
    For each mail, the worker:

    • Creates a MimeMessage with mailProcessorService.ProcessMail(mail).
    • Sends it through SendMailAsync(IMailClientService, MimeMessage, CancellationToken). The mail’s status (DeliverState, SentAt, RetryCount, etc.) is updated in the repository.
  5. 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 DomainEvents table via the IApplicationEventDispatcher.
  • 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 IdempotencyKey to 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.
  • Use Case:
    • Suitable when the client needs to traverse an entity hierarchy downwards without risk of circular JSON serialization.
    • Loading an EventDtoDown can recursively resolve its Halls and their Booths, but will not reference back to the Event.

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.
  • Use Case:
    • Suitable when the client needs to understand the context or parent relationship of an entity without traversing the entire tree.
    • A HallDtoUp contains its Event, but no list of Booths or other child entities.

Mapping & Serialization Considerations

  • Circular Dependency Prevention:

    • Mixing Up and Down DTOs within the same graph could create cycles.
    • Therefore, always use either Up or Down variants consistently within a given API response or ensure that you have automated tests in place that verify the correctness of your chosen approach.
  • AutoMapper Configuration:

    • Configure separate mapping profiles for Up and Down variants.
  • 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

  1. Data Fetching (Fetcher + RequestHelper)
    The EventImportFetcher retrieves the latest event data from the external ChefsInspiration API.
    It uses the RequestHelper, 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 an EventSyncResponseDto containing:
      • existingIds → all IDs still present in the source system
      • modifiedEvents → all new or updated events
      • cutoffTimestamp → the reference time for synchronization
  2. Data Processing (Fetcher)
    Once the data is received, the Fetcher maps the external DTOs into MesseApp domain models (Event and MediaMetadata)
    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
  3. Persistence Layer (Job Execution)
    The job then executes the import through the EventService and MediaService:

    • 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 null without 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 inflating Add* 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