Delta is an opinionated approach to implementing a 304 Not Modified
The approach uses a last updated timestamp from the database to generate an ETag. All dynamic requests then have that ETag checked/applied.
This approach works well when the frequency of updates is relatively low. In this scenario, the majory of requests will leverage the result in a 304 Not Modified being returned and the browser loading the content its cache.
Effectively consumers will always receive the most current data, while the load on the server remains very low.
See Milestones for release notes.
Assumes the following combination of technologies are being used:
- Frequency of updates to data is relatively low compared to reads
- ASP.NET Core
- Entity Framework Core
- Microsoft SQL Server EF Core Database Provider
- Either SQL Server Change Tracking and/or SQL Server Row Versioning
graph TD
Request
CalculateEtag[Calculate current ETag<br/>based on timestamp<br/>from web assembly and SQL]
IfNoneMatch{Has<br/>If-None-Match<br/>header?}
EtagMatch{Current<br/>Etag matches<br/>If-None-Match?}
AddETag[Add current ETag<br/>to Response headers]
304[Respond with<br/>304 Not-Modified]
Request --> CalculateEtag
CalculateEtag --> IfNoneMatch
IfNoneMatch -->|Yes| EtagMatch
IfNoneMatch -->|No| AddETag
EtagMatch -->|No| AddETag
EtagMatch -->|Yes| 304
The ETag is calculated from a combination several parts
The last write time of the web entry point assembly
var webAssemblyLocation = Assembly.GetEntryAssembly()!.Location;
AssemblyWriteTime = File.GetLastWriteTime(webAssemblyLocation).Ticks.ToString();
A combination of change_tracking_current_version (if tracking is enabled) and @@DBTS (row version timestamp)
declare @changeTracking bigint = change_tracking_current_version();
declare @timeStamp bigint = convert(bigint, @@dbts);
if (@changeTracking is null)
select cast(@timeStamp as varchar)
else
select cast(@timeStamp as varchar) + '-' + cast(@changeTracking as varchar)
An optional string suffix that is dynamically caculated at runtime based on the current HttpContext
.
var app = builder.Build();
app.UseDelta<SampleDbContext>(
suffix: httpContext => "MySuffix");
internal static string BuildEtag(string timeStamp, string? suffix)
{
if (suffix == null)
{
return $"\"{AssemblyWriteTime}-{timeStamp}\"";
}
return $"\"{AssemblyWriteTime}-{timeStamp}-{suffix}\"";
}
https://nuget.org/packages/Delta/
Enable row versioning in Entity Framework
public class SampleDbContext(DbContextOptions options) :
DbContext(options)
{
public DbSet<Employee> Employees { get; set; } = null!;
public DbSet<Company> Companies { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var company = modelBuilder.Entity<Company>();
company
.HasMany(_ => _.Employees)
.WithOne(_ => _.Company)
.IsRequired();
company
.Property(_ => _.RowVersion)
.IsRowVersion()
.HasConversion<byte[]>();
var employee = modelBuilder.Entity<Employee>();
employee
.Property(_ => _.RowVersion)
.IsRowVersion()
.HasConversion<byte[]>();
}
}
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSqlServer<SampleDbContext>(database.ConnectionString);
var app = builder.Build();
app.UseDelta<SampleDbContext>();
app.MapGroup("/group")
.UseDelta<SampleDbContext>()
.MapGet("/", () => "Hello Group!");
Optional control what requests Delta is executed on.
var app = builder.Build();
app.UseDelta<SampleDbContext>(
shouldExecute: httpContext =>
{
var path = httpContext.Request.Path.ToString();
return path.Contains("match");
});
var timeStamp = await dbContext.GetLastTimeStamp();
var timeStamp = await sqlConnection.GetLastTimeStamp();
Get a list of all databases with change tracking enabled.
var trackedDatabases = await sqlConnection.GetTrackedDatabases();
foreach (var db in trackedDatabases)
{
Trace.WriteLine(db);
}
Uses the following SQL:
select d.name
from sys.databases as d inner join
sys.change_tracking_databases as t on
t.database_id = d.database_id
Get a list of all tracked tables in database.
var trackedTables = await sqlConnection.GetTrackedTables();
foreach (var db in trackedTables)
{
Trace.WriteLine(db);
}
Uses the following SQL:
select t.Name
from sys.tables as t left join
sys.change_tracking_tables as c on t.[object_id] = c.[object_id]
where c.[object_id] is not null
Determine if change tracking is enabled for a database.
var isTrackingEnabled = await sqlConnection.IsTrackingEnabled();
Uses the following SQL:
select count(d.name)
from sys.databases as d inner join
sys.change_tracking_databases as t on
t.database_id = d.database_id
where d.name = '{connection.Database}'
Enable change tracking for a database.
await sqlConnection.EnableTracking();
Uses the following SQL:
alter database {connection.Database}
set change_tracking = on
(
change_retention = {retentionDays} days,
auto_cleanup = on
)
Disable change tracking for a database and all tables within that database.
await sqlConnection.DisableTracking();
Uses the following SQL:
alter table [{table}] disable change_tracking;
alter database [{connection.Database}] set change_tracking = off;
Enables change tracking for all tables listed, and disables change tracking for all tables not listed.
await sqlConnection.SetTrackedTables(["Companies"]);
Uses the following SQL:
alter database {connection.Database}
set change_tracking = on
(
change_retention = {retentionDays} days,
auto_cleanup = on
)
alter table [{table}] enable change_tracking
alter table [{table}] disable change_tracking;
Estuary designed by Daan from The Noun Project.