Accipitech.DedupeDoc
0.1.0
dotnet add package Accipitech.DedupeDoc --version 0.1.0
NuGet\Install-Package Accipitech.DedupeDoc -Version 0.1.0
<PackageReference Include="Accipitech.DedupeDoc" Version="0.1.0" />
<PackageVersion Include="Accipitech.DedupeDoc" Version="0.1.0" />
<PackageReference Include="Accipitech.DedupeDoc" />
paket add Accipitech.DedupeDoc --version 0.1.0
#r "nuget: Accipitech.DedupeDoc, 0.1.0"
#:package Accipitech.DedupeDoc@0.1.0
#addin nuget:?package=Accipitech.DedupeDoc&version=0.1.0
#tool nuget:?package=Accipitech.DedupeDoc&version=0.1.0
Accipitech.DedupeDoc
Tenant-aware document de-duplication for .NET: content-addressed blob storage (FluentStorage: local disk, AWS S3 or any S3-compatible endpoint) with SHA-256 dedup, version chains, and a metadata store that provisions its own tables at startup on SQL Server, MySQL, or PostgreSQL.
Extracted from and battle-tested against two production document pipelines.
How it works
- Every ingest hashes the content (streaming SHA-256) and stores the blob under a sharded,
content-addressed key:
{collection}/{aa}/{bb}/{cc}/{hash}. Identical content is stored once; every upload still gets its own metadata row (id, filename, tenant, timestamps). - Tenancy lives in the metadata database only (EF Core global query filters). Blobs are shared across tenants; a blob is deleted only when the last row in any tenant referencing it is deleted.
- Versioning is a linear chain:
IngestNewVersionAsyncresolves any chain member to the root, retroactively marks the root as v1, and appends. Re-uploading a version's identical content is a no-op that returns the existing version.
Quick start
// Program.cs — pick ONE provider package:
// Accipitech.DedupeDoc.SqlServer | .MySql | .PostgreSql
builder.Services.AddDedupeDoc(builder.Configuration, d =>
{
d.UseSqlServer(builder.Configuration.GetConnectionString("DedupeDoc")!);
d.UseLocalDiskStorage("/var/app-storage/documents"); // or d.UseS3Storage(...)
d.UseTenantProvider<MyClaimsTenantProvider>(); // omit for single-tenant apps
});
On startup the library creates/upgrades its own tables (dedupe.documents,
dedupe.document_versions — dedupe_-prefixed on MySQL) with its own migrations history
(__dedupe_migrations), never touching the host's EF migrations. Set
DedupeDocOptions.AutoMigrate = false to run IDedupeSchemaManager.MigrateAsync() from a
deployment job instead.
public sealed class MyClaimsTenantProvider(IHttpContextAccessor ctx) : IDedupeTenantProvider
{
public string? TenantId => ctx.HttpContext?.User.FindFirstValue("tenant");
}
Use it:
var doc = await dedupe.IngestAsync(stream, "cv.pdf"); // doc.BlobWasDeduplicated
var v2 = await dedupe.IngestNewVersionAsync(doc.Id, newStream);
await using var content = await dedupe.OpenReadAsync(v2.Id);
await dedupe.DeleteAsync(doc.Id); // blob freed with last ref
var stats = await dedupe.GetDeduplicationStatsAsync(); // bytes saved, per tenant
Configuration (appsettings.json, section DedupeDoc)
{
"DedupeDoc": {
"DefaultCollection": "documents",
"AutoMigrate": true,
"IncludeExtensionInBlobKey": false,
"Storage": {
"Provider": "S3",
"BasePath": "/var/app-storage/documents",
"S3": { "ServiceUrl": "https://minio.internal:9000", "Bucket": "docs",
"AccessKey": "...", "SecretKey": "...", "Region": null }
}
}
}
Host-owned migrations mode
If you'd rather ship the tables inside your app's own migrations pipeline:
// in your DbContext:
protected override void OnModelCreating(ModelBuilder modelBuilder)
=> modelBuilder.AddDedupeDocEntities(this, ctx => ctx.CurrentTenantId, schema: "dedupe");
// registration — no provider package needed:
builder.Services.AddDedupeDoc(d =>
{
d.UseHostDbContext<MyAppDbContext>();
d.UseLocalDiskStorage("/var/app-storage/documents");
});
Then dotnet ef migrations add in your app picks the tables up. Schema/table prefix are
free parameters here (they are fixed in auto-migrate mode).
Notes and limits
- MySQL: net8.0/net9.0 hosts use Pomelo (8.0.3 / 9.0.0); net10.0 hosts use the drop-in Microting fork of Pomelo (upstream Pomelo is unmaintained and its EF9 build fails at runtime against EF Core 10 — verified). Same API and namespaces; no code or migration changes between TFMs.
- EF Core 8 (net8.0) has no migration locking; on multi-node net8.0 deployments run migrations from a single job, or rely on the built-in retry.
DocumentResult.BlobWasDeduplicatedcan reveal that identical content exists in another tenant — treat it as internal telemetry, don't expose it to end users.- Blob-delete failures are logged and swallowed (an orphaned blob beats a dangling
reference); reclaim orphans with
IDedupeMaintenanceService.SweepOrphanedBlobsAsync(dry-run by default). - Non-seekable upload streams are spooled to a delete-on-close temp file automatically.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. 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 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
- FluentStorage (>= 6.0.4)
- FluentStorage.AWS (>= 6.0.2)
- Microsoft.EntityFrameworkCore.Relational (>= 10.0.11)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.0)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.0)
-
net8.0
- FluentStorage (>= 6.0.4)
- FluentStorage.AWS (>= 6.0.2)
- Microsoft.EntityFrameworkCore.Relational (>= 8.0.30)
- Microsoft.Extensions.Hosting.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 8.0.0)
-
net9.0
- FluentStorage (>= 6.0.4)
- FluentStorage.AWS (>= 6.0.2)
- Microsoft.EntityFrameworkCore.Relational (>= 9.0.19)
- Microsoft.Extensions.Hosting.Abstractions (>= 9.0.0)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 9.0.0)
NuGet packages (3)
Showing the top 3 NuGet packages that depend on Accipitech.DedupeDoc:
| Package | Downloads |
|---|---|
|
Accipitech.DedupeDoc.MySql
MySQL provider for Accipitech.DedupeDoc: bundled migrations and startup auto-provisioning of the dedupe tables. Pomelo on net8.0/net9.0; the actively maintained Microting fork of Pomelo on net10.0 (upstream Pomelo has no EF Core 10 release). |
|
|
Accipitech.DedupeDoc.SqlServer
SQL Server provider for Accipitech.DedupeDoc: bundled migrations and startup auto-provisioning of the dedupe schema. |
|
|
Accipitech.DedupeDoc.PostgreSql
PostgreSQL provider (Npgsql) for Accipitech.DedupeDoc: bundled migrations and startup auto-provisioning of the dedupe schema. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.1.0 | 155 | 8/31/2026 |