Simple SMS mocking API + dashboard - C# .NET + Blazor. Ship as a Docker image for local/dev testing and consume via a NuGet SDK.
Status: Blazor host + SDK classlib -
src/TextBox/TextBox.csprojhosts both the SMS mock API (/api/messages) and the dashboard (/),src/TextBox.Sdk/TextBox.Sdk.csprojis the NSwag-generated NuGet client.dotnet sln migratealready run - useTextBox.slnx.
- Mock SMS API - send/list/clear messages without a real provider. LiteDB file persistence (
MessageStore:Path, defaultData/textbox.db) with an in-memory read-through cache for tests and local dev. - Dashboard - phone-style Blazor UI (Tailwind CSS + daisyUI): conversations grouped by recipient at
/, chat view with message-detail popup at/conversation/{number}. - API docs - interactive Scalar reference at
/scalar(OpenAPI at/openapi/v1.json). - Docker image - one-command
docker runfor CI or local dev. - NuGet SDK - typed client to integrate tests/apps (
TextBox.Sdk).
| Light | Dark |
|---|---|
![]() |
![]() |
- .NET SDK
10.0.302(net10.0- check.csprojTargetFrameworkafter scaffolding) - ASP.NET Core (API) + Blazor (dashboard)
- Tailwind CSS v4 + daisyUI 5 (compiled from
Styles/app.csson build - needs Node.js;npm run watch:cssinsrc/TextBoxfor live rebuilds) - Scalar API reference (
Scalar.AspNetCore+Microsoft.AspNetCore.OpenApi) - Rider / Visual Studio
- .NET SDK 10.0.302+
- Node.js (for the Tailwind CLI invoked by the build; one-time
npm installinsrc/TextBox) - Docker (optional, for image)
Verify: dotnet --version -> 10.0.302
Single project - build and run immediately:
# Build & run (API + dashboard on the same host)
dotnet build TextBox.slnx
dotnet run --project src/TextBox/TextBox.csprojThen open / for the phone dashboard, /scalar for the interactive API reference, and use /api/messages for the mock SMS API:
# Send
curl -X POST http://localhost:5000/api/messages `
-H "Content-Type: application/json" `
-d '{"to":"+123","body":"hello"}'
# List (optional ?to= filter)
curl http://localhost:5000/api/messages
# Clear
curl -X DELETE http://localhost:5000/api/messagesPOST /api/messages requires an API key when one is configured (reads stay
open). Set it in appsettings.json — empty means open:
"ApiKey": {
"Key": "secret-1"
}curl -X POST http://localhost:5000/api/messages `
-H "Content-Type: application/json" `
-H "Authorization: Bearer secret-1" `
-d '{"to":"+123","body":"hello"}'Without a valid key the API returns 401 {"error":"Missing or invalid API key."}
(enforced through the ASP.NET Core authentication pipeline). The /scalar
reference documents the Bearer scheme, and the dashboard shows the
configured key in the side panel, with a copy button.
The dashboard updates in realtime over SignalR (/hubs/messages):
MessageReceived after each send, MessagesCleared after each clear.
No client authentication is required on the hub (local dev mock).
xUnit + NSubstitute (unit) + bUnit (Blazor UI) in tests/TextBox.Tests:
# All tests
dotnet test TextBox.slnx
# Single test
dotnet test TextBox.slnx --filter <TestName>Prebuilt image on GHCR (published from master — package page):
docker pull ghcr.io/parsagachkar/textbox:latest
docker run --rm -p 8080:8080 ghcr.io/parsagachkar/textbox:latestOr build locally:
# Build (context is the repo root)
docker build -f src/TextBox/Dockerfile -t textbox .
# Run (dashboard at http://localhost:8080, API docs at /scalar)
docker run --rm -p 8080:8080 textboxThe image runs as non-root and listens on 8080. Persist the LiteDB
file across restarts with a volume (it lives at /app/Data by default):
docker run --rm -p 8080:8080 -v textbox-data:/app/Data textboxConfigure via environment (same keys as appsettings.json):
docker run --rm -p 8080:8080 `
-e ApiKey__Key=secret-1 `
-e MessageStore__Path=/app/Data/textbox.db `
textbox.github/workflows/ci.yml runs on pushes/PRs to master: build, tests,
SDK pack (uploaded as an artifact), then a Docker build. Pushes to master
also push the image to ghcr.io/<owner>/textbox (:latest + :sha-<commit>).
Pushing a textbox-sdk-v* tag publishes the SDK to NuGet via trusted publishing (OIDC, no stored key).
See CONTRIBUTING.md. Commit messages and PR titles must follow Conventional Commits (enforced on PRs by CI).
Typed client (TextBox.Sdk, netstandard2.0, NSwag-generated) with full setup at /sdk:
dotnet add package TextBox.SdkConsume (no DI required — the key is optional, omit it when the API is open):
var client = new TextBoxClient(new TextBoxOptions
{
BaseAddress = "http://localhost:8080",
ApiKey = "secret-1",
});
var sent = await client.SendAsync(SendSmsRequest.Create("+123", "hello"));
var inbox = await client.ListAsync();Or with Microsoft DI: services.AddTextBoxClient("http://localhost:8080", apiKey: "secret-1");
Pack / regenerate (contributors):
dotnet pack src/TextBox.Sdk/TextBox.Sdk.csproj -c Release
./scripts/Update-OpenApiSnapshot.ps1 # re-export spec (key configured, keeps Bearer scheme)
./scripts/Regen-SdkClient.ps1 # regenerate committed Generated/
./scripts/Set-SdkVersion.ps1 -Version 0.2.0TextBox.slnx # use .slnx (migrated from .sln via `dotnet sln migrate`)
├── branding-assets/ # logo source (SVG + PNG) + README screenshots
├── src/
│ ├── TextBox/ # Blazor dashboard (/) + SMS mock API (/api/messages)
│ │ ├── Components/ # Blazor UI (Pages/Home.razor = inbox, Pages/Sdk.razor = SDK docs)
│ │ ├── Dockerfile # multi-stage image (base/build/publish/final), context = repo root
│ │ ├── Endpoints/ # Minimal API (MessageEndpoints)
│ │ ├── Models/ # SmsMessage / SendSmsRequest
│ │ └── Services/ # IMessageStore (LiteDB), options, validators
│ └── TextBox.Sdk/ # NuGet client (NSwag-generated + partials, netstandard2.0)
└── tests/
└── TextBox.Tests/ # xUnit/NSubstitute unit + bUnit UI tests
| Task | Command |
|---|---|
| Verify solution | dotnet sln TextBox.slnx list |
| Build all | dotnet build TextBox.slnx |
| Run app (API + dashboard) | dotnet run --project src/TextBox/TextBox.csproj |
| Run tests | dotnet test TextBox.slnx |
| Pack SDK | dotnet pack src/TextBox.Sdk/TextBox.Sdk.csproj -c Release |
| Bump SDK version | ./scripts/Set-SdkVersion.ps1 -Version 0.2.0 |
| Refresh OpenAPI snapshot | ./scripts/Update-OpenApiSnapshot.ps1 |
| Regenerate SDK client | ./scripts/Regen-SdkClient.ps1 |
| Seed demo data | ./scripts/Seed-FakeData.ps1 -ApiKey <key> -ClearFirst |
PowerShell 5.1 on Windows - use workdir param in tooling instead of cd.
TBD


