MILTON API (MILTON)

Overview

MILTON is the primary ASP.NET Core Web API that acts as the entry point for the frontend (MILTON.Client). It exposes REST endpoints via FastEndpoints and hosts SignalR hubs for real-time notifications. The project is responsible for routing commands and queries down to MILTON.BackendCore or publishing Wolverine messages for worker sagas like MILTON.DocumentGenerator.

Real-time Sync (SignalR)

The MILTON application hosts the DocumentHub at /hubs/document to push live state updates directly to the connected MILTON.Client browser instances. Connections manage visibility to relevant scopes to minimize traffic and maintain privacy by joining specialized SignalR groups.

Document and Project Groups

The hub implements two primary joining patterns:

  1. Document Groups (JoinDocumentGroup):

    • Clients call JoinDocumentGroup(Guid documentId) when viewing a specific document detail page.
    • The backend pushes granular logs (e.g., “Starting LLM request”, “Formatting snippet”), partial document updates, and completion/error flags specifically to the group named doc-{documentId}.
  2. Project Groups (JoinProjectGroup):

    • Clients call JoinProjectGroup(int projectId) when viewing list pages or dashboards.
    • The backend pushes higher-level updates, such as when a document’s overall status changes or when document sections summarize, to the group named project-{projectId}.

Component Subscription & Memory Leak Prevention

Subscriptions in the client must be carefully managed. When Blazor components are opened, they should invoke StartAsync on the SignalR connection and join the necessary group via JoinDocumentAsync or JoinProjectAsync in the client’s abstractions.

To avoid memory leaks associated with abandoned callbacks, components must explicitly implement IDisposable or IAsyncDisposable. In the destruction logic, they must unsubscribe from StateService events (OnProgress, OnCompleted, etc.) and invoke the corresponding LeaveDocumentAsync or LeaveProjectAsync to halt stream forwarding from the backend DocumentHub.

Caching (HybridCache)

The API uses .NET 10 HybridCache (in-process L1 + optional Redis L2) to short-circuit its hot read paths: project config/presets (config:{tenant}:{project}), full document reads (doc:{tenant}:{documentId}), the repo file browser (repo:{tenant}:{project}:{prefix}), template blueprints (template:{tenant}:...), the project-documents summary (project-docs:{tenant}:{project}), the per-user project list (projects:{tenant}:{user}), and the raw encrypted LLM presets (presets:{tenant}:{project}). Registration is centralized in MILTON.Platform (AddMiltonHybridCache) and wired in Program.cs; Redis is provided by the Aspire cache container (ConnectionStrings__cache). Redis is optional — without a connection string the cache runs L1-only and the app still works (offline-first).

Key/Tag conventions

All keys are tenant-scoped and go through MILTON.Shared.Caching.CacheKeys so the read side and the invalidation side cannot drift:

CacheKeyInvalidation tagTTL
Project configconfig:{tenantId}:{projectId}same as key5 min
Document (sections+blocks)doc:{tenantId}:{documentId}same as key5 min
Repo browserepo:{tenantId}:{projectId}:{prefix}repo:{tenantId}:{projectId} (whole project)10 min
Template list (project)template:{tenantId}:{projectId}same as key5 min
Template list (tenant-wide)template:{tenantId}:allsame as key5 min
Single templatetemplate:{tenantId}:{templateId}same as key5 min
Project documents summaryproject-docs:{tenantId}:{projectId}same as key1 min
Per-user project listprojects:{tenantId}:{userId}key + one project-meta:{tenantId}:{projectId} per project5 min
Raw (encrypted) LLM presetspresets:{tenantId}:{projectId}config:{tenantId}:{projectId} (reused)5 min

Invalidation rules

Every write path removes tags after a successful SaveChangesAsync:

  • Config writes (UpdateProjectConfigHandler): remove config:{tenant}:{project}, repo:{tenant}:{project}, and project-meta:{tenant}:{project} (refreshes every user’s project list; the config tag also evicts the presets:{tenant}:{project} entry, which deliberately reuses it).
  • Document writes (add/update/reorder block, add/reorder section, update section source files, delete document, PDF generation, Apply*GenerationHandler, InstantiateDocumentHandler): remove doc:{tenant}:{documentId} and project-docs:{tenant}:{projectId} (resolving the owning project with a tiny indexed query when only the document/block/section id is known).
  • Template writes (CreateTemplateEndpoint, UpdateTemplateEndpoint, DeleteTemplateEndpoint): remove template:{tenant}:{projectId} (when the template is project-scoped), template:{tenant}:{templateId}, and template:{tenant}:all. PreviewTemplateEndpoint renders in-memory and is never cached.
  • Project creation (CreateProjectHandler): remove projects:{tenant}:{userId} for the creating user (a new project appears in their list).
  • Clone completion (RepositoriesClonedMessage from MILTON.GitOperations): remove repo:{tenant}:{project} so every prefix is refreshed.

The per-user access checks (GetProjectConfigHandler, ListAllProjectsHandler) are deliberately not cached (authorization freshness); only the loads themselves are cached after the filter runs, with the user id part of the project-list key so no user ever reads another user’s list. The doc/config keys cache the DTO only — no secrets are stored (presence booleans like HasApiKey/HasGitToken are safe to cache), and the presets key caches only the raw encrypted LlmPresetsJson string (decryption + Vault substitution always run after the cache read; a plaintext API key never enters the cache).

5 items under this folder.