Skip to content

FoundatioFx/Foundatio.Sample

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

26 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Foundatio Sample

Build status Discord

A real-world sample showing how to build, run, and scale distributed .NET applications with Foundatio, Foundatio.Mediator, .NET 10, Aspire, and OpenTelemetry.

Start local with zero infrastructure, then scale to production by swapping a connection string.

Every Foundatio service (cache, queues, messaging, locking, storage) starts with an in-memory implementation. The Insulation layer replaces them with Redis-backed versions automatically when a connection string is present — no code changes, no feature flags.

What This Sample Demonstrates

Foundatio Feature What It Does Here
Foundatio.Mediator Convention-based handlers → auto-generated minimal API endpoints, SSE streaming, cascading events, middleware pipeline — zero boilerplate
Queues IQueue<T> for reliable async work dispatch — in-memory locally, Redis in production
Jobs QueueJobBase<T> for dedicated queue processors, WorkItemJob for shared work-item pools
Caching ICacheClient for read-through caching of processed values
File Storage IFileStorage for document persistence (in-memory or swap to S3/Azure/disk)
Messaging IMessageBus for pub/sub events across processes — powers SSE streaming and job coordination
Locking ILockProvider for distributed lock coordination backed by cache + message bus
Aspire Orchestrates Redis, the API, and a scaled-out job worker — dashboard shows traces, logs, metrics
OpenTelemetry ASP.NET Core, HTTP client, runtime metrics, and Foundatio activity sources export to OTLP
Health Checks /health for liveness, /ready for readiness with queue depth monitoring

Project Structure

src/
├── Samples.Core/           # Domain logic — in-memory Foundatio defaults
│   ├── Bootstrapper.cs     # Registers all services; runJobsInProcess flag
│   ├── Handlers/            # Mediator handlers (auto-generate API routes)
│   ├── Jobs/                # Background queue processor + work item handler
│   ├── Messages/            # Command/query/stream message records
│   ├── Models/              # Domain types and events
│   └── Middleware/           # Mediator pipeline middleware
├── Samples.Insulation/     # Production swaps — Redis for all the things
│   └── Bootstrapper.cs     # Replace in-memory → Redis via DI Replace()
├── Samples.Web/            # ASP.NET Core composition root
│   ├── Program.cs          # Wires Core + Insulation + OTel + Mediator
│   └── QueueHealthCheck.cs # Queue depth health check
├── Samples.Jobs/           # Worker process — just background jobs
│   └── Program.cs          # Same Core + Insulation, no HTTP
└── Samples.AppHost/        # Aspire orchestration
    └── Program.cs          # Redis + Web + Jobs(×3 replicas)

Quick Start

Run standalone (zero dependencies)

dotnet run --project src/Samples.Web

Everything runs in-process with in-memory services. Open http://localhost:5210/scalar for interactive API docs.

Run with Aspire (recommended)

aspire run

Aspire provisions Redis, starts the API (jobs off-loaded), and spins up 3 job worker replicas competing for queue work. Open the Aspire dashboard URL from the console to see distributed traces, structured logs, and metrics across all replicas.

API Endpoints

Routes are auto-generated by Foundatio.Mediator from handler method conventions:

Method Route Handler Description
POST /api/values ValueHandler Store a value → file storage → enqueue for processing
GET /api/values/{id} ValueHandler Retrieve a processed value from cache
DELETE /api/values/{id} ValueHandler Enqueue a work item for background deletion
GET /api/events EventStreamHandler SSE stream of real-time EntityChanged events
GET /health built-in Liveness check
GET /ready built-in Readiness check (includes queue depth)
GET /scalar Scalar Interactive API documentation

How It Works

Create a value — queues, jobs, caching, events

POST /api/values { "value": "hello" }
  → ValueHandler saves to IFileStorage, enqueues ValuesPost (carries Id + Value)
  → Returns 201 Created + publishes EntityChanged via cascading tuple return
  → ValuesPostJob (in worker) dequeues, caches value, publishes EntityChanged
  → Cache now has the value; GET /api/values/{id} returns it
  → EntityChangedHandler logs it; EventStreamHandler pushes it to SSE clients

Delete a value — work items with progress

DELETE /api/values/{id}
  → ValueHandler enqueues DeleteValueWorkItem to IQueue<WorkItemData>
  → WorkItemJob picks it up, runs DeleteValueWorkItemHandler
  → Handler removes from cache, reports progress (0% → 50% → 100%)
  → Publishes EntityChanged → SSE stream notifies clients

Real-time events — SSE streaming

Connect to GET /api/events from any HTTP client. The EventStreamHandler calls mediator.SubscribeAsync<EntityChanged>() which bridges the Foundatio IMessageBus into an IAsyncEnumerable<T> — Server-Sent Events with zero additional dependencies. Works across processes because the message bus is backed by Redis in production.

Architecture — Core / Insulation / Web

This follows the Exceptionless pattern:

Samples.Core registers every Foundatio service with in-memory implementations. The entire app works out of the box:

Samples.Core.Bootstrapper.RegisterServices(services, runJobsInProcess: true);

Samples.Insulation conditionally replaces registrations with Redis-backed implementations when a connection string is present:

Samples.Insulation.Bootstrapper.RegisterServices(services, redisConnectionString);
// Replaces: ICacheClient, IMessageBus, IQueue<ValuesPost>,
//           IQueue<WorkItemData>, ILockProvider

Samples.Web is the HTTP composition root. Samples.Jobs is the headless worker. Both call the same two bootstrappers — the only difference is whether jobs run in-process.

Scaling Out

The AppHost demonstrates real-world scale-out:

// AppHost/Program.cs
builder.AddProject<Projects.Samples_Web>("web")
    .WithEnvironment("RunJobsInProcess", "false")   // API does NOT run jobs
    .WithHttpHealthCheck("/health");

builder.AddProject<Projects.Samples_Jobs>("jobs")
    .WithReplicas(3);                                // 3 workers competing for queue items

Because Foundatio queues guarantee at-most-once delivery, multiple workers safely compete for items without duplicate processing. Need more throughput? Change WithReplicas(3) to WithReplicas(10) — that's it.

This same pattern scales from a single process to multiple containers to Kubernetes pods.

Health Checks

Endpoint Purpose What It Checks
/health Liveness Nothing — just confirms the process responds (no I/O)
/ready Readiness Queue depth < 1000 (reports queued, working, deadletter, errors)

Liveness runs zero checks by design. A network call to Redis or any external dependency during liveness is wrong — if Redis is down the process is still alive and should not be restarted. Only readiness gates traffic.

Configuration

{
  "ConnectionStrings": {
    "Redis": ""
  },
  "RunJobsInProcess": true,
  "Sample": {
    "AppName": "Foundatio Sample"
  }
}
Key Default Description
ConnectionStrings:Redis "" (empty) When empty, all services use in-memory. Set to a Redis URL to enable distributed mode.
RunJobsInProcess true When true, background jobs run in the web process. Set to false when using separate Samples.Jobs workers.
Sample:AppName "Foundatio Sample" Service name for OpenTelemetry resource tags.

When running via Aspire, Redis is provisioned automatically, RunJobsInProcess is set to false, and connection strings are injected via service discovery.

Links

Releases

No releases published

Packages

 
 
 

Contributors

Languages