+## Contents
+
+- [Overview](#overview)
+- [Requirements](#requirements)
+- [Quick Start](#quick-start)
+- [Project templates](#project-templates)
+- [Packages](#packages)
+- [Related projects](#related-projects)
+- [Architecture](#architecture)
+- [Documentation](#documentation)
+- [Build](#build)
+- [Test](#test)
+- [Contributing](#contributing)
+- [Versioning & Releases](#versioning--releases)
+- [Built on](#built-on)
+- [License](#license)
+
## Overview
**squid-std** is a batteries-included standard library for .NET, distilled from years and years
of building real-world server software. Instead of re-solving the same problems on every project,
it bundles the foundations you reach for again and again behind small, well-defined contracts:
-- **Security & hashing** — password hashing and verification (`HashUtils`), AES-GCM secret
- protection and a pluggable secret store (`ISecretProtector` / `ISecretStore`).
-- **Configuration** — a YAML-backed config manager with section registration and environment-variable expansion.
-- **Serialization** — unified JSON/YAML utilities (`JsonUtils`, `YamlUtils`) and a shared `IDataSerializer` / `IDataDeserializer`.
-- **String & platform helpers** — case converters (camel/kebab/snake/pascal/…), network, version, platform and resource utilities.
-- **Runtime services** — DI bootstrap, event bus, job system, timer/cron scheduler, metrics and storage.
-- **Infrastructure modules** — messaging (in-memory + RabbitMQ), caching (in-memory + Redis),
- database access, networking (TCP/UDP) and Lua scripting.
+- **Security & crypto** — password hashing (`HashUtils`), AES-GCM secret protection and a pluggable
+ secret store (`ISecretProtector` / `ISecretStore`), an OpenPGP keyring (`SquidStd.Crypto`), and
+ AWS KMS / Secrets Manager adapters.
+- **Configuration & serialization** — a YAML-backed config manager with section registration and
+ environment-variable expansion; unified JSON/YAML utilities and a shared `IDataSerializer`.
+- **Runtime services** — DI bootstrap, event bus, command dispatcher, actors (ordered per-entity
+ mailboxes), job system, timer/cron scheduler, metrics and health checks.
+- **Messaging & caching** — in-memory, RabbitMQ and AWS SQS/SNS transports; in-memory and Redis caches.
+- **Data & storage** — FreeSql data access, binary persistence (snapshot + WAL), local / S3 / MinIO
+ object storage, and a virtual filesystem (physical / zip / in-memory + an encrypted vault).
+- **Search, mail & workers** — Elasticsearch indexing with a constrained LINQ provider, IMAP/POP3
+ mail polling and an outbound mail queue, and a worker / manager runtime.
+- **Networking, scripting & observability** — TCP/UDP servers with a framing/middleware pipeline,
+ Lua scripting and Scriban templating, and OpenTelemetry tracing + metrics export.
Everything is modular: take only the packages you need, each behind a clean abstraction with an
in-memory implementation for tests and an external backend for production.
@@ -33,7 +56,7 @@ in-memory implementation for tests and an external backend for production.
## Requirements
- [.NET 10 SDK](https://dotnet.microsoft.com/download)
-- [Docker](https://www.docker.com/) — only for running the integration tests (Testcontainers spin up RabbitMQ and Redis).
+- [Docker](https://www.docker.com/) — only for the integration tests (Testcontainers spin up RabbitMQ, Redis, Elasticsearch, MinIO and LocalStack on demand).
## Quick Start
@@ -64,8 +87,63 @@ await bootstrap.StopAsync();
`StartAsync` / `StopAsync` lifecycle of every registered `ISquidStdService`. Use `RunAsync` to
block until cancellation for long-running hosts.
+### A fuller example: event bus + a provider
+
+```csharp
+using SquidStd.Core.Interfaces.Events;
+using SquidStd.Caching.Abstractions.Interfaces;
+using SquidStd.Caching.Extensions;
+using SquidStd.Services.Core.Services.Bootstrap;
+
+public sealed record OrderPlaced(string OrderId) : IEvent;
+
+var bootstrap = SquidStdBootstrap.Create()
+ .ConfigureServices(container => container.AddInMemoryCache());
+
+await bootstrap.StartAsync();
+
+var eventBus = bootstrap.Resolve(); // core service, always available
+var cache = bootstrap.Resolve();
+
+// React to domain events…
+using var subscription = eventBus.Subscribe(async (e, ct) =>
+ await cache.SetAsync($"order:{e.OrderId}", DateTimeOffset.UtcNow, TimeSpan.FromHours(1)));
+
+// …and publish them.
+await eventBus.PublishAsync(new OrderPlaced("A-1001"));
+
+await bootstrap.StopAsync();
+```
+
+The event bus, config manager, job system, scheduler and metrics are wired by the bootstrapper out
+of the box; modules such as caching are opted in through `ConfigureServices`.
+
+## Project templates
+
+Scaffold a ready-to-run project with the `dotnet new` template pack:
+
+```bash
+dotnet new install SquidStd.Templates
+```
+
+| Template | Short name | What you get |
+|---------------------|-----------------------|-----------------------------------------------------------------|
+| Console host | `squidstd-host` | A `SquidStdBootstrap` console host. |
+| ASP.NET minimal API | `squidstd-aspnetcore` | `UseSquidStd` + health checks + a sample endpoint + Dockerfile. |
+| Worker microservice | `squidstd-worker` | `AddWorkers` + a sample `IJobHandler` + Dockerfile. |
+| Worker manager | `squidstd-manager` | `AddWorkerManager` + `MapWorkerManagerEndpoints` + Dockerfile. |
+
+```bash
+dotnet new squidstd-worker -n Acme.Resizer
+dotnet new squidstd-manager -n Acme.Manager --messaging inmemory
+```
+
+`--messaging` (`rabbitmq` default | `inmemory`) is available on the worker and manager templates.
+
## Packages
+### Core & hosting
+
| Package | Description | Links |
|---------|-------------|-------|
| `SquidStd.Core` | Foundational contracts & utilities (config, event bus, jobs, metrics, serialization, YAML/JSON, Serilog sink). | [](src/SquidStd.Core/README.md) · [](https://www.nuget.org/packages/SquidStd.Core/) |
@@ -73,38 +151,103 @@ block until cancellation for long-running hosts.
| `SquidStd.Generators` | Roslyn source generators for event listener, service, config, worker, and Lua registration helpers. | [](src/SquidStd.Generators/README.md) · [](https://www.nuget.org/packages/SquidStd.Generators/) |
| `SquidStd.Services.Core` | Concrete services: config, event bus, jobs, timer/cron scheduler, dispatcher, metrics, health checks, secrets. | [](src/SquidStd.Services.Core/README.md) · [](https://www.nuget.org/packages/SquidStd.Services.Core/) |
| `SquidStd.AspNetCore` | ASP.NET Core host integration for the SquidStd service stack. | [](src/SquidStd.AspNetCore/README.md) · [](https://www.nuget.org/packages/SquidStd.AspNetCore/) |
+| `SquidStd.Plugin.Abstractions` | Plugin contracts (`ISquidStdPlugin`, metadata, context). | [](src/SquidStd.Plugin.Abstractions/README.md) · [](https://www.nuget.org/packages/SquidStd.Plugin.Abstractions/) |
+
+### Networking
+
+| Package | Description | Links |
+|---------|-------------|-------|
| `SquidStd.Network` | TCP/UDP servers & clients, sessions, framing/middleware pipeline, span readers/writers. | [](src/SquidStd.Network/README.md) · [](https://www.nuget.org/packages/SquidStd.Network/) |
+
+### Messaging & actors
+
+| Package | Description | Links |
+|---------|-------------|-------|
+| `SquidStd.Messaging.Abstractions` | Messaging contracts (`IMessageQueue`, `IQueueProvider`, serializer/metrics, listeners). | [](src/SquidStd.Messaging.Abstractions/README.md) · [](https://www.nuget.org/packages/SquidStd.Messaging.Abstractions/) |
+| `SquidStd.Messaging` | In-memory messaging transport (`AddInMemoryMessaging`). | [](src/SquidStd.Messaging/README.md) · [](https://www.nuget.org/packages/SquidStd.Messaging/) |
+| `SquidStd.Messaging.RabbitMq` | RabbitMQ messaging transport (`AddRabbitMqMessaging`). | [](src/SquidStd.Messaging.RabbitMq/README.md) · [](https://www.nuget.org/packages/SquidStd.Messaging.RabbitMq/) |
+| `SquidStd.Messaging.Sqs` | AWS SQS/SNS transport: `IQueueProvider` over SQS (redrive→DLQ) and `ITopicProvider` via SNS+SQS fan-out (`AddSqsMessaging`). | [](src/SquidStd.Messaging.Sqs/README.md) · [](https://www.nuget.org/packages/SquidStd.Messaging.Sqs/) |
+| `SquidStd.Actors` | Ordered, single-threaded, lock-free per-entity message processing on TPL Dataflow (`Actor`, `TellAsync`/`AskAsync`). | [](src/SquidStd.Actors/README.md) · [](https://www.nuget.org/packages/SquidStd.Actors/) |
+
+### Persistence & database
+
+| Package | Description | Links |
+|---------|-------------|-------|
| `SquidStd.Persistence.Abstractions` | Binary-persistence contracts (`IEntityStore`, `IPersistenceService`, journal/snapshot/registry, `PersistenceConfig`). | [](src/SquidStd.Persistence.Abstractions/README.md) · [](https://www.nuget.org/packages/SquidStd.Persistence.Abstractions/) |
| `SquidStd.Persistence` | In-memory entity store with durable binary snapshot + journal (WAL); serializer-agnostic engine. | [](src/SquidStd.Persistence/README.md) · [](https://www.nuget.org/packages/SquidStd.Persistence/) |
| `SquidStd.Persistence.MessagePack` | MessagePack-backed binary `IDataSerializer` for SquidStd.Persistence (recommended default). | [](src/SquidStd.Persistence.MessagePack/README.md) · [](https://www.nuget.org/packages/SquidStd.Persistence.MessagePack/) |
-| `SquidStd.Plugin.Abstractions` | Plugin contracts (`ISquidStdPlugin`, metadata, context). | [](src/SquidStd.Plugin.Abstractions/README.md) · [](https://www.nuget.org/packages/SquidStd.Plugin.Abstractions/) |
| `SquidStd.Database.Abstractions` | Provider-agnostic data-access contracts (`IDataAccess`, `BaseEntity`, `PagedResultData`). | [](src/SquidStd.Database.Abstractions/README.md) · [](https://www.nuget.org/packages/SquidStd.Database.Abstractions/) |
| `SquidStd.Database` | FreeSql-backed data access (CRUD/bulk/paging, URI connection strings, ZLinq helpers). | [](src/SquidStd.Database/README.md) · [](https://www.nuget.org/packages/SquidStd.Database/) |
-| `SquidStd.Messaging.Abstractions` | Messaging contracts (`IMessageQueue`, `IQueueProvider`, serializer/metrics, listeners). | [](src/SquidStd.Messaging.Abstractions/README.md) · [](https://www.nuget.org/packages/SquidStd.Messaging.Abstractions/) |
-| `SquidStd.Messaging` | In-memory messaging transport (`AddInMemoryMessaging`). | [](src/SquidStd.Messaging/README.md) · [](https://www.nuget.org/packages/SquidStd.Messaging/) |
-| `SquidStd.Messaging.RabbitMq` | RabbitMQ messaging transport (`AddRabbitMqMessaging`). | [](src/SquidStd.Messaging.RabbitMq/README.md) · [](https://www.nuget.org/packages/SquidStd.Messaging.RabbitMq/) |
-| `SquidStd.Messaging.Sqs` | AWS SQS/SNS transport: `IQueueProvider` over SQS (redrive→DLQ) and `ITopicProvider` via SNS+SQS fan-out (`AddSqsMessaging`). | [](src/SquidStd.Messaging.Sqs/README.md) · [](https://www.nuget.org/packages/SquidStd.Messaging.Sqs/) |
-| `SquidStd.Aws.Abstractions` | Shared AWS connection config (`AwsConfigEntry`: region, credentials, endpoint override) for AWS-SDK providers. | [](src/SquidStd.Aws.Abstractions/README.md) · [](https://www.nuget.org/packages/SquidStd.Aws.Abstractions/) |
-| `SquidStd.Telemetry.Abstractions` | Shared telemetry config (`TelemetryOptions`, OTLP protocol, `SquidStd.*` ActivitySource convention). | [](src/SquidStd.Telemetry.Abstractions/README.md) · [](https://www.nuget.org/packages/SquidStd.Telemetry.Abstractions/) |
-| `SquidStd.Telemetry.OpenTelemetry` | OpenTelemetry tracing + metrics export (OTLP/console), standard instrumentation, metrics-snapshot bridge (`AddSquidStdTelemetry`). | [](src/SquidStd.Telemetry.OpenTelemetry/README.md) · [](https://www.nuget.org/packages/SquidStd.Telemetry.OpenTelemetry/) |
+
+### Caching
+
+| Package | Description | Links |
+|---------|-------------|-------|
| `SquidStd.Caching.Abstractions` | Caching contracts (`ICacheService`, `ICacheProvider`, `CacheService` facade, metrics, connection string). | [](src/SquidStd.Caching.Abstractions/README.md) · [](https://www.nuget.org/packages/SquidStd.Caching.Abstractions/) |
| `SquidStd.Caching` | In-memory cache backend (`AddInMemoryCache`). | [](src/SquidStd.Caching/README.md) · [](https://www.nuget.org/packages/SquidStd.Caching/) |
| `SquidStd.Caching.Redis` | Redis cache backend (`AddRedisCache`). | [](src/SquidStd.Caching.Redis/README.md) · [](https://www.nuget.org/packages/SquidStd.Caching.Redis/) |
+
+### Storage & virtual filesystem
+
+| Package | Description | Links |
+|---------|-------------|-------|
| `SquidStd.Storage.Abstractions` | Storage contracts (`IStorageService`, `IObjectStorageService`, `StorageConfig`, `ListKeysAsync`). | [](src/SquidStd.Storage.Abstractions/README.md) · [](https://www.nuget.org/packages/SquidStd.Storage.Abstractions/) |
| `SquidStd.Storage` | Local file storage backend (`AddFileStorage`). | [](src/SquidStd.Storage/README.md) · [](https://www.nuget.org/packages/SquidStd.Storage/) |
| `SquidStd.Storage.S3` | S3/MinIO storage backend (`AddS3Storage`). | [](src/SquidStd.Storage.S3/README.md) · [](https://www.nuget.org/packages/SquidStd.Storage.S3/) |
-| `SquidStd.Scripting.Lua` | Lua scripting engine with attribute-based modules and event bridging. | [](src/SquidStd.Scripting.Lua/README.md) · [](https://www.nuget.org/packages/SquidStd.Scripting.Lua/) |
-| `SquidStd.Templating` | Scriban templating with a named-template registry and `templates/*.tmpl` auto-load (`AddTemplating`). | [](src/SquidStd.Templating/README.md) · [](https://www.nuget.org/packages/SquidStd.Templating/) |
-| `SquidStd.Workers.Abstractions` | Worker/manager shared contracts (`JobRequest`, `WorkerHeartbeat`, `WorkerInfo`, `WorkerChannels`). | [](src/SquidStd.Workers.Abstractions/README.md) · [](https://www.nuget.org/packages/SquidStd.Workers.Abstractions/) |
-| `SquidStd.Workers` | Worker runtime: consume jobs, dispatch to `IJobHandler`s, publish heartbeats (`AddWorkers`). | [](src/SquidStd.Workers/README.md) · [](https://www.nuget.org/packages/SquidStd.Workers/) |
-| `SquidStd.Workers.Manager` | Job enqueue, heartbeat registry, offline sweep, opt-in ASP.NET endpoints (`AddWorkerManager`). | [](src/SquidStd.Workers.Manager/README.md) · [](https://www.nuget.org/packages/SquidStd.Workers.Manager/) |
-| `SquidStd.Templates` | `dotnet new` templates for scaffolding SquidStd projects (host, ASP.NET, worker, manager). | [](src/SquidStd.Templates/README.md) · [](https://www.nuget.org/packages/SquidStd.Templates/) |
+| `SquidStd.Vfs.Abstractions` | Virtual filesystem contracts (`IVirtualFileSystem`, `ILockableFileSystem`, `VfsPath`). | [](src/SquidStd.Vfs.Abstractions/README.md) · [](https://www.nuget.org/packages/SquidStd.Vfs.Abstractions/) |
+| `SquidStd.Vfs` | Virtual filesystem providers — physical, in-memory, and zip — plus `VfsDirectories`, a VFS-backed `DirectoriesConfig`. | [](src/SquidStd.Vfs/README.md) · [](https://www.nuget.org/packages/SquidStd.Vfs/) |
+
+### Security — crypto & secrets
+
+| Package | Description | Links |
+|---------|-------------|-------|
+| `SquidStd.Crypto` | OpenPGP key management/operations over an indexed keyring (`SquidStd.Crypto.Pgp`, `RegisterPgp`) plus the encrypted VFS vault decorator (Argon2id + per-entry AES-GCM). | [](src/SquidStd.Crypto/README.md) · [](https://www.nuget.org/packages/SquidStd.Crypto/) |
+| `SquidStd.Secrets.Aws` | AWS adapters for the secret seams — KMS envelope `ISecretProtector` and Secrets Manager `ISecretStore`. | [](src/SquidStd.Secrets.Aws/README.md) · [](https://www.nuget.org/packages/SquidStd.Secrets.Aws/) |
+
+### Search
+
+| Package | Description | Links |
+|---------|-------------|-------|
| `SquidStd.Search.Abstractions` | Search/indexing contracts (`IIndexableEntity`, `[SearchIndex]`, `ISearchService`). | [](src/SquidStd.Search.Abstractions/README.md) · [](https://www.nuget.org/packages/SquidStd.Search.Abstractions/) |
| `SquidStd.Search.Elasticsearch` | Elasticsearch indexing + constrained LINQ query provider (`AddElasticsearch`). | [](src/SquidStd.Search.Elasticsearch/README.md) · [](https://www.nuget.org/packages/SquidStd.Search.Elasticsearch/) |
+
+### Mail
+
+| Package | Description | Links |
+|---------|-------------|-------|
| `SquidStd.Mail.Abstractions` | Mail contracts (`MailMessage`, `MailReceivedEvent`, `IMailReader`, `MailOptions`). | [](src/SquidStd.Mail.Abstractions/README.md) · [](https://www.nuget.org/packages/SquidStd.Mail.Abstractions/) |
| `SquidStd.Mail.MailKit` | IMAP/POP3 mail poller that publishes `MailReceivedEvent` (`AddMail`). | [](src/SquidStd.Mail.MailKit/README.md) · [](https://www.nuget.org/packages/SquidStd.Mail.MailKit/) |
| `SquidStd.Mail.Queue` | Outbound mail send queue over the messaging queue (`AddMailQueue`, `IMailQueue`). | [](src/SquidStd.Mail.Queue/README.md) · [](https://www.nuget.org/packages/SquidStd.Mail.Queue/) |
+### Workers
+
+| Package | Description | Links |
+|---------|-------------|-------|
+| `SquidStd.Workers.Abstractions` | Worker/manager shared contracts (`JobRequest`, `WorkerHeartbeat`, `WorkerInfo`, `WorkerChannels`). | [](src/SquidStd.Workers.Abstractions/README.md) · [](https://www.nuget.org/packages/SquidStd.Workers.Abstractions/) |
+| `SquidStd.Workers` | Worker runtime: consume jobs, dispatch to `IJobHandler`s, publish heartbeats (`AddWorkers`). | [](src/SquidStd.Workers/README.md) · [](https://www.nuget.org/packages/SquidStd.Workers/) |
+| `SquidStd.Workers.Manager` | Job enqueue, heartbeat registry, offline sweep, opt-in ASP.NET endpoints (`AddWorkerManager`). | [](src/SquidStd.Workers.Manager/README.md) · [](https://www.nuget.org/packages/SquidStd.Workers.Manager/) |
+
+### Scripting & templating
+
+| Package | Description | Links |
+|---------|-------------|-------|
+| `SquidStd.Scripting.Lua` | Lua scripting engine with attribute-based modules and event bridging. | [](src/SquidStd.Scripting.Lua/README.md) · [](https://www.nuget.org/packages/SquidStd.Scripting.Lua/) |
+| `SquidStd.Templating` | Scriban templating with a named-template registry and `templates/*.tmpl` auto-load (`AddTemplating`). | [](src/SquidStd.Templating/README.md) · [](https://www.nuget.org/packages/SquidStd.Templating/) |
+
+### Telemetry
+
+| Package | Description | Links |
+|---------|-------------|-------|
+| `SquidStd.Telemetry.Abstractions` | Shared telemetry config (`TelemetryOptions`, OTLP protocol, `SquidStd.*` ActivitySource convention). | [](src/SquidStd.Telemetry.Abstractions/README.md) · [](https://www.nuget.org/packages/SquidStd.Telemetry.Abstractions/) |
+| `SquidStd.Telemetry.OpenTelemetry` | OpenTelemetry tracing + metrics export (OTLP/console), standard instrumentation, metrics-snapshot bridge (`AddSquidStdTelemetry`). | [](src/SquidStd.Telemetry.OpenTelemetry/README.md) · [](https://www.nuget.org/packages/SquidStd.Telemetry.OpenTelemetry/) |
+
+### Shared & tooling
+
+| Package | Description | Links |
+|---------|-------------|-------|
+| `SquidStd.Aws.Abstractions` | Shared AWS connection config (`AwsConfigEntry`: region, credentials, endpoint override) for AWS-SDK providers. | [](src/SquidStd.Aws.Abstractions/README.md) · [](https://www.nuget.org/packages/SquidStd.Aws.Abstractions/) |
+| `SquidStd.Templates` | `dotnet new` templates for scaffolding SquidStd projects (host, ASP.NET, worker, manager). | [](src/SquidStd.Templates/README.md) · [](https://www.nuget.org/packages/SquidStd.Templates/) |
+
## Related projects
- **[Felix Network](https://github.com/tgiachi/SquidStd-Felix)** — a standalone secure binary
@@ -148,8 +291,8 @@ dotnet build SquidStd.slnx
dotnet test SquidStd.slnx
```
-Integration tests (RabbitMQ, Redis) need Docker running; they use Testcontainers to start
-disposable containers automatically.
+Integration tests need Docker running; Testcontainers starts disposable RabbitMQ, Redis,
+Elasticsearch, MinIO and LocalStack containers automatically.
## Contributing
@@ -165,6 +308,19 @@ Releases are automated with [semantic-release](https://semantic-release.gitbook.
numbers and the changelog are derived from the conventional-commit history, and the NuGet packages
are published on release. Package versions follow [Semantic Versioning](https://semver.org/).
+## Built on
+
+squid-std stands on a small set of well-established libraries:
+
+- **DI & logging** — [DryIoc](https://github.com/dadhi/DryIoc), [Serilog](https://serilog.net/)
+- **Data** — [FreeSql](https://github.com/dotnetcore/FreeSql), [MessagePack](https://github.com/MessagePack-CSharp/MessagePack-CSharp), [YamlDotNet](https://github.com/aaubry/YamlDotNet)
+- **Messaging & cache** — [RabbitMQ.Client](https://github.com/rabbitmq/rabbitmq-dotnet-client), [StackExchange.Redis](https://github.com/StackExchange/StackExchange.Redis), [AWS SDK for .NET](https://github.com/aws/aws-sdk-net)
+- **Search, mail & storage** — [Elastic.Clients.Elasticsearch](https://github.com/elastic/elasticsearch-net), [MailKit](https://github.com/jstedfast/MailKit), [Minio](https://github.com/minio/minio-dotnet)
+- **Crypto** — [PgpCore](https://github.com/mattosaurus/PgpCore) over [BouncyCastle](https://www.bouncycastle.org/)
+- **Scripting & templating** — [MoonSharp](https://www.moonsharp.org/) (Lua), [Scriban](https://github.com/scriban/scriban)
+- **Observability** — [OpenTelemetry](https://opentelemetry.io/)
+- **Scheduling** — [Cronos](https://github.com/HangfireIO/Cronos)
+
## License
MIT - see [LICENSE](LICENSE).
diff --git a/SquidStd.slnx b/SquidStd.slnx
index c970f273..bbcd9f9d 100644
--- a/SquidStd.slnx
+++ b/SquidStd.slnx
@@ -37,6 +37,10 @@
+
+
+
+
@@ -55,6 +59,10 @@
+
+
+
+
diff --git a/docs/articles/actors.md b/docs/articles/actors.md
new file mode 100644
index 00000000..ee50eff9
--- /dev/null
+++ b/docs/articles/actors.md
@@ -0,0 +1 @@
+[!include[](../../src/SquidStd.Actors/README.md)]
diff --git a/docs/articles/concepts/abstractions-first.md b/docs/articles/concepts/abstractions-first.md
new file mode 100644
index 00000000..30e42aa4
--- /dev/null
+++ b/docs/articles/concepts/abstractions-first.md
@@ -0,0 +1,21 @@
+# Abstractions first
+
+SquidStd is built abstractions-first: code depends on contracts, and concrete backends are chosen at composition time. This is what makes a SquidStd application easy to test and easy to retarget.
+
+## Contract package plus provider packages
+
+Each capability is a contract package paired with one or more provider packages. The contract package — `*.Abstractions` — defines the interfaces and DTOs. Provider packages implement them. Your application references the abstraction and depends on the provider only in the host project. See the [architecture](architecture.md) overview for how this shapes the package graph.
+
+## In-memory for tests, external for prod
+
+Most capabilities ship an in-memory provider for tests and an external-backend provider for production:
+
+- **Messaging** — in-memory, or `SquidStd.Messaging.RabbitMq` / `SquidStd.Messaging.Sqs`.
+- **Caching** — in-memory, or `SquidStd.Caching.Redis`.
+- **Storage** — file-backed, or `SquidStd.Storage.S3` (S3 and MinIO).
+
+Tests run fast and deterministically against the in-memory provider; production swaps in the external backend.
+
+## Swapping providers without touching call sites
+
+Because call sites depend only on the contract, switching backends is a registration change in the host — `container.AddInMemoryMessaging()` versus `container.AddRabbitMqMessaging(...)` — with no edits to the code that publishes or consumes messages. See [dependency injection](dependency-injection.md) for the registration pattern that makes the swap a one-line change.
diff --git a/docs/articles/concepts/architecture.md b/docs/articles/concepts/architecture.md
new file mode 100644
index 00000000..ddd86bd0
--- /dev/null
+++ b/docs/articles/concepts/architecture.md
@@ -0,0 +1,34 @@
+# Architecture
+
+SquidStd is a modular .NET toolkit. Rather than a single monolithic library, it ships one package per capability, so an application depends only on the pieces it actually uses.
+
+## Modular by design
+
+Every capability is split in two: an `*.Abstractions` package that holds the contracts (interfaces and DTOs) and one or more provider packages that implement them. For example `SquidStd.Messaging.Abstractions` defines the messaging contracts, while `SquidStd.Messaging`, `SquidStd.Messaging.RabbitMq`, and `SquidStd.Messaging.Sqs` provide implementations. This keeps call sites coupled to contracts, not to a concrete backend. See [abstractions first](abstractions-first.md) for why this matters.
+
+## Layers
+
+The dependency flow runs in one direction:
+
+- **Core** (`SquidStd.Core`) — primitives, options, and the building blocks everything else sits on.
+- **Abstractions** — per-capability contract packages.
+- **Providers** — concrete implementations of those contracts.
+- **Host** — `SquidStdBootstrap` composes services and runs them.
+
+Higher layers depend on lower ones, never the reverse.
+
+## Package graph
+
+```mermaid
+graph TD
+ Core[SquidStd.Core] --> Services[SquidStd.Services.Core]
+ Abstr[*.Abstractions contracts] --> Providers[Provider packages]
+ Services --> Host[SquidStdBootstrap host]
+ Providers --> Host
+ Host --> App[Your application]
+```
+
+## Next
+
+- [Bootstrap lifecycle](bootstrap-lifecycle.md) — how the host starts and stops services.
+- [Abstractions first](abstractions-first.md) — the contract-plus-provider pattern in depth.
diff --git a/docs/articles/concepts/bootstrap-lifecycle.md b/docs/articles/concepts/bootstrap-lifecycle.md
new file mode 100644
index 00000000..247b8da7
--- /dev/null
+++ b/docs/articles/concepts/bootstrap-lifecycle.md
@@ -0,0 +1,51 @@
+# Bootstrap lifecycle
+
+`SquidStdBootstrap` is the entry point that wires up dependency injection and drives the lifecycle of every registered service. The flow is always the same: create, configure, start, stop.
+
+## Create
+
+Begin by creating the bootstrap from `SquidStdOptions`:
+
+```csharp
+var bootstrap = SquidStdBootstrap.Create(new SquidStdOptions
+{
+ ConfigName = "squidstd",
+ RootDirectory = AppContext.BaseDirectory
+});
+```
+
+`ConfigName` selects the configuration file and `RootDirectory` anchors relative paths.
+
+## ConfigureServices
+
+Register your services into the DryIoc container:
+
+```csharp
+bootstrap.ConfigureServices(container =>
+{
+ container.AddSomething();
+});
+```
+
+See [dependency injection](dependency-injection.md) for the container and the `AddXxx` / `RegisterXxx` pattern.
+
+## Start and stop over ISquidStdService
+
+Services implementing `ISquidStdService` participate in the lifecycle. On `StartAsync` they are started in registration order; on `StopAsync` they are stopped in reverse order, so dependencies remain available while their dependents shut down.
+
+```mermaid
+sequenceDiagram
+ participant App
+ participant Bootstrap as SquidStdBootstrap
+ participant Svc as ISquidStdService(s)
+ App->>Bootstrap: Create(options)
+ App->>Bootstrap: ConfigureServices(container)
+ App->>Bootstrap: StartAsync()
+ Bootstrap->>Svc: StartAsync() (in order)
+ App->>Bootstrap: StopAsync()
+ Bootstrap->>Svc: StopAsync() (reverse order)
+```
+
+## RunAsync for long-running hosts
+
+For long-running hosts, call `RunAsync`. It starts every service and then blocks until cancellation, stopping services cleanly on shutdown. Resolve dependencies anywhere with `bootstrap.Resolve()`. See the [architecture](architecture.md) overview for how the host fits the layers.
diff --git a/docs/articles/concepts/dependency-injection.md b/docs/articles/concepts/dependency-injection.md
new file mode 100644
index 00000000..6133434a
--- /dev/null
+++ b/docs/articles/concepts/dependency-injection.md
@@ -0,0 +1,32 @@
+# Dependency injection
+
+SquidStd resolves every service through dependency injection. Understanding the container and its registration pattern is the key to wiring an application together.
+
+## DryIoc container
+
+The DI container is [DryIoc](https://github.com/dadhi/DryIoc), exposed as `IContainer`. It is fast, supports rich lifetimes, and validates required services at resolution time. Because the container owns validation, constructor dependencies that arrive from DI do not need manual null guards.
+
+## The AddXxx / RegisterXxx extension pattern
+
+Modules register themselves through C# 14 `extension(IContainer)` members named `AddXxx(...)` or `RegisterXxx(...)`. Each capability ships its own registration entry point, so wiring a module is a single call:
+
+```csharp
+container.AddInMemoryMessaging();
+container.RegisterCoreServices();
+```
+
+This keeps registration discoverable and colocated with the package that owns it.
+
+## Resolving through the bootstrap
+
+Once configured, resolve services through the bootstrap:
+
+```csharp
+var bus = bootstrap.Resolve();
+```
+
+In most code you let constructor injection do the work and never call `Resolve` directly. See [bootstrap lifecycle](bootstrap-lifecycle.md) for where `ConfigureServices` runs.
+
+## Singletons and lifecycle
+
+Most infrastructure services are registered as singletons and live for the life of the host. Services implementing `ISquidStdService` are additionally started and stopped by the bootstrap, so a singleton can hold long-lived resources that are released on shutdown.
diff --git a/docs/articles/concepts/messaging-models.md b/docs/articles/concepts/messaging-models.md
new file mode 100644
index 00000000..30b98907
--- /dev/null
+++ b/docs/articles/concepts/messaging-models.md
@@ -0,0 +1,27 @@
+# Messaging models
+
+SquidStd offers three in-process messaging models. Each fits a different shape of communication, and they can be mixed freely within one application.
+
+## Event Bus
+
+The Event Bus (`IEventBus`) is a stateless broadcast. Publishers call `PublishAsync` and any number of subscribers registered with `Subscribe` receive the event. Senders do not know who listens, and there is no return value. Use it for fan-out notifications where many parts of the system react to something that happened.
+
+## Command Dispatcher
+
+The Command Dispatcher is a stateless request routed to a single handler that returns a result. Unlike the Event Bus there is exactly one handler per command, and the caller awaits its result. Use it for request/response work — validating input, performing an operation, returning an outcome — where ownership of the action is unambiguous.
+
+## Actors
+
+Actors (`Actor`) provide an ordered, stateful, per-entity mailbox. Each actor processes its messages one at a time in order, so state inside an actor needs no locks. Send fire-and-forget messages with `TellAsync` or request a reply with `AskAsync`. Use actors when you need serialized access to per-entity state, such as a single account, device, or session.
+
+## Choosing
+
+```mermaid
+flowchart TD
+ Q{Need ordered, per-entity state?} -->|yes| A[Actors]
+ Q -->|no| R{Request → single handler with a result?}
+ R -->|yes| C[Command Dispatcher]
+ R -->|no| E[Event Bus broadcast]
+```
+
+All three are contract-first; see [abstractions first](abstractions-first.md) for swapping in external transports.
diff --git a/docs/articles/crypto.md b/docs/articles/crypto.md
new file mode 100644
index 00000000..3ba608c8
--- /dev/null
+++ b/docs/articles/crypto.md
@@ -0,0 +1 @@
+[!include[](../../src/SquidStd.Crypto/README.md)]
diff --git a/docs/articles/getting-started.md b/docs/articles/getting-started.md
index 88533d39..94412372 100644
--- a/docs/articles/getting-started.md
+++ b/docs/articles/getting-started.md
@@ -7,14 +7,19 @@ dotnet add package SquidStd.Services.Core
```
```csharp
-using DryIoc;
-using SquidStd.Services.Core.Extensions;
+using SquidStd.Core.Data.Bootstrap;
+using SquidStd.Services.Core.Services.Bootstrap;
-var container = new Container();
+// Core services wired automatically: config manager, event bus, command dispatcher,
+// job system, timer/cron scheduler, metrics, health checks, storage and secrets.
+var bootstrap = SquidStdBootstrap.Create(
+ new SquidStdOptions { ConfigName = "squidstd", RootDirectory = AppContext.BaseDirectory });
-// config manager + event bus + jobs + timer wheel + dispatcher + metrics + storage + secrets
-container.RegisterCoreServices("squidstd", Directory.GetCurrentDirectory());
+await bootstrap.StartAsync();
+// … resolve services, opt into modules with bootstrap.ConfigureServices(…) …
+await bootstrap.StopAsync();
```
-From here, add focused packages as needed — see the per-package guides in the sidebar and the
-[API reference](../api/index.md).
+Opt into modules with `ConfigureServices`, e.g. `container.AddInMemoryCache()`. See the
+[Concepts](concepts/bootstrap-lifecycle.md) for the full lifecycle and
+[Packages](getting-started.md) for per-module reference.
diff --git a/docs/articles/guides/choosing-caching.md b/docs/articles/guides/choosing-caching.md
new file mode 100644
index 00000000..2c7769f7
--- /dev/null
+++ b/docs/articles/guides/choosing-caching.md
@@ -0,0 +1,19 @@
+# Choosing a cache
+
+The caching abstraction (`ICacheService`) has two backends. Both expose the same
+API, so you can develop against in-memory and promote to Redis without code changes.
+
+| Backend | Package · entrypoint | Use case | Scope | Survives restart | Ops cost |
+|---|---|---|---|---|---|
+| In-memory | `SquidStd.Caching` · `AddInMemoryCache` | Tests, single instance | Per-process | No | None |
+| Redis | `SquidStd.Caching.Redis` · `AddRedisCache` | Multiple instances, shared cache | Distributed | Yes (with persistence) | Run/host Redis |
+
+```csharp
+bootstrap.ConfigureServices(container => container.AddRedisCache("localhost:6379"));
+```
+
+## Recommendation
+
+Use `AddInMemoryCache` for tests and single-instance deployments; switch to
+`AddRedisCache` as soon as you run more than one instance or need the cache to
+outlive the process.
diff --git a/docs/articles/guides/choosing-messaging.md b/docs/articles/guides/choosing-messaging.md
new file mode 100644
index 00000000..3af5c59f
--- /dev/null
+++ b/docs/articles/guides/choosing-messaging.md
@@ -0,0 +1,19 @@
+# Choosing a messaging backend
+
+SquidStd exposes one messaging abstraction with three interchangeable backends.
+Pick by where you run and what delivery guarantees you need.
+
+| Backend | Package · entrypoint | Use case | Ordering | Durability | Ops cost |
+|---|---|---|---|---|---|
+| In-memory | `SquidStd.Messaging` · `AddInMemoryMessaging` | Tests, single-process apps | Per-process | None (lost on restart) | None |
+| RabbitMQ | `SquidStd.Messaging.RabbitMq` · `AddRabbitMqMessaging` | Self-hosted services, work queues | Per-queue | Durable queues | Run a broker |
+| SQS/SNS | `SquidStd.Messaging.Sqs` · `AddSqsMessaging` | AWS-native, serverless | FIFO queues only | Managed, durable | Managed (AWS) |
+
+```csharp
+bootstrap.ConfigureServices(container => container.AddRabbitMqMessaging("amqp://localhost"));
+```
+
+## Recommendation
+
+Use `AddInMemoryMessaging` for tests and single-process apps, `AddRabbitMqMessaging`
+when you self-host, and `AddSqsMessaging` when you are already on AWS.
diff --git a/docs/articles/guides/choosing-search-database.md b/docs/articles/guides/choosing-search-database.md
new file mode 100644
index 00000000..fa5c3292
--- /dev/null
+++ b/docs/articles/guides/choosing-search-database.md
@@ -0,0 +1,23 @@
+# Choosing search & database
+
+Full-text search and relational persistence are different tools. Use the search
+provider for queries over text and documents, and the database module (backed by
+FreeSql) for structured, relational data.
+
+| Need | Module · entrypoint | Provider(s) | Use case |
+|---|---|---|---|
+| Full-text / document search | `SquidStd.Search.Elasticsearch` · `AddElasticsearch` | Elasticsearch | Relevance ranking, faceting, log/document search |
+| Relational data | `SquidStd.Database` · `RegisterDatabase` | FreeSql: Sqlite, PostgreSQL, MySql, SqlServer | Transactional records, joins, constraints |
+
+The database provider is selected via `DatabaseProviderType` (`Sqlite`,
+`PostgreSQL`, `MySql`, `SqlServer`) in the `database` config section.
+
+```csharp
+bootstrap.ConfigureServices(container => container.RegisterDatabase());
+```
+
+## Recommendation
+
+Reach for `AddElasticsearch` when the core requirement is searching text or
+documents by relevance; use `RegisterDatabase` with the FreeSql provider that
+matches your engine for everything relational. They compose — many apps use both.
diff --git a/docs/articles/guides/choosing-storage.md b/docs/articles/guides/choosing-storage.md
new file mode 100644
index 00000000..594f918e
--- /dev/null
+++ b/docs/articles/guides/choosing-storage.md
@@ -0,0 +1,23 @@
+# Choosing a storage backend
+
+The storage abstraction (`IStorageService`) stores blobs by key. There are two
+providers; the S3 provider also targets any S3-compatible server such as MinIO.
+
+| Backend | Package · entrypoint | Use case | Shared across hosts | Ops cost |
+|---|---|---|---|---|
+| Local file | `SquidStd.Storage` · `AddFileStorage` | Dev, single host, simple persistence | No | None |
+| S3 | `SquidStd.Storage.S3` · `AddS3Storage` | AWS-native object storage | Yes | Managed (AWS) |
+| MinIO | `SquidStd.Storage.S3` · `AddS3Storage` (set `ServiceUrl`) | Self-hosted S3-compatible storage | Yes | Run MinIO |
+
+```csharp
+bootstrap.ConfigureServices(container => container.AddS3Storage(new S3StorageOptions
+{
+ // ServiceUrl points at AWS S3 or a self-hosted MinIO endpoint
+}));
+```
+
+## Recommendation
+
+Use `AddFileStorage` for development and single-host apps. Use `AddS3Storage`
+against AWS S3 in the cloud, or against a MinIO endpoint (via `ServiceUrl`) when
+you need S3 semantics on self-hosted infrastructure.
diff --git a/docs/articles/guides/configuration.md b/docs/articles/guides/configuration.md
new file mode 100644
index 00000000..092468f5
--- /dev/null
+++ b/docs/articles/guides/configuration.md
@@ -0,0 +1,45 @@
+# Configuration
+
+SquidStd loads configuration from a single YAML file. Each module registers a
+strongly-typed section; the config manager deserializes it, expands environment
+variables, and publishes the populated object into the container so you can
+resolve it like any other service.
+
+## Steps
+
+1. **Point the bootstrap at your config file.** `SquidStdOptions.ConfigName` is the
+ logical file name (default `squidstd`, so `squidstd.yaml`) and
+ `RootDirectory` is the directory it is searched in.
+2. **Register a section.** Call `RegisterConfigSection` with the section
+ name (the top-level YAML key). Provide a `createDefault` factory so the file
+ is generated with sensible defaults on first run.
+3. **Use environment variables.** Any `string` property whose value contains a
+ `$VAR` token is expanded from the environment when the section is loaded.
+ Unknown tokens are left untouched.
+4. **Read values.** Resolve the config type from the container wherever you need it.
+
+```csharp
+public sealed class MyServiceConfig
+{
+ public string Endpoint { get; set; } = string.Empty; // e.g. "https://$API_HOST"
+ public int Retries { get; set; } = 3;
+}
+
+var bootstrap = SquidStdBootstrap.Create(
+ new SquidStdOptions { ConfigName = "squidstd", RootDirectory = AppContext.BaseDirectory });
+
+bootstrap.ConfigureServices(container =>
+ container.RegisterConfigSection("myService", static () => new MyServiceConfig()));
+
+await bootstrap.StartAsync();
+
+var config = container.Resolve();
+```
+
+The corresponding `squidstd.yaml`:
+
+```yaml
+myService:
+ endpoint: "https://$API_HOST"
+ retries: 5
+```
diff --git a/docs/articles/guides/observability.md b/docs/articles/guides/observability.md
new file mode 100644
index 00000000..77370620
--- /dev/null
+++ b/docs/articles/guides/observability.md
@@ -0,0 +1,35 @@
+# Observability
+
+`SquidStd.Telemetry.OpenTelemetry` wires OpenTelemetry tracing and metrics into
+the bootstrap as a managed service. Spans flow from SquidStd's own
+`ActivitySource` instances and from anything else in your process.
+
+## Steps
+
+1. **Add the package** `SquidStd.Telemetry.OpenTelemetry`.
+2. **Register telemetry** with `AddSquidStdTelemetry`, passing a `TelemetryOptions`
+ with your `ServiceName`.
+3. **Choose an exporter.** Set `OtlpEndpoint` / `OtlpProtocol` to ship to a
+ collector (the default endpoint is `http://localhost:4317`, gRPC). For local
+ debugging set `EnableConsoleExporter = true` to also print spans to stdout.
+4. **Tune sampling** with `TracingSampleRatio` (0..1) and toggle pipelines with
+ `EnableTracing` / `EnableMetrics`.
+
+```csharp
+bootstrap.ConfigureServices(container =>
+ container.AddSquidStdTelemetry(new TelemetryOptions
+ {
+ ServiceName = "orders-worker",
+ OtlpEndpoint = "http://otel-collector:4317",
+ OtlpProtocol = OtlpProtocolType.Grpc,
+ EnableConsoleExporter = false,
+ TracingSampleRatio = 1.0
+ }));
+```
+
+## ActivitySource convention
+
+SquidStd modules name their `ActivitySource` instances with the `SquidStd.*`
+prefix (for example `SquidStd.Messaging`). Subscribe to that prefix in your
+collector or tracer-provider configuration to capture all framework spans, and
+follow the same convention for your own application sources.
diff --git a/docs/articles/guides/security.md b/docs/articles/guides/security.md
new file mode 100644
index 00000000..df2373f5
--- /dev/null
+++ b/docs/articles/guides/security.md
@@ -0,0 +1,43 @@
+# Security
+
+SquidStd ships focused primitives for the three common secret-handling needs:
+hashing credentials, protecting payloads with a key, and storing named secrets.
+
+## Steps
+
+1. **Hash passwords** with `HashUtils` (PBKDF2-SHA256). Never store plaintext.
+
+ ```csharp
+ var stored = HashUtils.HashPassword(password);
+ var ok = HashUtils.VerifyPassword(password, stored);
+ ```
+
+2. **Protect payloads** through `ISecretProtector`. The default
+ (`SquidStd.Services.Core`) is AES-GCM; for managed keys use
+ `RegisterKmsSecretProtector` from `SquidStd.Secrets.Aws`.
+
+ ```csharp
+ byte[] protectedData = protector.Protect(plaintext);
+ byte[] clear = protector.Unprotect(protectedData);
+ ```
+
+3. **Store named secrets** behind `ISecretStore`. The default is file-backed; use
+ `RegisterAwsSecretsManagerStore` (`SquidStd.Secrets.Aws`) for AWS Secrets Manager.
+
+ ```csharp
+ await store.SetAsync("db/password", value);
+ var secret = await store.GetAsync("db/password");
+ ```
+
+4. **Use a PGP keyring** for armored encryption/signing by registering a key
+ store with `RegisterPgp` from `SquidStd.Crypto`.
+
+ ```csharp
+ container.RegisterPgp(resolver => myPgpKeyStore);
+ ```
+
+## Recommendation
+
+Use `HashUtils` for credentials, `ISecretProtector` (AES-GCM locally, KMS in the
+cloud) for encrypting blobs, and `ISecretStore` for named secrets — backed by
+files in development and AWS Secrets Manager in production.
diff --git a/docs/articles/guides/testing.md b/docs/articles/guides/testing.md
new file mode 100644
index 00000000..ab1d29b6
--- /dev/null
+++ b/docs/articles/guides/testing.md
@@ -0,0 +1,41 @@
+# Testing
+
+For fast, isolated tests, bootstrap SquidStd with in-memory providers instead of
+real infrastructure. The in-memory backends implement the same abstractions as
+the production providers, so the code under test never changes between unit and
+integration runs.
+
+## Steps
+
+1. **Build a fixture** that creates a `SquidStdBootstrap` and starts it once per
+ test class.
+2. **Opt into in-memory providers** in `ConfigureServices`: `AddInMemoryCache`,
+ `AddInMemoryMessaging`, and an in-memory VFS via `RegisterVfs` with
+ `InMemoryFileSystem`.
+3. **Resolve abstractions** (`ICacheService`, the event bus, `IVirtualFileSystem`, …)
+ from the container in your tests.
+4. **Swap to real backends** in a separate integration suite by replacing the
+ `Add…` calls with `AddRedisCache`, `AddRabbitMqMessaging`, `AddS3Storage`, etc.
+
+```csharp
+public sealed class TestHostFixture : IAsyncDisposable
+{
+ public ISquidStdBootstrap Bootstrap { get; }
+
+ public TestHostFixture()
+ {
+ Bootstrap = SquidStdBootstrap.Create(
+ new SquidStdOptions { ConfigName = "squidstd", RootDirectory = AppContext.BaseDirectory });
+
+ Bootstrap.ConfigureServices(container => container
+ .AddInMemoryCache()
+ .AddInMemoryMessaging()
+ .RegisterVfs(_ => new InMemoryFileSystem()));
+ }
+
+ public async ValueTask DisposeAsync() => await Bootstrap.StopAsync();
+}
+```
+
+Start the bootstrap (`await Bootstrap.StartAsync()`) before your first test and
+resolve services from its container.
diff --git a/docs/articles/persistence-abstractions.md b/docs/articles/persistence-abstractions.md
new file mode 100644
index 00000000..96f9fb84
--- /dev/null
+++ b/docs/articles/persistence-abstractions.md
@@ -0,0 +1 @@
+[!include[](../../src/SquidStd.Persistence.Abstractions/README.md)]
diff --git a/docs/articles/persistence-messagepack.md b/docs/articles/persistence-messagepack.md
new file mode 100644
index 00000000..a3bc077d
--- /dev/null
+++ b/docs/articles/persistence-messagepack.md
@@ -0,0 +1 @@
+[!include[](../../src/SquidStd.Persistence.MessagePack/README.md)]
diff --git a/docs/articles/persistence.md b/docs/articles/persistence.md
new file mode 100644
index 00000000..2104287c
--- /dev/null
+++ b/docs/articles/persistence.md
@@ -0,0 +1 @@
+[!include[](../../src/SquidStd.Persistence/README.md)]
diff --git a/docs/articles/secrets-aws.md b/docs/articles/secrets-aws.md
new file mode 100644
index 00000000..2418ebfa
--- /dev/null
+++ b/docs/articles/secrets-aws.md
@@ -0,0 +1 @@
+[!include[](../../src/SquidStd.Secrets.Aws/README.md)]
diff --git a/docs/articles/toc.yml b/docs/articles/toc.yml
index 2669ef5b..cc2d4e4e 100644
--- a/docs/articles/toc.yml
+++ b/docs/articles/toc.yml
@@ -1,3 +1,33 @@
+- name: Concepts
+ items:
+ - name: Architecture
+ href: concepts/architecture.md
+ - name: Bootstrap lifecycle
+ href: concepts/bootstrap-lifecycle.md
+ - name: Dependency injection
+ href: concepts/dependency-injection.md
+ - name: Abstractions first
+ href: concepts/abstractions-first.md
+ - name: Messaging models
+ href: concepts/messaging-models.md
+- name: Guides
+ items:
+ - name: Configuration
+ href: guides/configuration.md
+ - name: Observability
+ href: guides/observability.md
+ - name: Testing
+ href: guides/testing.md
+ - name: Security
+ href: guides/security.md
+ - name: Choosing a messaging backend
+ href: guides/choosing-messaging.md
+ - name: Choosing a cache
+ href: guides/choosing-caching.md
+ - name: Choosing a storage backend
+ href: guides/choosing-storage.md
+ - name: Choosing search & database
+ href: guides/choosing-search-database.md
- name: Getting Started
href: getting-started.md
- name: SquidStd.Core
@@ -18,6 +48,12 @@
href: database-abstractions.md
- name: SquidStd.Database
href: database.md
+- name: SquidStd.Persistence.Abstractions
+ href: persistence-abstractions.md
+- name: SquidStd.Persistence
+ href: persistence.md
+- name: SquidStd.Persistence.MessagePack
+ href: persistence-messagepack.md
- name: SquidStd.Messaging.Abstractions
href: messaging-abstractions.md
- name: SquidStd.Messaging
@@ -26,6 +62,8 @@
href: messaging-rabbitmq.md
- name: SquidStd.Messaging.Sqs
href: messaging-sqs.md
+- name: SquidStd.Actors
+ href: actors.md
- name: SquidStd.Caching.Abstractions
href: caching-abstractions.md
- name: SquidStd.Caching
@@ -38,6 +76,10 @@
href: storage.md
- name: SquidStd.Storage.S3
href: storage-s3.md
+- name: SquidStd.Vfs.Abstractions
+ href: vfs-abstractions.md
+- name: SquidStd.Vfs
+ href: vfs.md
- name: SquidStd.Scripting.Lua
href: scripting-lua.md
- name: SquidStd.Templating
@@ -72,5 +114,9 @@
href: telemetry-opentelemetry.md
- name: SquidStd.Aws.Abstractions
href: aws-abstractions.md
+- name: SquidStd.Crypto
+ href: crypto.md
+- name: SquidStd.Secrets.Aws
+ href: secrets-aws.md
- name: Felix Network
href: felix.md
diff --git a/docs/articles/vfs-abstractions.md b/docs/articles/vfs-abstractions.md
new file mode 100644
index 00000000..6de38df3
--- /dev/null
+++ b/docs/articles/vfs-abstractions.md
@@ -0,0 +1 @@
+[!include[](../../src/SquidStd.Vfs.Abstractions/README.md)]
diff --git a/docs/articles/vfs.md b/docs/articles/vfs.md
new file mode 100644
index 00000000..247ccdc1
--- /dev/null
+++ b/docs/articles/vfs.md
@@ -0,0 +1 @@
+[!include[](../../src/SquidStd.Vfs/README.md)]
diff --git a/docs/index.md b/docs/index.md
index 54cd303d..ebd366c9 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -8,11 +8,19 @@ _layout: landing
# SquidStd
-A modular .NET toolkit: foundational service contracts and utilities, a DryIoc-based service stack,
-networking, plugins, data access, messaging, and Lua scripting — published as focused NuGet packages.
+A batteries-included, modular standard library for .NET 10 — distilled from years of building
+real-world server software. Each capability ships behind a small contract with an in-memory
+implementation for tests and a production backend, published as a focused NuGet package.
-- **[Getting started](articles/getting-started.md)** — install and bootstrap the core services.
-- **[API reference](api/index.md)** — the full type/member documentation.
-- **Packages** — see the per-package guides under [Articles](articles/getting-started.md).
-- **[Felix Network](articles/felix.md)** — companion secure binary mesh-networking library
- (.NET + C/ESP32).
+Security & crypto, configuration, persistence, messaging, caching, storage, a virtual filesystem,
+search, mail, workers, actors, telemetry, scripting — take only what you need.
+
+## Start here
+
+- **[Tutorials](tutorials/index.md)** — learn by building: bootstrap, caching, messaging, workers,
+ crypto, persistence, and more.
+- **[Guides](articles/guides/configuration.md)** — task-focused how-to and "which provider" decision guides.
+- **[Concepts](articles/concepts/architecture.md)** — the architecture and the ideas behind it.
+- **[Packages](articles/getting-started.md)** — per-package reference.
+- **[API reference](api/index.md)** — full type/member documentation.
+- **[Felix Network](articles/felix.md)** — companion secure binary mesh-networking library (.NET + C/ESP32).
diff --git a/docs/tutorials/actors.md b/docs/tutorials/actors.md
new file mode 100644
index 00000000..8097cd62
--- /dev/null
+++ b/docs/tutorials/actors.md
@@ -0,0 +1,50 @@
+# Actors
+
+Build a single-consumer actor that mutates state without locks, then talk to it with
+fire-and-forget and request/response messages.
+
+## What you'll build
+
+A `CounterActor` (`SquidStd.Actors`) that processes messages one at a time inside `ReceiveAsync`,
+driven by `TellAsync` (fire-and-forget) and `AskAsync` (request/response).
+
+## Prerequisites
+
+- .NET 10 SDK
+- `dotnet add package SquidStd.Actors`
+
+## Steps
+
+### 1. Define the message contract and the actor
+
+A marker interface groups the messages; `Increment` is fire-and-forget, `GetTotal` derives from
+`ActorRequest` to carry a reply. The actor mutates `_total` without locks because messages
+are processed one at a time.
+
+[!code-csharp[](../../samples/SquidStd.Samples.Actors/Program.cs#step-1)]
+
+### 2. Send fire-and-forget messages
+
+`TellAsync` enqueues a message and returns without waiting for it to be handled.
+
+[!code-csharp[](../../samples/SquidStd.Samples.Actors/Program.cs#step-2)]
+
+### 3. Ask for a reply
+
+`AskAsync` enqueues a request and awaits the typed reply the actor sends with
+`request.Reply(...)`.
+
+[!code-csharp[](../../samples/SquidStd.Samples.Actors/Program.cs#step-3)]
+
+## Run it
+
+```bash
+dotnet run --project samples/SquidStd.Samples.Actors
+```
+
+Prints `Total: 8`.
+
+## Next steps
+
+- [Messaging models](../articles/concepts/messaging-models.md)
+- [SquidStd.Actors reference](../articles/actors.md)
diff --git a/docs/tutorials/command-dispatcher.md b/docs/tutorials/command-dispatcher.md
new file mode 100644
index 00000000..2c90c77a
--- /dev/null
+++ b/docs/tutorials/command-dispatcher.md
@@ -0,0 +1,49 @@
+# Command dispatcher
+
+Register typed command handlers, dispatch commands against a context, and read back what matched.
+
+## What you'll build
+
+A `Container` with a `RegisterCommandDispatcher` and several handlers
+(`SquidStd.Services.Core`), dispatching commands that carry a `Session` context. One command type
+has two handlers — both run on dispatch.
+
+## Prerequisites
+
+- .NET 10 SDK
+- `dotnet add package SquidStd.Services.Core`
+
+## Steps
+
+### 1. Register the dispatcher and handlers
+
+Register the dispatcher for the context type, then each handler. `EchoCommand` has two handlers
+(`EchoHandler` and `AuditHandler`); both will run. The subscription loop is what
+`CommandDispatcherActivator` does at runtime — inlined here to keep the sample
+self-contained.
+
+[!code-csharp[](../../samples/SquidStd.Samples.Commands/Program.cs#step-1)]
+
+### 2. Dispatch commands with a context
+
+The context (here a `Session`) is passed explicitly at dispatch time — in a server this is the
+session the message arrived on.
+
+[!code-csharp[](../../samples/SquidStd.Samples.Commands/Program.cs#step-2)]
+
+### 3. Read the dispatch result
+
+`DispatchAsync` returns a result reporting whether any handler matched and how many ran. Nothing
+is registered for `UnknownCommand`, so `Matched` is `false` and `HandlerCount` is `0`.
+
+[!code-csharp[](../../samples/SquidStd.Samples.Commands/Program.cs#step-3)]
+
+## Run it
+
+```bash
+dotnet run --project samples/SquidStd.Samples.Commands
+```
+
+## Next steps
+
+- [Messaging models](../articles/concepts/messaging-models.md)
diff --git a/docs/tutorials/crypto.md b/docs/tutorials/crypto.md
new file mode 100644
index 00000000..95070492
--- /dev/null
+++ b/docs/tutorials/crypto.md
@@ -0,0 +1,55 @@
+# Crypto (PGP)
+
+Generate a PGP key pair, persist it to disk, encrypt and sign a message, then reload the keyring from disk.
+
+## What you'll build
+
+A host that registers the PGP keyring, service, and a file-backed key store
+(`SquidStd.Crypto.Pgp`), generates a key, performs an encrypt-and-sign round-trip, and proves the
+keys survive a restart by reloading them from the armored `.asc` files on disk.
+
+## Prerequisites
+
+- .NET 10 SDK
+- `dotnet add package SquidStd.Crypto`
+
+## Steps
+
+### 1. Register the PGP services and a file-backed key store
+
+`RegisterPgp` wires the keyring and `IPgpService`; the factory chooses the key store — here a
+`FilePgpKeyStore` that reads and writes armored `.asc` files in a directory.
+
+[!code-csharp[](../../samples/SquidStd.Samples.Crypto/Program.cs#step-1)]
+
+### 2. Generate a key and save it to disk
+
+Generate a key pair, import it into the keyring so the service can resolve it by identity, then
+persist the keyring to the file-backed store.
+
+[!code-csharp[](../../samples/SquidStd.Samples.Crypto/Program.cs#step-2)]
+
+### 3. Encrypt, sign, decrypt and verify
+
+`EncryptAndSignForAsync` produces armored ciphertext for a recipient; `DecryptAndVerifyAsync`
+returns the plaintext together with the signature status.
+
+[!code-csharp[](../../samples/SquidStd.Samples.Crypto/Program.cs#step-3)]
+
+### 4. Reload the keyring from disk
+
+A brand-new `PgpKeyring` loads the same keys back from the store, proving the on-disk material
+survives a restart.
+
+[!code-csharp[](../../samples/SquidStd.Samples.Crypto/Program.cs#step-4)]
+
+## Run it
+
+```bash
+dotnet run --project samples/SquidStd.Samples.Crypto
+```
+
+## Next steps
+
+- [Security guide](../articles/guides/security.md)
+- [SquidStd.Crypto reference](../articles/crypto.md)
diff --git a/docs/tutorials/persistence.md b/docs/tutorials/persistence.md
new file mode 100644
index 00000000..25a28a2f
--- /dev/null
+++ b/docs/tutorials/persistence.md
@@ -0,0 +1,50 @@
+# Persistence
+
+Keep entities in an in-memory store backed by a durable binary snapshot plus a journal, so state
+survives a restart.
+
+## What you'll build
+
+A standalone demo of `SquidStd.Persistence`: a `Player` store that loads existing state on
+startup, appends every change to a journal, and captures a snapshot. Run it twice — the second run
+reloads what the first saved.
+
+## Prerequisites
+
+- .NET 10 SDK
+- `dotnet add package SquidStd.Persistence` (and `SquidStd.Persistence.MessagePack` for the
+ binary serializer)
+
+## Steps
+
+### 1. Initialize and load existing state
+
+`InitializeAsync` replays the snapshot and journal from the save directory; `GetStore`
+returns the typed store for an entity registered in the `PersistenceEntityRegistry`.
+
+[!code-csharp[](../../samples/SquidStd.Samples.Persistence/Program.cs#step-1)]
+
+### 2. Mutate the store
+
+Every `UpsertAsync` / `RemoveAsync` is appended to the journal, so the change is durable before
+the next snapshot.
+
+[!code-csharp[](../../samples/SquidStd.Samples.Persistence/Program.cs#step-2)]
+
+### 3. Snapshot and trim the journal
+
+`SaveSnapshotAsync` captures the full state and trims the journal. Re-run the sample to see the
+state reload.
+
+[!code-csharp[](../../samples/SquidStd.Samples.Persistence/Program.cs#step-3)]
+
+## Run it
+
+```bash
+dotnet run --project samples/SquidStd.Samples.Persistence
+dotnet run --project samples/SquidStd.Samples.Persistence # reloads the saved state
+```
+
+## Next steps
+
+- [SquidStd.Persistence reference](../articles/persistence.md)
diff --git a/docs/tutorials/secrets.md b/docs/tutorials/secrets.md
new file mode 100644
index 00000000..a98460de
--- /dev/null
+++ b/docs/tutorials/secrets.md
@@ -0,0 +1,55 @@
+# Secrets (KMS / Secrets Manager)
+
+Wire an AWS KMS-backed secret protector and a Secrets Manager store, then envelope-encrypt and
+store values.
+
+## What you'll build
+
+A host that registers `ISecretProtector` (KMS envelope encryption) and `ISecretStore`
+(AWS Secrets Manager) from `SquidStd.Secrets.Aws`, then exercises protect/unprotect and
+set/get/list.
+
+## Prerequisites
+
+- .NET 10 SDK
+- `dotnet add package SquidStd.Secrets.Aws`
+- The live calls (steps 2–3) need a KMS + Secrets Manager endpoint. The sample is
+ compile-and-wire focused: it resolves the services without AWS and only runs the live calls
+ when `SQUIDSTD_RUN_AWS=1` is set with [LocalStack](https://localstack.cloud) (or real AWS)
+ reachable at the configured endpoint.
+
+## Steps
+
+### 1. Register the KMS protector and Secrets Manager store
+
+`RegisterKmsSecretProtector` and `RegisterAwsSecretsManagerStore` take the KMS key alias, the
+secret name prefix, and the AWS endpoint — pointed here at a LocalStack `ServiceUrl`.
+
+[!code-csharp[](../../samples/SquidStd.Samples.Secrets/Program.cs#step-1)]
+
+### 2. Envelope-encrypt a value
+
+`Protect` requests a KMS data key, encrypts the payload with it, and wraps the encrypted data
+key alongside the ciphertext; `Unprotect` reverses it.
+
+[!code-csharp[](../../samples/SquidStd.Samples.Secrets/Program.cs#step-2)]
+
+### 3. Store, fetch and list secrets
+
+`ISecretStore` reads and writes named secrets through Secrets Manager; `ListNamesAsync` streams
+the names under the configured prefix.
+
+[!code-csharp[](../../samples/SquidStd.Samples.Secrets/Program.cs#step-3)]
+
+## Run it
+
+```bash
+dotnet run --project samples/SquidStd.Samples.Secrets
+# with the live calls:
+SQUIDSTD_RUN_AWS=1 dotnet run --project samples/SquidStd.Samples.Secrets
+```
+
+## Next steps
+
+- [Security guide](../articles/guides/security.md)
+- [SquidStd.Secrets.Aws reference](../articles/secrets-aws.md)
diff --git a/docs/tutorials/toc.yml b/docs/tutorials/toc.yml
index 5696494d..2d88e934 100644
--- a/docs/tutorials/toc.yml
+++ b/docs/tutorials/toc.yml
@@ -34,3 +34,15 @@
href: plugins.md
- name: Scaffolding with dotnet new
href: scaffolding.md
+- name: Actors
+ href: actors.md
+- name: Command dispatcher
+ href: command-dispatcher.md
+- name: Persistence
+ href: persistence.md
+- name: Virtual filesystem
+ href: vfs.md
+- name: Crypto (PGP)
+ href: crypto.md
+- name: Secrets (KMS / Secrets Manager)
+ href: secrets.md
diff --git a/docs/tutorials/vfs.md b/docs/tutorials/vfs.md
new file mode 100644
index 00000000..92082caa
--- /dev/null
+++ b/docs/tutorials/vfs.md
@@ -0,0 +1,53 @@
+# Virtual filesystem
+
+Read and write files through an abstract virtual filesystem, then layer an encrypted vault on top.
+
+## What you'll build
+
+A host that resolves `IVirtualFileSystem` (`SquidStd.Vfs`) backed by an in-memory store, plus an
+encrypted-vault round-trip using `CryptoFileSystem` (`SquidStd.Crypto.Vfs`).
+
+## Prerequisites
+
+- .NET 10 SDK
+- `dotnet add package SquidStd.Vfs` (and `SquidStd.Crypto.Vfs` for the encrypted vault)
+
+## Steps
+
+### 1. Register a virtual filesystem
+
+`RegisterVfs` wires `IVirtualFileSystem`; the factory chooses the backend — here a plain
+in-memory filesystem.
+
+[!code-csharp[](../../samples/SquidStd.Samples.Vfs/Program.cs#step-1)]
+
+### 2. Write and read a file
+
+The same `IVirtualFileSystem` API works regardless of the backend. `ReadAllBytesAsync` returns
+`null` only when the path is absent.
+
+[!code-csharp[](../../samples/SquidStd.Samples.Vfs/Program.cs#step-2)]
+
+### 3. Encrypted vault round-trip
+
+`CryptoFileSystem` encrypts every entry over a backend filesystem. The lifecycle is
+unlock → write → dispose; disposing locks the vault (zeroing the key and flushing the encrypted
+index) and disposes the backend, so a `ZipFileSystem` backend flushes its archive to disk.
+Re-opening a fresh instance over the same file with the passphrase decrypts the data at rest.
+
+This sample backs the vault with a single on-disk zip file via `ZipFileSystem`, writes a secret,
+disposes the vault, then re-opens a brand-new instance over the same file to prove on-disk
+persistence. The DI helper `RegisterCryptoVault` wires exactly this — a vault over a single-file
+zip — as a lockable singleton.
+
+[!code-csharp[](../../samples/SquidStd.Samples.Vfs/Program.cs#step-3)]
+
+## Run it
+
+```bash
+dotnet run --project samples/SquidStd.Samples.Vfs
+```
+
+## Next steps
+
+- [SquidStd.Vfs reference](../articles/vfs.md)
diff --git a/samples/SquidStd.Samples.Actors/Program.cs b/samples/SquidStd.Samples.Actors/Program.cs
new file mode 100644
index 00000000..01f2f0bc
--- /dev/null
+++ b/samples/SquidStd.Samples.Actors/Program.cs
@@ -0,0 +1,53 @@
+using SquidStd.Actors;
+using SquidStd.Actors.Interfaces;
+
+await using var counter = new CounterActor();
+
+#region step-2
+
+// Fire-and-forget messages: TellAsync enqueues without awaiting a reply.
+await counter.TellAsync(new Increment(5));
+await counter.TellAsync(new Increment(3));
+
+#endregion
+
+#region step-3
+
+// Request/response: AskAsync enqueues a request and awaits its typed reply.
+var total = await counter.AskAsync(new GetTotal());
+
+Console.WriteLine($"Total: {total}");
+
+#endregion
+
+#region step-1
+
+// The message contract: a marker interface, a fire-and-forget message, and an ask request.
+internal interface ICounterMessage;
+
+internal sealed record Increment(int By) : ICounterMessage;
+
+internal sealed record GetTotal : ActorRequest, ICounterMessage;
+
+// A single-consumer actor: state is mutated without locks inside ReceiveAsync.
+internal sealed class CounterActor : Actor
+{
+ private int _total;
+
+ protected override ValueTask ReceiveAsync(ICounterMessage message, CancellationToken cancellationToken)
+ {
+ switch (message)
+ {
+ case Increment increment:
+ _total += increment.By;
+ break;
+ case GetTotal request:
+ request.Reply(_total);
+ break;
+ }
+
+ return ValueTask.CompletedTask;
+ }
+}
+
+#endregion
diff --git a/samples/SquidStd.Samples.Actors/SquidStd.Samples.Actors.csproj b/samples/SquidStd.Samples.Actors/SquidStd.Samples.Actors.csproj
new file mode 100644
index 00000000..dd253c83
--- /dev/null
+++ b/samples/SquidStd.Samples.Actors/SquidStd.Samples.Actors.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
diff --git a/samples/SquidStd.Samples.Commands/Program.cs b/samples/SquidStd.Samples.Commands/Program.cs
index 56e23f85..fa133520 100644
--- a/samples/SquidStd.Samples.Commands/Program.cs
+++ b/samples/SquidStd.Samples.Commands/Program.cs
@@ -4,6 +4,9 @@
using SquidStd.Core.Interfaces.Commands;
using SquidStd.Services.Core.Extensions;
+#region step-1
+
+// Register the dispatcher and the handlers. EchoCommand has two handlers — both run on dispatch.
var container = new Container();
container.RegisterCommandDispatcher();
container.RegisterCommandHandler();
@@ -19,6 +22,10 @@
registration.Subscribe(dispatcher, container);
}
+#endregion
+
+#region step-2
+
// The context (here a Session) is passed explicitly at dispatch time — in a server this is the
// session the message arrived on. See RegisterSeededCommandDispatcher for building it from a seed.
var session = new Session();
@@ -26,10 +33,17 @@
await Dispatch(dispatcher, new PingCommand(), session);
await Dispatch(dispatcher, new EchoCommand("hello world"), session);
-// Unknown command path: nothing registered for UnknownCommand.
+#endregion
+
+#region step-3
+
+// The result reports whether any handler matched and how many ran — here nothing is registered
+// for UnknownCommand, so Matched is false and HandlerCount is 0.
var unknown = await dispatcher.DispatchAsync(new UnknownCommand(), session);
Console.WriteLine($"UnknownCommand -> matched={unknown.Matched} handlers={unknown.HandlerCount}");
+#endregion
+
return;
static async Task Dispatch(ICommandDispatcher dispatcher, TCommand command, Session context)
diff --git a/samples/SquidStd.Samples.Crypto/Program.cs b/samples/SquidStd.Samples.Crypto/Program.cs
new file mode 100644
index 00000000..8fdd0532
--- /dev/null
+++ b/samples/SquidStd.Samples.Crypto/Program.cs
@@ -0,0 +1,79 @@
+using System.Text;
+using SquidStd.Core.Data.Bootstrap;
+using SquidStd.Crypto.Pgp.Extensions;
+using SquidStd.Crypto.Pgp.Interfaces;
+using SquidStd.Crypto.Pgp.Services;
+using SquidStd.Services.Core.Services.Bootstrap;
+
+var bootstrap = SquidStdBootstrap.Create(
+ new SquidStdOptions
+ {
+ ConfigName = "squidstd",
+ RootDirectory = AppContext.BaseDirectory
+ }
+);
+
+var keyStoreDirectory = Path.Combine(AppContext.BaseDirectory, "pgp-keys");
+
+#region step-1
+
+// Register the PGP keyring, service, and a file-backed key store (armored .asc files).
+bootstrap.ConfigureServices(container => container.RegisterPgp(_ => new FilePgpKeyStore(keyStoreDirectory)));
+
+#endregion
+
+await bootstrap.StartAsync();
+
+#region step-2
+
+const string identity = "alice@example.com";
+const string passphrase = "correct horse battery staple";
+
+var pgp = bootstrap.Resolve();
+var keyring = bootstrap.Resolve();
+var keyStore = bootstrap.Resolve();
+
+// Generate a key pair and import it so the service can resolve it by identity. A freshly
+// generated key always carries secret material, so PrivateArmored is non-null here.
+var key = pgp.GenerateKey(identity, passphrase);
+keyring.Import(key.PrivateArmored!);
+
+// Persist the keyring to the file-backed store: this creates the directory and writes the
+// armored .asc files to disk.
+await keyring.SaveAsync(keyStore);
+
+Console.WriteLine($"Generated key {key.KeyId} for {key.Identity}; saved to {keyStoreDirectory}");
+
+#endregion
+
+#region step-3
+
+// Encrypt + sign for the recipient, then decrypt + verify the round-trip.
+var armored = await pgp.EncryptAndSignForAsync(
+ identity,
+ Encoding.UTF8.GetBytes("attack at dawn"),
+ identity,
+ passphrase
+);
+
+var result = await pgp.DecryptAndVerifyAsync(armored, passphrase);
+
+Console.WriteLine(
+ $"Decrypted: '{Encoding.UTF8.GetString(result.Data)}' (signed: {result.IsSigned}, valid: {result.IsValid})"
+);
+
+#endregion
+
+#region step-4
+
+// Persistence round-trip: a brand-new keyring loads the same keys back from disk.
+var reloaded = new PgpKeyring();
+await reloaded.LoadAsync(keyStore);
+
+Console.WriteLine(
+ $"Reloaded {reloaded.Keys.Count} key(s) from disk; contains '{identity}': {reloaded.Contains(identity)}"
+);
+
+#endregion
+
+await bootstrap.StopAsync();
diff --git a/samples/SquidStd.Samples.Crypto/SquidStd.Samples.Crypto.csproj b/samples/SquidStd.Samples.Crypto/SquidStd.Samples.Crypto.csproj
new file mode 100644
index 00000000..11ec9605
--- /dev/null
+++ b/samples/SquidStd.Samples.Crypto/SquidStd.Samples.Crypto.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
diff --git a/samples/SquidStd.Samples.Secrets/Program.cs b/samples/SquidStd.Samples.Secrets/Program.cs
new file mode 100644
index 00000000..7f98cb9e
--- /dev/null
+++ b/samples/SquidStd.Samples.Secrets/Program.cs
@@ -0,0 +1,84 @@
+using System.Text;
+using SquidStd.Aws.Abstractions.Data.Config;
+using SquidStd.Core.Data.Bootstrap;
+using SquidStd.Core.Interfaces.Secrets;
+using SquidStd.Secrets.Aws.Extensions;
+using SquidStd.Services.Core.Services.Bootstrap;
+
+var bootstrap = SquidStdBootstrap.Create(
+ new SquidStdOptions
+ {
+ ConfigName = "squidstd",
+ RootDirectory = AppContext.BaseDirectory
+ }
+);
+
+#region step-1
+
+// Wire the KMS-backed protector and the Secrets Manager store against a LocalStack endpoint.
+bootstrap.ConfigureServices(container =>
+{
+ container.RegisterKmsSecretProtector(options =>
+ {
+ options.KeyId = "alias/app";
+ options.Aws = new AwsConfigEntry
+ {
+ Region = "us-east-1",
+ ServiceUrl = "http://localhost:4566"
+ };
+ });
+
+ container.RegisterAwsSecretsManagerStore(options =>
+ {
+ options.NamePrefix = "myapp/";
+ options.Aws = new AwsConfigEntry
+ {
+ Region = "us-east-1",
+ ServiceUrl = "http://localhost:4566"
+ };
+ });
+
+ return container;
+});
+
+#endregion
+
+await bootstrap.StartAsync();
+
+var protector = bootstrap.Resolve();
+var store = bootstrap.Resolve();
+
+// The calls below need a live KMS + Secrets Manager endpoint (e.g. LocalStack), so they
+// only run when explicitly enabled. The wiring above compiles and resolves without AWS.
+if (Environment.GetEnvironmentVariable("SQUIDSTD_RUN_AWS") is null)
+{
+ Console.WriteLine("Set SQUIDSTD_RUN_AWS=1 with LocalStack running to exercise the live calls.");
+ await bootstrap.StopAsync();
+ return;
+}
+
+#region step-2
+
+// Envelope-encrypt a value with a KMS data key, then decrypt it back.
+var protectedBytes = protector.Protect(Encoding.UTF8.GetBytes("api-token"));
+var plaintext = protector.Unprotect(protectedBytes);
+
+Console.WriteLine($"Unprotected: {Encoding.UTF8.GetString(plaintext)}");
+
+#endregion
+
+#region step-3
+
+// Store, fetch, and list secrets through the Secrets Manager store.
+await store.SetAsync("db-password", "s3cr3t");
+var password = await store.GetAsync("db-password");
+Console.WriteLine($"db-password: {password}");
+
+await foreach (var name in store.ListNamesAsync())
+{
+ Console.WriteLine($"secret: {name}");
+}
+
+#endregion
+
+await bootstrap.StopAsync();
diff --git a/samples/SquidStd.Samples.Secrets/SquidStd.Samples.Secrets.csproj b/samples/SquidStd.Samples.Secrets/SquidStd.Samples.Secrets.csproj
new file mode 100644
index 00000000..3c6463e0
--- /dev/null
+++ b/samples/SquidStd.Samples.Secrets/SquidStd.Samples.Secrets.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
diff --git a/samples/SquidStd.Samples.Vfs/Program.cs b/samples/SquidStd.Samples.Vfs/Program.cs
new file mode 100644
index 00000000..1a577c35
--- /dev/null
+++ b/samples/SquidStd.Samples.Vfs/Program.cs
@@ -0,0 +1,64 @@
+using System.Text;
+using SquidStd.Core.Data.Bootstrap;
+using SquidStd.Crypto.Vfs.Services;
+using SquidStd.Services.Core.Services.Bootstrap;
+using SquidStd.Vfs.Abstractions.Interfaces;
+using SquidStd.Vfs.Extensions;
+using SquidStd.Vfs.Services;
+
+var bootstrap = SquidStdBootstrap.Create(
+ new SquidStdOptions
+ {
+ ConfigName = "squidstd",
+ RootDirectory = AppContext.BaseDirectory
+ }
+);
+
+#region step-1
+
+// Register a plain in-memory virtual filesystem.
+bootstrap.ConfigureServices(container => container.RegisterVfs(_ => new InMemoryFileSystem()));
+
+#endregion
+
+await bootstrap.StartAsync();
+
+#region step-2
+
+// Write and read a file through the virtual filesystem.
+var vfs = bootstrap.Resolve();
+
+await vfs.WriteAllBytesAsync("notes/hello.txt", Encoding.UTF8.GetBytes("plain content"));
+var bytes = await vfs.ReadAllBytesAsync("notes/hello.txt");
+
+// The file was just written, so it is present (the API returns null only when absent).
+Console.WriteLine($"VFS read: {Encoding.UTF8.GetString(bytes!)}");
+
+#endregion
+
+#region step-3
+
+// Encrypted vault on a single on-disk zip file: unlock -> write -> dispose (flushes to disk),
+// then re-open a brand-new instance over the same file to prove the data round-trips at rest.
+var vaultPath = Path.Combine(Path.GetTempPath(), "squidstd-sample.vault");
+
+using (var vault = new CryptoFileSystem(new ZipFileSystem(vaultPath)))
+{
+ vault.Unlock("vault passphrase");
+ await vault.WriteAllBytesAsync("secret.txt", Encoding.UTF8.GetBytes("top secret"));
+} // Dispose -> Lock (zeroes the key, flushes the encrypted index) -> flushes the zip to disk
+
+// Re-open the same encrypted file with the passphrase; only the right passphrase decrypts it.
+using (var reopened = new CryptoFileSystem(new ZipFileSystem(vaultPath)))
+{
+ reopened.Unlock("vault passphrase");
+
+ var secret = await reopened.ReadAllBytesAsync("secret.txt");
+ Console.WriteLine($"Vault read after reopen: {Encoding.UTF8.GetString(secret!)}");
+}
+
+File.Delete(vaultPath);
+
+#endregion
+
+await bootstrap.StopAsync();
diff --git a/samples/SquidStd.Samples.Vfs/SquidStd.Samples.Vfs.csproj b/samples/SquidStd.Samples.Vfs/SquidStd.Samples.Vfs.csproj
new file mode 100644
index 00000000..14019d32
--- /dev/null
+++ b/samples/SquidStd.Samples.Vfs/SquidStd.Samples.Vfs.csproj
@@ -0,0 +1,17 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
diff --git a/src/SquidStd.Abstractions/README.md b/src/SquidStd.Abstractions/README.md
index 3a83a2d9..00daacf7 100644
--- a/src/SquidStd.Abstractions/README.md
+++ b/src/SquidStd.Abstractions/README.md
@@ -1,16 +1,5 @@
-
-
-
-
SquidStd.Abstractions
-
-
-
-
-
-
-
DryIoc-based dependency-injection plumbing for SquidStd. It defines the `ISquidStdService` lifecycle
contract and the container extensions used to register services and configuration sections in a uniform,
discoverable way (tracked through ordered registration lists).
@@ -21,17 +10,6 @@ discoverable way (tracked through ordered registration lists).
dotnet add package SquidStd.Abstractions
```
-## Features
-
-- `ISquidStdService` — a `StartAsync`/`StopAsync` lifecycle contract for managed services.
-- `RegisterEventListenerAttribute` — mark `IEventListener` classes for generated registration.
-- `RegisterStdServiceAttribute` — mark service implementations for generated lifecycle registration.
-- `RegisterConfigSectionAttribute` — mark config models for generated config-section registration.
-- `RegisterStdService()` — register a singleton service and record it in the
- ordered service list (with optional priority).
-- `RegisterConfigSection(sectionName)` — register a YAML config section for the config manager.
-- `AddToRegisterTypedList(...)` — maintain ordered registration lists in the container.
-
## Usage
```csharp
@@ -59,6 +37,10 @@ container.RegisterConfigSection("my");
| `ServiceRegistrationData` | Ordered service registration record. |
| `ConfigRegistrationData` | Config section registration record. |
+## Related
+
+- Tutorial: [Source generators: registration](https://tgiachi.github.io/squid-std/tutorials/source-generators-registration.html)
+
## License
MIT — part of [SquidStd](https://github.com/tgiachi/squid-std).
diff --git a/src/SquidStd.Actors/Actor.cs b/src/SquidStd.Actors/Actor.cs
index 59f065cf..23cd542e 100644
--- a/src/SquidStd.Actors/Actor.cs
+++ b/src/SquidStd.Actors/Actor.cs
@@ -21,7 +21,7 @@ public abstract class Actor : IAsyncDisposable
private readonly CancellationTokenSource _shutdown;
private readonly ConcurrentDictionary _outstanding;
private readonly ILogger _logger;
- private bool _disposed;
+ private int _disposed;
/// Number of messages waiting in the mailbox.
public int PendingCount
@@ -38,14 +38,16 @@ protected Actor(ActorOptions? options = null)
_outstanding = new ConcurrentDictionary();
_logger = Log.ForContext(GetType());
+ // The mailbox is deliberately NOT bound to _shutdown.Token: cancelling that token would abort
+ // the block and discard queued messages. Dispose instead Completes the block to drain the queue,
+ // and only cancels _shutdown (observed by handlers via ReceiveAsync) once the drain budget elapses.
var blockOptions = new ExecutionDataflowBlockOptions
{
MaxDegreeOfParallelism = 1,
EnsureOrdered = true,
BoundedCapacity = _options.OverflowPolicy == ActorOverflowPolicy.Unbounded
? DataflowBlockOptions.Unbounded
- : _options.Capacity,
- CancellationToken = _shutdown.Token
+ : _options.Capacity
};
_mailbox = new ActionBlock(ProcessAsync, blockOptions);
@@ -163,33 +165,56 @@ private async Task ProcessAsync(TMessage message)
private void ThrowIfDisposed()
{
- if (_disposed)
+ if (Volatile.Read(ref _disposed) != 0)
{
throw new ObjectDisposedException(GetType().Name);
}
}
- /// Completes the mailbox, drains in-flight work, and faults any still-pending requests.
+ private async Task TryDrainAsync(TimeSpan timeout)
+ {
+ try
+ {
+ await _mailbox.Completion.WaitAsync(timeout);
+
+ return true;
+ }
+ catch (TimeoutException)
+ {
+ return false;
+ }
+ catch (Exception ex)
+ {
+ // The mailbox completed in a faulted/cancelled state; that still counts as drained.
+ _logger.Debug(ex, "Actor {ActorType} mailbox completed with fault during dispose", GetType().Name);
+
+ return true;
+ }
+ }
+
+ ///
+ /// Completes the mailbox and drains queued work within ,
+ /// then cancels any handlers still running and faults requests that never completed.
+ ///
public async ValueTask DisposeAsync()
{
- if (_disposed)
+ if (Interlocked.Exchange(ref _disposed, 1) != 0)
{
return;
}
- _disposed = true;
- _shutdown.Cancel();
_mailbox.Complete();
- try
+ if (!await TryDrainAsync(_options.ShutdownDrainTimeout))
{
- await _mailbox.Completion;
- }
- catch (Exception ex)
- {
- _logger.Debug(ex, "Actor {ActorType} mailbox completed with fault during dispose", GetType().Name);
+ // A handler is still running past the drain budget: cancel cancellation-honoring handlers
+ // and give them one more grace period to unwind before abandoning the drain.
+ _shutdown.Cancel();
+ await TryDrainAsync(_options.ShutdownDrainTimeout);
}
+ _shutdown.Cancel(); // idempotent; ensures handler tokens are observed cancelled before teardown
+
foreach (var request in _outstanding.Keys)
{
request.Fail(new ObjectDisposedException(GetType().Name));
diff --git a/src/SquidStd.Actors/Data/ActorOptions.cs b/src/SquidStd.Actors/Data/ActorOptions.cs
index 3bdf693e..fefb36da 100644
--- a/src/SquidStd.Actors/Data/ActorOptions.cs
+++ b/src/SquidStd.Actors/Data/ActorOptions.cs
@@ -15,4 +15,11 @@ public sealed class ActorOptions
/// Behavior when a fire-and-forget handler throws.
public ActorErrorPolicy ErrorPolicy { get; init; } = ActorErrorPolicy.Isolate;
+
+ ///
+ /// How long drains queued messages
+ /// before cancelling in-flight handlers. Queued work runs to completion within this budget;
+ /// once it elapses, the actor cancels and faults any still-pending requests.
+ ///
+ public TimeSpan ShutdownDrainTimeout { get; init; } = TimeSpan.FromSeconds(5);
}
diff --git a/src/SquidStd.Actors/README.md b/src/SquidStd.Actors/README.md
index d71004a4..c88652e1 100644
--- a/src/SquidStd.Actors/README.md
+++ b/src/SquidStd.Actors/README.md
@@ -53,6 +53,15 @@ using SquidStd.Actors.Extensions;
using var sub = session.SubscribeToEventBus(eventBus, (UserJoinedEvent e) => new SendText($":{e.Nick} JOIN"));
```
+## Key types
+
+| Type | Purpose |
+|---------------------------|----------------------------------------------------|
+| `Actor` | Mailbox base class (`TellAsync` / `AskAsync`). |
+| `ActorRequest` | Base record for request/response messages. |
+| `IActorRequest` | Request contract (implement directly if not using the base). |
+| `ActorOptions` | Capacity / overflow / error configuration. |
+
## Options
`ActorOptions` controls the mailbox:
@@ -62,22 +71,21 @@ using var sub = session.SubscribeToEventBus(eventBus, (UserJoinedEvent e) => new
| `Capacity` | bounded mailbox size | `1024` |
| `OverflowPolicy` | `Wait` / `DropNewest` / `Unbounded` | `Wait` |
| `ErrorPolicy` | `Isolate` / `StopOnError` | `Isolate`|
+| `ShutdownDrainTimeout` | any `TimeSpan` | `5s` |
- **Wait**: `TellAsync` awaits until capacity frees (back-pressure).
- **DropNewest**: `TellAsync` returns `false` when full.
- **Isolate**: a throwing handler is logged and skipped; the actor stays alive. `AskAsync` exceptions
always propagate to the caller regardless of policy.
-`DisposeAsync` completes the mailbox, drains in-flight work, and faults any still-pending requests.
+`DisposeAsync` completes the mailbox and drains queued messages — every `Tell` runs and every `Ask`
+replies — within `ShutdownDrainTimeout`. If a handler is still running when that budget elapses, the
+actor cancels its handlers and faults any requests that never completed with `ObjectDisposedException`.
-## Key types
+## Related
-| Type | Purpose |
-|---------------------------|----------------------------------------------------|
-| `Actor` | Mailbox base class (`TellAsync` / `AskAsync`). |
-| `ActorRequest` | Base record for request/response messages. |
-| `IActorRequest` | Request contract (implement directly if not using the base). |
-| `ActorOptions` | Capacity / overflow / error configuration. |
+- Tutorial: [Actors](https://tgiachi.github.io/squid-std/tutorials/actors.html)
+- Concept: [Messaging models](https://tgiachi.github.io/squid-std/articles/concepts/messaging-models.html)
## License
diff --git a/src/SquidStd.AspNetCore/README.md b/src/SquidStd.AspNetCore/README.md
index 2c621eb4..bf097116 100644
--- a/src/SquidStd.AspNetCore/README.md
+++ b/src/SquidStd.AspNetCore/README.md
@@ -1,16 +1,5 @@
-
-
-
-
SquidStd.AspNetCore
-
-
-
-
-
-
-
ASP.NET Core integration for SquidStd. A single `builder.UseSquidStd(...)` call wires the SquidStd
DryIoc container into the web host and registers a hosted service that starts and stops every
`ISquidStdService` alongside the application lifecycle.
@@ -21,15 +10,6 @@ DryIoc container into the web host and registers a hosted service that starts an
dotnet add package SquidStd.AspNetCore
```
-## Features
-
-- `WebApplicationBuilder.UseSquidStd(...)` — plug the SquidStd container and services into a web app.
-- Configures DryIoc as the host's service-provider factory.
-- Registers `SquidStdHostedService` to start/stop SquidStd services with the host.
-- Optional `SquidStdOptions` configuration callback.
-- `WebApplicationBuilder.AddSquidStdHealthChecks()` — bridges every SquidStd `IHealthCheck` into the standard ASP.NET Core
- health-check system (one entry per check), exposed via the standard `app.MapHealthChecks(...)`.
-
## Usage
```csharp
@@ -71,6 +51,10 @@ Each registered `IHealthCheck` appears as its own entry in the report. Check nam
| `SquidStdHostedService` | Hosted service bridging SquidStd service lifecycle to the host. |
| `SquidStdHealthChecksExtensions` | `AddSquidStdHealthChecks(...)` — bridge to ASP.NET Core health checks. |
+## Related
+
+- Tutorial: [Build an ASP.NET Core app](https://tgiachi.github.io/squid-std/tutorials/aspnetcore-app.html)
+
## License
MIT — part of [SquidStd](https://github.com/tgiachi/squid-std).
diff --git a/src/SquidStd.Aws.Abstractions/README.md b/src/SquidStd.Aws.Abstractions/README.md
index 311af5db..20cc1518 100644
--- a/src/SquidStd.Aws.Abstractions/README.md
+++ b/src/SquidStd.Aws.Abstractions/README.md
@@ -26,3 +26,13 @@ var aws = new AwsConfigEntry
When `AccessKey`/`SecretKey` are null, consumers fall back to the AWS default credential chain
(environment variables, shared profile, IAM role). When `ServiceUrl` is set, consumers point their
client at that endpoint instead of the regional AWS endpoint.
+
+## Key types
+
+| Type | Purpose |
+|------|---------|
+| `AwsConfigEntry` | Region, optional credentials and an optional endpoint override (e.g. LocalStack). |
+
+## License
+
+MIT — part of [SquidStd](https://github.com/tgiachi/squid-std).
diff --git a/src/SquidStd.Caching.Abstractions/README.md b/src/SquidStd.Caching.Abstractions/README.md
index 93d452fe..81fa611d 100644
--- a/src/SquidStd.Caching.Abstractions/README.md
+++ b/src/SquidStd.Caching.Abstractions/README.md
@@ -1,16 +1,5 @@
-
-
-
-
SquidStd.Caching.Abstractions
-
-
-
-
-
-
-
Backend-agnostic caching contracts for SquidStd. It defines the typed cache-aside facade
(`ICacheService`), the low-level byte provider/metrics contracts, and the shared `CacheService`
facade that applies key-prefixing, default TTL and cache-aside once over any provider. Pick a
@@ -22,14 +11,6 @@ backend implementation (in-memory or Redis) from a companion package.
dotnet add package SquidStd.Caching.Abstractions
```
-## Features
-
-- `ICacheService` — typed `GetAsync` / `SetAsync` / `RemoveAsync` / `ExistsAsync` / `GetOrSetAsync` facade.
-- `ICacheProvider` — the raw byte-level backend contract implemented per provider.
-- `CacheService` — shared facade that serializes values, applies the key prefix and default TTL, and implements cache-aside.
-- `ICacheMetrics` (+ `CacheMetricsProvider`, `NoOpCacheMetrics`) — hit/miss/set/remove metrics.
-- `CacheOptions` and `CacheConnectionString` — configuration and connection parsing.
-
## Usage
```csharp
@@ -51,6 +32,10 @@ public Task GetOrComputeAsync(ICacheService cache)
| `CacheOptions` | Default TTL and key prefix. |
| `CacheConnectionString` | `scheme://host[?params]` parsing into `CacheOptions`. |
+## Related
+
+- Tutorial: [Caching](https://tgiachi.github.io/squid-std/tutorials/caching.html)
+
## License
MIT — part of [SquidStd](https://github.com/tgiachi/squid-std).
diff --git a/src/SquidStd.Caching.Redis/README.md b/src/SquidStd.Caching.Redis/README.md
index ebaa1742..6359de0d 100644
--- a/src/SquidStd.Caching.Redis/README.md
+++ b/src/SquidStd.Caching.Redis/README.md
@@ -1,16 +1,5 @@
-
-
-
-
SquidStd.Caching.Redis
-
-
-
-
-
-
-
Redis backend for SquidStd.Caching. Implements `ICacheProvider` on top of StackExchange.Redis,
so the same `ICacheService` API reads and writes a real Redis server with native key expiry.
Registered with a single `AddRedisCache(...)` call.
@@ -21,14 +10,6 @@ Registered with a single `AddRedisCache(...)` call.
dotnet add package SquidStd.Caching.Redis
```
-## Features
-
-- One-line registration: `container.AddRedisCache(connectionString)` or with `RedisCacheOptions`.
-- Redis-backed `ICacheProvider` reusing the shared `ICacheService` facade and serializer.
-- Native TTL via Redis key expiry (`SET ... EX`).
-- Connection via a `redis://` connection string or an explicit StackExchange.Redis configuration string.
-- Built-in hit/miss metrics via `CacheMetricsProvider`.
-
## Usage
```csharp
@@ -52,6 +33,10 @@ var user = await cache.GetAsync