Cohort 0.9.0
dotnet add package Cohort --version 0.9.0
NuGet\Install-Package Cohort -Version 0.9.0
<PackageReference Include="Cohort" Version="0.9.0" />
<PackageVersion Include="Cohort" Version="0.9.0" />
<PackageReference Include="Cohort" />
paket add Cohort --version 0.9.0
#r "nuget: Cohort, 0.9.0"
#:package Cohort@0.9.0
#addin nuget:?package=Cohort&version=0.9.0
#tool nuget:?package=Cohort&version=0.9.0
<p align="center"> <h1 align="center">Cohort</h1> <p align="center"> <a href="https://www.nuget.org/packages/Cohort"><img src="https://img.shields.io/nuget/v/Cohort?label=Cohort" alt="NuGet" /></a> <img src="https://img.shields.io/badge/license-MIT-blue" alt="License" /> </p> </p>
Retention belongs in your model. Cohort lets you annotate EF Core entities with a retention category, map each category to a rule, and then preview, sweep or erase rows through ordinary application services. Cohort handles the awkward parts: tenant scoping, legal holds, batched Postgres mutations, right-to-erasure and an audit trail of everything it touched.
You get the same result as a pile of nightly SQL jobs, but the policy sits next to the entity it governs. Startup refuses to run a retention model it can't execute safely.
Prerequisites
- .NET 9 or later, with EF Core 9 or later (EF Core 10 hosts are supported).
- PostgreSQL through the Npgsql EF Core provider. Cohort's SQL is Postgres-only.
Install
dotnet add package Cohort
Three steps in
1. Annotate the entities you retain
using Cohort.Domain;
[Retain("session-notes", nameof(CreatedAt))]
[RetentionEntityId("a3f467fe-c5d0-4f17-9897-83c373cc1dc8")]
public sealed class SessionNote
{
public Guid Id { get; set; }
public Guid TenantId { get; set; }
public DateTimeOffset CreatedAt { get; set; }
public string Body { get; set; } = "";
}
[Retain("case-contacts", nameof(CreatedAt))]
[RetentionEntityId("b7316df4-7db5-46ad-aea7-f65c4b430f73")]
public sealed class CaseContact
{
public Guid Id { get; set; }
public Guid TenantId { get; set; }
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset? AnonymisedAt { get; set; }
[Anonymise(AnonymiseMethod.Null)]
public string? Email { get; set; }
[Anonymise(AnonymiseMethod.EmptyString)]
public string FullName { get; set; } = "";
}
[Retain] names the category and the column to age rows by. [RetentionEntityId] is a
stable UUID that survives class and table renames. Never change it. Unannotated entities are
left alone.
2. Map categories to rules
The annotation says which category an entity belongs to. Your rule provider says what that category means:
using Cohort.Application;
using Cohort.Domain;
public sealed class RetentionRules : IRetentionRuleProvider
{
public RetentionCategoryCapabilities? GetCapabilities(string category) => category switch
{
"session-notes" => new([Strategy.Purge]),
"case-contacts" => new([Strategy.Anonymise]),
_ => null,
};
public Task<RetentionRule?> ResolveAsync(RetentionResolutionContext context, CancellationToken ct)
{
RetentionRule? rule = context.Category switch
{
"session-notes" => new RetentionRule(TimeSpan.FromDays(30), Strategy.Purge),
"case-contacts" => new RetentionRule(TimeSpan.FromDays(365), Strategy.Anonymise),
_ => null,
};
return Task.FromResult(rule);
}
}
ResolveAsync sees the tenant and the time, so rules can vary per tenant or jurisdiction.
GetCapabilities declares every strategy a category could ever resolve to, which lets
startup check that every entity can actually carry it out.
3. Register Cohort and migrate
builder.Services.AddSingleton<IRetentionRuleProvider, RetentionRules>();
builder.Services.AddCohort<AppDbContext>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ConfigureCohortTables();
}
Then add an EF Core migration and apply it. Cohort keeps its audit ledger, holds and handler queue in five tables of your database. It checks that schema at startup but never creates or changes it, so the migration stays yours.
Run it
Set a cron schedule and the hosted worker sweeps every tenant:
{ "Cohort": { "Schedule": "0 2 * * *" } }
Or call the services yourself:
var now = DateTimeOffset.UtcNow;
await services.GetRequiredService<IRetentionPreview>().PreviewAsync(tenant, now, ct);
await services.GetRequiredService<IRetentionSweep>().SweepAsync(tenant, now, ct);
await services.GetRequiredService<IRetentionErasureService>()
.EraseAsync(tenant, new ErasureScope("user", userId), now, ct);
Thirty-day-old session notes are deleted, and year-old case contacts keep their row but lose their email and name. Every query is scoped to the tenant, held rows are skipped, and the run is written to Cohort's ledger.
Also in the box
- Right-to-erasure: erase one
subject's rows immediately. Columns name the kind of subject they hold
(
[ErasureSubject("user")]), so a user erasure never touches a person column. A positiveLegalMinand active holds still block it. - Legal holds: a held row
survives every strategy.
IRetentionDeletionlets your own deletes respect holds too. - Row handlers: capture a row before mutation, then clean up blobs or publish events after commit, with at-least-once delivery and dead-lettering.
- Audit observers: export each committed run event to your own systems.
- History pruning: Cohort's own ledger has retention too, and it's opt-in.
- Dry runs, a kill switch, per-tenant passes and batched transactions, all covered under Running retention.
Documentation
Getting Started · Retention Rules · Configuration · Startup Validation · Limitations (what Cohort deliberately doesn't do)
License
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. 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. |
-
net9.0
- Cronos (>= 0.11.1)
- Microsoft.EntityFrameworkCore (>= 9.0.0)
- Microsoft.EntityFrameworkCore.Relational (>= 9.0.0)
- Microsoft.Extensions.Hosting.Abstractions (>= 9.0.0)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 9.0.0)
- Npgsql (>= 9.0.2)
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.9.0 | 90 | 10/9/2026 |
| 0.8.0 | 211 | 10/5/2026 |
| 0.7.3 | 177 | 9/30/2026 |
| 0.7.2 | 94 | 9/29/2026 |
| 0.7.1 | 87 | 9/29/2026 |
| 0.7.0 | 87 | 9/29/2026 |
| 0.6.3 | 256 | 8/11/2026 |
| 0.6.2 | 329 | 7/13/2026 |
| 0.6.1 | 128 | 7/12/2026 |
| 0.5.0 | 129 | 6/12/2026 |
| 0.4.1 | 127 | 6/12/2026 |
| 0.4.0 | 125 | 6/12/2026 |
| 0.3.1 | 174 | 4/14/2026 |
| 0.3.0 | 118 | 4/14/2026 |
| 0.2.0 | 135 | 4/14/2026 |
| 0.1.2 | 121 | 4/14/2026 |
| 0.1.1 | 126 | 4/12/2026 |
| 0.1.0 | 126 | 4/12/2026 |