diff --git a/.gitignore b/.gitignore
index bc12bcb8..5c33783c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -801,4 +801,9 @@ src/.vscode/
/src/Sa.Media.FFmpeg/build/artifacts/
/src/Sa.Media.FFmpeg/build/build.*/
-*.lscache
\ No newline at end of file
+*.lscache
+
+# ai
+.qwen/
+.agents/
+/.github/skills
diff --git a/QWEN.md b/QWEN.md
new file mode 100644
index 00000000..b41f744b
--- /dev/null
+++ b/QWEN.md
@@ -0,0 +1,140 @@
+# Sa — .NET 10 Experimental AOT Library Suite
+
+## Project Overview
+
+**Sa** is a collection of reusable .NET 10 libraries focused on infrastructure patterns for distributed systems. It targets **.NET 10.0**, uses **Native AOT**, and follows the **Central Package Management (CPM)** pattern via `Directory.Packages.props`.
+
+### Libraries
+
+| Library | Purpose |
+|---|---|
+| **Sa** | Shared utility classes (LockRenewer, MurmurHash3, Retry, extensions) consumed by other libs via `` |
+| **Sa.Configuration** | Command-line argument parsing (`Arguments`) and secure secrets management from files/env vars/host key files |
+| **Sa.Configuration.PostgreSql** | PostgreSQL-backed dynamic configuration source — changes in DB reflect in-app without redeploy |
+| **Sa.Data.PostgreSql** | Lightweight Npgsql client wrapper |
+| **Sa.Data.S3** | S3 data client (Minio-compatible) |
+| **Sa.HybridFileStorage** | Hybrid file storage abstraction with automatic provider failover (FileSystem ↔ S3 ↔ Postgres) |
+| **Sa.HybridFileStorage.FileSystem** | FileSystem provider implementation |
+| **Sa.HybridFileStorage.S3** | S3 provider implementation |
+| **Sa.HybridFileStorage.Postgres** | PostgreSQL provider implementation |
+| **Sa.Media** | Async, memory-efficient WAV file reader (`AsyncWavReader`) |
+| **Sa.Media.FFmpeg** | FFmpeg .NET wrapper with built-in binaries (Win x64 / Linux), audio conversion, metadata extraction, channel split/join, DI support |
+| **Sa.Outbox** | Base Outbox pattern infrastructure for reliable message publishing |
+| **Sa.Outbox.PostgreSql** | PostgreSQL Outbox implementation — parallel processing, tenant support, scheduled data cleanup |
+| **Sa.Partitional.PostgreSql** | Declarative PostgreSQL table partitioning (time: day/month/year; list; range) with migration/deletion schedules |
+| **Sa.Schedule** | Scheduled task executor with failure strategies (close app, stop job, stop all jobs, ignore) |
+| **Sa.Utils.WorkQueue** | Async queue with concurrency limiting, built on `System.Threading.Channels` |
+
+### Samples
+
+Located in `src/Samples/`: Configuration.Web, FFMpeg.Console, HybridFileStorage.Console, Partitional.ConsoleApp, PgOutbox.ConsoleApp, Schedule.Console, Storage.Tests.
+
+### Tests
+
+Located in `src/Tests/`: 15 test projects using **xunit v3**, **Testcontainers** (PostgreSQL + Minio) for integration tests. Test fixtures in `src/Tests/Fixtures/`.
+
+---
+
+## Building and Running
+
+### Prerequisites
+
+- .NET 10 SDK
+- PowerShell (for local build scripts)
+
+### Build Commands
+
+```powershell
+# Full build (clean + restore + build)
+.\build\do_build.ps1
+
+# Run all tests
+.\build\do_test.ps1
+
+# Package NuGet packages (produces .nupkg + .snupkg in dist/)
+.\build\do_package.ps1
+
+# Push to local registry
+.\build\do_push_local.ps1
+
+# Push to prod (nuget.org)
+.\build\do_push_prod.ps1
+```
+
+### Direct dotnet commands
+
+```powershell
+# Restore
+dotnet restore src/Sa.slnx -c Release
+
+# Build
+dotnet build src/Sa.slnx -c Release -v n
+
+# Test (all)
+dotnet test src/Sa.slnx -v n
+
+# Test CI (skip tests requiring local Docker infrastructure)
+dotnet test src/Sa.slnx --filter "Category!=Local"
+```
+
+### GitHub Actions
+
+Workflow in `.github/workflows/` — builds on `main` branch push/PR. Uses `dotnet 9.x` runtime in CI (despite targeting net10.0). Note: tests are commented out in CI.
+
+---
+
+## Architecture Notes
+
+### Shared Code Pattern
+
+Common utilities live in `src/Sa/` and are linked into consuming projects via MSBuild ``. This avoids duplication while keeping projects independently buildable. Linked classes include:
+
+- `Classes/`: LockRenewer, MurmurHash3, Retry, ResetLazy, Section, MimeTypeMap, IArrayPool, LockRenewer
+- `Extensions/`: DateTimeExtensions, EnumerableExtensions, ExceptionExtensions, SpanExtensions, StringExtensions, NumericExtensions, StrToExtensions, GuidExtensions
+
+### Project Dependencies
+
+```
+Sa.Utils.WorkQueue → (none)
+Sa.Schedule → Sa.Utils.WorkQueue
+Sa.Outbox → Sa.Schedule
+Sa.Partitional.PostgreSql → Sa.Schedule + Sa.Data.PostgreSql
+Sa.Outbox.PostgreSql → Sa.Outbox + Sa.Partitional.PostgreSql (+ object pool, recycler mem stream)
+Sa.Data.S3 → (none, just Npgsql indirectly)
+Sa.HybridFileStorage → (base abstraction)
+Sa.HybridFileStorage.S3 → Sa.Data.S3 + Sa.HybridFileStorage
+Sa.Configuration → Microsoft.Extensions.Hosting
+Sa.Configuration.PostgreSql → (standalone)
+```
+
+### Common Properties (inherited by all packages)
+
+From `Common.Properties.xml`:
+- Target: `net10.0`
+- AOT: `PublishAot=true`, `IsAotCompatible=true`
+- Nullable: enabled
+- Analyzers: enabled
+- TrimmerSingleWarn: false
+- Symbols: included
+- License: MIT
+
+From `Common.NuGet.Properties.xml`: additional shared package references (logging, DI, SourceLink).
+
+### Testing Conventions
+
+- Framework: **xunit v3** (not classic xunit)
+- Integration tests use **Testcontainers** (PostgreSQL + Minio)
+- Test projects import `Host.Test.Properties.xml` for common test config
+- Local-dependent tests are tagged `Category!=Local` for CI
+
+---
+
+## Development Conventions
+
+- **ImplicitUsings** and **Nullable** enabled across all projects
+- **Central Package Management** — all versions in `Directory.Packages.props`
+- **SourceLink** enabled for debug symbol linking to GitHub
+- **InternalsVisibleTo** used for test project access to internal members
+- No `_editorconfig` rules beyond standard .NET conventions
+- All projects use SDK-style csproj format
+- Solution managed via `.slnx` (new solution format)
diff --git a/README-ru.md b/README-ru.md
new file mode 100644
index 00000000..fec05fa5
--- /dev/null
+++ b/README-ru.md
@@ -0,0 +1,260 @@
+# Sa — Набор инфраструктурных библиотек для .NET 10
+
+Нейрохерня - Серия переиспользуемых .NET 10-библиотек, сфокусированных на инфраструктурных паттернах для распределённых систем. Целевая платформа — **.NET 10.0**, используется **Native AOT**, применяется паттерн **Central Package Management (CPM)** через `Directory.Packages.props`.
+
+---
+
+### [Sa](src/Sa) — Общие утилиты
+
+Ядро экосистемы **Sa**: базовые классы и методы расширения, линкуемые в другие пакеты через ``. Целевая платформа — **.NET 10.0**, совместимость с **Native AOT**, нулевые внешние зависимости.
+
+
+Смотрите [полную документацию API](src/Sa/Readme.md).
+
+---
+
+### [Sa.Outbox.PostgreSql](src/Sa.Outbox.PostgreSql) — Реализация Outbox на PostgreSQL
+
+Реализация паттерна **Transactional Outbox** на PostgreSQL для гарантированной доставки сообщений в распределённых системах. Предотвращает потерю сообщений и гарантирует обработку даже при сбоях.
+
+- **Гарантированная доставка**: сообщения хранятся в БД до успешной обработки
+- **Параллельная обработка**: несколько воркеров безопасно конкурируют за задачи через `SKIP LOCKED`
+- **Мультитенантность**: изоляция и параллелизм по арендаторам
+- **Авто-масштабирование**: runtime-изменение параллелизма без перезапуска
+- **Планируемая очистка**: автоматическое удаление старых партиций
+- **Self-bootstrapping**: авто-регистрация настроек консьюмера при первом запуске
+- **Immutable настройки**: `OutboxConsumerSettings` record с fluent-билдером
+
+See [full README](src/Sa.Outbox.PostgreSql/Readme.md).
+
+---
+
+### [Sa.Partitional.PostgreSql](src/Sa.Partitional.PostgreSql) — Декларативное партиционирование PostgreSQL
+
+Объявление партицирования таблиц PostgreSQL (range: день/месяц/год; list) с автоматической миграцией, планированием очистки и in-memory кэшем.
+
+- **Range-партиционирование** по дню, месяцу или году с авто-именованием по timestamp
+- **List-партиционирование** по строковым/числовым ключам с иерархическими дочерними партициями
+- **Fluent-билдер** для объявления таблиц, настройки fillfactor, кастомных ограничений и миграций
+- **Автоматическая миграция** — предсоздание будущих партиций как фоновая задача
+- **Автоматическая очистка** — удаление старых партиций по настраиваемому окну удержания
+- **In-memory кэш** — избегает повторных запросов к каталогу; инвалидируется при runtime-изменениях
+- **StrOrNum** — discriminated union для типобезопасных значений ключей партиций
+
+See the full [Guide](src/Sa.Partitional.PostgreSql/Guide.md) and [API Reference](src/Sa.Partitional.PostgreSql/ApiReference.md).
+
+---
+
+### [Sa.Schedule](src/Sa.Schedule) — Планировщик задач
+
+Конфигурация и выполнение задач по расписанию — cron, интервалы, одноразовые запуски.
+
+| Возможность | Описание |
+|-------------|----------|
+| **Cron-тайминги** | Любое cron-выражение через `IJobTiming.FromCron()` |
+| **Интервальное расписание** | `EverySeconds`, `EveryMinutes`, `EveryHours`, `EveryDays` |
+| **Одноразовые задачи** | `RunOnce()` с опциональной начальной задержкой |
+| **Стратегии ошибок** | `CloseApplication`, `AbortJob`, `StopAllJobs`, `Ignore` |
+| **Повторные попытки** | Настраиваемое количество ретраев на ошибку |
+| **Интерцепторы** | Кросс логика через `IJobInterceptor` |
+| **Обработчики ошибок** | Глобальные `HandleError`-хендлеры на уровне планировщика |
+| **DI-интеграция** | `AddSaSchedule(Action)` с `BackgroundService` |
+
+```csharp
+builder.Services.AddSaSchedule(b => b
+ .AddJob()
+ .WithName("cleanup")
+ .EveryHours(1)
+ .ConfigureErrorHandling(eh => eh.IfErrorRetry(3).ThenAbortJob())
+);
+```
+
+---
+
+### [Sa.HybridFileStorage](src/Sa.HybridFileStorage) — Гибридное файловое хранилище
+
+Абстракция файлового хранилища с автоматическим failover между провайдерами (FileSystem ↔ S3 ↔ PostgreSQL).
+
+| Возможность | Описание |
+|-------------|----------|
+| **Мультипровайдер** | FileSystem, S3 (Minio), PostgreSQL — подключайте сколько угодно |
+| **Автоматический failover** | При недоступности одного провайдера — переход к следующему |
+| **Интерцепторы** | `before`, `after`, `onError` хуки на каждом провайдере |
+| **Пакетные операции** | `CopyToScopeBatchAsync` с параллелизмом и прогрессом |
+| **Расширения** | `CopyFromFileAsync`, `CopyToBasketAsync` для удобства |
+| **InMemory-провайдер** | Для тестирования: `AddSaInMemoryFileStorage()` |
+
+```csharp
+builder.Services.AddSaHybridFileStorage(cfg => cfg
+ .ConfigureStorage((sp, c) => c
+ .AddStorage(new FileSystemStorage("disk"))
+ .AddStorage(new S3Storage("s3"))
+ )
+);
+```
+
+---
+
+### [Sa.Configuration](src/Sa.Configuration) — Аргументы командной строки и секреты
+
+| Компонент | Назначение |
+|-----------|------------|
+| **Arguments** | Парсер CLI-аргументов в стиле dictionary — поддержка одиночных и множественных значений, типизированные геттеры (`GetBool`, `GetInt`, `GetTimeSpan` и т.д.) |
+| **Secrets** | Безопасное управление секретами из файлов, переменных окружения и генерируемых host-key файлов. Поддержка chained stores и environment-aware загрузки |
+
+```csharp
+var args = Arguments.CreateDefault();
+var dbPassword = args["db-password"]; // string?
+var timeout = args.GetTimeSpan("timeout"); // TimeSpan?
+```
+
+---
+
+### [Sa.Configuration.PostgreSql](src/Sa.Configuration.PostgreSql) — Динамическая конфигурация из PostgreSQL
+
+Добавляет источник конфигурации из БД — изменения отражаются в приложении без перекомпиляции и редеплоя.
+
+```csharp
+builder.Configuration.AddSaPostgreSqlConfiguration(new PostgreSqlConfigurationOptions(
+ connectionString: "Host=localhost;Database=myapp",
+ selectSql: "SELECT config_key, config_value FROM app_config",
+ parameters: Array.Empty()
+));
+```
+
+---
+
+### [Sa.Media](src/Sa.Media) — Асинхронное чтение WAV
+
+Памятно-эффективный асинхронный читатель WAV-файлов с конвертацией форматов.
+
+| Метод | Описание |
+|-------|----------|
+| `CreateFromFile(path)` | Открыть файл по пути |
+| `GetHeaderAsync()` | Считать WAV-заголовок |
+| `ReadSamplesPerChannelAsync()` | Потоковое чтение сэмплов по каналам |
+| `ReadDoubleSamplesAsync()` | Нормализованные double-сэмплы [-1..1] |
+| `ConvertToFormatAsync()` | Конвертация в PCM16/24/32, IEEE float |
+| `ReadStreamableChunksAsync()` | Чанки фиксированного размера для streaming |
+
+```csharp
+using var reader = AsyncWavReader.CreateFromFile("audio.wav");
+await foreach (var packet in reader.ReadDoubleSamplesAsync())
+{
+ Console.WriteLine($"Ch{packet.ChannelId}: {packet.Sample:F4}");
+}
+```
+
+---
+
+### [Sa.Media.FFmpeg](src/Sa.Media.FFmpeg) — Обёртка FFmpeg для .NET
+
+FFmpeg из коробки со встроенными бинарниками (Windows x64 + Linux) и DI.
+
+| Интерфейс | Назначение |
+|-----------|------------|
+| `IFFMpegExecutor` | Конвертация аудио/видео (PCM16LE, MP3, OGG) |
+| `IFFProbeExecutor` | Извлечение метаданных (длина, каналы, частота, битрейт) |
+| `IPcmS16LeChannelManipulator` | Разделение/объединение каналов |
+| `IFFMpegLocator` | Автопоиск исполняемого FFmpeg |
+
+```csharp
+builder.Services.AddSaFFMpeg();
+
+var probe = IFFProbeExecutor.Default;
+var meta = await probe.GetMetaInfo("input.mp3");
+Console.WriteLine($"Duration: {meta.Duration}s, Channels: {meta.Channels}");
+```
+
+---
+
+### [Sa.Data.PostgreSql](src/Sa.Data.PostgreSql) — Лёгкая обёртка Npgsql
+
+Без ORM-overhead, с DI, Native AOT и минимальными аллокациями.
+
+| Метод | Описание |
+|-------|----------|
+| `ExecuteNonQuery` | INSERT / UPDATE / DELETE / DDL с возвратом rowCount |
+| `ExecuteScalar / ExecuteScalarTyped` | Одиночное значение с авто-кастомом |
+| `ExecuteReader` | Потоковое чтение через callback (без загрузки в память) |
+| `ExecuteReaderList` | Сборка всех строк в `List` |
+| `ExecuteReaderFirst` | Первое значение первого столбца |
+| `ExecuteReaderSingle` | Безопасное scalar-значение |
+| `BeginBinaryImport` | Быстрый COPY BINARY для массового импорта |
+| `PgRetryStrategy` | Повторы с jitter для transient-ошибок Npgsql |
+
+---
+
+### [Sa.Utils.WorkQueue](src/Sa.Utils.WorkQueue) — Асинхронная очередь с ограничением параллелизма
+
+Высокопроизводительная очередь задач на базе `System.Threading.Channels` с ограниченной ёмкостью, динамическим контролем параллелизма и стратегиями масштабирования.
+
+| Возможность | Описание |
+|-------------|----------|
+| **Ограниченная очередь** | Back-pressure через `BoundedChannel` |
+| **Динамический параллелизм** | Изменяйте `ConcurrencyLimit` на лету |
+| **Стратегии масштабирования** | `Lifo` • `Fifo` • `RoundRobin` • `Random` |
+| **Стратегии ошибок** | `Continue`, `StopReader`, `ShutdownQueue` |
+| **Обратные вызовы статусов** | `Running` → `Completed` / `Faulted` / `Cancelled` / `Aborted` |
+| **Логирование без аллокаций** | `[LoggerMessage]` source generator |
+
+See [full README](src/Sa.Utils.WorkQueue/Readme.md).
+
+---
+
+## Образцы
+
+В `src/Samples/`:
+
+| Образец | Описание |
+|---------|----------|
+| [Configuration.Web](src/Samples/Configuration.Web) | CLI-аргументы + секреты в ASP.NET |
+| [FFmpeg.Console](src/Samples/FFmpeg.Console) | Извлечение метаданных FFmpeg |
+| [HybridFileStorage.Console](src/Samples/HybridFileStorage.Console) | Мульти-провайдерное хранилище |
+| [Partitional.ConsoleApp](src/Samples/Partitional.ConsoleApp) | Декларативное партиционирование |
+| [PgOutbox.ConsoleApp](src/Samples/PgOutbox.ConsoleApp) | Паттерн Outbox |
+| [Schedule.Console](src/Samples/Schedule.Console) | Планировщик задач |
+| [Storage.Tests](src/Samples/Storage.Tests) | Тесты гибридного хранилища |
+
+---
+
+## Тесты
+
+В `src/Tests/`: 15 тестовых проектов на **xunit v3** с **Testcontainers** (PostgreSQL + Minio) для интеграционных тестов.
+
+---
+
+## Сборка
+
+```powershell
+# Полная сборка
+.\build\do_build.ps1
+
+# Запуск тестов
+.\build\do_test.ps1
+
+# Создание NuGet-пакетов
+.\build\do_package.ps1
+```
+
+Прямые команды dotnet:
+
+```powershell
+dotnet restore src/Sa.slnx -c Release
+dotnet build src/Sa.slnx -c Release -v n
+dotnet test src/Sa.slnx -v n
+```
+
+---
+
+## Архитектура
+
+- Целевая платформа — **.NET 10.0** с **Native AOT**
+- **Central Package Management** — версии в `Directory.Packages.props`
+- Общие утилиты в **Sa** линкуются в consuming-проект через ``
+- Все пакеты используют SDK-style csproj с implicit usings, nullable и анализаторами
+- Решение управляется через `.slnx`
+
+## Лицензия
+
+MIT
diff --git a/README.md b/README.md
index 0142ee8f..5f0589ce 100644
--- a/README.md
+++ b/README.md
@@ -1,70 +1,290 @@
-# sa
+# Sa — .NET 10 Infrastructure Libraries
-dot net10 experimental aot project
+Reusable infrastructure libraries for distributed .NET 10 systems — **Native AOT compatible**, **nullable enabled**, built on modern .NET primitives.
+---
-## [Sa.Outbox.PostgreSql](src/Sa.Outbox.PostgreSql)
+## Libraries
-Designed for implementing the Outbox pattern using PostgreSQL, which is used to ensure reliable message delivery in distributed systems. It helps prevent message loss and guarantees that messages will be processed even in the event of failures.
+### [Sa](src/Sa) — Shared Utilities
-- Reliable message delivery: Ensures that messages are stored in the database until they are successfully processed.
-- Parallel processing: Enables messages to be processed in parallel, increasing system performance.
-- Flexibility: Supports various types of messages and their handlers.
-- Tenant support: Allows for even distribution of load.
-- Data cleaning: scheduled deletion of old data.
+Core utility library consumed by other packages via ``. Targets **.NET 10.0**, **Native AOT compatible**, zero external dependencies.
-## [Sa.Partitional.PostgreSql](src/Sa.Partitional.PostgreSql)
+See [full API reference](src/Sa/Readme.md).
-A library designed for managing table partitioning in PostgreSQL with the aim of improving performance and manageability for large volumes of data.
+---
-- Declaratively describe a partitioned table by time (day, month, year).
-- Define partitions based on lists of keys for rows or numbers.
-- Set a schedule for migrations to create new partitions.
-- Set a schedule for deleting old partitions.
-- Manage partitions.
+### [Sa.Configuration](src/Sa.Configuration) — CLI Arguments & Secrets
-## [Sa.Schedule](src/Sa.Schedule)
+| Type | Purpose |
+|------|---------|
+| `Arguments` | Command-line argument parser — dictionary-like access, typed getters (`GetBool`, `GetInt`, `GetTimeSpan`, etc.) |
+| `Secrets` | Secure secrets management from files, environment variables, and host-key files. Supports chained stores and templating (`${secret:key}`) |
-`Sa.Schedule` provides a way to configure and execute tasks on a schedule.
+```csharp
+// Arguments
+var args = new Arguments(argsArray);
+var port = args.GetInt("port") ?? 8080;
-- It allows you to manage a set of tasks that will be executed at specific times or at defined intervals.
-- You can start and stop tasks.
-- Define failure strategies: close the application, stop job, stop all jobs, or ignore the failure.
+// Secrets
+var secrets = Secrets.CreateDefault();
+var populated = secrets.PopulateSecrets("Host={db_host};Password=${db_password}");
+```
-## [Sa.HybridFileStorage](src/Sa.HybridFileStorage)
+---
-`IHybridFileStorage` - interface designed for hybrid file storage systems that facilitates the management of file operations, ensuring reliable and resilient access to files across multiple storage providers.
+### [Sa.Configuration.PostgreSql](src/Sa.Configuration.PostgreSql) — Dynamic DB Configuration
-- Supports file operations such as uploading, downloading, and deleting files.
-- Integrates multiple storage providers (e.g., file system, s3, PostgreSQL) for enhanced reliability.
-- Automatically switches between providers in case one becomes unavailable, ensuring continuous access to files.
-- Promotes improved resilience and availability of file data in applications requiring dependable storage management.
+PostgreSQL-backed `IConfigurationSource` — changes in the database reflect in-app without redeploy.
-## [Sa.Configuration](src/Sa.Configuration)
+```csharp
+builder.Configuration.AddSaPostgreSqlConfiguration(new PostgreSqlConfigurationOptions(
+ connectionString: "Host=localhost;Database=myapp",
+ selectSql: "SELECT key, value FROM app_config",
+ parameters: Array.Empty()));
+```
-- `Arguments` class parses command-line arguments in a C# application, enabling easy retrieval of parameter values through a dictionary-like interface. It supports both single-value and multi-value parameters for flexible command-line configurations.
-- `Secrets` class securely manages sensitive information, such as API keys and database passwords, from various sources. It can load secrets from files, environment variables, and dynamically generated host key files.
+Supports parameterised queries and `PgRetryStrategy` for transient error handling.
-## [Sa.Configuration.PostgreSql](src/Sa.Configuration.PostgreSql)
+---
-`AddPostgreSqlConfiguration` extension method allows you to add a PostgreSQL-based configuration source to an IConfigurationBuilder.
+### [Sa.Data.PostgreSql](src/Sa.Data.PostgreSql) — Lightweight Npgsql Wrapper
-- This setup allows for dynamic configuration management, where changes in the database can be reflected in the application without needing to recompile or redeploy.
+Thin wrapper over Npgsql for typical database operations — zero ORM overhead, Native AOT friendly.
-## [Sa.Media](src/Sa.Media)
+| Method | Description |
+|--------|-------------|
+| `ExecuteNonQueryAsync` | INSERT / UPDATE / DELETE / DDL with row count return |
+| `ExecuteScalarAsync` / `ExecuteScalarTypedAsync` | Single value with auto-cast |
+| `ExecuteReaderAsync` | Streaming row reading via callback (no full result in memory) |
+| `ExecuteReaderListAsync` | Collect all rows into `List` |
+| `ExecuteReaderFirstAsync` | First column of first row (Guid, TimeSpan, DateTime, int, long, etc.) |
+| `ExecuteReaderSingleAsync` / `TryExecuteReaderSingleAsync` | Safe scalar with nullability |
+| `ExecuteTransactionAsync` | Atomic operations with auto commit/rollback |
+| `BeginBinaryImportAsync` | Fast COPY BINARY for bulk inserts |
+| `PgRetryStrategy` | Retry with jitter for transient Npgsql errors |
-- `AsyncWavReader` async and memory-efficient WAV file reader for .NET
+DI registration: `AddSaPostgreSqlDataSource()`.
-## [Sa.Media.FFmpeg](src/Sa.Media.FFmpeg)
+---
-FFmpeg .NET Wrapper - ready to use out of the box with minimal setup
+### [Sa.Data.S3](src/Sa.Data.S3) — S3 Data Client
-- Extract metadata from media files (duration, channels, sample rate, etc.)
-- Convert audio to: WAV, MP3, MP4, OGG ..
-- Splits/Join audio file by channels
-- Built-in FFmpeg binaries for Windows x64 and Linux
-- Supports Dependency Injection (DI) via standard IServiceCollection integration
+Minio-compatible S3 client for data operations.
-## Sa.Utils
+---
-- [Sa.Utils.WorkQueue](src/Sa.Utils.WorkQueue) - async Queue with Concurrency Limiting
\ No newline at end of file
+### [Sa.Outbox](src/Sa.Outbox) — Transactional Outbox Core
+
+Base infrastructure for the **Transactional Outbox** pattern — guarantees atomic message recording alongside business operations within a single database transaction, with reliable delivery, retries, blocking, multi-threading, and multi-tenancy support.
+
+Defines abstractions; concrete DB work (PostgreSQL, SQL Server, etc.) is implemented by providers (`Sa.Outbox.PostgreSql`, `Sa.Outbox.SqlServer`).
+
+| Key Type | Purpose |
+|----------|---------|
+| `IOutboxBuilder` | Fluent configuration builder |
+| `IOutboxMessagePublisher` | Publish messages to outbox |
+| `IConsumer` | Message consumer interface |
+| `IOutboxContextOperations` | Delivery status change operations (`Ok`, `Error`, `Warn`, `Postpone`, etc.) |
+| `OutboxConsumerSettings` | Immutable snapshot of consumer group settings (interval, batches, concurrency, retries…) |
+| `OutboxConsumerSettingsBuilder` | Fluent builder for creating/updating `OutboxConsumerSettings` |
+| `IOutboxConsumerManager` | Runtime manager: atomic swap, pause/resume, change subscriptions |
+| `IDeliverySnapshot` | Read-only view of registered deliveries for diagnostics |
+| `DeliveryStatus` / `DeliveryStatusCode` | HTTP-like delivery status codes |
+| `ExponentialBackoffRetryStrategy` | Exponential backoff with jitter |
+
+See individual provider READMEs for full usage examples.
+
+---
+
+### [Sa.Outbox.PostgreSql](src/Sa.Outbox.PostgreSql) — PostgreSQL Provider
+
+Production-ready PostgreSQL implementation with UUID v7 IDs, BINARY COPY bulk insertion, `SKIP LOCKED` concurrent consumption, advisory locks for offset coordination, and automated partition migration/cleanup.
+
+See [full README](src/Sa.Outbox.PostgreSql/Readme.md).
+
+---
+
+### [Sa.Partitional.PostgreSql](src/Sa.Partitional.PostgreSql) — Declarative Partitioning
+
+Declarative PostgreSQL table partitioning for .NET 10 — range (day/month/year) and list partitioning with automated migration, cleanup scheduling, and in-memory caching.
+
+- **Range partitioning** by day, month, or year with automatic timestamp-based naming
+- **List partitioning** by string or numeric keys with hierarchical child partitions
+- **Fluent builder API** for declaring tables, tuning fillfactor, custom constraints, and migrations
+- **Automated migration** — pre-create future partitions as a background job
+- **Automated cleanup** — drop old partitions past a configurable retention window
+- **In-memory cache** — avoids repeated catalog queries; auto-invalidates on runtime changes
+- **StrOrNum** discriminated union for type-safe partition key values
+
+See [Guide](src/Sa.Partitional.PostgreSql/Guide.md) and [API Reference](src/Sa.Partitional.PostgreSql/ApiReference.md).
+
+---
+
+### [Sa.Schedule](src/Sa.Schedule) — Scheduled Task Executor
+
+Configurable and executable scheduled tasks with failure strategies.
+
+| Feature | Description |
+|---------|-------------|
+| **Flexible timing** | Cron expressions, fixed intervals (seconds/minutes/hours/days), one-shot delays |
+| **Failure strategies** | `CloseApplication`, `AbortJob`, `StopAllJobs`, or `Ignore` |
+| **Retry on failure** | Configurable retry count per job |
+| **Concurrency control** | Per-job `ConcurrencyLimit` and `MaxConcurrency` |
+| **Interceptors** | `IJobInterceptor` for pre/post execution hooks |
+| **Error handlers** | Global `Func` error handlers |
+| **Runtime management** | Start, stop, restart individual schedulers via `IScheduler` |
+
+```csharp
+builder.Services.AddSaSchedule(builder => builder
+ .UseHostedService()
+ .AddJob()
+ .WithName("cleanup")
+ .EveryHours(1)
+ .ConfigureErrorHandling(eh => eh.IfErrorRetry(3).ThenAbortJob()));
+```
+
+---
+
+### [Sa.HybridFileStorage](src/Sa.HybridFileStorage) — Multi-Provider File Storage
+
+`IHybridFileStorage` abstracts file operations across multiple storage providers (FileSystem, S3, PostgreSQL) with automatic failover.
+
+| Capability | Description |
+|------------|-------------|
+| **Upload / Download / Delete** | Standard file operations with streaming |
+| **Multi-provider** | Register any `IFileStorage` implementation |
+| **Automatic failover** | Tries providers sequentially; aggregates errors if all fail |
+| **Batch operations** | Parallel batch upload/download with progress reporting |
+| **Interceptors** | Pre/post/on-error hooks per provider |
+| **Built-in providers** | `InMemoryFileStorage` (testing), plus `FileSystem`, `S3`, `Postgres` in separate packages |
+
+```csharp
+builder.Services.AddSaHybridFileStorage(cfg => cfg
+ .ConfigureStorage(sp => sp
+ .AddStorage(new FileSystemStorage("/data/uploads"))
+ .AddStorage(new S3Storage("s3-bucket"))));
+```
+
+Providers: [`Sa.HybridFileStorage.FileSystem`](src/Sa.HybridFileStorage.FileSystem), [`Sa.HybridFileStorage.S3`](src/Sa.HybridFileStorage.S3), [`Sa.HybridFileStorage.Postgres`](src/Sa.HybridFileStorage.Postgres).
+
+---
+
+### [Sa.Media](src/Sa.Media) — Async WAV Reader
+
+Memory-efficient, fully async WAV file reader built on `System.IO.Pipelines`.
+
+| Method | Description |
+|--------|-------------|
+| `CreateFromFile` / `Create(Stream)` | Factory methods |
+| `GetHeaderAsync` | Parse WAV header |
+| `ReadSamplesPerChannelAsync` | Raw bytes per channel |
+| `ReadDoubleSamplesAsync` | Normalized double samples |
+| `ConvertToFormatAsync` | Convert to PCM16/24/32, IEEE float |
+| `ReadStreamableChunksAsync` | Streaming chunks with configurable batch size |
+
+```csharp
+using var reader = AsyncWavReader.CreateFromFile("audio.wav");
+var header = await reader.GetHeaderAsync();
+await foreach (var packet in reader.ReadDoubleSamplesAsync())
+{
+ Console.WriteLine($"Ch{packet.ChannelId}: {packet.Sample}");
+}
+```
+
+---
+
+### [Sa.Media.FFmpeg](src/Sa.Media.FFmpeg) — FFmpeg .NET Wrapper
+
+Ready-to-use FFmpeg integration with built-in binaries (Windows x64 + Linux) and DI support.
+
+| Interface | Purpose |
+|-----------|---------|
+| `IFFMpegExecutor` | Audio/video conversion (PCM16LE, MP3, OGG) |
+| `IFFProbeExecutor` | Metadata extraction (duration, channels, sample rate, bitrate) |
+| `IPcmS16LeChannelManipulator` | Channel split/join operations |
+| `IFFMpegLocator` | Auto-discovery of FFmpeg executable |
+
+```csharp
+builder.Services.AddSaFFMpeg();
+
+var probe = IFFProbeExecutor.Default;
+var meta = await probe.GetMetaInfo("input.mp3");
+Console.WriteLine($"Duration: {meta.Duration}s, Channels: {meta.Channels}");
+```
+
+---
+
+### [Sa.Utils.WorkQueue](src/Sa.Utils.WorkQueue) — Async Queue with Concurrency Limiting
+
+High-performance task queue built on `System.Threading.Channels` with bounded capacity, dynamic concurrency scaling, and multiple reader-scaling strategies.
+
+| Feature | Description |
+|---------|-------------|
+| **Bounded queue** | Back-pressure via `BoundedChannel` — overflow handled by `Wait`, `DropWrite`, `DropOldest` |
+| **Dynamic concurrency** | Change `ConcurrencyLimit` at runtime |
+| **Scaling strategies** | `Lifo` • `Fifo` • `RoundRobin` • `Random` |
+| **Error strategies** | `Continue`, `StopReader`, `ShutdownQueue` |
+| **Status callbacks** | `Running` → `Completed` / `Faulted` / `Cancelled` / `Aborted` |
+| **Zero-allocation logging** | `[LoggerMessage]` source generator |
+
+See [full README](src/Sa.Utils.WorkQueue/Readme.md).
+
+---
+
+## Samples
+
+Located in `src/Samples/`:
+
+| Sample | Description |
+|--------|-------------|
+| [Configuration.Web](src/Samples/Configuration.Web) | CLI args + secrets in ASP.NET |
+| [FFmpeg.Console](src/Samples/FFmpeg.Console) | FFmpeg metadata extraction |
+| [HybridFileStorage.Console](src/Samples/HybridFileStorage.Console) | Multi-provider file storage |
+| [Partitional.ConsoleApp](src/Samples/Partitional.ConsoleApp) | Declarative partitioning |
+| [PgOutbox.ConsoleApp](src/Samples/PgOutbox.ConsoleApp) | Outbox pattern demo |
+| [Schedule.Console](src/Samples/Schedule.Console) | Scheduled task executor |
+| [Storage.Tests](src/Samples/Storage.Tests) | Hybrid file storage tests |
+
+---
+
+## Tests
+
+Located in `src/Tests/`: 15 test projects using **xunit v3** and **Testcontainers** (PostgreSQL + Minio) for integration tests.
+
+---
+
+## Building
+
+```powershell
+# Full build
+.\build\do_build.ps1
+
+# Run tests
+.\build\do_test.ps1
+
+# Package NuGet packages
+.\build\do_package.ps1
+```
+
+Direct dotnet commands:
+
+```powershell
+dotnet restore src/Sa.slnx -c Release
+dotnet build src/Sa.slnx -c Release -v n
+dotnet test src/Sa.slnx -v n
+```
+
+---
+
+## Architecture
+
+- Targets **.NET 10.0** with **Native AOT**
+- Uses **Central Package Management** (`Directory.Packages.props`)
+- Shared utilities in **Sa** are linked into consuming projects
+- All packages use SDK-style csproj with implicit usings, nullable, and analyzers
+- Solution managed via `.slnx`
+
+## License
+
+MIT
diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index 79e17c72..a7086e1f 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -23,6 +23,5 @@
-
diff --git a/src/Sa.Configuration.PostgreSql/DatabaseConfigurationProvider.cs b/src/Sa.Configuration.PostgreSql/DatabaseConfigurationProvider.cs
index a033bf0c..b2f5ffb3 100644
--- a/src/Sa.Configuration.PostgreSql/DatabaseConfigurationProvider.cs
+++ b/src/Sa.Configuration.PostgreSql/DatabaseConfigurationProvider.cs
@@ -2,12 +2,13 @@
using Microsoft.Extensions.Configuration;
using Sa.Data.PostgreSql;
-using System.Threading;
+
///
/// Configuration provider that loads settings from a PostgreSQL database.
///
-public sealed class DatabaseConfigurationProvider(PostgreSqlConfigurationOptions options) : ConfigurationProvider
+public sealed class DatabaseConfigurationProvider(PostgreSqlConfigurationOptions options)
+ : ConfigurationProvider
{
///
/// Loads configuration from PostgreSQL database.
@@ -15,16 +16,16 @@ public sealed class DatabaseConfigurationProvider(PostgreSqlConfigurationOptions
public override void Load()
{
PgRetryStrategy
- .ExecuteWithRetry(async _ => await LoadAsync(options))
+ .ExecuteWithRetry(async _ => await LoadAsync())
.AsTask()
.GetAwaiter()
.GetResult();
}
///
- /// Loads configuration from PostgreSQL database asynchronously.
+ /// Asynchronously loads configuration from PostgreSQL database.
///
- private async Task LoadAsync(PostgreSqlConfigurationOptions options)
+ private async Task LoadAsync()
{
try
{
diff --git a/src/Sa.Configuration.PostgreSql/Readme-ru.md b/src/Sa.Configuration.PostgreSql/Readme-ru.md
new file mode 100644
index 00000000..4fe8a000
--- /dev/null
+++ b/src/Sa.Configuration.PostgreSql/Readme-ru.md
@@ -0,0 +1,129 @@
+# Sa.Configuration.PostgreSql
+
+Динамический источник конфигурации для .NET, загружающий настройки из PostgreSQL. Изменения в БД применяются к работающему приложению без перезапуска — достаточно вызвать `Reload()` на `IConfigurationRoot`.
+
+---
+
+## Возможности
+
+- **Живая конфигурация**: значения хранятся в БД и могут быть изменены во время выполнения
+- **Параметризированные SQL-запросы**: поддержка `@named_parameters` через `NpgsqlParameter`
+- **Автоматические повторы**: встроенная стратегия повторов (`PgRetryStrategy`) с детекцией транзитных ошибок Npgsql
+- **Обрезка ключей/значений**: пробелы автоматически обрезаются и у ключей, и у значений
+- **Безопасная обработка NULL**: `NULL` в БД → `null` в конфиге; пустая строка → `string.Empty`
+
+---
+
+## Публичный API
+
+| Тип | Назначение |
+|-----|-----------|
+| `PostgreSqlConfigurationOptions` | Immutable record: `ConnectionString`, `SelectSql`, `Parameters` |
+| `DatabaseConfigurationSource` | Реализация `IConfigurationSource` |
+| `DatabaseConfigurationProvider` | `ConfigurationProvider`, загружающий пары ключ-значение из БД |
+| `Setup.AddSaPostgreSqlConfiguration()` | Метод-расширение для `IConfigurationBuilder` |
+
+---
+
+## Быстрый старт
+
+```csharp
+using Sa.Configuration.PostgreSql;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.Configuration.AddSaPostgreSqlConfiguration(new PostgreSqlConfigurationOptions(
+ ConnectionString: "Host=localhost;Database=myapp;Username=app;Password=secret",
+ SelectSql: "SELECT key, value FROM app_settings"
+));
+
+var app = builder.Build();
+
+// Чтение настроек
+var theme = app.Configuration["theme"]; // → "dark"
+var lang = app.Configuration["language"]; // → "en"
+```
+
+---
+
+## Параметризированные запросы
+
+Используйте `@parameters` для фильтрации по клиенту/арендатору:
+
+```csharp
+builder.Configuration.AddSaPostgreSqlConfiguration(new PostgreSqlConfigurationOptions(
+ ConnectionString: "...",
+ SelectSql: "SELECT key, value FROM client_settings WHERE client_id = @client_id",
+ Parameters: [new NpgsqlParameter("client_id", "acme-corp")]
+));
+```
+
+---
+
+## Обновления живой конфигурации
+
+Когда строки в таблице `app_settings` изменяются, приложение может подхватить новые значения:
+
+```csharp
+// После изменения строк в базе данных:
+((IConfigurationRoot)app.Configuration).Reload();
+
+// Или вручную:
+provider.Reload(); // DatabaseConfigurationProvider реализует IConfigurationProvider
+```
+
+---
+
+## Поведение загрузки
+
+| Сценарий | Результат |
+|----------|----------|
+| Ключ пустой или состоит только из пробелов | Пропускается |
+| Значение `NULL` в БД | Сохраняется как `null` |
+| Значение пустая строка в БД | Сохраняется как `string.Empty` |
+| Ошибка подключения | `InvalidOperationException` с оригинальным исключением как `InnerException` |
+
+---
+
+## Схема таблицы
+
+Минимальная таблица, необходимая для провайдера:
+
+```sql
+CREATE TABLE app_settings (
+ key VARCHAR PRIMARY KEY,
+ value TEXT
+);
+
+-- Пример данных
+INSERT INTO app_settings (key, value) VALUES
+ ('theme', 'dark'),
+ ('language', 'en'),
+ ('debug_mode', ''); -- пустая строка
+```
+
+---
+
+## Зависимости
+
+- `Microsoft.Extensions.Configuration`
+- `Sa.Data.PostgreSql` (обёртка Npgsql с PgRetryStrategy и IPgDataSource)
+
+---
+
+## Структура проекта
+
+```
+src/Sa.Configuration.PostgreSql/
+├── PostgreSqlConfigurationOptions.cs # Record опций
+├── DatabaseConfigurationSource.cs # IConfigurationSource
+├── DatabaseConfigurationProvider.cs # ConfigurationProvider + повторы
+├── Setup.cs # Метод-расширение AddSaPostgreSqlConfiguration()
+└── Readme.md # ← вы здесь
+```
+
+---
+
+## Лицензия
+
+MIT
diff --git a/src/Sa.Configuration.PostgreSql/Readme.md b/src/Sa.Configuration.PostgreSql/Readme.md
index f96a96f6..b66b721b 100644
--- a/src/Sa.Configuration.PostgreSql/Readme.md
+++ b/src/Sa.Configuration.PostgreSql/Readme.md
@@ -1,32 +1,105 @@
# Sa.Configuration.PostgreSql
-The `AddPostgreSqlConfiguration` extension method allows you to add a PostgreSQL-based configuration source to an IConfigurationBuilder. This enables your application to load configuration settings directly from a PostgreSQL database.
+A dynamic configuration source for .NET that loads settings from PostgreSQL. Changes in the database are applied to the running application without restart — just call `Reload()` on `IConfigurationRoot`.
-## Key Components
-- PostgreSqlConfigurationOptions: A record that holds the connection string, SQL query, and optional parameters for querying the database.
-- DatabaseConfigurationSource: Implements IConfigurationSource and creates a DatabaseConfigurationProvider to fetch configuration data.
-- DatabaseConfigurationProvider: Inherits from ConfigurationProvider and overrides the Load method to execute the SQL query and populate the configuration data.
+## Features
+
+- **Live configuration**: values are stored in the database and can be changed at runtime
+- **Parameterized SQL queries**: supports `@named_parameters` via `NpgsqlParameter`
+- **Automatic retry**: built-in retry strategy (`PgRetryStrategy`) with detection of Npgsql transient errors
+- **Key/value trimming**: whitespace is automatically trimmed from both keys and values
+- **Safe NULL handling**: `NULL` in DB → `null` in config; empty string → `string.Empty`
+
+## Public API
+
+| Type | Purpose |
+|------|---------|
+| `PostgreSqlConfigurationOptions` | Immutable record: `ConnectionString`, `SelectSql`, `Parameters` |
+| `DatabaseConfigurationSource` | `IConfigurationSource` implementation |
+| `DatabaseConfigurationProvider` | `ConfigurationProvider` that loads key-value pairs from DB |
+| `Setup.AddSaPostgreSqlConfiguration()` | Extension method for `IConfigurationBuilder` |
+
+## Quick Start
-## Example Usage
```csharp
-using Microsoft.Extensions.Configuration;
using Sa.Configuration.PostgreSql;
-var builder = new ConfigurationBuilder();
+var builder = WebApplication.CreateBuilder(args);
+
+builder.Configuration.AddSaPostgreSqlConfiguration(new PostgreSqlConfigurationOptions(
+ ConnectionString: "Host=localhost;Database=myapp;Username=app;Password=secret",
+ SelectSql: "SELECT key, value FROM app_settings"
+));
+
+var app = builder.Build();
+
+// Reading settings
+var theme = app.Configuration["theme"]; // → "dark"
+var lang = app.Configuration["language"]; // → "en"
+```
+
+## Parameterized Queries
+
+Use `@parameters` for filtering by client/tenant:
+
+```csharp
+builder.Configuration.AddSaPostgreSqlConfiguration(new PostgreSqlConfigurationOptions(
+ ConnectionString: "...",
+ SelectSql: "SELECT key, value FROM client_settings WHERE client_id = @client_id",
+ Parameters: [new NpgsqlParameter("client_id", "acme-corp")]
+));
+```
+
+## Live Configuration Updates
-// Define PostgreSqlConfigurationOptions
-var options = new PostgreSqlConfigurationOptions(
- "Host=my_host;Database=my_db;Username=my_user;Password=my_pw",
- "SELECT key, value FROM configuration"
+When rows in the `app_settings` table change, the application can pick up new values:
+
+```csharp
+// After modifying rows in the database:
+((IConfigurationRoot)app.Configuration).Reload();
+
+// Or manually:
+provider.Reload(); // DatabaseConfigurationProvider implements IConfigurationProvider
+```
+
+## Load Behavior
+
+| Scenario | Result |
+|----------|--------|
+| Key is empty or whitespace only | Skipped |
+| Value is `NULL` in DB | Stored as `null` |
+| Value is an empty string in DB | Stored as `string.Empty` |
+| Connection error | `InvalidOperationException` with the original exception as `InnerException` |
+
+## Table Schema
+
+Minimum table required for the provider:
+
+```sql
+CREATE TABLE app_settings (
+ key VARCHAR PRIMARY KEY,
+ value TEXT
);
-// Add PostgreSQL configuration to the builder
-builder.AddSaPostgreSqlConfiguration(options);
+-- Sample data
+INSERT INTO app_settings (key, value) VALUES
+ ('theme', 'dark'),
+ ('language', 'en'),
+ ('debug_mode', ''); -- empty string
+```
+
+## Dependencies
+
+- `Microsoft.Extensions.Configuration`
+- `Sa.Data.PostgreSql` (Npgsql wrapper with PgRetryStrategy and IPgDataSource)
-// Build the configuration
-var configuration = builder.Build();
+## Project Layout
-// Access configuration values
-string setting1 = configuration["Setting1"];
-Console.WriteLine($"Setting1: {setting1}");
+```
+src/Sa.Configuration.PostgreSql/
+├── PostgreSqlConfigurationOptions.cs # Options record
+├── DatabaseConfigurationSource.cs # IConfigurationSource
+├── DatabaseConfigurationProvider.cs # ConfigurationProvider + retry
+├── Setup.cs # Extension method AddSaPostgreSqlConfiguration()
+└── Readme.md # ← you are here
```
diff --git a/src/Sa.Configuration.PostgreSql/Sa.Configuration.PostgreSql.csproj b/src/Sa.Configuration.PostgreSql/Sa.Configuration.PostgreSql.csproj
index af0b5247..546a04fe 100644
--- a/src/Sa.Configuration.PostgreSql/Sa.Configuration.PostgreSql.csproj
+++ b/src/Sa.Configuration.PostgreSql/Sa.Configuration.PostgreSql.csproj
@@ -3,7 +3,7 @@
- 0.9.1
+ 0.10.0
add a PostgreSQL-based configuration source to IConfiguration
diff --git a/src/Sa.Configuration/CommandLine/Readme-ru.md b/src/Sa.Configuration/CommandLine/Readme-ru.md
new file mode 100644
index 00000000..0c35bb1f
--- /dev/null
+++ b/src/Sa.Configuration/CommandLine/Readme-ru.md
@@ -0,0 +1,208 @@
+# Arguments — Парсинг аргументов командной строки
+
+Парсинг и потребление аргументов командной строки в .NET-приложениях через простой dictionary-like API. Поддерживает форматы `--flag=value`, `--flag value`, короткие опции (`-x`) и типизированные геттеры.
+
+> **Важно:** парсер удаляет ведущие тире из имён параметров. При обращении к значению используйте ключ **без** лидирующих `-` или `--`.
+> Пример: `--config_db` в CLI → `args["config_db"]` в коде. `-v` в CLI → `args["v"]` в коде.
+
+## Быстрый старт
+
+```csharp
+using Sa.Configuration.CommandLine;
+
+// Парсим args (по умолчанию берёт Environment.GetCommandLineArgs())
+var args = new Arguments(args);
+
+// Доступ через индексатор (возвращает null, если ключ отсутствует) — ключи без ведущих тире
+string? db = args["config_db"];
+string? file = args["config_file"];
+bool debug = args.IsPresent("debug"); // true если флаг присутствует и истинен
+
+// Типизированные помощники (возвращают nullable, null при отсутствии/невалидности)
+int? port = args.GetInt("port");
+float? timeout = args.GetFloat("timeout");
+long offset = args.GetLong("offset");
+TimeSpan ttl = args.GetTimeSpan("ttl");
+bool verbose = args.GetBool("v"); // "true"/"1"/"yes"/"on" → true
+```
+
+### Минимальное консольное приложение
+
+```csharp
+using Sa.Configuration.CommandLine;
+
+var arguments = new Arguments(args);
+
+Console.WriteLine($"БД: {arguments["db"] ?? "(по умолчанию)"}");
+Console.WriteLine($"Порт: {arguments.GetInt("port") ?? 5432}");
+Console.WriteLine($"Debug: {arguments.IsPresent("debug")}");
+Console.WriteLine($"TTL: {arguments.GetTimeSpan("ttl") ?? TimeSpan.Zero}");
+```
+
+Запуск:
+
+```bash
+dotnet run -- --db mydb --port 9999 --debug --ttl 00:05:00 -v
+```
+
+Вывод:
+
+```
+БД: mydb
+Порт: 9999
+Debug: True
+TTL: 00:05:00
+```
+
+---
+
+## Поддерживаемые форматы
+
+| Формат | Ввод в CLI | Ключ в словаре | Значение |
+|--------|-----------|---------------|---------|
+| Длинный флаг + пробел | `--key value` | `"key"` | `"value"` |
+| Равно | `--key=value` | `"key"` | `"value"` |
+| Короткий флаг + пробел | `-k value` | `"k"` | `"value"` |
+| Короткое равно | `-k=v` | `"k"` | `"v"` |
+| Булев флаг | `--debug` | `"debug"` | `"true"` |
+| Значение в кавычках | `--name "hello world"` | `"name"` | `"hello world"` |
+
+---
+
+## Справочник API
+
+### Конструктор
+
+```csharp
+public Arguments(params IReadOnlyList args)
+```
+
+Создаёт экземпляр из списка строк аргументов.
+
+### Статическая фабрика
+
+```csharp
+public static Arguments CreateDefault(string[]? args = null)
+```
+
+Шорткат, который использует `Environment.GetCommandLineArgs()` когда `args` равен null.
+
+```csharp
+var args = Arguments.CreateDefault(); // читает Process.GetCurrentProcess().CommandLine
+```
+
+### Индексатор
+
+```csharp
+public string? this[string param] { get; }
+```
+
+Возвращает значение по имени параметра или `null`, если не найдено. Ключи хранятся без ведущих тире.
+
+```csharp
+var db = args["database"]; // null если --database никогда не передавали
+```
+
+### Contains / IsPresent
+
+```csharp
+public bool Contains(string param) // true если ключ существует (даже если значение пустое)
+public bool IsPresent(string param) // true если ключ существует И значение не null
+```
+
+`IsPresent` различает отсутствующий флаг и присутствующий, но пустой.
+
+### Типизированные геттеры
+
+Все возвращают `T?` (nullable) и дают `null` когда параметр отсутствует или не распарсивается.
+
+| Метод | Тип возврата | Пример |
+|-------|-------------|--------|
+| `GetBool(string)` | `bool?` | `args.GetBool("verbose")` — принимает `true/1/yes/on` |
+| `GetInt(string)` | `int?` | `args.GetInt("port")` |
+| `GetFloat(string)`| `float?`| `args.GetFloat("ratio")` |
+| `GetLong(string)` | `long?` | `args.GetLong("offset")` |
+| `GetTimeSpan(string)` | `TimeSpan?` | `args.GetTimeSpan("delay")` |
+
+Все числовые парсинги используют `CultureInfo.InvariantCulture`.
+
+### Исходные параметры
+
+```csharp
+public IReadOnlyDictionary Parameters { get; }
+```
+
+Возвращает полный словарь распарсенных параметров. Ключи хранятся без ведущих тире.
+
+---
+
+## Интеграция с Microsoft.Extensions.Configuration
+
+Регистрация аргументов командной строки как источника `IConfiguration`:
+
+```csharp
+using Microsoft.Extensions.Configuration;
+using Sa.Configuration.CommandLine;
+
+var configuration = new ConfigurationBuilder()
+ .AddSaCommandLine(args) // <-- добавляет CLI аргументы как источник конфига
+ .AddJsonFile("appsettings.json", optional: true)
+ .Build();
+
+// Доступ через индексатор IConfiguration — ключи тоже без тире
+var db = configuration["db"];
+var port = configuration["port"];
+```
+
+Порядок важен: источники, зарегистрированные **позже**, переопределяют ранние. Размещайте `AddSaCommandLine` перед JSON/файловыми источниками, если хотите, чтобы CLI имел приоритет:
+
+```csharp
+new ConfigurationBuilder()
+ .AddJsonFile("appsettings.json") // базовые значения по умолчанию
+ .AddSaCommandLine(args) // переопределения из CLI
+ .Build();
+```
+
+---
+
+## Запуск приложения
+
+### Из терминала
+
+```bash
+# Длинные флаги с разделителем пробелом
+dotnet run --project MyApp.dll --db production --port 5432 --debug
+
+# Синтаксис с равно
+dotnet run --project MyApp.dll --db=production --port=5432
+
+# Смешанные форматы
+dotnet run -- -d --db=prod -p 3306 --ttl 30s
+```
+
+### Из Visual Studio / VS Code
+
+Установите аргументы в launchSettings.json:
+
+```json
+{
+ "profiles": {
+ "MyApp": {
+ "commandName": "Project",
+ "commandLineArgs": "--db test --port 9999 --debug --ttl 00:01:00"
+ }
+ }
+}
+```
+
+---
+
+## Краевые случаи
+
+| Ввод в CLI | Ключ в словаре | Значение |
+|-----------|---------------|---------|
+| `--flag` (без значения) | `"flag"` | `"true"` |
+| `--flag=` (пустое) | `"flag"` | `""` |
+| `--flag "quoted value"` | `"flag"` | `"quoted value"` |
+| `-short=value` | `"short"` | `"value"` |
+| Неизвестный формат | Игнорируется молча | — |
diff --git a/src/Sa.Configuration/CommandLine/Readme.md b/src/Sa.Configuration/CommandLine/Readme.md
index 8a497924..57ce9998 100644
--- a/src/Sa.Configuration/CommandLine/Readme.md
+++ b/src/Sa.Configuration/CommandLine/Readme.md
@@ -1,68 +1,208 @@
-# Arguments Class
+# Arguments — Command-Line Parsing
-The `Arguments` class provides a robust way to parse command-line arguments, making it easier to manage application configurations. It handles various parameter formats and provides helper methods for different data types.
+Parse and consume command-line arguments in .NET apps with a simple dictionary-like API. Supports `--flag=value`, `--flag value`, short options (`-x`), and typed getters.
-## Features
+> **Key detail:** the parser strips leading dashes from parameter names. When you access a value, use the key **without** leading `-` or `--`.
+> Example: `--config_db` in CLI → `args["config_db"]` in code. `-v` in CLI → `args["v"]` in code.
-- Parse command-line arguments with support for various formats
-- Access parameters by name using indexer syntax
-- Helper methods for common data types (bool, int, float, long, TimeSpan)
-- Support for both `--param=value` and `--param value` formats
-- Support for short options like `-ip_override=127.0.0.1`
+## Quick Start
-## Usage
-
-### Basic Usage
+```csharp
+using Sa.Configuration.CommandLine;
+
+// Parse args (defaults to Environment.GetCommandLineArgs())
+var args = new Arguments(args);
+
+// Indexer access (returns null if absent) — keys have leading dashes stripped
+string? db = args["config_db"];
+string? file = args["config_file"];
+bool debug = args.IsPresent("debug"); // true if flag present & truthy
+
+// Typed helpers (return nullable, null on missing/invalid)
+int? port = args.GetInt("port");
+float? timeout = args.GetFloat("timeout");
+long offset = args.GetLong("offset");
+TimeSpan ttl = args.GetTimeSpan("ttl");
+bool verbose = args.GetBool("v"); // "true"/"1"/"yes"/"on" → true
+```
-To use the Arguments class, create an instance and access parameters using the indexer syntax:
+### Minimal Console App
```csharp
-// Create an instance of the Arguments class, passing the command-line arguments
+using Sa.Configuration.CommandLine;
+
var arguments = new Arguments(args);
-// Retrieve values for specific parameters
-string? configDb = arguments["--config_db"];
-string? configFile = arguments["--config_file"];
-string? configNLog = arguments["--config_nlog"];
-string? ipOverride = arguments["-ip_override"];
+Console.WriteLine($"DB: {arguments["db"] ?? "(default)"}");
+Console.WriteLine($"Port: {arguments.GetInt("port") ?? 5432}");
+Console.WriteLine($"Debug: {arguments.IsPresent("debug")}");
+Console.WriteLine($"TTL: {arguments.GetTimeSpan("ttl") ?? TimeSpan.Zero}");
+```
+
+Run:
+
+```bash
+dotnet run -- --db mydb --port 9999 --debug --ttl 00:05:00 -v
+```
+
+Output:
-// Display the retrieved values
-Console.WriteLine("Configuration Database: " + (configDb ?? "Not provided"));
-Console.WriteLine("Configuration File: " + (configFile ?? "Not provided"));
-Console.WriteLine("NLog Configuration: " + (configNLog ?? "Not provided"));
-Console.WriteLine("IP Override: " + (ipOverride ?? "Not provided"));
```
+DB: mydb
+Port: 9999
+Debug: True
+TTL: 00:05:00
+```
+
+---
+
+## Supported Formats
+
+| Format | CLI Input | Dictionary Key | Value |
+|--------|-----------|---------------|-------|
+| Long flag + space | `--key value` | `"key"` | `"value"` |
+| Equals sign | `--key=value` | `"key"` | `"value"` |
+| Short flag + space | `-k value` | `"k"` | `"value"` |
+| Short equals | `-k=v` | `"k"` | `"v"` |
+| Boolean flag | `--debug` | `"debug"` | `"true"` |
+| Quoted value | `--name "hello world"` | `"name"` | `"hello world"` |
-### Advanced Usage
+---
-The class also provides helper methods for different data types:
+## API Reference
+
+### Constructor
```csharp
-// Check if a parameter exists
-if (arguments.Contains("--config_db"))
-{
- // Get parameter as boolean
- bool? nosjmp = arguments.GetBool("-nosjmp");
+public Arguments(params IReadOnlyList args)
+```
- // Get parameter as integer
- int? port = arguments.GetInt("--port");
+Creates an instance from a list of argument strings.
- // Get parameter as float
- float? timeout = arguments.GetFloat("--timeout");
+### Static Factory
- // Get parameter as TimeSpan
- TimeSpan? duration = arguments.GetTimeSpan("--duration");
-}
+```csharp
+public static Arguments CreateDefault(string[]? args = null)
+```
+
+Shortcut that uses `Environment.GetCommandLineArgs()` when `args` is null.
+
+```csharp
+var args = Arguments.CreateDefault(); // reads Process.GetCurrentProcess().CommandLine
+```
+
+### Indexer
+
+```csharp
+public string? this[string param] { get; }
+```
+
+Returns the value for a parameter name, or `null` if not found. Keys are stored without leading dashes.
+
+```csharp
+var db = args["database"]; // null if --database was never passed
+```
+
+### Contains / IsPresent
+
+```csharp
+public bool Contains(string param) // true if key exists (even if value is empty)
+public bool IsPresent(string param) // true if key exists AND value is non-null
```
+`IsPresent` distinguishes between a missing flag and a present-but-empty flag.
+
+### Typed Getters
+
+All return `T?` (nullable) and yield `null` when the parameter is absent or unparsable.
+
+| Method | Return Type | Example |
+|--------|-------------|---------|
+| `GetBool(string)` | `bool?` | `args.GetBool("verbose")` — accepts `true/1/yes/on` |
+| `GetInt(string)` | `int?` | `args.GetInt("port")` |
+| `GetFloat(string)`| `float?`| `args.GetFloat("ratio")` |
+| `GetLong(string)` | `long?` | `args.GetLong("offset")` |
+| `GetTimeSpan(string)` | `TimeSpan?` | `args.GetTimeSpan("delay")` |
+
+All numeric parsing uses `CultureInfo.InvariantCulture`.
+
+### Raw Parameters
+
+```csharp
+public IReadOnlyDictionary Parameters { get; }
+```
+
+Returns the full dictionary of parsed parameters. Keys are stored without leading dashes.
+
+---
+
+## Integration with Microsoft.Extensions.Configuration
+
+Register command-line arguments as an `IConfiguration` source:
+
+```csharp
+using Microsoft.Extensions.Configuration;
+using Sa.Configuration.CommandLine;
+
+var configuration = new ConfigurationBuilder()
+ .AddSaCommandLine(args) // <-- adds CLI args as config source
+ .AddJsonFile("appsettings.json", optional: true)
+ .Build();
+
+// Access via IConfiguration indexer — keys still have dashes stripped
+var db = configuration["db"];
+var port = configuration["port"];
+```
+
+Order matters: sources registered **later** override earlier ones. Place `AddSaCommandLine` before JSON/file sources if you want CLI to win:
+
+```csharp
+new ConfigurationBuilder()
+ .AddJsonFile("appsettings.json") // base defaults
+ .AddSaCommandLine(args) // overrides from CLI
+ .Build();
+```
+
+---
+
## Running the Application
-To run the application with command-line arguments, you can use the command line or terminal. Here are examples:
+### From terminal
```bash
-# Standard usage
-dotnet run Some.exe --config_db /opt/service_configs/config_db.json --config_file /opt/service_configs/appsettings.json --config_nlog /opt/service_configs/NLog.config -ip_override=127.0.0.1 -nosjmp
+# Long flags with space separator
+dotnet run --project MyApp.dll --db production --port 5432 --debug
+
+# Equals syntax
+dotnet run --project MyApp.dll --db=production --port=5432
+
+# Mixed formats
+dotnet run -- -d --db=prod -p 3306 --ttl 30s
+```
+
+### From Visual Studio / VS Code
+
+Set arguments in launchSettings.json:
+
+```json
+{
+ "profiles": {
+ "MyApp": {
+ "commandName": "Project",
+ "commandLineArgs": "--db test --port 9999 --debug --ttl 00:01:00"
+ }
+ }
+}
+```
+
+---
+
+## Edge Cases
-# Alternative format
-dotnet run Some.exe --config_db=/opt/service_configs/config_db.json --config_file=/opt/service_configs/appsettings.json --config_nlog=/opt/service_configs/NLog.config -ip_override="127.0.0.1" -nosjmp
-```
\ No newline at end of file
+| CLI Input | Dictionary Key | Value |
+|-----------|---------------|-------|
+| `--flag` (no value) | `"flag"` | `"true"` |
+| `--flag=` (empty) | `"flag"` | `""` |
+| `--flag "quoted value"` | `"flag"` | `"quoted value"` |
+| `-short=value` | `"short"` | `"value"` |
+| Unknown format | Ignored silently | — |
diff --git a/src/Sa.Configuration/Readme-ru.md b/src/Sa.Configuration/Readme-ru.md
new file mode 100644
index 00000000..af5f7f3e
--- /dev/null
+++ b/src/Sa.Configuration/Readme-ru.md
@@ -0,0 +1,284 @@
+# Sa.Configuration
+
+Безопасное управление секретами и парсер командной строки в экосистеме .NET `Microsoft.Extensions.Configuration`. Секреты автоматически подставляются в конфигурацию без ручного кода приложения.
+
+---
+
+## Возможности
+
+- **Автоматическая подстановка секретов**: плейсхолдеры `{{key}}` заменяются реальными значениями из файлов, переменных окружения или аргументов командной строки
+- **Защита от циклов**: встроенная защита от бесконечной рекурсии при разрешении плейсхолдеров
+- **Опциональные плейсхолдеры**: `{{?key}}` — если секрет не найден, возвращается `null` вместо исключения
+- **Цепочка хранилищ**: несколько источников секретов с приоритетным порядком
+- **Парсер аргументов**: поддерживает форматы `--key value`, `--key=value`, `-flag`
+- **Среды разработки**: автоматическая загрузка `secrets.{Environment}.txt` (Development/Staging/Production)
+
+---
+
+## Быстрый старт
+
+### 1. Регистрация в `Program.cs`
+
+```csharp
+using Sa.Configuration;
+
+var builder = WebApplication.CreateBuilder(args);
+
+// Подключение аргументов + секретов из файлов/ENV/CLI
+builder.Configuration.AddSaConfiguration();
+
+var app = builder.Build();
+```
+
+### 2. Файл секретов (`secrets.txt`)
+
+```ini
+# Postgres
+sa_pg_host=localhost
+sa_pg_user=postgres
+sa_pg_port=5432
+sa_pg_database=myapp
+sa_pg_schema=public
+sa_pg_password=superSecret123
+
+# API ключи
+api_key=abc123xyz
+jwt_secret=h8k2m9p0
+```
+
+> ⚠️ Добавьте `secrets*.txt` в `.gitignore`!
+
+### 3. Плейсхолдеры в `appsettings.json`
+
+```json
+{
+ "secret": "{{sa_secret}}",
+
+ "sa": {
+ "pg": {
+ "connection": "User ID={{sa_pg_user}};Password={{sa_pg_password}};Host={{sa_pg_host}};Port={{sa_pg_port}};Database={{sa_pg_database}};Pooling=true;SearchPath={{sa_pg_schema}};Command Timeout=180;"
+ }
+ },
+
+ "ExternalApi": {
+ "ApiKey": "{{api_key}}"
+ }
+}
+```
+
+### 4. Чтение конфигурации
+
+```csharp
+var pgConn = app.Configuration["sa:pg:connection"];
+// → "User ID=postgres;Password=superSecret123;Host=localhost;..."
+```
+
+---
+
+## Приоритет секретов
+
+Секреты ищутся в порядке убывания приоритета:
+
+| # | Источник | Пример файла |
+|---|----------|-------------|
+| 1 | Базовый файл секретов | `secrets.txt` |
+| 2 | Файл конкретной среды | `secrets.Development.txt` |
+| 3 | Переменные окружения | `SA_PG_PASSWORD=...` |
+| 4 | Аргументы командной строки | `--sa_pg_password=...` |
+
+Первый источник, имеющий значение, побеждает. Это позволяет переопределять секреты для каждой среды.
+
+---
+
+## Опциональные плейсхолдеры
+
+Используйте `{{?key}}` вместо `{{key}}`, чтобы избежать ошибки при отсутствии секрета:
+
+```json
+{
+ "optional_feature": "{{?feature_flag}}"
+}
+```
+
+Если `feature_flag` не найден ни в одном хранилище, возвращается `null`.
+
+---
+
+## Использование с Sa.Configuration.PostgreSql
+
+```csharp
+using Sa.Configuration;
+using Sa.Configuration.PostgreSql;
+
+var builder = WebApplication.CreateBuilder(args);
+
+// Сначала стандартные источники (appsettings.json, secrets.txt)
+builder.Configuration.AddSaConfiguration();
+
+// Затем динамические настройки из базы данных
+builder.Configuration.AddSaPostgreSqlConfiguration(new PostgreSqlConfigurationOptions(
+ ConnectionString: "...",
+ SelectSql: "SELECT key, value FROM app_settings"
+));
+
+var app = builder.Build();
+```
+
+---
+
+## Аргументы — Парсер командной строки
+
+```csharp
+using Sa.Configuration.CommandLine;
+
+// some.exe --config_db /share/data.db --debug
+var args = new Arguments(args);
+
+string? configDb = args["config_db"]; // → "/share/data.db"
+bool? debug = args.GetBool("debug"); // → true
+int? port = args.GetInt("port"); // → null
+TimeSpan? timeout = args.GetTimeSpan("timeout");
+```
+
+Поддерживаемые форматы:
+
+```
+--key value
+--key=value
+-key value
+-key=value
+-flag → flag=true (булев флаг)
+```
+
+Типизированные методы возвращают `null`, когда параметр отсутствует или невалиден:
+
+| Метод | Возвращаемый тип | Преобразование |
+|-------|-----------------|----------------|
+| `GetBool()` | `bool?` | `"true"/"1"/"yes"/"on"` → `true` |
+| `GetInt()` | `int?` | `int.TryParse(..., InvariantCulture)` |
+| `GetFloat()` | `float?` | то же самое |
+| `GetLong()` | `long?` | то же самое |
+| `GetTimeSpan()` | `TimeSpan?` | `TimeSpan.TryParse(..., InvariantCulture)` |
+
+Дополнительные методы:
+
+| Метод | Возвращаемый тип | Описание |
+|-------|-----------------|---------|
+| `Contains(param)` | `bool` | Проверяет наличие параметра |
+| `IsPresent(param)` | `bool` | Параметр существует И имеет непустое значение |
+
+---
+
+## Секреты — Управление секретами
+
+### Создание по умолчанию
+
+```csharp
+using Sa.Configuration.SecretStore;
+
+// Стандартная цепочка: File → File.Env → EnvVar → CommandLine
+var secrets = Secrets.CreateDefault();
+```
+
+### Пользовательская цепочка
+
+```csharp
+var secrets = new Secrets(
+ new FileSecretStore("my-secrets.txt"),
+ new EnvironmentVariableSecretStore(),
+ new InMemorySecretStore(new Dictionary {
+ { "override_key", "override_value" }
+ })
+);
+```
+
+### Добавление на лету
+
+```csharp
+secrets.AddStore(new FileSecretStore("additional-secrets.txt"));
+```
+
+### Подстановка плейсхолдеров
+
+```csharp
+string template = "Server={{host}};Password={{password}}";
+string result = secrets.PopulateSecrets(template);
+// → "Server=localhost;Password=s3cret!"
+```
+
+### Получение одного секрета
+
+```csharp
+string? password = secrets.GetSecret("sa_pg_password");
+```
+
+### Определение имени среды
+
+```csharp
+string env = Secrets.GetEnvironmentName();
+// → "Development", "Staging", "Production" и т.д.
+```
+
+---
+
+## Публичный API
+
+### Пространство имён `Sa.Configuration`
+
+| Тип | Назначение |
+|-----|-----------|
+| `Setup.AddSaConfiguration()` | Главная точка входа: подключение аргументов + обработка секретов |
+
+### Пространство имён `Sa.Configuration.CommandLine`
+
+| Тип | Назначение |
+|-----|-----------|
+| `Arguments` | Парсер аргументов командной строки |
+| `Arguments.CreateDefault()` | Создаёт из `Environment.GetCommandLineArgs()` |
+| `Setup.AddSaCommandLine()` | Метод-расширение для `IConfigurationBuilder` |
+
+### Пространство имён `Sa.Configuration.SecretStore`
+
+| Тип | Назначение |
+|-----|-----------|
+| `Secrets` | Основной класс управления секретами, реализует `ISecretService` |
+| `Secrets.CreateDefault()` | Стандартная цепочка хранилищ |
+| `Secrets.GetEnvironmentName()` | Определяет среду (`DOTNET_ENVIRONMENT` / `ASPNETCORE_ENVIRONMENT`) |
+| `SecretOptions` | Опции для `CreateDefault()`: `FileName`, `Args`, `EnvironmentName` |
+| `ISecretService` | Интерфейс: `PopulateSecrets()` + `GetSecret()` |
+| `ISecretStore` | Интерфейс: `GetSecret(string key)` |
+| `Setup.AddSaPostSecretProcessing()` | Метод-расширение: применяет `ISecretService` к конфигу ПОСЛЕ загрузки других источников |
+
+### Хранилища секретов (`Sa.Configuration.SecretStore.Stories`)
+
+| Класс | Описание |
+|-------|---------|
+| `FileSecretStore` | Загружает `key=value` из текстового файла (пропускает комментарии `#`) |
+| `EnvironmentVariableSecretStore` | Читает из `Environment.GetEnvironmentVariable()` |
+| `CommandLineArgsSecretStore` | Берёт секреты из `Arguments` |
+| `InMemorySecretStore` | Словарь в памяти, fluent `.AddSecret()` |
+
+---
+
+## Как это работает
+
+```
+┌──────────────────────────────────────────────────────┐
+│ 1. appsettings.json содержит: │
+│ "connection": "Host={{sa_pg_host}};Password={{...}}"│
+├──────────────────────────────────────────────────────┤
+│ 2. secrets.txt содержит: │
+│ sa_pg_host=localhost │
+│ sa_pg_password=s3cret! │
+├──────────────────────────────────────────────────────┤
+│ 3. AddSaPostSecretProcessing подставляет плейсхолдеры:│
+│ IConfiguration["sa:pg:connection"] │
+│ → "Host=localhost;Password=s3cret!;..." │
+└──────────────────────────────────────────────────────┘
+```
+
+---
+
+## Лицензия
+
+MIT
diff --git a/src/Sa.Configuration/Readme.md b/src/Sa.Configuration/Readme.md
index 880ac51d..bef68115 100644
--- a/src/Sa.Configuration/Readme.md
+++ b/src/Sa.Configuration/Readme.md
@@ -1,168 +1,284 @@
-# Working with Secrets via Configuration
+# Sa.Configuration
-The `Sa.Configuration` library provides **secure and transparent integration of secrets** into the standard .NET configuration system. All sensitive data is automatically substituted during configuration loading — without manual processing in application code.
+Secure secrets management and command-line argument parsing within the .NET `Microsoft.Extensions.Configuration` ecosystem. Secrets are automatically substituted into configuration without manual application code.
---
-## How It Works
+## Features
-1. **Load secrets** from secure sources (files, environment variables)
-2. **Automatic substitution** of values in configuration during loading
-3. **Transparent usage** via standard `IConfiguration`
+- **Automatic secret substitution**: `{{key}}` placeholders are replaced with real values from files, environment variables, or command-line arguments
+- **Cycle protection**: built-in guard against infinite recursion during placeholder resolution
+- **Optional placeholders**: `{{?key}}` — if the secret is not found, returns `null` instead of throwing
+- **Chained Stores**: multiple secret sources with priority ordering
+- **Argument parser**: supports `--key value`, `--key=value`, `-flag` formats
+- **Environments**: automatic loading of `secrets.{Environment}.txt` (Development/Staging/Production)
---
-## Setup
+## Quick Start
-### 1. Registration in `Program.cs`
+### 1. Register in `Program.cs`
```csharp
using Sa.Configuration;
var builder = WebApplication.CreateBuilder(args);
+// Connects arguments + secrets from files/env vars/command line
builder.Configuration.AddSaConfiguration();
var app = builder.Build();
```
-### 2. Secret Sources
-
-Secrets are loaded from the following sources (in priority order):
-
-| Source | Description | Example |
-|--------|-------------|---------|
-| **Secrets file** | Text file with `key=value` pairs | `secrets.txt` |
-| **Environment variables** | System environment variables | `SA_PG_PASSWORD=myPass` |
-| **Command-line arguments** | Application startup parameters | `--sa_pg_port=5432` |
-
----
-
-## Secrets File Format (`secrets.txt`)
+### 2. Secrets File (`secrets.txt`)
```ini
# Postgres
sa_pg_host=localhost
sa_pg_user=postgres
sa_pg_port=5432
-sa_pg_database=postgres
+sa_pg_database=myapp
sa_pg_schema=public
sa_pg_password=superSecret123
-# Other secrets
-sa_secret=TOP SECRET!
+# API keys
api_key=abc123xyz
+jwt_secret=h8k2m9p0
```
-> ⚠️ **Important**: The `secrets.txt` file must be excluded from version control (.gitignore)
-
----
+> ⚠️ Add `secrets*.txt` to `.gitignore`!
-## Usage in `appsettings.json`
-
-Specify **placeholders** in the format `{{secret_key}}`:
+### 3. Placeholders in `appsettings.json`
```json
{
"secret": "{{sa_secret}}",
-
+
"sa": {
"pg": {
"connection": "User ID={{sa_pg_user}};Password={{sa_pg_password}};Host={{sa_pg_host}};Port={{sa_pg_port}};Database={{sa_pg_database}};Pooling=true;SearchPath={{sa_pg_schema}};Command Timeout=180;"
}
},
-
+
"ExternalApi": {
"ApiKey": "{{api_key}}"
}
}
```
+### 4. Reading Configuration
+
+```csharp
+var pgConn = app.Configuration["sa:pg:connection"];
+// → "User ID=postgres;Password=superSecret123;Host=localhost;..."
+```
+
---
-## Code Example
+## Secret Priority Order
-### Retrieving values via `IConfiguration`
+Secrets are looked up in descending priority order:
-```csharp
-var todosApi = app.MapGroup("/settings");
+| # | Source | Example File |
+|---|--------|-------------|
+| 1 | Base secrets file | `secrets.txt` |
+| 2 | Environment-specific file | `secrets.Development.txt` |
+| 3 | Environment variables | `SA_PG_PASSWORD=...` |
+| 4 | Command-line arguments | `--sa_pg_password=...` |
+
+The first source that has a value wins. This allows overriding secrets per environment.
+
+---
+
+## Optional Placeholders
-todosApi.MapGet("/", (IConfiguration configuration) => new Settings[] {
- new (Key: "pg_connection", Value: configuration["sa:pg:connection"]),
- new (Key: "theme", Value: configuration["theme"]),
- new (Key: "secret", Value: configuration["secret"])
-}).WithName("GetSettings");
+Use `{{?key}}` instead of `{{key}}` to avoid an error when a secret is missing:
+
+```json
+{
+ "optional_feature": "{{?feature_flag}}"
+}
```
+If `feature_flag` is not found in any store, `null` is returned.
---
-## What Happens Under the Hood
+## Usage with Sa.Configuration.PostgreSql
-```
-┌─────────────────────────────────────────────────────────┐
-│ 1. appsettings.json contains: │
-│ "connection": "Host={{sa_pg_host}};Password={{...}}" │
-├─────────────────────────────────────────────────────────┤
-│ 2. secrets.txt contains: │
-│ sa_pg_host=localhost │
-│ sa_pg_password=superSecret123 │
-├─────────────────────────────────────────────────────────┤
-│ 3. IConfiguration["sa:pg:connection"] returns: │
-│ "Host=localhost;Password=superSecret123;..." │
-└─────────────────────────────────────────────────────────┘
+```csharp
+using Sa.Configuration;
+using Sa.Configuration.PostgreSql;
+
+var builder = WebApplication.CreateBuilder(args);
+
+// First, standard sources (appsettings.json, secrets.txt)
+builder.Configuration.AddSaConfiguration();
+
+// Then, dynamic settings from the database
+builder.Configuration.AddSaPostgreSqlConfiguration(new PostgreSqlConfigurationOptions(
+ ConnectionString: "...",
+ SelectSql: "SELECT key, value FROM app_settings"
+));
+
+var app = builder.Build();
```
---
-## Advantages
+## Arguments — Command-Line Argument Parser
+
+```csharp
+using Sa.Configuration.CommandLine;
-- **Security**: secrets are not stored in code or configuration files
-- **Flexibility**: supports multiple secret sources
-- **Simplicity**: transparent operation through standard `IConfiguration`
-- **Debugging**: easy to switch secrets via environment variables or arguments
+// some.exe --config_db /share/data.db --debug
+var args = new Arguments(args);
----
+string? configDb = args["config_db"]; // → "/share/data.db"
+bool? debug = args.GetBool("debug"); // → true
+int? port = args.GetInt("port"); // → null
+TimeSpan? timeout = args.GetTimeSpan("timeout");
+```
-## Tips
+Supported formats:
-- For local development, create `secrets.Development.txt`; for production — use environment variables
-- Never commit secret files to the repository
-- Use different secret files for different environments (dev, staging, prod)
+```
+--key value
+--key=value
+-key value
+-key=value
+-flag → flag=true (boolean flag)
+```
+
+Typed methods return `null` when the parameter is absent or invalid:
+
+| Method | Return Type | Conversion |
+|--------|------------|------------|
+| `GetBool()` | `bool?` | `"true"/"1"/"yes"/"on"` → `true` |
+| `GetInt()` | `int?` | `int.TryParse(..., InvariantCulture)` |
+| `GetFloat()` | `float?` | same as above |
+| `GetLong()` | `long?` | same as above |
+| `GetTimeSpan()` | `TimeSpan?` | `TimeSpan.TryParse(..., InvariantCulture)` |
+
+Additional methods:
+
+| Method | Return Type | Description |
+|--------|------------|-------------|
+| `Contains(param)` | `bool` | Checks if parameter exists |
+| `IsPresent(param)` | `bool` | Parameter exists AND has a non-null value |
---
-# Core Classes
+## Secrets — Secrets Management
+
+### Creating Defaults
-## Arguments Class
+```csharp
+using Sa.Configuration.SecretStore;
-The `Arguments` class is designed to parse command-line arguments passed to a C# application. It provides a dictionary-like interface for easy parameter retrieval and supports both single-value and multi-value parameters.
+// Standard chain: File → File.Env → EnvVar → CommandLine
+var secrets = Secrets.CreateDefault();
+```
-**Key Features:**
-- **Parameter Parsing**: Splits command-line arguments into key-value pairs
-- **Easy Retrieval**: Access parameter values using an indexer
-- **Default Handling**: Automatically assigns default values for boolean flags
+### Custom Chain
-**Example:**
```csharp
-// some.exe --config_db /share/data.db
-var arguments = new Arguments(args);
-string? configDb = arguments["config_db"];
+var secrets = new Secrets(
+ new FileSecretStore("my-secrets.txt"),
+ new EnvironmentVariableSecretStore(),
+ new InMemorySecretStore(new Dictionary {
+ { "override_key", "override_value" }
+ })
+);
```
----
+### Fluent Addition at Runtime
+
+```csharp
+secrets.AddStore(new FileSecretStore("additional-secrets.txt"));
+```
+
+### Placeholder Substitution
+
+```csharp
+string template = "Server={{host}};Password={{password}}";
+string result = secrets.PopulateSecrets(template);
+// → "Server=localhost;Password=s3cret!"
+```
-## Secrets Class
+### Getting a Single Secret
-The `Secrets` class provides a secure mechanism for managing sensitive information such as API keys and database passwords from various sources. It supports loading secrets from files, environment variables, and dynamically generated host key files.
+```csharp
+string? password = secrets.GetSecret("sa_pg_password");
+```
-**Key Features:**
-- **Chained Secret Stores**: Combines multiple sources for retrieving secrets
-- **Dynamic Loading**: Supports environment-specific configurations
-- **Placeholder Replacement**: Easily populates strings with secret values
+### Environment Name Resolution
-**Example:**
```csharp
-string input = "Database password: {{db_password}}";
-string? populatedString = service.PopulateSecrets(input);
+string env = Secrets.GetEnvironmentName();
+// → "Development", "Staging", "Production", etc.
```
+
+---
+
+## Public API
+
+### Namespace `Sa.Configuration`
+
+| Type | Purpose |
+|------|---------|
+| `Setup.AddSaConfiguration()` | Main entry-point: connects arguments + secret processing |
+
+### Namespace `Sa.Configuration.CommandLine`
+
+| Type | Purpose |
+|------|---------|
+| `Arguments` | Command-line argument parser |
+| `Arguments.CreateDefault()` | Creates from `Environment.GetCommandLineArgs()` |
+| `Setup.AddSaCommandLine()` | Extension method for `IConfigurationBuilder` |
+
+### Namespace `Sa.Configuration.SecretStore`
+
+| Type | Purpose |
+|------|---------|
+| `Secrets` | Main secrets management class, implements `ISecretService` |
+| `Secrets.CreateDefault()` | Standard store chain |
+| `Secrets.GetEnvironmentName()` | Resolves environment (`DOTNET_ENVIRONMENT` / `ASPNETCORE_ENVIRONMENT`) |
+| `SecretOptions` | Options for `CreateDefault()`: `FileName`, `Args`, `EnvironmentName` |
+| `ISecretService` | Interface: `PopulateSecrets()` + `GetSecret()` |
+| `ISecretStore` | Interface: `GetSecret(string key)` |
+| `Setup.AddSaPostSecretProcessing()` | Extension method: applies `ISecretService` to config AFTER other sources are loaded |
+
+### Secret Stores (`Sa.Configuration.SecretStore.Stories`)
+
+| Class | Description |
+|-------|-------------|
+| `FileSecretStore` | Loads `key=value` from a text file (skips `#` comments) |
+| `EnvironmentVariableSecretStore` | Reads from `Environment.GetEnvironmentVariable()` |
+| `CommandLineArgsSecretStore` | Pulls secrets from `Arguments` |
+| `InMemorySecretStore` | Dictionary in memory, fluent `.AddSecret()` |
+
+---
+
+## How It Works
+
+```
+┌──────────────────────────────────────────────────────┐
+│ 1. appsettings.json contains: │
+│ "connection": "Host={{sa_pg_host}};Password={{...}}"│
+├──────────────────────────────────────────────────────┤
+│ 2. secrets.txt contains: │
+│ sa_pg_host=localhost │
+│ sa_pg_password=s3cret! │
+├──────────────────────────────────────────────────────┤
+│ 3. AddSaPostSecretProcessing substitutes placeholders:│
+│ IConfiguration["sa:pg:connection"] │
+│ → "Host=localhost;Password=s3cret!;..." │
+└──────────────────────────────────────────────────────┘
+```
+
+---
+
+## License
+
+MIT
diff --git a/src/Sa.Configuration/Sa.Configuration.csproj b/src/Sa.Configuration/Sa.Configuration.csproj
index 6041109a..4258918b 100644
--- a/src/Sa.Configuration/Sa.Configuration.csproj
+++ b/src/Sa.Configuration/Sa.Configuration.csproj
@@ -3,7 +3,7 @@
- 0.9.1
+ 0.10.0
extensions for Configuration
diff --git a/src/Sa.Configuration/SecretStore/Engine/SecretService.cs b/src/Sa.Configuration/SecretStore/Engine/SecretService.cs
index db545376..058e3459 100644
--- a/src/Sa.Configuration/SecretStore/Engine/SecretService.cs
+++ b/src/Sa.Configuration/SecretStore/Engine/SecretService.cs
@@ -69,7 +69,7 @@ internal sealed partial class SecretService(ISecretStore secretStore) : ISecretS
}
private static bool IsSearchPositionValid(string inputString, int currentPosition)
- => inputString.Length >= currentPosition;
+ => currentPosition < inputString.Length;
private static string NormalizeValue(string secretValue)
{
diff --git a/src/Sa.Data.PostgreSql/DbCommandExtensions.cs b/src/Sa.Data.PostgreSql/DbCommandExtensions.cs
index cf1e0731..ee5ed1fb 100644
--- a/src/Sa.Data.PostgreSql/DbCommandExtensions.cs
+++ b/src/Sa.Data.PostgreSql/DbCommandExtensions.cs
@@ -7,7 +7,7 @@ namespace Sa.Data.PostgreSql;
public static class DbCommandExtensions
{
///
- /// Добавляет параметр с именем {prefix}{index}, используя минимальные аллокации.
+ /// Adds a parameter with name {prefix}{index}, using minimal allocations.
///
public static NpgsqlCommand AddParameter(
this NpgsqlCommand command,
@@ -26,17 +26,22 @@ public static NpgsqlCommand AddParameter(
return command;
}
-
+ ///
+ /// Adds a parameter — infers the value type from the argument.
+ ///
public static NpgsqlCommand AddParam(
this NpgsqlCommand command,
string prefix,
- T value,
+ T? value,
int index)
where TProvider : INamePrefixProvider
{
var paramName = CachedParamNames.Default.Get(prefix, index);
- var param = new NpgsqlParameter(paramName, value);
+ var param = command.CreateParameter();
+ param.ParameterName = paramName;
+ param.Value = value is null ? DBNull.Value : (object)value!;
command.Parameters.Add(param);
+
return command;
}
}
diff --git a/src/Sa.Data.PostgreSql/IPgDataSource.cs b/src/Sa.Data.PostgreSql/IPgDataSource.cs
index 11978fc2..63deb9ce 100644
--- a/src/Sa.Data.PostgreSql/IPgDataSource.cs
+++ b/src/Sa.Data.PostgreSql/IPgDataSource.cs
@@ -28,9 +28,72 @@ Task ExecuteNonQuery(string sql, CancellationToken cancellationToken = defa
Task