Webority.Retention
0.2.14
dotnet add package Webority.Retention --version 0.2.14
NuGet\Install-Package Webority.Retention -Version 0.2.14
<PackageReference Include="Webority.Retention" Version="0.2.14" />
<PackageVersion Include="Webority.Retention" Version="0.2.14" />
<PackageReference Include="Webority.Retention" />
paket add Webority.Retention --version 0.2.14
#r "nuget: Webority.Retention, 0.2.14"
#:package Webority.Retention@0.2.14
#addin nuget:?package=Webority.Retention&version=0.2.14
#tool nuget:?package=Webority.Retention&version=0.2.14
Webority.Retention
Deletes finished work rows from a product's SQL tables once they pass their retention window: webhook intake, outboxes, idempotency records, side effects, job state. The tables and their rules come from the product's storage register, so the rules live in one place and a push check can verify them.
Using it in a product
1. List the work tables in the register
.webority/data-placement.json names every table with a MAX column or that grows by appending. Entries of class workflow:<days> are trimmed:
{
"schemaVersion": 1,
"tables": [
{ "table": "dbo.InboundWebhookDelivery", "class": "workflow:30", "finishedAt": "ProcessedDateTimeUtc", "status": { "column": "Status", "finished": ["Processed", "Ignored"] } },
{ "table": "dbo.NotificationOutboxMessage", "class": "workflow:30", "key": "MessageId", "finishedAt": "SentDateTimeUtc" },
{ "table": "dbo.SalesLead", "class": "record" }
]
}
| Field | Meaning |
|---|---|
table |
schema.table, or the table alone for dbo |
class |
workflow:<days> (1 to 3650) is trimmed; record, bounded-text, document-pointer, analytics and history:<years> are read and left alone |
key |
The bigint or int key column; Id when left out |
finishedAt |
The UTC time the row finished (datetime2, datetime or datetimeoffset); a row where it is null is never deleted |
status |
Optional: column and the finished values. Pending and failed statuses are left out, so those rows are never deleted |
A wrong entry, or one that names a missing table or column, fails the whole run before anything is deleted.
Each table needs an unfiltered index whose first column is finishedAt (with status as its second column when the entry has one), because each run reads the oldest due rows first. A table without one fails the run before anything is deleted, and the message names the index to add.
2. Copy the register into the Functions output
<None Include="..\.webority\data-placement.json" Link="data-placement.json" CopyToOutputDirectory="PreserveNewest" />
3. Register and configure
services.AddWeborityRetention();
"Retention": {
"RegisterPath": "data-placement.json",
"ConnectionStringName": "WeborityDbConnection",
"Brake": { "Mode": "DryRun" }
}
| Setting | Default | Meaning |
|---|---|---|
Retention:RegisterPath |
required | The register; a relative path is read from the app's base directory |
Retention:ConnectionStringName |
required | Which ConnectionStrings:<name> to trim |
Retention:BatchSize |
2,000 | Rows per delete statement (100 to 4,000, below SQL Server's lock escalation) |
Retention:TimeBudget |
4 minutes | A run stops cleanly when it passes this; the next run carries on |
Retention:CommandTimeoutSeconds |
60 | Per statement |
Retention:Brake:Mode |
DryRun |
Live deletes; DryRun only logs what would go |
Retention:Brake:MaxPerRun |
50,000 | Most rows one run takes from one table |
Retention:Brake:MaxShareOfPopulation |
1.0 | No share limit: a quiet table can be entirely due |
Start in DryRun, read the run log, then set Retention:Brake:Mode to Live in the app settings. No deploy is needed.
4. Call it from a timer
public sealed class RetentionFunction(RetentionRunner runner)
{
[Function("Retention")]
public Task RunAsync([TimerTrigger("0 15 * * * *")] TimerInfo timer, CancellationToken cancellationToken)
{
return runner.RunAsync(cancellationToken);
}
}
RunAsync returns a RetentionRunReport with one result per table: Trimmed, NothingDue, DryRun, Refused or OutOfTime. When the brake refuses a table, the other tables are still trimmed, and the run then throws RetentionRunFailedException so it shows as failed.
How a run works
- Reads every
workflow:<days>entry and checks its table, columns, types and finished-time index against the live schema. - For each table, reads the row count from the partition metadata and the keys of up to
MaxPerRundue rows, oldest first. - Checks the plan with the
Webority.Jobsdestructive-run guard (jobRetention). - In
Live, deletes in batches. Each delete repeats the due check, so a row that changed after it was planned stays.
Build, test, release
dotnet restore Webority.Retention.slnx
dotnet build Webority.Retention.slnx -c Release --no-restore -v q
dotnet test --solution Webority.Retention.slnx -c Release --no-build
A merge to main publishes to nuget.org; the version of record is VersionPrefix in Directory.Build.props.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.Data.SqlClient (>= 7.1.1)
- Microsoft.Extensions.Configuration.Abstractions (>= 10.0.14)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.14)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.14)
- Microsoft.Extensions.Options.DataAnnotations (>= 10.0.14)
- System.Security.Cryptography.Pkcs (>= 10.0.14)
- Webority.Jobs (>= 0.34.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.2.14 | 0 | 11/5/2026 |
| 0.2.13 | 146 | 11/3/2026 |
| 0.2.12 | 156 | 11/3/2026 |
| 0.2.11 | 103 | 11/3/2026 |
| 0.2.10 | 86 | 11/2/2026 |
| 0.2.9 | 109 | 11/2/2026 |
| 0.2.8 | 195 | 11/2/2026 |
| 0.2.7 | 123 | 11/2/2026 |
| 0.2.6 | 100 | 11/1/2026 |
| 0.2.5 | 139 | 11/1/2026 |
| 0.2.4 | 92 | 11/1/2026 |
| 0.2.3 | 112 | 11/1/2026 |
| 0.2.2 | 91 | 11/1/2026 |
| 0.2.1 | 181 | 10/30/2026 |
| 0.2.0 | 94 | 10/29/2026 |
| 0.1.4 | 92 | 10/29/2026 |
| 0.1.3 | 89 | 10/29/2026 |
| 0.1.2 | 92 | 10/28/2026 |
| 0.1.1 | 99 | 10/28/2026 |
| 0.1.0 | 106 | 10/28/2026 |