diff --git a/Directory.Build.props b/Directory.Build.props
index 4d3a6fc6..c93c3323 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -69,7 +69,7 @@
-
+
@@ -77,7 +77,7 @@
README.md
-
+
diff --git a/README.md b/README.md
index 306d9ab4..c4efbf33 100644
--- a/README.md
+++ b/README.md
@@ -87,6 +87,15 @@ block until cancellation for long-running hosts.
| `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.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/) |
+| `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/) |
## Architecture
diff --git a/SquidStd.slnx b/SquidStd.slnx
index 6d73a395..9188a0a0 100644
--- a/SquidStd.slnx
+++ b/SquidStd.slnx
@@ -1,29 +1,51 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
diff --git a/docs/articles/mail-abstractions.md b/docs/articles/mail-abstractions.md
new file mode 100644
index 00000000..c480642b
--- /dev/null
+++ b/docs/articles/mail-abstractions.md
@@ -0,0 +1 @@
+[!include[](../../src/SquidStd.Mail.Abstractions/README.md)]
diff --git a/docs/articles/mail-mailkit.md b/docs/articles/mail-mailkit.md
new file mode 100644
index 00000000..43c11c3a
--- /dev/null
+++ b/docs/articles/mail-mailkit.md
@@ -0,0 +1 @@
+[!include[](../../src/SquidStd.Mail.MailKit/README.md)]
diff --git a/docs/articles/mail-queue.md b/docs/articles/mail-queue.md
new file mode 100644
index 00000000..a23eecd3
--- /dev/null
+++ b/docs/articles/mail-queue.md
@@ -0,0 +1 @@
+[!include[](../../src/SquidStd.Mail.Queue/README.md)]
diff --git a/docs/articles/search-abstractions.md b/docs/articles/search-abstractions.md
new file mode 100644
index 00000000..34bd075b
--- /dev/null
+++ b/docs/articles/search-abstractions.md
@@ -0,0 +1 @@
+[!include[](../../src/SquidStd.Search.Abstractions/README.md)]
diff --git a/docs/articles/search-elasticsearch.md b/docs/articles/search-elasticsearch.md
new file mode 100644
index 00000000..bff4c934
--- /dev/null
+++ b/docs/articles/search-elasticsearch.md
@@ -0,0 +1 @@
+[!include[](../../src/SquidStd.Search.Elasticsearch/README.md)]
diff --git a/docs/articles/templates.md b/docs/articles/templates.md
new file mode 100644
index 00000000..8cbeaa7e
--- /dev/null
+++ b/docs/articles/templates.md
@@ -0,0 +1 @@
+[!include[](../../src/SquidStd.Templates/README.md)]
diff --git a/docs/articles/toc.yml b/docs/articles/toc.yml
index 280a0eb9..fcc51d45 100644
--- a/docs/articles/toc.yml
+++ b/docs/articles/toc.yml
@@ -44,6 +44,18 @@
href: workers.md
- name: SquidStd.Workers.Manager
href: workers-manager.md
+- name: SquidStd.Templates
+ href: templates.md
+- name: SquidStd.Search.Abstractions
+ href: search-abstractions.md
+- name: SquidStd.Search.Elasticsearch
+ href: search-elasticsearch.md
+- name: SquidStd.Mail.Abstractions
+ href: mail-abstractions.md
+- name: SquidStd.Mail.MailKit
+ href: mail-mailkit.md
+- name: SquidStd.Mail.Queue
+ href: mail-queue.md
- name: Serialization
href: serialization.md
- name: Scheduler
diff --git a/docs/docfx.json b/docs/docfx.json
index 8565ef16..26c8aa44 100644
--- a/docs/docfx.json
+++ b/docs/docfx.json
@@ -15,7 +15,7 @@
"build": {
"content": [
{ "files": [ "api/**.{yml,md}" ] },
- { "files": [ "articles/**.{md,yml}", "*.md", "toc.yml" ] }
+ { "files": [ "articles/**.{md,yml}", "tutorials/**.{md,yml}", "*.md", "toc.yml" ] }
],
"resource": [
{ "files": [ "images/**" ] }
diff --git a/docs/toc.yml b/docs/toc.yml
index 019419e8..f780a685 100644
--- a/docs/toc.yml
+++ b/docs/toc.yml
@@ -2,5 +2,7 @@
href: index.md
- name: Articles
href: articles/
+- name: Tutorials
+ href: tutorials/
- name: API
href: api/
diff --git a/docs/tutorials/aspnetcore-app.md b/docs/tutorials/aspnetcore-app.md
new file mode 100644
index 00000000..93f37afe
--- /dev/null
+++ b/docs/tutorials/aspnetcore-app.md
@@ -0,0 +1,55 @@
+# Host SquidStd in an ASP.NET Core app
+
+Wire SquidStd into an ASP.NET Core Minimal API and expose its health checks over HTTP.
+
+## What you'll build
+
+A Minimal API web app that uses SquidStd (`SquidStd.AspNetCore`) as its DryIoc-backed service provider, surfaces
+the registered SquidStd health checks at `/health`, and answers a simple root endpoint.
+
+## Prerequisites
+
+- .NET 10 SDK
+- `dotnet add package SquidStd.AspNetCore`
+
+## Steps
+
+### 1. Register SquidStd on the builder
+
+`UseSquidStd` swaps in DryIoc as the ASP.NET Core service provider and bootstraps SquidStd. The root directory is
+taken from the host environment automatically, so you only set the config name.
+
+[!code-csharp[](../../samples/SquidStd.Samples.AspNetCore/Program.cs#step-1)]
+
+### 2. Bridge the health checks
+
+`AddSquidStdHealthChecks` registers every SquidStd `IHealthCheck` as a standard ASP.NET Core health check. Call it
+after `UseSquidStd`.
+
+[!code-csharp[](../../samples/SquidStd.Samples.AspNetCore/Program.cs#step-2)]
+
+### 3. Build the app and map endpoints
+
+Build the app, expose the health checks with the standard `MapHealthChecks`, add a root endpoint, and run.
+
+[!code-csharp[](../../samples/SquidStd.Samples.AspNetCore/Program.cs#step-3)]
+
+## Run it
+
+```bash
+dotnet run --project samples/SquidStd.Samples.AspNetCore
+```
+
+Browse to `/` for the "SquidStd up" message and to `/health` for the aggregated health-check status.
+
+## How it works
+
+`UseSquidStd` creates an owned DryIoc container, registers it through `DryIocServiceProviderFactory`, and hooks the
+SquidStd lifecycle into the ASP.NET Core host via a hosted service. `AddSquidStdHealthChecks` resolves the SquidStd
+`IHealthCheck` instances from that container and adapts each one into the standard health-check pipeline, so
+`MapHealthChecks` reports them like any other ASP.NET Core check.
+
+## See also
+
+- [SquidStd.AspNetCore reference](../articles/aspnetcore.md)
+- Previous: [Getting started](getting-started.md)
diff --git a/docs/tutorials/caching.md b/docs/tutorials/caching.md
new file mode 100644
index 00000000..2d7d84c0
--- /dev/null
+++ b/docs/tutorials/caching.md
@@ -0,0 +1,52 @@
+# Caching (in-memory + Redis)
+
+Cache typed values with a TTL, then swap the in-memory backend for Redis without changing your code.
+
+## What you'll build
+
+A host that resolves `ICacheService` (`SquidStd.Caching.Abstractions`) backed by the in-memory provider
+(`SquidStd.Caching`), and the one-line change to use Redis (`SquidStd.Caching.Redis`) instead.
+
+## Prerequisites
+
+- .NET 10 SDK
+- `dotnet add package SquidStd.Caching` (and `SquidStd.Caching.Redis` for the Redis backend)
+- For Redis: `docker run -p 6379:6379 redis`
+
+## Steps
+
+### 1. Register the in-memory cache
+
+[!code-csharp[](../../samples/SquidStd.Samples.Caching/Program.cs#step-1)]
+
+### 2. Set and get typed values
+
+`ICacheService` stores typed values with an optional TTL; `GetAsync` returns `null` on a miss.
+
+[!code-csharp[](../../samples/SquidStd.Samples.Caching/Program.cs#step-2)]
+
+### 3. Switch to Redis
+
+Replace the registration with the Redis backend — the `ICacheService` usage is identical:
+
+```csharp
+container.AddRedisCache("redis://localhost:6379");
+```
+
+## Run it
+
+```bash
+dotnet run --project samples/SquidStd.Samples.Caching
+```
+
+Prints `hello`.
+
+## How it works
+
+`ICacheService` is the typed facade over an `ICacheProvider`; the provider is the swappable backend (in-memory or
+Redis). Values are serialized with the shared `IDataSerializer`, so the same code works on either backend.
+
+## See also
+
+- [SquidStd.Caching reference](../articles/caching.md)
+- [SquidStd.Caching.Redis reference](../articles/caching-redis.md)
diff --git a/docs/tutorials/database.md b/docs/tutorials/database.md
new file mode 100644
index 00000000..b1a86e10
--- /dev/null
+++ b/docs/tutorials/database.md
@@ -0,0 +1,52 @@
+# Database (FreeSql + SQLite)
+
+Persist a typed entity and read it back with paging, using SQLite with no external database.
+
+## What you'll build
+
+A host that registers the database subsystem (`SquidStd.Database`) and resolves the generic data access
+`IDataAccess` (`SquidStd.Database.Abstractions`) to insert and page over a simple entity. It runs against a
+local SQLite file out of the box.
+
+## Prerequisites
+
+- .NET 10 SDK
+- `dotnet add package SquidStd.Database`
+- No external database: the default connection string is `sqlite://squidstd.db`, and the schema is auto-migrated on
+ startup.
+
+## Steps
+
+### 1. Register the database service
+
+`RegisterDatabase` wires the `database` config section, the `IDatabaseService` (which owns the FreeSql instance), and
+the open-generic `IDataAccess<>`. Starting the bootstrap starts the database service and auto-creates the schema.
+
+[!code-csharp[](../../samples/SquidStd.Samples.Database/Program.cs#step-1)]
+
+### 2. Insert and page over an entity
+
+Resolve `IDataAccess` for any `BaseEntity`-derived type. `InsertAsync` assigns the id and timestamps;
+`GetPagedAsync` returns a `PagedResultData` with items and paging metadata.
+
+[!code-csharp[](../../samples/SquidStd.Samples.Database/Program.cs#step-2)]
+
+## Run it
+
+```bash
+dotnet run --project samples/SquidStd.Samples.Database
+```
+
+Prints the two inserted products ordered by price, with the total count and page metadata.
+
+## How it works
+
+`IDatabaseService` builds a single FreeSql ORM from the URI-style connection string (the scheme selects the provider:
+`sqlite`, `postgres`, `mysql`, `sqlserver`). `IDataAccess` is the generic repository over any
+`BaseEntity`: it exposes CRUD, bulk operations, a composable `Query()` (FreeSql `ISelect`), and `GetPagedAsync` for
+page + total-count reads. Every entity gets a `Guid` id plus UTC `Created`/`Updated` timestamps from `BaseEntity`.
+
+## See also
+
+- [SquidStd.Database reference](../articles/database.md)
+- [SquidStd.Database.Abstractions reference](../articles/database-abstractions.md)
diff --git a/docs/tutorials/email.md b/docs/tutorials/email.md
new file mode 100644
index 00000000..16fae30d
--- /dev/null
+++ b/docs/tutorials/email.md
@@ -0,0 +1,60 @@
+# Email: receive, send, and queue
+
+Poll a mailbox for inbound mail, send outbound mail over SMTP, and queue mail for background delivery.
+
+## What you'll build
+
+A host using `SquidStd.Mail.MailKit` and `SquidStd.Mail.Queue`: an IMAP poller that raises a `MailReceivedEvent`
+for each new email, an SMTP sender (`IMailSender`), and a fire-and-forget send queue (`IMailQueue`) backed by
+messaging. The contracts live in `SquidStd.Mail.Abstractions`.
+
+## Prerequisites
+
+- .NET 10 SDK
+- `dotnet add package SquidStd.Mail.MailKit`
+- `dotnet add package SquidStd.Mail.Queue`
+- Real IMAP/SMTP credentials are only needed to actually connect; the sample compiles and runs without them.
+
+## Steps
+
+### 1. Receive: poll a mailbox
+
+`AddMail` registers an IMAP/POP3 poller; each received message is published as a `MailReceivedEvent`, which an
+`IAsyncEventListener` handles.
+
+[!code-csharp[](../../samples/SquidStd.Samples.Email/Program.cs#step-1)]
+
+### 2. Send: outbound SMTP
+
+`AddMailSender` registers `IMailSender`; build an `OutgoingMailMessage` and call `SendAsync`.
+
+[!code-csharp[](../../samples/SquidStd.Samples.Email/Program.cs#step-2)]
+
+### 3. Queue: background delivery
+
+`AddInMemoryMessaging` plus `AddMailQueue` register `IMailQueue`; `EnqueueAsync` hands the message to a
+background consumer that sends it via the SMTP sender.
+
+[!code-csharp[](../../samples/SquidStd.Samples.Email/Program.cs#step-3)]
+
+## Run it
+
+```bash
+dotnet run --project samples/SquidStd.Samples.Email
+```
+
+It registers the poller, sender, and queue, registers the received-mail listener, and enqueues a message.
+Pass `--send` to also attempt a synchronous SMTP send (requires a reachable server).
+
+## How it works
+
+The poller runs on the timer wheel, fetches new messages over IMAP/POP3 with MailKit, and publishes a
+`MailReceivedEvent` on the event bus. `IMailSender` maps `OutgoingMailMessage` to a MIME message and sends it.
+`AddMailQueue` layers a queue over `SquidStd.Messaging`: `EnqueueAsync` publishes to the queue and a background
+consumer (`MailSendConsumerService`) drains it through `IMailSender`, decoupling request latency from delivery.
+
+## See also
+
+- [SquidStd.Mail.MailKit reference](../articles/mail-mailkit.md)
+- [SquidStd.Mail.Queue reference](../articles/mail-queue.md)
+- [SquidStd.Mail.Abstractions reference](../articles/mail-abstractions.md)
diff --git a/docs/tutorials/events-jobs-scheduling.md b/docs/tutorials/events-jobs-scheduling.md
new file mode 100644
index 00000000..80f3a594
--- /dev/null
+++ b/docs/tutorials/events-jobs-scheduling.md
@@ -0,0 +1,58 @@
+# Events, jobs and scheduling
+
+Publish in-process events, run work on a thread pool, and schedule recurring jobs with cron.
+
+## What you'll build
+
+A host that uses three core services from `SquidStd.Services.Core`: the `IEventBus` (publish/subscribe), the
+`IJobSystem` (thread-pool work), and the `ICronScheduler` (cron-based recurring jobs).
+
+## Prerequisites
+
+- .NET 10 SDK
+- `dotnet add package SquidStd.Services.Core`
+
+The cron scheduler and timer wheel are opt-in — enable them with `RegisterSchedulerServices()`:
+
+[!code-csharp[](../../samples/SquidStd.Samples.EventsJobsScheduling/Program.cs#step-1)]
+
+## Steps
+
+### 1. Publish and handle an event
+
+Implement `IAsyncEventListener`, register it, and publish an `IEvent`. Every registered listener is awaited.
+
+[!code-csharp[](../../samples/SquidStd.Samples.EventsJobsScheduling/Program.cs#step-1)]
+
+### 2. Run work on the job system
+
+`IJobSystem.ScheduleAsync` runs an `Action` on a worker thread and returns a `Task` that completes when it finishes.
+
+[!code-csharp[](../../samples/SquidStd.Samples.EventsJobsScheduling/Program.cs#step-2)]
+
+### 3. Schedule a cron job
+
+`ICronScheduler.Schedule` takes a name, a 5-field cron expression (UTC), and an async handler invoked on each
+occurrence.
+
+[!code-csharp[](../../samples/SquidStd.Samples.EventsJobsScheduling/Program.cs#step-3)]
+
+## Run it
+
+```bash
+dotnet run --project samples/SquidStd.Samples.EventsJobsScheduling
+```
+
+You'll see `received: hello`, `job ran on a worker thread`, and—if you let it run across a 5-minute boundary—
+`cron tick`.
+
+## How it works
+
+The event bus dispatches to sync and async listeners; the job system is a fixed-size worker-thread pool; the cron
+scheduler is driven by the timer wheel (registered by `RegisterSchedulerServices`).
+
+## See also
+
+- [SquidStd.Services.Core reference](../articles/services-core.md)
+- [SquidStd.Core reference](../articles/core.md)
+- Previous: [Getting started](getting-started.md)
diff --git a/docs/tutorials/getting-started.md b/docs/tutorials/getting-started.md
new file mode 100644
index 00000000..efbc9def
--- /dev/null
+++ b/docs/tutorials/getting-started.md
@@ -0,0 +1,56 @@
+# Getting started: your first SquidStd host
+
+Build a minimal SquidStd application: a bootstrapper that loads config, configures logging, and runs the
+registered services until shutdown.
+
+## What you'll build
+
+A console host on top of `SquidStdBootstrap` (from `SquidStd.Services.Core`, which brings in `SquidStd.Core`
+and `SquidStd.Abstractions`). It loads its config, wires the default services, and runs until you stop it.
+
+## Prerequisites
+
+- .NET 10 SDK
+- `dotnet add package SquidStd.Services.Core`
+
+## Steps
+
+### 1. Create the bootstrapper
+
+`SquidStdBootstrap.Create` takes a `SquidStdOptions` (config name + root directory) and registers the core
+services (config, logging, event bus, jobs, timers…) into an owned DryIoc container.
+
+[!code-csharp[](../../samples/SquidStd.Samples.GettingStarted/Program.cs#step-1)]
+
+### 2. Start the services
+
+`StartAsync` starts every registered `ISquidStdService` in priority order.
+
+[!code-csharp[](../../samples/SquidStd.Samples.GettingStarted/Program.cs#step-2)]
+
+### 3. Run until shutdown
+
+`RunAsync` starts (if not already started), waits for cancellation, then stops the services cleanly. Use either
+`StartAsync` plus your own loop, or just `RunAsync`.
+
+[!code-csharp[](../../samples/SquidStd.Samples.GettingStarted/Program.cs#step-3)]
+
+## Run it
+
+```bash
+dotnet run --project samples/SquidStd.Samples.GettingStarted
+```
+
+The host starts, logs the service lifecycle, and waits until you press Ctrl+C.
+
+## How it works
+
+`SquidStdBootstrap` is the composition root: it builds the container, registers the core services, loads the
+config sections, and orchestrates the `ISquidStdService` lifecycle. Every other SquidStd module plugs into this
+container through its `Add…` extension methods.
+
+## See also
+
+- [SquidStd.Services.Core reference](../articles/services-core.md)
+- [SquidStd.Core reference](../articles/core.md)
+- Next: [Events, jobs and scheduling](events-jobs-scheduling.md)
diff --git a/docs/tutorials/index.md b/docs/tutorials/index.md
new file mode 100644
index 00000000..3ab9e45e
--- /dev/null
+++ b/docs/tutorials/index.md
@@ -0,0 +1,29 @@
+# Tutorials
+
+Task-oriented, step-by-step guides. For per-type API details, see the Articles and
+API sections.
+
+## Fundamentals
+- [Getting started: host, config, logging](getting-started.md)
+- [Events, jobs and scheduling](events-jobs-scheduling.md)
+- [ASP.NET apps and health checks](aspnetcore-app.md)
+
+## Data & storage
+- [Caching (in-memory + Redis)](caching.md)
+- [Storage (files & objects, local + S3)](storage.md)
+- [Database access](database.md)
+
+## Messaging & workers
+- [Messaging: queues & pub/sub](messaging.md)
+- [A worker system end-to-end](worker-system.md)
+
+## Mail & search
+- [Email: receive, send, queue](email.md)
+- [Indexing and search with Elasticsearch](search.md)
+
+## Tooling & extras
+- [Templating with Scriban](templating.md)
+- [Lua scripting](scripting-lua.md)
+- [TCP networking](networking.md)
+- [Plugins](plugins.md)
+- [Scaffolding with `dotnet new`](scaffolding.md)
diff --git a/docs/tutorials/messaging.md b/docs/tutorials/messaging.md
new file mode 100644
index 00000000..b69535a4
--- /dev/null
+++ b/docs/tutorials/messaging.md
@@ -0,0 +1,51 @@
+# Messaging: queues and pub/sub
+
+Send work to a competing-consumers queue, and broadcast events to a fan-out topic.
+
+## What you'll build
+
+A host using `SquidStd.Messaging`: an `IMessageQueue` (one consumer handles each message) and an `IMessageTopic`
+(every subscriber receives each message). The same APIs work over RabbitMQ via `SquidStd.Messaging.RabbitMq`.
+
+## Prerequisites
+
+- .NET 10 SDK
+- `dotnet add package SquidStd.Messaging` (or `SquidStd.Messaging.RabbitMq`)
+
+## Steps
+
+### 1. Register in-memory messaging
+
+[!code-csharp[](../../samples/SquidStd.Samples.Messaging/Program.cs#step-1)]
+
+### 2. Queue: competing consumers
+
+`Subscribe` a listener and `PublishAsync` to a named queue. Each message is handled by exactly one consumer.
+
+[!code-csharp[](../../samples/SquidStd.Samples.Messaging/Program.cs#step-2)]
+
+### 3. Topic: fan-out pub/sub
+
+A topic delivers each published message to every current subscriber.
+
+[!code-csharp[](../../samples/SquidStd.Samples.Messaging/Program.cs#step-3)]
+
+## Run it
+
+```bash
+dotnet run --project samples/SquidStd.Samples.Messaging
+```
+
+Prints `queue handled order-1` and `topic saw order-2`.
+
+## How it works
+
+Queues use competing-consumers with retry and dead-lettering (`MessagingOptions`); topics use transient fan-out
+(at-most-once). Swap `AddInMemoryMessaging()` for `AddRabbitMqMessaging(...)` for a durable broker — the
+`IMessageQueue`/`IMessageTopic` code is unchanged.
+
+## See also
+
+- [SquidStd.Messaging reference](../articles/messaging.md)
+- [SquidStd.Messaging.RabbitMq reference](../articles/messaging-rabbitmq.md)
+- Related: [A worker system end-to-end](worker-system.md)
diff --git a/docs/tutorials/networking.md b/docs/tutorials/networking.md
new file mode 100644
index 00000000..b125208a
--- /dev/null
+++ b/docs/tutorials/networking.md
@@ -0,0 +1,51 @@
+# TCP networking (server + client)
+
+Stand up a TCP server, subscribe to its lifecycle and data events, then round-trip a message from a client.
+
+## What you'll build
+
+A `SquidTcpServer` (`SquidStd.Network`) bound to a loopback endpoint that logs client connections and received
+payloads, plus a `SquidStdTcpClient` that connects and sends bytes. The server run is guarded behind `--run` so
+`dotnet run` returns immediately by default.
+
+## Prerequisites
+
+- .NET 10 SDK
+- `dotnet add package SquidStd.Network`
+- A free local TCP port (the sample uses 9099 on loopback)
+
+## Steps
+
+### 1. Create the server and subscribe to events
+
+Construct `SquidTcpServer` with an `IPEndPoint`, then subscribe to `OnClientConnect` and `OnDataReceived`. The data
+event carries the source client and the payload as `ReadOnlyMemory`.
+
+[!code-csharp[](../../samples/SquidStd.Samples.Networking/Program.cs#step-1)]
+
+### 2. Start the server and connect a client
+
+`StartAsync` begins accepting connections. `SquidStdTcpClient.ConnectAsync` opens an outbound connection and starts
+its receive loop; `SendAsync` writes the payload, which surfaces on the server's `OnDataReceived`.
+
+[!code-csharp[](../../samples/SquidStd.Samples.Networking/Program.cs#step-2)]
+
+## Run it
+
+```bash
+dotnet run --project samples/SquidStd.Samples.Networking -- --run
+```
+
+Without `--run` the program just prints a hint and exits. With `--run` it prints the connecting session and the
+received message (`hello squid`).
+
+## How it works
+
+`SquidTcpServer` recreates its listening socket on every `StartAsync`, so Stop/Start cycles are supported. Each
+accepted socket is wrapped in a `SquidStdTcpClient` whose events (`OnConnected`, `OnDataReceived`, `OnDisconnected`,
+`OnException`) are re-raised by the server. An optional `INetFramer` lets you emit one `OnDataReceived` per logical
+frame instead of per socket read, and `INetMiddleware` components can transform inbound and outbound payloads.
+
+## See also
+
+- [SquidStd.Network reference](../articles/network.md)
diff --git a/docs/tutorials/plugins.md b/docs/tutorials/plugins.md
new file mode 100644
index 00000000..1cff0815
--- /dev/null
+++ b/docs/tutorials/plugins.md
@@ -0,0 +1,54 @@
+# Plugins
+
+Implement the SquidStd plugin contract and have a plugin register its own services into the container.
+
+## What you'll build
+
+A console app that defines a plugin implementing `ISquidStdPlugin` (`SquidStd.Plugin.Abstractions`), describes
+it with `PluginMetadata`, and calls `Configure` to register a service that the host then resolves.
+
+## Prerequisites
+
+- .NET 10 SDK
+- `dotnet add package SquidStd.Plugin.Abstractions`
+
+## Steps
+
+### 1. Implement the plugin contract
+
+A plugin exposes a `PluginMetadata` (its identity and dependencies) and a `Configure` method that registers its
+services, handlers, and integrations into the DryIoc container.
+
+[!code-csharp[](../../samples/SquidStd.Samples.Plugins/Program.cs#step-1)]
+
+### 2. Describe, configure, and resolve
+
+The host builds a `PluginContext` for boot-time data, invokes `Configure` on the plugin, and then resolves the
+services the plugin registered.
+
+[!code-csharp[](../../samples/SquidStd.Samples.Plugins/Program.cs#step-2)]
+
+## Run it
+
+```bash
+dotnet run --project samples/SquidStd.Samples.Plugins
+```
+
+Prints:
+
+```
+Loading Weather Plugin v1.0.0 by SquidStd Samples
+Hello squid, the weather plugin is online.
+```
+
+## How it works
+
+`ISquidStdPlugin` is the contract a host loads: `Metadata` is the source of truth for plugin identity and load
+order (`Dependencies`), while `Configure(IContainer, PluginContext)` is called during container configuration so
+the plugin can register everything it contributes. `PluginContext.Data` carries host-supplied boot values into
+the plugin. A real host discovers plugin assemblies and calls `Configure` in dependency order; this sample wires
+one plugin by hand so the contract is fully runnable without external assemblies.
+
+## See also
+
+- [SquidStd.Plugin.Abstractions reference](../articles/plugin-abstractions.md)
diff --git a/docs/tutorials/scaffolding.md b/docs/tutorials/scaffolding.md
new file mode 100644
index 00000000..d852beb3
--- /dev/null
+++ b/docs/tutorials/scaffolding.md
@@ -0,0 +1,68 @@
+# Scaffolding projects with `dotnet new`
+
+Generate ready-to-run SquidStd projects from the command line with the `SquidStd.Templates` pack.
+
+## What you'll build
+
+Nothing by hand — you'll install the template pack and scaffold a console host, an ASP.NET app, a worker
+microservice, and a worker manager, each pre-wired to SquidStd.
+
+## Prerequisites
+
+- .NET 10 SDK
+
+## Steps
+
+### 1. Install the template pack
+
+```bash
+dotnet new install SquidStd.Templates
+```
+
+This registers the SquidStd templates; list them with `dotnet new list squidstd`.
+
+### 2. Scaffold a project
+
+```bash
+# console host
+dotnet new squidstd-host -n Acme.Host
+
+# ASP.NET minimal API (UseSquidStd + health checks)
+dotnet new squidstd-aspnetcore -n Acme.Api
+
+# worker microservice (consumes jobs, publishes heartbeats)
+dotnet new squidstd-worker -n Acme.Worker
+
+# worker manager (enqueue jobs + ASP.NET endpoints)
+dotnet new squidstd-manager -n Acme.Manager
+```
+
+### 3. Pick a messaging transport (worker / manager)
+
+The `squidstd-worker` and `squidstd-manager` templates accept `--messaging`:
+
+```bash
+dotnet new squidstd-worker -n Acme.Worker --messaging rabbitmq # default
+dotnet new squidstd-worker -n Acme.Worker --messaging inmemory
+```
+
+## Run it
+
+```bash
+cd Acme.Worker
+dotnet run
+```
+
+The generated project references the SquidStd packages at the same version as the template pack and is ready to
+build and run.
+
+## How it works
+
+`SquidStd.Templates` is a `dotnet new` template pack. Each template ships a runnable project (with a `Dockerfile`
+for the service templates) whose `PackageReference` versions are injected to match the pack, so a scaffolded app
+always lines up with the libraries it targets.
+
+## See also
+
+- [SquidStd.Templates reference](../articles/templates.md)
+- Related: [A worker system end-to-end](worker-system.md)
diff --git a/docs/tutorials/scripting-lua.md b/docs/tutorials/scripting-lua.md
new file mode 100644
index 00000000..5b231783
--- /dev/null
+++ b/docs/tutorials/scripting-lua.md
@@ -0,0 +1,54 @@
+# Scripting with Lua
+
+Embed a Lua engine in your host, expose C# values to scripts, and read results back.
+
+## What you'll build
+
+A console host that registers the `IScriptEngineService` (`SquidStd.Scripting.Lua`) through the SquidStd
+bootstrap, runs a small Lua script, and prints values evaluated by the engine.
+
+## Prerequisites
+
+- .NET 10 SDK
+- `dotnet add package SquidStd.Scripting.Lua`
+- `dotnet add package SquidStd.Services.Core`
+
+## Steps
+
+### 1. Register the Lua engine
+
+The engine needs a `LuaEngineConfig` (it watches a scripts directory) and is started by the bootstrap as a
+SquidStd service. The `DirectoriesConfig` it depends on is already registered by the core services.
+
+[!code-csharp[](../../samples/SquidStd.Samples.ScriptingLua/Program.cs#step-1)]
+
+### 2. Run a script and read results
+
+`RegisterGlobal` exposes a C# value under an exact name, `ExecuteScript` runs Lua code, and
+`ExecuteFunction` evaluates an expression and returns a `ScriptResult` whose `Data` holds the value.
+
+[!code-csharp[](../../samples/SquidStd.Samples.ScriptingLua/Program.cs#step-2)]
+
+## Run it
+
+```bash
+dotnet run --project samples/SquidStd.Samples.ScriptingLua
+```
+
+Prints:
+
+```
+3 + 4 = 7
+result = hello from C# and lua
+```
+
+## How it works
+
+`IScriptEngineService` wraps a MoonSharp `Script`. Registering it with `RegisterStdService` lets the bootstrap
+call its `StartAsync` during `StartAsync`, which wires up modules, constants, and a file watcher over the
+configured scripts directory. Globals you register become Lua variables, and `ExecuteFunction` evaluates a
+`return ` and surfaces the value through `ScriptResult.Data`.
+
+## See also
+
+- [SquidStd.Scripting.Lua reference](../articles/scripting-lua.md)
diff --git a/docs/tutorials/search.md b/docs/tutorials/search.md
new file mode 100644
index 00000000..b5cfd683
--- /dev/null
+++ b/docs/tutorials/search.md
@@ -0,0 +1,60 @@
+# Search: index and query with Elasticsearch
+
+Index typed documents and query them with LINQ, translated to the Elasticsearch query DSL.
+
+## What you'll build
+
+A host using `SquidStd.Search.Elasticsearch`: register the provider, mark a record as indexable, then index a
+document and query it back with a strongly-typed `IQueryable`. The contracts live in
+`SquidStd.Search.Abstractions`, so your code stays decoupled from the Elasticsearch client.
+
+## Prerequisites
+
+- .NET 10 SDK
+- `dotnet add package SquidStd.Search.Elasticsearch`
+- A running Elasticsearch (only needed to actually execute queries):
+
+ ```bash
+ docker run -p 9200:9200 -e discovery.type=single-node -e xpack.security.enabled=false elasticsearch:8.6.1
+ ```
+
+## Steps
+
+### 1. Register the Elasticsearch provider
+
+`AddElasticsearch` registers the client and `ISearchService` against your cluster endpoint.
+
+[!code-csharp[](../../samples/SquidStd.Samples.Search/Program.cs#step-1)]
+
+### 2. Define an indexable document
+
+Implement `IIndexableEntity` (supplying the document id) and tag the type with `[SearchIndex("orders")]`.
+
+[!code-csharp[](../../samples/SquidStd.Samples.Search/Program.cs#step-2)]
+
+### 3. Index and query
+
+`IndexAsync` stores a document; `Query()` returns a LINQ surface whose async terminals
+(`ToListAsync`, `CountAsync`, `FirstOrDefaultAsync`) execute against the cluster.
+
+[!code-csharp[](../../samples/SquidStd.Samples.Search/Program.cs#step-3)]
+
+## Run it
+
+```bash
+dotnet run --project samples/SquidStd.Samples.Search
+```
+
+With Elasticsearch running it prints `found 1 open order(s)`.
+
+## How it works
+
+`ISearchService.Query()` returns a constrained `IQueryable` backed by a custom provider. The expression
+tree is translated to the Elasticsearch query DSL by `ElasticExpressionTranslator`; the async terminals and the
+`.Match`/`.FullText` markers come from `SquidStd.Search.Elasticsearch.Linq`. The target index is resolved from
+the `[SearchIndex]` attribute (with `${VAR}` expansion) or the lowercased type name.
+
+## See also
+
+- [SquidStd.Search.Elasticsearch reference](../articles/search-elasticsearch.md)
+- [SquidStd.Search.Abstractions reference](../articles/search-abstractions.md)
diff --git a/docs/tutorials/storage.md b/docs/tutorials/storage.md
new file mode 100644
index 00000000..9be3be47
--- /dev/null
+++ b/docs/tutorials/storage.md
@@ -0,0 +1,52 @@
+# Object storage (file system + S3)
+
+Save and load typed objects by key on the local file system, then swap to S3 without changing your code.
+
+## What you'll build
+
+A host that resolves `IObjectStorageService` (`SquidStd.Storage.Abstractions`) backed by the file-system provider
+(`SquidStd.Storage`), and the one-line change to use S3 instead.
+
+## Prerequisites
+
+- .NET 10 SDK
+- `dotnet add package SquidStd.Storage` (and `SquidStd.Storage.S3` for the S3 backend)
+- For S3: an S3-compatible endpoint and bucket (e.g. AWS S3 or MinIO)
+
+## Steps
+
+### 1. Register file storage
+
+[!code-csharp[](../../samples/SquidStd.Samples.Storage/Program.cs#step-1)]
+
+### 2. Save and load a typed object
+
+`IObjectStorageService` stores typed objects by logical key; `LoadAsync` returns `null` on a miss.
+
+[!code-csharp[](../../samples/SquidStd.Samples.Storage/Program.cs#step-2)]
+
+### 3. Switch to S3
+
+Replace the registration with the S3 backend — the `IObjectStorageService` usage is identical:
+
+```csharp
+container.AddS3Storage(/* endpoint, bucket and credentials */);
+```
+
+## Run it
+
+```bash
+dotnet run --project samples/SquidStd.Samples.Storage
+```
+
+Prints `squid `.
+
+## How it works
+
+`IObjectStorageService` is the typed facade over a swappable storage backend (local file system or S3). Values are
+serialized with the shared `IDataSerializer`, so the same `SaveAsync`/`LoadAsync` code works on either backend.
+
+## See also
+
+- [SquidStd.Storage reference](../articles/storage.md)
+- [SquidStd.Storage.S3 reference](../articles/storage-s3.md)
diff --git a/docs/tutorials/templating.md b/docs/tutorials/templating.md
new file mode 100644
index 00000000..65b389ac
--- /dev/null
+++ b/docs/tutorials/templating.md
@@ -0,0 +1,51 @@
+# Templating (Scriban)
+
+Render Scriban templates from inline strings or registered named templates against a model.
+
+## What you'll build
+
+A host that resolves `ITemplateRenderer` (`SquidStd.Templating`), renders an inline template against a model, then
+registers a named template and renders it by name.
+
+## Prerequisites
+
+- .NET 10 SDK
+- `dotnet add package SquidStd.Templating`
+- No external infrastructure required
+
+## Steps
+
+### 1. Register templating
+
+[!code-csharp[](../../samples/SquidStd.Samples.Templating/Program.cs#step-1)]
+
+### 2. Render an inline template
+
+`RenderAsync` compiles and renders an ad-hoc template string against a model; properties are resolved with the
+Scriban `{{ ... }}` syntax.
+
+[!code-csharp[](../../samples/SquidStd.Samples.Templating/Program.cs#step-2)]
+
+### 3. Register and render a named template
+
+`Register` compiles and caches a template under a name; `RenderByNameAsync` renders it later without recompiling.
+
+[!code-csharp[](../../samples/SquidStd.Samples.Templating/Program.cs#step-3)]
+
+## Run it
+
+```bash
+dotnet run --project samples/SquidStd.Samples.Templating
+```
+
+Prints `Hi squid!` then `Welcome aboard, squid.`.
+
+## How it works
+
+`ITemplateRenderer` wraps the Scriban engine: `RenderAsync` handles one-off templates, while `Register` plus
+`RenderByNameAsync` lets you compile a template once and reuse it by name. The model is bound by property name using
+Scriban's case-insensitive member access.
+
+## See also
+
+- [SquidStd.Templating reference](../articles/templating.md)
diff --git a/docs/tutorials/toc.yml b/docs/tutorials/toc.yml
new file mode 100644
index 00000000..f6942f09
--- /dev/null
+++ b/docs/tutorials/toc.yml
@@ -0,0 +1,32 @@
+- name: Overview
+ href: index.md
+- name: Getting started
+ href: getting-started.md
+- name: Events, jobs and scheduling
+ href: events-jobs-scheduling.md
+- name: Caching
+ href: caching.md
+- name: Messaging
+ href: messaging.md
+- name: ASP.NET apps and health checks
+ href: aspnetcore-app.md
+- name: Storage
+ href: storage.md
+- name: Database access
+ href: database.md
+- name: A worker system end-to-end
+ href: worker-system.md
+- name: Email
+ href: email.md
+- name: Indexing and search
+ href: search.md
+- name: Templating
+ href: templating.md
+- name: Lua scripting
+ href: scripting-lua.md
+- name: TCP networking
+ href: networking.md
+- name: Plugins
+ href: plugins.md
+- name: Scaffolding with dotnet new
+ href: scaffolding.md
diff --git a/docs/tutorials/worker-system.md b/docs/tutorials/worker-system.md
new file mode 100644
index 00000000..b66d2577
--- /dev/null
+++ b/docs/tutorials/worker-system.md
@@ -0,0 +1,65 @@
+# Build a worker system
+
+Run a background job system end to end: register workers, enqueue a job through the manager, and handle it.
+
+## What you'll build
+
+A console host that wires in-memory messaging, the worker runtime (`SquidStd.Workers`), and the worker manager
+(`SquidStd.Workers.Manager`) on top of `SquidStdBootstrap`. It enqueues a `greet` job and a handler picks it up
+and prints a greeting.
+
+## Prerequisites
+
+- .NET 10 SDK
+- `dotnet add package SquidStd.Workers`
+- `dotnet add package SquidStd.Workers.Manager`
+- `dotnet add package SquidStd.Messaging`
+- `dotnet add package SquidStd.Services.Core`
+
+## Steps
+
+### 1. Register messaging, workers and the manager
+
+`ConfigureServices` runs against the bootstrap's DryIoc container. Add the in-memory queue, the worker runtime, a
+job handler, and the manager (which provides the job scheduler).
+
+[!code-csharp[](../../samples/SquidStd.Samples.WorkerSystem/Program.cs#step-1)]
+
+### 2. Implement the job handler
+
+A handler implements `IJobHandler`: it declares the `JobName` it processes and does the work in `HandleAsync`,
+reading any string parameters off the `JobRequest`.
+
+[!code-csharp[](../../samples/SquidStd.Samples.WorkerSystem/Program.cs#step-2)]
+
+### 3. Start, enqueue, and stop
+
+Start the host, resolve the `IJobScheduler`, enqueue a `greet` job with a parameter, give the worker a moment to
+consume it, then stop cleanly.
+
+[!code-csharp[](../../samples/SquidStd.Samples.WorkerSystem/Program.cs#step-3)]
+
+## Run it
+
+```bash
+dotnet run --project samples/SquidStd.Samples.WorkerSystem
+```
+
+You'll see `Hello, squid! (job: greet)` once the worker consumes the enqueued job.
+
+When hosting inside an ASP.NET Core app, you can instead expose the manager over HTTP with
+`app.MapWorkerManagerEndpoints()`, which maps `GET /workers`, `GET /workers/{id}`, and `POST /jobs`.
+
+## How it works
+
+The manager's `IJobScheduler` publishes a `JobRequest` onto the messaging queue. The worker runtime's consumer
+service reads the queue, the dispatcher routes each request to the `IJobHandler` whose `JobName` matches, and the
+handler runs the work. Messaging, workers, and the manager are independent modules that compose through the shared
+DryIoc container.
+
+## See also
+
+- [SquidStd.Workers reference](../articles/workers.md)
+- [SquidStd.Workers.Manager reference](../articles/workers-manager.md)
+- [SquidStd.Workers.Abstractions reference](../articles/workers-abstractions.md)
+- Previous: [Events, jobs and scheduling](events-jobs-scheduling.md)
diff --git a/samples/SquidStd.Samples.AspNetCore/Program.cs b/samples/SquidStd.Samples.AspNetCore/Program.cs
new file mode 100644
index 00000000..2ec4105d
--- /dev/null
+++ b/samples/SquidStd.Samples.AspNetCore/Program.cs
@@ -0,0 +1,18 @@
+using SquidStd.AspNetCore.Extensions;
+
+var builder = WebApplication.CreateBuilder(args);
+
+#region step-1
+builder.UseSquidStd(o => o.ConfigName = "squidstd");
+#endregion
+
+#region step-2
+builder.AddSquidStdHealthChecks();
+#endregion
+
+#region step-3
+var app = builder.Build();
+app.MapHealthChecks("/health");
+app.MapGet("/", () => "SquidStd up");
+app.Run();
+#endregion
diff --git a/samples/SquidStd.Samples.AspNetCore/SquidStd.Samples.AspNetCore.csproj b/samples/SquidStd.Samples.AspNetCore/SquidStd.Samples.AspNetCore.csproj
new file mode 100644
index 00000000..6183042e
--- /dev/null
+++ b/samples/SquidStd.Samples.AspNetCore/SquidStd.Samples.AspNetCore.csproj
@@ -0,0 +1,18 @@
+
+
+
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/SquidStd.Samples.Caching/Program.cs b/samples/SquidStd.Samples.Caching/Program.cs
new file mode 100644
index 00000000..a5c3baf2
--- /dev/null
+++ b/samples/SquidStd.Samples.Caching/Program.cs
@@ -0,0 +1,27 @@
+using SquidStd.Caching.Abstractions.Interfaces;
+using SquidStd.Caching.Extensions;
+using SquidStd.Core.Data.Bootstrap;
+using SquidStd.Services.Core.Services.Bootstrap;
+
+var bootstrap = SquidStdBootstrap.Create(new SquidStdOptions
+{
+ ConfigName = "squidstd",
+ RootDirectory = AppContext.BaseDirectory
+});
+
+#region step-1
+bootstrap.ConfigureServices(container => container.AddInMemoryCache());
+#endregion
+
+await bootstrap.StartAsync();
+
+#region step-2
+var cache = bootstrap.Resolve();
+
+await cache.SetAsync("greeting", "hello", TimeSpan.FromMinutes(5));
+var value = await cache.GetAsync("greeting");
+
+Console.WriteLine(value);
+#endregion
+
+await bootstrap.StopAsync();
diff --git a/samples/SquidStd.Samples.Caching/SquidStd.Samples.Caching.csproj b/samples/SquidStd.Samples.Caching/SquidStd.Samples.Caching.csproj
new file mode 100644
index 00000000..370bfaed
--- /dev/null
+++ b/samples/SquidStd.Samples.Caching/SquidStd.Samples.Caching.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
diff --git a/samples/SquidStd.Samples.Database/Program.cs b/samples/SquidStd.Samples.Database/Program.cs
new file mode 100644
index 00000000..d2adee30
--- /dev/null
+++ b/samples/SquidStd.Samples.Database/Program.cs
@@ -0,0 +1,45 @@
+using SquidStd.Core.Data.Bootstrap;
+using SquidStd.Database.Abstractions.Data.Entities;
+using SquidStd.Database.Abstractions.Interfaces.Data;
+using SquidStd.Database.Extensions;
+using SquidStd.Services.Core.Services.Bootstrap;
+
+var bootstrap = SquidStdBootstrap.Create(new SquidStdOptions
+{
+ ConfigName = "squidstd",
+ RootDirectory = AppContext.BaseDirectory
+});
+
+#region step-1
+bootstrap.ConfigureServices(container => container.RegisterDatabase());
+
+await bootstrap.StartAsync();
+#endregion
+
+#region step-2
+var products = bootstrap.Resolve>();
+
+await products.InsertAsync(new Product { Name = "Squid Plushie", Price = 19.99m });
+await products.InsertAsync(new Product { Name = "Kraken Mug", Price = 12.50m });
+
+var page = await products.GetPagedAsync(
+ page: 1,
+ pageSize: 10,
+ orderBy: product => product.Price);
+
+Console.WriteLine($"Found {page.TotalCount} product(s) on page {page.Page}/{page.TotalPages}:");
+
+foreach (var product in page.Items)
+{
+ Console.WriteLine($" {product.Name} - {product.Price:C}");
+}
+#endregion
+
+await bootstrap.StopAsync();
+
+internal sealed class Product : BaseEntity
+{
+ public string Name { get; set; } = string.Empty;
+
+ public decimal Price { get; set; }
+}
diff --git a/samples/SquidStd.Samples.Database/SquidStd.Samples.Database.csproj b/samples/SquidStd.Samples.Database/SquidStd.Samples.Database.csproj
new file mode 100644
index 00000000..7735fb2e
--- /dev/null
+++ b/samples/SquidStd.Samples.Database/SquidStd.Samples.Database.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
diff --git a/samples/SquidStd.Samples.Email/Program.cs b/samples/SquidStd.Samples.Email/Program.cs
new file mode 100644
index 00000000..a8b339cf
--- /dev/null
+++ b/samples/SquidStd.Samples.Email/Program.cs
@@ -0,0 +1,81 @@
+using SquidStd.Core.Data.Bootstrap;
+using SquidStd.Core.Interfaces.Events;
+using SquidStd.Mail.Abstractions.Data;
+using SquidStd.Mail.Abstractions.Data.Config;
+using SquidStd.Mail.Abstractions.Data.Events;
+using SquidStd.Mail.Abstractions.Interfaces;
+using SquidStd.Mail.Abstractions.Types.Mail;
+using SquidStd.Mail.MailKit.Extensions;
+using SquidStd.Mail.Queue.Extensions;
+using SquidStd.Mail.Queue.Interfaces;
+using SquidStd.Messaging.Extensions;
+using SquidStd.Services.Core.Services.Bootstrap;
+
+var bootstrap = SquidStdBootstrap.Create(new SquidStdOptions
+{
+ ConfigName = "squidstd",
+ RootDirectory = AppContext.BaseDirectory
+});
+
+#region step-1
+bootstrap.ConfigureServices(container => container.AddMail(new MailOptions
+{
+ Protocol = MailProtocolType.Imap,
+ Host = "imap.example.com",
+ Port = 993,
+ Username = "alice@example.com",
+ Password = "app-password"
+}));
+#endregion
+
+#region step-2
+bootstrap.ConfigureServices(container => container.AddMailSender(new SmtpOptions
+{
+ Host = "smtp.example.com",
+ Port = 587
+}));
+#endregion
+
+#region step-3
+bootstrap.ConfigureServices(container => container
+ .AddInMemoryMessaging()
+ .AddMailQueue());
+#endregion
+
+await bootstrap.StartAsync();
+
+// Inbound: react to each received email on the event bus.
+var eventBus = bootstrap.Resolve();
+eventBus.RegisterAsyncListener(new MailReceivedLogger());
+
+var outgoing = new OutgoingMailMessage
+{
+ To = [new MailAddress("Bob", "bob@example.com")],
+ Subject = "Hi",
+ HtmlBody = "
Hi
"
+};
+
+// Outbound: queue for background sending (no network call).
+var queue = bootstrap.Resolve();
+await queue.EnqueueAsync(outgoing);
+
+// Or send synchronously; guarded so the sample runs without a live SMTP server.
+if (args.Contains("--send"))
+{
+ var sender = bootstrap.Resolve();
+ await sender.SendAsync(outgoing);
+}
+
+await bootstrap.StopAsync();
+
+/// Logs every received email as it arrives on the event bus.
+public sealed class MailReceivedLogger : IAsyncEventListener
+{
+ /// Handles a received-mail event.
+ public Task HandleAsync(MailReceivedEvent eventData, CancellationToken cancellationToken)
+ {
+ Console.WriteLine($"received: {eventData.Message.Subject}");
+
+ return Task.CompletedTask;
+ }
+}
diff --git a/samples/SquidStd.Samples.Email/SquidStd.Samples.Email.csproj b/samples/SquidStd.Samples.Email/SquidStd.Samples.Email.csproj
new file mode 100644
index 00000000..1d68b9d9
--- /dev/null
+++ b/samples/SquidStd.Samples.Email/SquidStd.Samples.Email.csproj
@@ -0,0 +1,18 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/SquidStd.Samples.EventsJobsScheduling/Program.cs b/samples/SquidStd.Samples.EventsJobsScheduling/Program.cs
new file mode 100644
index 00000000..5e377260
--- /dev/null
+++ b/samples/SquidStd.Samples.EventsJobsScheduling/Program.cs
@@ -0,0 +1,54 @@
+using SquidStd.Core.Data.Bootstrap;
+using SquidStd.Core.Interfaces.Events;
+using SquidStd.Core.Interfaces.Jobs;
+using SquidStd.Core.Interfaces.Scheduling;
+using SquidStd.Services.Core.Extensions;
+using SquidStd.Services.Core.Services.Bootstrap;
+
+var bootstrap = SquidStdBootstrap.Create(new SquidStdOptions
+{
+ ConfigName = "squidstd",
+ RootDirectory = AppContext.BaseDirectory
+});
+
+// The cron scheduler and timer wheel are opt-in.
+bootstrap.ConfigureServices(container => container.RegisterSchedulerServices());
+
+await bootstrap.StartAsync();
+
+#region step-1
+var eventBus = bootstrap.Resolve();
+eventBus.RegisterAsyncListener(new PingListener());
+await eventBus.PublishAsync(new PingEvent("hello"), CancellationToken.None);
+#endregion
+
+#region step-2
+var jobs = bootstrap.Resolve();
+await jobs.ScheduleAsync(() => Console.WriteLine("job ran on a worker thread"));
+#endregion
+
+#region step-3
+var cron = bootstrap.Resolve();
+cron.Schedule("heartbeat", "*/5 * * * *", _ =>
+{
+ Console.WriteLine("cron tick");
+
+ return Task.CompletedTask;
+});
+#endregion
+
+await bootstrap.StopAsync();
+
+/// A sample event published on the bus.
+public sealed record PingEvent(string Message) : IEvent;
+
+/// Handles .
+public sealed class PingListener : IAsyncEventListener
+{
+ public Task HandleAsync(PingEvent eventData, CancellationToken cancellationToken)
+ {
+ Console.WriteLine($"received: {eventData.Message}");
+
+ return Task.CompletedTask;
+ }
+}
diff --git a/samples/SquidStd.Samples.EventsJobsScheduling/SquidStd.Samples.EventsJobsScheduling.csproj b/samples/SquidStd.Samples.EventsJobsScheduling/SquidStd.Samples.EventsJobsScheduling.csproj
new file mode 100644
index 00000000..52bad92e
--- /dev/null
+++ b/samples/SquidStd.Samples.EventsJobsScheduling/SquidStd.Samples.EventsJobsScheduling.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
diff --git a/samples/SquidStd.Samples.GettingStarted/Program.cs b/samples/SquidStd.Samples.GettingStarted/Program.cs
new file mode 100644
index 00000000..bea03ed3
--- /dev/null
+++ b/samples/SquidStd.Samples.GettingStarted/Program.cs
@@ -0,0 +1,18 @@
+using SquidStd.Core.Data.Bootstrap;
+using SquidStd.Services.Core.Services.Bootstrap;
+
+#region step-1
+var bootstrap = SquidStdBootstrap.Create(new SquidStdOptions
+{
+ ConfigName = "squidstd",
+ RootDirectory = AppContext.BaseDirectory
+});
+#endregion
+
+#region step-2
+await bootstrap.StartAsync();
+#endregion
+
+#region step-3
+await bootstrap.RunAsync();
+#endregion
diff --git a/samples/SquidStd.Samples.GettingStarted/SquidStd.Samples.GettingStarted.csproj b/samples/SquidStd.Samples.GettingStarted/SquidStd.Samples.GettingStarted.csproj
new file mode 100644
index 00000000..52bad92e
--- /dev/null
+++ b/samples/SquidStd.Samples.GettingStarted/SquidStd.Samples.GettingStarted.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
diff --git a/samples/SquidStd.Samples.Messaging/Program.cs b/samples/SquidStd.Samples.Messaging/Program.cs
new file mode 100644
index 00000000..9f7f70a0
--- /dev/null
+++ b/samples/SquidStd.Samples.Messaging/Program.cs
@@ -0,0 +1,55 @@
+using SquidStd.Core.Data.Bootstrap;
+using SquidStd.Messaging.Abstractions.Interfaces;
+using SquidStd.Messaging.Extensions;
+using SquidStd.Services.Core.Services.Bootstrap;
+
+var bootstrap = SquidStdBootstrap.Create(new SquidStdOptions
+{
+ ConfigName = "squidstd",
+ RootDirectory = AppContext.BaseDirectory
+});
+
+#region step-1
+bootstrap.ConfigureServices(container => container.AddInMemoryMessaging());
+#endregion
+
+await bootstrap.StartAsync();
+
+#region step-2
+var queue = bootstrap.Resolve();
+using (queue.Subscribe("orders", new OrderListener()))
+{
+ await queue.PublishAsync("orders", new OrderPlaced("order-1"));
+ await Task.Delay(200);
+}
+#endregion
+
+#region step-3
+var topic = bootstrap.Resolve();
+using (topic.Subscribe("order-events", (order, _) =>
+{
+ Console.WriteLine($"topic saw {order.Id}");
+
+ return Task.CompletedTask;
+}))
+{
+ await topic.PublishAsync("order-events", new OrderPlaced("order-2"));
+ await Task.Delay(200);
+}
+#endregion
+
+await bootstrap.StopAsync();
+
+/// A sample message.
+public sealed record OrderPlaced(string Id);
+
+/// Competing-consumers listener for the orders queue.
+public sealed class OrderListener : IQueueMessageListenerAsync
+{
+ public Task HandleAsync(OrderPlaced message, CancellationToken cancellationToken)
+ {
+ Console.WriteLine($"queue handled {message.Id}");
+
+ return Task.CompletedTask;
+ }
+}
diff --git a/samples/SquidStd.Samples.Messaging/SquidStd.Samples.Messaging.csproj b/samples/SquidStd.Samples.Messaging/SquidStd.Samples.Messaging.csproj
new file mode 100644
index 00000000..602ca8e5
--- /dev/null
+++ b/samples/SquidStd.Samples.Messaging/SquidStd.Samples.Messaging.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
diff --git a/samples/SquidStd.Samples.Networking/Program.cs b/samples/SquidStd.Samples.Networking/Program.cs
new file mode 100644
index 00000000..18967535
--- /dev/null
+++ b/samples/SquidStd.Samples.Networking/Program.cs
@@ -0,0 +1,34 @@
+using System.Net;
+using System.Text;
+using SquidStd.Network.Client;
+using SquidStd.Network.Server;
+
+#region step-1
+var endPoint = new IPEndPoint(IPAddress.Loopback, 9099);
+var server = new SquidTcpServer(endPoint);
+
+server.OnClientConnect += (_, args) =>
+ Console.WriteLine($"Client connected: session {args.Client.SessionId}");
+
+server.OnDataReceived += (_, args) =>
+ Console.WriteLine($"Received {args.Data.Length} byte(s): {Encoding.UTF8.GetString(args.Data.Span)}");
+#endregion
+
+if (!args.Contains("--run"))
+{
+ Console.WriteLine("Pass --run to start the TCP server and round-trip a message.");
+
+ return;
+}
+
+#region step-2
+await server.StartAsync(CancellationToken.None);
+
+var client = await SquidStdTcpClient.ConnectAsync(endPoint);
+await client.SendAsync(Encoding.UTF8.GetBytes("hello squid"), CancellationToken.None);
+
+await Task.Delay(200);
+
+await client.DisposeAsync();
+await server.DisposeAsync();
+#endregion
diff --git a/samples/SquidStd.Samples.Networking/SquidStd.Samples.Networking.csproj b/samples/SquidStd.Samples.Networking/SquidStd.Samples.Networking.csproj
new file mode 100644
index 00000000..76741092
--- /dev/null
+++ b/samples/SquidStd.Samples.Networking/SquidStd.Samples.Networking.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
diff --git a/samples/SquidStd.Samples.Plugins/Program.cs b/samples/SquidStd.Samples.Plugins/Program.cs
new file mode 100644
index 00000000..b4a13771
--- /dev/null
+++ b/samples/SquidStd.Samples.Plugins/Program.cs
@@ -0,0 +1,56 @@
+using DryIoc;
+using SquidStd.Plugin.Abstractions.Data;
+using SquidStd.Plugin.Abstractions.Interfaces.Plugins;
+
+namespace SquidStd.Samples.Plugins;
+
+#region step-1
+public interface IGreeter
+{
+ string Greet(string name);
+}
+
+public sealed class WeatherGreeter : IGreeter
+{
+ public string Greet(string name)
+ => $"Hello {name}, the weather plugin is online.";
+}
+
+public sealed class WeatherPlugin : ISquidStdPlugin
+{
+ public PluginMetadata Metadata { get; } = new()
+ {
+ Id = "squidstd.weather",
+ Name = "Weather Plugin",
+ Version = new Version(1, 0, 0),
+ Author = "SquidStd Samples",
+ Description = "Registers a greeter service.",
+ Dependencies = []
+ };
+
+ public void Configure(IContainer container, PluginContext context)
+ {
+ container.Register(Reuse.Singleton);
+ }
+}
+#endregion
+
+internal static class Program
+{
+ private static void Main()
+ {
+ #region step-2
+ var container = new Container();
+ var context = new PluginContext();
+ context.Data["startedAt"] = DateTimeOffset.UtcNow;
+
+ ISquidStdPlugin plugin = new WeatherPlugin();
+
+ Console.WriteLine($"Loading {plugin.Metadata.Name} v{plugin.Metadata.Version} by {plugin.Metadata.Author}");
+ plugin.Configure(container, context);
+
+ var greeter = container.Resolve();
+ Console.WriteLine(greeter.Greet("squid"));
+ #endregion
+ }
+}
diff --git a/samples/SquidStd.Samples.Plugins/SquidStd.Samples.Plugins.csproj b/samples/SquidStd.Samples.Plugins/SquidStd.Samples.Plugins.csproj
new file mode 100644
index 00000000..3cd1aff6
--- /dev/null
+++ b/samples/SquidStd.Samples.Plugins/SquidStd.Samples.Plugins.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
diff --git a/samples/SquidStd.Samples.ScriptingLua/Program.cs b/samples/SquidStd.Samples.ScriptingLua/Program.cs
new file mode 100644
index 00000000..b9f2fddc
--- /dev/null
+++ b/samples/SquidStd.Samples.ScriptingLua/Program.cs
@@ -0,0 +1,50 @@
+using DryIoc;
+using SquidStd.Abstractions.Extensions.Services;
+using SquidStd.Core.Data.Bootstrap;
+using SquidStd.Scripting.Lua.Data.Config;
+using SquidStd.Scripting.Lua.Interfaces.Scripts;
+using SquidStd.Scripting.Lua.Services;
+using SquidStd.Services.Core.Services.Bootstrap;
+
+var bootstrap = SquidStdBootstrap.Create(new SquidStdOptions
+{
+ ConfigName = "squidstd",
+ RootDirectory = AppContext.BaseDirectory
+});
+
+#region step-1
+var scriptsDirectory = Path.Combine(AppContext.BaseDirectory, "scripts");
+Directory.CreateDirectory(scriptsDirectory);
+
+bootstrap.ConfigureServices(container =>
+{
+ var engineConfig = new LuaEngineConfig(
+ luarcDirectory: AppContext.BaseDirectory,
+ scriptsDirectory: scriptsDirectory,
+ engineName: "SquidStd.Samples.ScriptingLua",
+ engineVersion: "1.0.0"
+ );
+
+ container.RegisterInstance(engineConfig);
+ container.RegisterStdService();
+
+ return container;
+});
+#endregion
+
+await bootstrap.StartAsync();
+
+#region step-2
+var engine = bootstrap.Resolve();
+
+engine.RegisterGlobal("greeting", "hello from C#");
+engine.ExecuteScript("result = greeting .. ' and lua'");
+
+var sum = engine.ExecuteFunction("3 + 4");
+var message = engine.ExecuteFunction("result");
+
+Console.WriteLine($"3 + 4 = {sum.Data}");
+Console.WriteLine($"result = {message.Data}");
+#endregion
+
+await bootstrap.StopAsync();
diff --git a/samples/SquidStd.Samples.ScriptingLua/SquidStd.Samples.ScriptingLua.csproj b/samples/SquidStd.Samples.ScriptingLua/SquidStd.Samples.ScriptingLua.csproj
new file mode 100644
index 00000000..d480fed2
--- /dev/null
+++ b/samples/SquidStd.Samples.ScriptingLua/SquidStd.Samples.ScriptingLua.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
diff --git a/samples/SquidStd.Samples.Search/Program.cs b/samples/SquidStd.Samples.Search/Program.cs
new file mode 100644
index 00000000..2526a60f
--- /dev/null
+++ b/samples/SquidStd.Samples.Search/Program.cs
@@ -0,0 +1,46 @@
+using SquidStd.Core.Data.Bootstrap;
+using SquidStd.Search.Abstractions.Attributes;
+using SquidStd.Search.Abstractions.Interfaces;
+using SquidStd.Search.Elasticsearch.Data.Config;
+using SquidStd.Search.Elasticsearch.Extensions;
+using SquidStd.Search.Elasticsearch.Linq;
+using SquidStd.Services.Core.Services.Bootstrap;
+
+var bootstrap = SquidStdBootstrap.Create(new SquidStdOptions
+{
+ ConfigName = "squidstd",
+ RootDirectory = AppContext.BaseDirectory
+});
+
+#region step-1
+bootstrap.ConfigureServices(container => container.AddElasticsearch(new ElasticsearchOptions
+{
+ Uri = new Uri("http://localhost:9200")
+}));
+#endregion
+
+await bootstrap.StartAsync();
+
+var search = bootstrap.Resolve();
+
+#region step-3
+await search.IndexAsync(new Order("1", "open", 150), refresh: true);
+
+var open = await search.Query()
+ .Where(o => o.Status == "open")
+ .ToListAsync();
+
+Console.WriteLine($"found {open.Count} open order(s)");
+#endregion
+
+await bootstrap.StopAsync();
+
+#region step-2
+/// An order document stored in the orders index.
+[SearchIndex("orders")]
+public sealed record Order(string Id, string Status, int Total) : IIndexableEntity
+{
+ /// Stable document id within the index.
+ public string IndexId => Id;
+}
+#endregion
diff --git a/samples/SquidStd.Samples.Search/SquidStd.Samples.Search.csproj b/samples/SquidStd.Samples.Search/SquidStd.Samples.Search.csproj
new file mode 100644
index 00000000..e5b2ff55
--- /dev/null
+++ b/samples/SquidStd.Samples.Search/SquidStd.Samples.Search.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
diff --git a/samples/SquidStd.Samples.Storage/Program.cs b/samples/SquidStd.Samples.Storage/Program.cs
new file mode 100644
index 00000000..ae1de94c
--- /dev/null
+++ b/samples/SquidStd.Samples.Storage/Program.cs
@@ -0,0 +1,29 @@
+using SquidStd.Core.Data.Bootstrap;
+using SquidStd.Services.Core.Services.Bootstrap;
+using SquidStd.Storage.Abstractions.Interfaces;
+using SquidStd.Storage.Extensions;
+
+var bootstrap = SquidStdBootstrap.Create(new SquidStdOptions
+{
+ ConfigName = "squidstd",
+ RootDirectory = AppContext.BaseDirectory
+});
+
+#region step-1
+bootstrap.ConfigureServices(container => container.AddFileStorage());
+#endregion
+
+await bootstrap.StartAsync();
+
+#region step-2
+var storage = bootstrap.Resolve();
+
+await storage.SaveAsync("user:1", new User("squid", "squid@stormwind.it"));
+var loaded = await storage.LoadAsync("user:1");
+
+Console.WriteLine($"{loaded?.Name} <{loaded?.Email}>");
+#endregion
+
+await bootstrap.StopAsync();
+
+internal sealed record User(string Name, string Email);
diff --git a/samples/SquidStd.Samples.Storage/SquidStd.Samples.Storage.csproj b/samples/SquidStd.Samples.Storage/SquidStd.Samples.Storage.csproj
new file mode 100644
index 00000000..44a230f1
--- /dev/null
+++ b/samples/SquidStd.Samples.Storage/SquidStd.Samples.Storage.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
diff --git a/samples/SquidStd.Samples.Templating/Program.cs b/samples/SquidStd.Samples.Templating/Program.cs
new file mode 100644
index 00000000..86341eaf
--- /dev/null
+++ b/samples/SquidStd.Samples.Templating/Program.cs
@@ -0,0 +1,34 @@
+using SquidStd.Core.Data.Bootstrap;
+using SquidStd.Services.Core.Services.Bootstrap;
+using SquidStd.Templating.Extensions;
+using SquidStd.Templating.Interfaces;
+
+var bootstrap = SquidStdBootstrap.Create(new SquidStdOptions
+{
+ ConfigName = "squidstd",
+ RootDirectory = AppContext.BaseDirectory
+});
+
+#region step-1
+bootstrap.ConfigureServices(container => container.AddTemplating());
+#endregion
+
+await bootstrap.StartAsync();
+
+#region step-2
+var renderer = bootstrap.Resolve();
+
+var greeting = await renderer.RenderAsync("Hi {{ user.name }}!", new { User = new { Name = "squid" } });
+
+Console.WriteLine(greeting);
+#endregion
+
+#region step-3
+renderer.Register("welcome", "Welcome aboard, {{ user.name }}.");
+
+var welcome = await renderer.RenderByNameAsync("welcome", new { User = new { Name = "squid" } });
+
+Console.WriteLine(welcome);
+#endregion
+
+await bootstrap.StopAsync();
diff --git a/samples/SquidStd.Samples.Templating/SquidStd.Samples.Templating.csproj b/samples/SquidStd.Samples.Templating/SquidStd.Samples.Templating.csproj
new file mode 100644
index 00000000..f7de606d
--- /dev/null
+++ b/samples/SquidStd.Samples.Templating/SquidStd.Samples.Templating.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
diff --git a/samples/SquidStd.Samples.WorkerSystem/Program.cs b/samples/SquidStd.Samples.WorkerSystem/Program.cs
new file mode 100644
index 00000000..620f7be6
--- /dev/null
+++ b/samples/SquidStd.Samples.WorkerSystem/Program.cs
@@ -0,0 +1,53 @@
+using SquidStd.Core.Data.Bootstrap;
+using SquidStd.Messaging.Extensions;
+using SquidStd.Services.Core.Services.Bootstrap;
+using SquidStd.Workers.Abstractions.Data;
+using SquidStd.Workers.Extensions;
+using SquidStd.Workers.Interfaces;
+using SquidStd.Workers.Manager.Extensions;
+using SquidStd.Workers.Manager.Interfaces;
+
+var bootstrap = SquidStdBootstrap.Create(new SquidStdOptions
+{
+ ConfigName = "squidstd",
+ RootDirectory = AppContext.BaseDirectory
+});
+
+#region step-1
+bootstrap.ConfigureServices(c =>
+{
+ c.AddInMemoryMessaging();
+ c.AddWorkers();
+ c.AddJobHandler();
+ c.AddWorkerManager();
+ return c;
+});
+#endregion
+
+#region step-3
+await bootstrap.StartAsync();
+
+var scheduler = bootstrap.Resolve();
+await scheduler.EnqueueAsync("greet", new Dictionary { ["name"] = "squid" });
+
+await Task.Delay(500);
+await bootstrap.StopAsync();
+#endregion
+
+#region step-2
+internal sealed class GreetJobHandler : IJobHandler
+{
+ public string JobName
+ {
+ get { return "greet"; }
+ }
+
+ public Task HandleAsync(JobRequest job, CancellationToken cancellationToken)
+ {
+ var name = job.Parameters.TryGetValue("name", out var value) ? value : "world";
+ Console.WriteLine($"Hello, {name}! (job: {job.JobName})");
+
+ return Task.CompletedTask;
+ }
+}
+#endregion
diff --git a/samples/SquidStd.Samples.WorkerSystem/SquidStd.Samples.WorkerSystem.csproj b/samples/SquidStd.Samples.WorkerSystem/SquidStd.Samples.WorkerSystem.csproj
new file mode 100644
index 00000000..160a8bde
--- /dev/null
+++ b/samples/SquidStd.Samples.WorkerSystem/SquidStd.Samples.WorkerSystem.csproj
@@ -0,0 +1,18 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
diff --git a/src/SquidStd.Abstractions/README.md b/src/SquidStd.Abstractions/README.md
index 38d1732a..f41238e9 100644
--- a/src/SquidStd.Abstractions/README.md
+++ b/src/SquidStd.Abstractions/README.md
@@ -44,13 +44,13 @@ container.RegisterConfigSection("my");
## Key types
-| Type | Purpose |
-|------|---------|
-| `ISquidStdService` | Async start/stop lifecycle for managed services. |
-| `RegisterStdServiceExtension` | `RegisterStdService<,>` container extension. |
-| `RegisterConfigSectionExtension` | `RegisterConfigSection<>` container extension. |
-| `ServiceRegistrationData` | Ordered service registration record. |
-| `ConfigRegistrationData` | Config section registration record. |
+| Type | Purpose |
+|----------------------------------|--------------------------------------------------|
+| `ISquidStdService` | Async start/stop lifecycle for managed services. |
+| `RegisterStdServiceExtension` | `RegisterStdService<,>` container extension. |
+| `RegisterConfigSectionExtension` | `RegisterConfigSection<>` container extension. |
+| `ServiceRegistrationData` | Ordered service registration record. |
+| `ConfigRegistrationData` | Config section registration record. |
## License
diff --git a/src/SquidStd.AspNetCore/Extensions/SquidStdAspNetCoreBuilderExtensions.cs b/src/SquidStd.AspNetCore/Extensions/SquidStdAspNetCoreBuilderExtensions.cs
index 3e8aac31..927e4dc1 100644
--- a/src/SquidStd.AspNetCore/Extensions/SquidStdAspNetCoreBuilderExtensions.cs
+++ b/src/SquidStd.AspNetCore/Extensions/SquidStdAspNetCoreBuilderExtensions.cs
@@ -2,7 +2,6 @@
using DryIoc.Microsoft.DependencyInjection;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Hosting;
using SquidStd.AspNetCore.Services;
using SquidStd.Core.Data.Bootstrap;
using SquidStd.Services.Core.Services.Bootstrap;
diff --git a/src/SquidStd.AspNetCore/Extensions/SquidStdHealthChecksExtensions.cs b/src/SquidStd.AspNetCore/Extensions/SquidStdHealthChecksExtensions.cs
index 090f6180..ab54d1ff 100644
--- a/src/SquidStd.AspNetCore/Extensions/SquidStdHealthChecksExtensions.cs
+++ b/src/SquidStd.AspNetCore/Extensions/SquidStdHealthChecksExtensions.cs
@@ -2,7 +2,6 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
-using Microsoft.Extensions.Hosting;
using SquidStd.AspNetCore.Services;
using SquidHealthCheck = SquidStd.Core.Interfaces.Health.IHealthCheck;
@@ -26,8 +25,11 @@ public WebApplicationBuilder AddSquidStdHealthChecks()
{
ArgumentNullException.ThrowIfNull(builder);
- if (!builder.Host.Properties.TryGetValue(SquidStdAspNetCoreBuilderExtensions.ContainerPropertyKey, out var value)
- || value is not IContainer container)
+ if (!builder.Host.Properties.TryGetValue(
+ SquidStdAspNetCoreBuilderExtensions.ContainerPropertyKey,
+ out var value
+ ) ||
+ value is not IContainer container)
{
throw new InvalidOperationException("Call UseSquidStd before AddSquidStdHealthChecks.");
}
@@ -38,11 +40,11 @@ public WebApplicationBuilder AddSquidStdHealthChecks()
foreach (var check in checks)
{
healthChecks.Add(
- new HealthCheckRegistration(
+ new(
check.Name,
_ => new SquidStdHealthCheckAdapter(check),
- failureStatus: HealthStatus.Unhealthy,
- tags: null
+ HealthStatus.Unhealthy,
+ null
)
);
}
diff --git a/src/SquidStd.AspNetCore/README.md b/src/SquidStd.AspNetCore/README.md
index e1308be0..2c621eb4 100644
--- a/src/SquidStd.AspNetCore/README.md
+++ b/src/SquidStd.AspNetCore/README.md
@@ -27,7 +27,8 @@ dotnet add package SquidStd.AspNetCore
- 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(...)`.
+- `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
@@ -64,11 +65,11 @@ Each registered `IHealthCheck` appears as its own entry in the report. Check nam
## Key types
-| Type | Purpose |
-|------|---------|
-| `SquidStdAspNetCoreBuilderExtensions` | `UseSquidStd(...)` builder extension. |
-| `SquidStdHostedService` | Hosted service bridging SquidStd service lifecycle to the host. |
-| `SquidStdHealthChecksExtensions` | `AddSquidStdHealthChecks(...)` — bridge to ASP.NET Core health checks. |
+| Type | Purpose |
+|---------------------------------------|------------------------------------------------------------------------|
+| `SquidStdAspNetCoreBuilderExtensions` | `UseSquidStd(...)` builder extension. |
+| `SquidStdHostedService` | Hosted service bridging SquidStd service lifecycle to the host. |
+| `SquidStdHealthChecksExtensions` | `AddSquidStdHealthChecks(...)` — bridge to ASP.NET Core health checks. |
## License
diff --git a/src/SquidStd.AspNetCore/Services/SquidStdHostedService.cs b/src/SquidStd.AspNetCore/Services/SquidStdHostedService.cs
index 158e7d04..b279eb7f 100644
--- a/src/SquidStd.AspNetCore/Services/SquidStdHostedService.cs
+++ b/src/SquidStd.AspNetCore/Services/SquidStdHostedService.cs
@@ -21,13 +21,9 @@ public SquidStdHostedService(ISquidStdBootstrap bootstrap)
///
public async Task StartAsync(CancellationToken cancellationToken)
- {
- await _bootstrap.StartAsync(cancellationToken);
- }
+ => await _bootstrap.StartAsync(cancellationToken);
///
public async Task StopAsync(CancellationToken cancellationToken)
- {
- await _bootstrap.StopAsync(cancellationToken);
- }
+ => await _bootstrap.StopAsync(cancellationToken);
}
diff --git a/src/SquidStd.Caching.Abstractions/Data/Config/CacheConnectionString.cs b/src/SquidStd.Caching.Abstractions/Data/Config/CacheConnectionString.cs
index 33ab8f4f..8d124dec 100644
--- a/src/SquidStd.Caching.Abstractions/Data/Config/CacheConnectionString.cs
+++ b/src/SquidStd.Caching.Abstractions/Data/Config/CacheConnectionString.cs
@@ -63,7 +63,11 @@ public static CacheConnectionString Parse(string connectionString)
var query = HttpUtility.ParseQueryString(uri.Query);
var parameters = query.AllKeys
.Where(static key => key is not null)
- .ToFrozenDictionary(key => key!, key => query[key] ?? string.Empty, StringComparer.OrdinalIgnoreCase);
+ .ToFrozenDictionary(
+ key => key!,
+ key => query[key] ?? string.Empty,
+ StringComparer.OrdinalIgnoreCase
+ );
return new(
uri.Scheme,
diff --git a/src/SquidStd.Caching.Abstractions/Interfaces/ICacheMetrics.cs b/src/SquidStd.Caching.Abstractions/Interfaces/ICacheMetrics.cs
index cf7be0af..bb966b18 100644
--- a/src/SquidStd.Caching.Abstractions/Interfaces/ICacheMetrics.cs
+++ b/src/SquidStd.Caching.Abstractions/Interfaces/ICacheMetrics.cs
@@ -11,9 +11,9 @@ public interface ICacheMetrics
/// Records a cache miss.
void OnMiss(string key);
- /// Records a value being stored.
- void OnSet(string key);
-
/// Records a key being removed.
void OnRemove(string key);
+
+ /// Records a value being stored.
+ void OnSet(string key);
}
diff --git a/src/SquidStd.Caching.Abstractions/Interfaces/ICacheProvider.cs b/src/SquidStd.Caching.Abstractions/Interfaces/ICacheProvider.cs
index 7b27ddab..de8a47ae 100644
--- a/src/SquidStd.Caching.Abstractions/Interfaces/ICacheProvider.cs
+++ b/src/SquidStd.Caching.Abstractions/Interfaces/ICacheProvider.cs
@@ -7,15 +7,15 @@ namespace SquidStd.Caching.Abstractions.Interfaces;
///
public interface ICacheProvider : ISquidStdService
{
+ /// Returns whether a key exists.
+ Task ExistsAsync(string key, CancellationToken cancellationToken = default);
+
/// Gets the raw value for a key, or null when absent.
Task?> GetAsync(string key, CancellationToken cancellationToken = default);
- /// Stores a raw value with an optional time-to-live.
- Task SetAsync(string key, ReadOnlyMemory value, TimeSpan? ttl, CancellationToken cancellationToken = default);
-
/// Removes a key; returns whether it existed.
Task RemoveAsync(string key, CancellationToken cancellationToken = default);
- /// Returns whether a key exists.
- Task ExistsAsync(string key, CancellationToken cancellationToken = default);
+ /// Stores a raw value with an optional time-to-live.
+ Task SetAsync(string key, ReadOnlyMemory value, TimeSpan? ttl, CancellationToken cancellationToken = default);
}
diff --git a/src/SquidStd.Caching.Abstractions/Interfaces/ICacheService.cs b/src/SquidStd.Caching.Abstractions/Interfaces/ICacheService.cs
index 8d8dec96..db57782e 100644
--- a/src/SquidStd.Caching.Abstractions/Interfaces/ICacheService.cs
+++ b/src/SquidStd.Caching.Abstractions/Interfaces/ICacheService.cs
@@ -5,18 +5,12 @@ namespace SquidStd.Caching.Abstractions.Interfaces;
///
public interface ICacheService
{
- /// Gets a typed value, or default when absent.
- Task GetAsync(string key, CancellationToken cancellationToken = default);
-
- /// Stores a typed value with an optional time-to-live (falls back to the default TTL).
- Task SetAsync(string key, T value, TimeSpan? ttl = null, CancellationToken cancellationToken = default);
-
- /// Removes a key; returns whether it existed.
- Task RemoveAsync(string key, CancellationToken cancellationToken = default);
-
/// Returns whether a key exists.
Task ExistsAsync(string key, CancellationToken cancellationToken = default);
+ /// Gets a typed value, or default when absent.
+ Task GetAsync(string key, CancellationToken cancellationToken = default);
+
/// Returns the cached value, or computes, stores and returns it on a miss.
Task GetOrSetAsync(
string key,
@@ -24,4 +18,10 @@ Task GetOrSetAsync(
TimeSpan? ttl = null,
CancellationToken cancellationToken = default
);
+
+ /// Removes a key; returns whether it existed.
+ Task RemoveAsync(string key, CancellationToken cancellationToken = default);
+
+ /// Stores a typed value with an optional time-to-live (falls back to the default TTL).
+ Task SetAsync(string key, T value, TimeSpan? ttl = null, CancellationToken cancellationToken = default);
}
diff --git a/src/SquidStd.Caching.Abstractions/README.md b/src/SquidStd.Caching.Abstractions/README.md
index 9e5cbdfd..93d452fe 100644
--- a/src/SquidStd.Caching.Abstractions/README.md
+++ b/src/SquidStd.Caching.Abstractions/README.md
@@ -42,14 +42,14 @@ public Task GetOrComputeAsync(ICacheService cache)
## Key types
-| Type | Purpose |
-|------|---------|
-| `ICacheService` | Typed cache-aside facade. |
-| `ICacheProvider` | Byte-level backend contract (per provider). |
-| `CacheService` | Shared facade: serialization, key prefix, default TTL, cache-aside. |
-| `ICacheMetrics` | Hit/miss/set/remove metrics sink. |
-| `CacheOptions` | Default TTL and key prefix. |
-| `CacheConnectionString` | `scheme://host[?params]` parsing into `CacheOptions`. |
+| Type | Purpose |
+|-------------------------|---------------------------------------------------------------------|
+| `ICacheService` | Typed cache-aside facade. |
+| `ICacheProvider` | Byte-level backend contract (per provider). |
+| `CacheService` | Shared facade: serialization, key prefix, default TTL, cache-aside. |
+| `ICacheMetrics` | Hit/miss/set/remove metrics sink. |
+| `CacheOptions` | Default TTL and key prefix. |
+| `CacheConnectionString` | `scheme://host[?params]` parsing into `CacheOptions`. |
## License
diff --git a/src/SquidStd.Caching.Abstractions/Services/CacheMetricsProvider.cs b/src/SquidStd.Caching.Abstractions/Services/CacheMetricsProvider.cs
index 36722cb3..4835ad1f 100644
--- a/src/SquidStd.Caching.Abstractions/Services/CacheMetricsProvider.cs
+++ b/src/SquidStd.Caching.Abstractions/Services/CacheMetricsProvider.cs
@@ -18,22 +18,6 @@ public sealed class CacheMetricsProvider : ICacheMetrics, IMetricProvider
///
public string ProviderName => "cache";
- ///
- public void OnHit(string key)
- => Interlocked.Increment(ref _hits);
-
- ///
- public void OnMiss(string key)
- => Interlocked.Increment(ref _misses);
-
- ///
- public void OnSet(string key)
- => Interlocked.Increment(ref _sets);
-
- ///
- public void OnRemove(string key)
- => Interlocked.Increment(ref _removes);
-
///
public ValueTask> CollectAsync(CancellationToken cancellationToken = default)
{
@@ -53,4 +37,20 @@ public ValueTask> CollectAsync(CancellationToken can
return ValueTask.FromResult>(samples);
}
+
+ ///
+ public void OnHit(string key)
+ => Interlocked.Increment(ref _hits);
+
+ ///
+ public void OnMiss(string key)
+ => Interlocked.Increment(ref _misses);
+
+ ///
+ public void OnRemove(string key)
+ => Interlocked.Increment(ref _removes);
+
+ ///
+ public void OnSet(string key)
+ => Interlocked.Increment(ref _sets);
}
diff --git a/src/SquidStd.Caching.Abstractions/Services/CacheService.cs b/src/SquidStd.Caching.Abstractions/Services/CacheService.cs
index c8ddc290..4cb661d8 100644
--- a/src/SquidStd.Caching.Abstractions/Services/CacheService.cs
+++ b/src/SquidStd.Caching.Abstractions/Services/CacheService.cs
@@ -33,6 +33,10 @@ public CacheService(
_keyPrefix = options.KeyPrefix;
}
+ ///
+ public Task ExistsAsync(string key, CancellationToken cancellationToken = default)
+ => _provider.ExistsAsync(Prefixed(key), cancellationToken);
+
///
public async Task GetAsync(string key, CancellationToken cancellationToken = default)
{
@@ -50,31 +54,6 @@ public CacheService(
return _deserializer.Deserialize(bytes.Value);
}
- ///
- public async Task SetAsync(string key, T value, TimeSpan? ttl = null, CancellationToken cancellationToken = default)
- {
- var bytes = _serializer.Serialize(value);
- await _provider.SetAsync(Prefixed(key), bytes, ttl ?? _defaultTtl, cancellationToken);
- _metrics.OnSet(key);
- }
-
- ///
- public async Task RemoveAsync(string key, CancellationToken cancellationToken = default)
- {
- var removed = await _provider.RemoveAsync(Prefixed(key), cancellationToken);
-
- if (removed)
- {
- _metrics.OnRemove(key);
- }
-
- return removed;
- }
-
- ///
- public Task ExistsAsync(string key, CancellationToken cancellationToken = default)
- => _provider.ExistsAsync(Prefixed(key), cancellationToken);
-
///
public async Task GetOrSetAsync(
string key,
@@ -101,6 +80,27 @@ public async Task GetOrSetAsync(
return value;
}
+ ///
+ public async Task RemoveAsync(string key, CancellationToken cancellationToken = default)
+ {
+ var removed = await _provider.RemoveAsync(Prefixed(key), cancellationToken);
+
+ if (removed)
+ {
+ _metrics.OnRemove(key);
+ }
+
+ return removed;
+ }
+
+ ///
+ public async Task SetAsync(string key, T value, TimeSpan? ttl = null, CancellationToken cancellationToken = default)
+ {
+ var bytes = _serializer.Serialize(value);
+ await _provider.SetAsync(Prefixed(key), bytes, ttl ?? _defaultTtl, cancellationToken);
+ _metrics.OnSet(key);
+ }
+
private string Prefixed(string key)
=> _keyPrefix.Length == 0 ? key : _keyPrefix + key;
}
diff --git a/src/SquidStd.Caching.Abstractions/Services/NoOpCacheMetrics.cs b/src/SquidStd.Caching.Abstractions/Services/NoOpCacheMetrics.cs
index c52877a1..285be016 100644
--- a/src/SquidStd.Caching.Abstractions/Services/NoOpCacheMetrics.cs
+++ b/src/SquidStd.Caching.Abstractions/Services/NoOpCacheMetrics.cs
@@ -10,19 +10,11 @@ public sealed class NoOpCacheMetrics : ICacheMetrics
/// Shared instance.
public static NoOpCacheMetrics Instance { get; } = new();
- public void OnHit(string key)
- {
- }
+ public void OnHit(string key) { }
- public void OnMiss(string key)
- {
- }
+ public void OnMiss(string key) { }
- public void OnSet(string key)
- {
- }
+ public void OnRemove(string key) { }
- public void OnRemove(string key)
- {
- }
+ public void OnSet(string key) { }
}
diff --git a/src/SquidStd.Caching.Abstractions/SquidStd.Caching.Abstractions.csproj b/src/SquidStd.Caching.Abstractions/SquidStd.Caching.Abstractions.csproj
index f73ae1d5..92ecae12 100644
--- a/src/SquidStd.Caching.Abstractions/SquidStd.Caching.Abstractions.csproj
+++ b/src/SquidStd.Caching.Abstractions/SquidStd.Caching.Abstractions.csproj
@@ -8,8 +8,8 @@
-
-
+
+
diff --git a/src/SquidStd.Caching.Redis/README.md b/src/SquidStd.Caching.Redis/README.md
index c6502da9..ebaa1742 100644
--- a/src/SquidStd.Caching.Redis/README.md
+++ b/src/SquidStd.Caching.Redis/README.md
@@ -46,11 +46,11 @@ var user = await cache.GetAsync