ChangeTracker.Npgsql
1.0.1
dotnet add package ChangeTracker.Npgsql --version 1.0.1
NuGet\Install-Package ChangeTracker.Npgsql -Version 1.0.1
<PackageReference Include="ChangeTracker.Npgsql" Version="1.0.1" />
<PackageVersion Include="ChangeTracker.Npgsql" Version="1.0.1" />
<PackageReference Include="ChangeTracker.Npgsql" />
paket add ChangeTracker.Npgsql --version 1.0.1
#r "nuget: ChangeTracker.Npgsql, 1.0.1"
#:package ChangeTracker.Npgsql@1.0.1
#addin nuget:?package=ChangeTracker.Npgsql&version=1.0.1
#tool nuget:?package=ChangeTracker.Npgsql&version=1.0.1
ChangeTracker
ChangeTracker is inspired by Delta Project
Change Tracker is a library for efficient HTTP caching using database change tracking. It implements 304 Not Modified responses by generating ETags based on database timestamps, reducing server load while ensuring clients always receive current data.
๐ Overview
ChangeTracker monitors database changes and generates ETags that combine:
- Assembly write time (when your application was built)
- Database timestamp (last data modification time)
- Custom suffix (optional runtime context)
When a client requests data with a cached ETag, the server compares it with the current state.
- If unchanged: it returns 304 Not Modified and the client uses its cached copy.
- If changed: fresh data is returned with a new ETag.
ETags follow this format:
{AssemblyWriteTime}-{DbTimeStamp}-{Suffix}
๐ก Ideal Use Case
- Read-heavy applications where data changes less frequently than it's read
- APIs serving semi-static data that changes periodically
- Applications needing reduced server load without compromising data freshness
๐ Documentation
- PostgreSQL docs
- SQL Server docs
๐ ๏ธ How It Works
Assembly Write Time
The last modification time of your web application's assembly is handled by the AssemblyTimestampProvider, which implements the IAssemblyTimestampProvider interface.
public sealed class AssemblyTimestampProvider(Assembly assembly) : IAssemblyTimestampProvider
{
public DateTimeOffset GetWriteTime()
{
ArgumentNullException.ThrowIfNull(assembly, nameof(assembly));
if (!File.Exists(assembly.Location))
throw new FileNotFoundException($"Assembly file not found at '{assembly.Location}'");
return File.GetLastWriteTimeUtc(assembly.Location);
}
}
Database Timestamp
Tracks when data was last modified. Implementation varies by database:
- PostgresSQL timestamp calculation
- SQL Server timestamp calculation
Custom Suffix (Optional)
Dynamic string based on HTTP context for fine-grained cache control:
var builder = WebApplication.CreateBuilder(args);
{
builder.Services
.AddTracker(options =>
{
options.Suffix = (httpContext) => "Suffix";
});
}
var app = builder.Build();
{
app.UseTracker(options =>
{
options.Suffix = (httpContext) => "Suffix";
});
app.MapGet("route", () => { })
.WithTracking(options =>
{
options.Suffix = (httpContext) => "Suffix";
});
}
ETag Generation & Comparison
For comparison and generation of ETags, see the implementation in DefaultETagProvider of the IETagProvider interface.
Chanage Tracker Client Registration
Tracker services can be registered using the AddTracker extension method, which accepts a GlobalOptions configuration object.
builder.Services.AddTracker();
builder.Services.AddTracker(new GlobalOptions()
{
CacheControl = "max-age=60, stale-while-revalidate=60, stale-if-error=86400",
});
builder.Services.AddTracker(options =>
{
options.Filter = (httpContext) => true;
});
Provider Documentation
For ChangeTracker to function correctly, you must register a database-specific source provider. This component monitors database changes and provides timestamps for ETag generation.
Detailed implementation guides for each database:
- PostgreSQL docs
- SQL Server docs
๐ง Usage
Controller Action (MVC/Web API)
Apply caching to specific endpoints using the [Track] attribute:
[HttpGet]
[Track(tables: ["roles"], cacheControl: "no-cache")]
public ActionResult<IEnumerable<Role>> GetAll()
{
return dbContext.Roles.ToList();
}
Middleware Configuration
Apply caching globally:
app.UseTracker(options =>
{
options.CacheControl = "max-age=60, stale-while-revalidate=60, stale-if-error=86400";
options.Filter = (httpContext) => httpContext.Request.Path.Value.Contains("/api/");
});
Minimal APIs
Configure tracking directly on minimal API endpoints:
app.MapGet("/api/user-profile", () =>
{
// Your endpoint logic
})
.WithTracking(options =>
{
options.Tables = ["users", "profiles", "preferences"];
options.CacheControl = "max-age=300"; // 5 minutes
});
Fast Endpoints
Configure tracking directly with Fast Endpoints:
builder.Services.AddTrackerFastEndpoints();
[Track(tables: ["roles"])] //attribute usage is optional, if not specified options will be fully taked from DI
public sealed class MyEndpoint : Endpoint<EmptyRequest>
{
public override void Configure()
{
PreProcessor<TrackerPreProcessor<EmptyRequest>>();
}
}
//or global
app.UseFastEndpoints((config) =>
{
config.Endpoints.Configurator = ep =>
{
ep.PreProcessor<GlobalTrackerPreProcessor>(Order.Before);
};
});
๐งช Verifying behavior
Testing Cache Hits
- Open your application in a browser
- Open Developer Tools (F12)
- Navigate to the Network tab
- Refresh the page
Cached responses will show:
- Status: 304 Not Modified
- Request Header: if-none-match (with ETag value)
- Response Header: etag (current ETag)
Testing Cache Misses
To test the full request pipeline:
- Open Developer Tools โ Network tab
- Check "Disable cache" in the toolbar
- Refresh the page
This prevents the browser from sending if-none-match, forcing a cache miss and full server execution.
| 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
- ChangeTracker.Core (>= 1.0.1)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.1)
- Microsoft.Extensions.ObjectPool (>= 10.0.1)
- Npgsql.EntityFrameworkCore.PostgreSQL (>= 10.0.0)
-
net8.0
- ChangeTracker.Core (>= 1.0.1)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- Microsoft.Extensions.ObjectPool (>= 8.0.22)
- Npgsql.EntityFrameworkCore.PostgreSQL (>= 8.0.11)
-
net9.0
- ChangeTracker.Core (>= 1.0.1)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.11)
- Microsoft.Extensions.ObjectPool (>= 9.0.11)
- Npgsql.EntityFrameworkCore.PostgreSQL (>= 9.0.4)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.