Cohort 0.9.0

dotnet add package Cohort --version 0.9.0
                    
NuGet\Install-Package Cohort -Version 0.9.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Cohort" Version="0.9.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Cohort" Version="0.9.0" />
                    
Directory.Packages.props
<PackageReference Include="Cohort" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Cohort --version 0.9.0
                    
#r "nuget: Cohort, 0.9.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Cohort@0.9.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Cohort&version=0.9.0
                    
Install as a Cake Addin
#tool nuget:?package=Cohort&version=0.9.0
                    
Install as a Cake Tool

<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 positive LegalMin and active holds still block it.
  • Legal holds: a held row survives every strategy. IRetentionDeletion lets 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

MIT

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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