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:
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();
snippet source | anchor
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)
snippet source | anchor
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");
snippet source | anchor
internal static string BuildEtag(string timeStamp, string? suffix)
{
if (suffix == null)
{
return $"\"{AssemblyWriteTime}-{timeStamp}\"";
}
return $"\"{AssemblyWriteTime}-{timeStamp}-{suffix}\"";
}
snippet source | anchor
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[]>();
}
}
snippet source | anchor
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSqlServer<SampleDbContext>(database.ConnectionString);
var app = builder.Build();
app.UseDelta<SampleDbContext>();
snippet source | anchor
app.MapGroup("/group")
.UseDelta<SampleDbContext>()
.MapGet("/", () => "Hello Group!");
snippet source | anchor
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");
});
snippet source | anchor
DbContext
:var timeStamp = await dbContext.GetLastTimeStamp();
snippet source | anchor
DbConnection
:var timeStamp = await sqlConnection.GetLastTimeStamp();
snippet source | anchor
Get a list of all databases with change tracking enabled.
var trackedDatabases = await sqlConnection.GetTrackedDatabases();
foreach (var db in trackedDatabases)
{
Trace.WriteLine(db);
}
snippet source | anchor
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
snippet source | anchor
Get a list of all tracked tables in database.
var trackedTables = await sqlConnection.GetTrackedTables();
foreach (var db in trackedTables)
{
Trace.WriteLine(db);
}
snippet source | anchor
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
snippet source | anchor
Determine if change tracking is enabled for a database.
var isTrackingEnabled = await sqlConnection.IsTrackingEnabled();
snippet source | anchor
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}'
snippet source | anchor
Enable change tracking for a database.
await sqlConnection.EnableTracking();
snippet source | anchor
Uses the following SQL:
alter database {connection.Database}
set change_tracking = on
(
change_retention = {retentionDays} days,
auto_cleanup = on
)
snippet source | anchor
Disable change tracking for a database and all tables within that database.
await sqlConnection.DisableTracking();
snippet source | anchor
Uses the following SQL:
alter table [{table}] disable change_tracking;
snippet source | anchor
alter database [{connection.Database}] set change_tracking = off;
snippet source | anchor
Enables change tracking for all tables listed, and disables change tracking for all tables not listed.
await sqlConnection.SetTrackedTables(["Companies"]);
snippet source | anchor
Uses the following SQL:
alter database {connection.Database}
set change_tracking = on
(
change_retention = {retentionDays} days,
auto_cleanup = on
)
snippet source | anchor
alter table [{table}] enable change_tracking
snippet source | anchor
alter table [{table}] disable change_tracking;
snippet source | anchor
Estuary designed by Daan from The Noun Project.