{"id":17182404,"url":"https://github.com/simoncropp/delta","last_synced_at":"2025-05-14T00:08:25.467Z","repository":{"id":64278727,"uuid":"572273596","full_name":"SimonCropp/Delta","owner":"SimonCropp","description":"An approach to implementing a 304 Not Modified leveraging DB change tracking","archived":false,"fork":false,"pushed_at":"2025-05-13T09:34:14.000Z","size":713,"stargazers_count":1363,"open_issues_count":0,"forks_count":32,"subscribers_count":16,"default_branch":"main","last_synced_at":"2025-05-13T10:40:40.796Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/SimonCropp.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"license.txt","code_of_conduct":"code_of_conduct.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null},"funding":{"github":"SimonCropp"}},"created_at":"2022-11-29T23:10:40.000Z","updated_at":"2025-05-13T09:34:17.000Z","dependencies_parsed_at":"2024-11-06T16:38:28.892Z","dependency_job_id":"eee3ed12-4632-4106-85a8-3a27fcb9bf43","html_url":"https://github.com/SimonCropp/Delta","commit_stats":{"total_commits":365,"total_committers":4,"mean_commits":91.25,"dds":0.452054794520548,"last_synced_commit":"97af367eb4abab5772630ea61bffbd4aa1012f59"},"previous_names":[],"tags_count":11,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SimonCropp%2FDelta","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SimonCropp%2FDelta/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SimonCropp%2FDelta/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SimonCropp%2FDelta/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/SimonCropp","download_url":"https://codeload.github.com/SimonCropp/Delta/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254043996,"owners_count":22005055,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-15T00:37:00.336Z","updated_at":"2025-05-14T00:08:25.460Z","avatar_url":"https://github.com/SimonCropp.png","language":"C#","funding_links":["https://github.com/sponsors/SimonCropp"],"categories":[],"sub_categories":[],"readme":"\u003c!--\nGENERATED FILE - DO NOT EDIT\nThis file was generated by [MarkdownSnippets](https://github.com/SimonCropp/MarkdownSnippets).\nSource File: /readme.source.md\nTo change this file edit the source file and then run MarkdownSnippets.\n--\u003e\n\n# \u003cimg src=\"/src/icon.png\" height=\"30px\"\u003e Delta\n\n[![Build status](https://ci.appveyor.com/api/projects/status/20t96gnsmysklh09/branch/main?svg=true)](https://ci.appveyor.com/project/SimonCropp/Delta)\n[![NuGet Status](https://img.shields.io/nuget/v/Delta.svg?label=Delta)](https://www.nuget.org/packages/Delta/)\n[![NuGet Status](https://img.shields.io/nuget/v/Delta.EF.svg?label=Delta.EF)](https://www.nuget.org/packages/Delta.EF/)\n[![NuGet Status](https://img.shields.io/nuget/v/Delta.SqlServer.svg?label=Delta.SqlServer)](https://www.nuget.org/packages/Delta.SqlServer/)\n\nDelta is an approach to implementing a [304 Not Modified](https://www.keycdn.com/support/304-not-modified) leveraging DB change tracking.\n\nThe approach uses a last updated timestamp from the database to generate an [ETag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag). All dynamic requests then have that ETag checked/applied.\n\nThis approach works well when the frequency of updates is relatively low. In this scenario, the majority of requests will leverage the result in a 304 Not Modified being returned and the browser loading the content its cache.\n\nEffectively consumers will always receive the most current data, while the load on the server remains low.\n\n**See [Milestones](../../milestones?state=closed) for release notes.**\n\n\n### Powered by\n\n[![JetBrains logo.](https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg)](https://jb.gg/OpenSourceSupport)\n\n\n## Assumptions\n\nFrequency of updates to data is relatively low compared to reads\n\n\n### SQL Server\n\nFor SQL Server the transaction log is used (via [dm_db_log_stats](https://learn.microsoft.com/en-us/sql/relational-databases/system-dynamic-management-views/sys-dm-db-log-stats-transact-sql)) if the current user has the `VIEW SERVER STATE` permission.\n\nIf `VIEW SERVER STATE` is not allowed then a combination of [Change Tracking](https://learn.microsoft.com/en-us/sql/relational-databases/track-changes/track-data-changes-sql-server) and/or [Row Versioning](https://learn.microsoft.com/en-us/sql/t-sql/data-types/rowversion-transact-sql) is used.\n\nGive the above certain kinds of operations will be detected:\n\n|             | Transaction Log | Change Tracking | Row Versioning | Change Tracking\u003cbr\u003eand Row Versioning |\n|-------------|:---------------:|:---------------:|:--------------:|:----------------------------------:|\n| Insert      |        ✅      |        ✅       |        ✅     |                  ✅                |\n| Update      |        ✅      |        ✅       |        ✅     |                  ✅                |\n| Hard Delete |        ✅      |        ✅       |        ❌     |                  ✅                |\n| Soft Delete |        ✅      |        ✅       |        ✅     |                  ✅                |\n| Truncate    |        ✅      |        ❌       |        ❌     |                  ❌                |\n\n\n\n### Postgres\n\nPostgres required [track_commit_timestamp](https://www.postgresql.org/docs/17/runtime-config-replication.html#GUC-TRACK-COMMIT-TIMESTAMP) to be enabled. This can be done using `ALTER SYSTEM SET track_commit_timestamp to \"on\"` and then restarting the Postgres service\n\n\n## 304 Not Modified Flow\n\n```mermaid\ngraph TD\n    Request\n    CalculateEtag[Calculate current ETag\u003cbr/\u003ebased on timestamp\u003cbr/\u003efrom web assembly and SQL]\n    IfNoneMatch{Has\u003cbr/\u003eIf-None-Match\u003cbr/\u003eheader?}\n    EtagMatch{Current\u003cbr/\u003eEtag matches\u003cbr/\u003eIf-None-Match?}\n    AddETag[Add current ETag\u003cbr/\u003eto Response headers]\n    304[Respond with\u003cbr/\u003e304 Not-Modified]\n    Request --\u003e CalculateEtag\n    CalculateEtag --\u003e IfNoneMatch\n    IfNoneMatch --\u003e|Yes| EtagMatch\n    IfNoneMatch --\u003e|No| AddETag\n    EtagMatch --\u003e|No| AddETag\n    EtagMatch --\u003e|Yes| 304\n```\n\n\n## ETag calculation logic\n\nThe ETag is calculated from a combination several parts\n\n\n### AssemblyWriteTime\n\nThe last write time of the web entry point assembly\n\n\u003c!-- snippet: AssemblyWriteTime --\u003e\n\u003ca id='snippet-AssemblyWriteTime'\u003e\u003c/a\u003e\n```cs\nvar webAssemblyLocation = Assembly.GetEntryAssembly()!.Location;\nAssemblyWriteTime = File.GetLastWriteTime(webAssemblyLocation).Ticks.ToString();\n```\n\u003csup\u003e\u003ca href='/src/Delta/DeltaExtensions_Shared.cs#L44-L49' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-AssemblyWriteTime' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\n\n### SQL timestamp\n\n\n#### SQL Server\n\n\n##### `VIEW SERVER STATE` permission\n\nTransaction log is used via [dm_db_log_stats](https://learn.microsoft.com/en-us/sql/relational-databases/system-dynamic-management-views/sys-dm-db-log-stats-transact-sql).\n\n```sql\nselect log_end_lsn\nfrom sys.dm_db_log_stats(db_id())\n```\n\n\n##### No `VIEW SERVER STATE` permission\n\nA combination of [change_tracking_current_version](https://learn.microsoft.com/en-us/sql/relational-databases/system-functions/change-tracking-current-version-transact-sql) (if tracking is enabled) and [@@DBTS (row version timestamp)](https://learn.microsoft.com/en-us/sql/t-sql/functions/dbts-transact-sql)\n\n```sql\ndeclare @changeTracking bigint = change_tracking_current_version();\ndeclare @timeStamp bigint = convert(bigint, @@dbts);\n\nif (@changeTracking is null)\n  select cast(@timeStamp as varchar)\nelse\n  select cast(@timeStamp as varchar) + '-' + cast(@changeTracking as varchar)\n```\n\n\n#### Postgres\n\n```sql\nselect pg_last_committed_xact();\n```\n\n\n### Suffix\n\nAn optional string suffix that is dynamically calculated at runtime based on the current `HttpContext`.\n\n\u003c!-- snippet: Suffix --\u003e\n\u003ca id='snippet-Suffix'\u003e\u003c/a\u003e\n```cs\nvar app = builder.Build();\napp.UseDelta(suffix: httpContext =\u003e \"MySuffix\");\n```\n\u003csup\u003e\u003ca href='/src/DeltaTests/Usage.cs#L9-L14' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-Suffix' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\n\n### Combining the above\n\n\u003c!-- snippet: BuildEtag --\u003e\n\u003ca id='snippet-BuildEtag'\u003e\u003c/a\u003e\n```cs\ninternal static string BuildEtag(string timeStamp, string? suffix)\n{\n    if (suffix == null)\n    {\n        return $\"\\\"{AssemblyWriteTime}-{timeStamp}\\\"\";\n    }\n\n    return $\"\\\"{AssemblyWriteTime}-{timeStamp}-{suffix}\\\"\";\n}\n```\n\u003csup\u003e\u003ca href='/src/Delta/DeltaExtensions_Shared.cs#L152-L164' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-BuildEtag' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\n\n## NuGet\n\nDelta is shipped as two nugets:\n\n * [Delta](https://nuget.org/packages/Delta/): Delivers functionality using SqlConnection and SqlTransaction.\n * [Delta.EF](https://nuget.org/packages/Delta.EF/): Delivers functionality using [SQL Server EF Database Provider](https://learn.microsoft.com/en-us/ef/core/providers/sql-server/?tabs=dotnet-core-cli).\n\nOnly one of the above should be used.\n\n\n## Usage\n\n\n### SQL Server DB Schema\n\nExample SQL schema:\n\n\u003c!-- snippet: Usage.Schema.verified.sql --\u003e\n\u003ca id='snippet-Usage.Schema.verified.sql'\u003e\u003c/a\u003e\n```sql\n-- Tables\n\nCREATE TABLE [dbo].[Companies](\n\t[Id] [uniqueidentifier] NOT NULL,\n\t[RowVersion] [timestamp] NOT NULL,\n\t[Content] [nvarchar](max) NULL,\n CONSTRAINT [PK_Companies] PRIMARY KEY CLUSTERED \n(\n\t[Id] ASC\n) ON [PRIMARY]\n) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]\n\nALTER TABLE [dbo].[Companies] ENABLE CHANGE_TRACKING WITH(TRACK_COLUMNS_UPDATED = OFF)\n\nCREATE TABLE [dbo].[Employees](\n\t[Id] [uniqueidentifier] NOT NULL,\n\t[RowVersion] [timestamp] NOT NULL,\n\t[CompanyId] [uniqueidentifier] NOT NULL,\n\t[Content] [nvarchar](max) NULL,\n\t[Age] [int] NOT NULL,\n CONSTRAINT [PK_Employees] PRIMARY KEY CLUSTERED \n(\n\t[Id] ASC\n) ON [PRIMARY]\n) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]\n\nCREATE NONCLUSTERED INDEX [IX_Employees_CompanyId] ON [dbo].[Employees]\n(\n\t[CompanyId] ASC\n) ON [PRIMARY]\n```\n\u003csup\u003e\u003ca href='/src/DeltaTests/Usage.Schema.verified.sql#L1-L30' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-Usage.Schema.verified.sql' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\n\n### Postgres DB Schema\n\nExample SQL schema:\n\n\u003c!-- snippet: PostgresSchema --\u003e\n\u003ca id='snippet-PostgresSchema'\u003e\u003c/a\u003e\n```cs\ncreate table IF NOT EXISTS public.\"Companies\"\n(\n    \"Id\" uuid not null\n        constraint \"PK_Companies\"\n            primary key,\n    \"Content\" text\n);\n\nalter table public.\"Companies\"\n    owner to postgres;\n\ncreate table IF NOT EXISTS public.\"Employees\"\n(\n    \"Id\" uuid not null\n        constraint \"PK_Employees\"\n            primary key,\n    \"CompanyId\" uuid not null\n        constraint \"FK_Employees_Companies_CompanyId\"\n            references public.\"Companies\"\n            on delete cascade,\n    \"Content\"   text,\n    \"Age\"       integer not null\n);\n\nalter table public.\"Employees\"\n    owner to postgres;\n\ncreate index IF NOT EXISTS \"IX_Employees_CompanyId\"\n    on public.\"Employees\" (\"CompanyId\");\n```\n\u003csup\u003e\u003ca href='/src/WebApplicationPostgres/PostgresDbBuilder.cs#L9-L39' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-PostgresSchema' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\n\n### Add to WebApplicationBuilder\n\n\n#### SQL Server\n\n\u003c!-- snippet: UseDeltaSqlServer --\u003e\n\u003ca id='snippet-UseDeltaSqlServer'\u003e\u003c/a\u003e\n```cs\nvar builder = WebApplication.CreateBuilder();\nbuilder.Services.AddScoped(_ =\u003e new SqlConnection(connectionString));\nvar app = builder.Build();\napp.UseDelta();\n```\n\u003csup\u003e\u003ca href='/src/WebApplicationSqlServer/Program.cs#L8-L15' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-UseDeltaSqlServer' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\n\n#### PostgreSQL\n\n\u003c!-- snippet: UseDeltaPostgres --\u003e\n\u003ca id='snippet-UseDeltaPostgres'\u003e\u003c/a\u003e\n```cs\nvar builder = WebApplication.CreateBuilder();\nbuilder.Services.AddScoped(_ =\u003e new NpgsqlConnection(connectionString));\nvar app = builder.Build();\napp.UseDelta();\n```\n\u003csup\u003e\u003ca href='/src/WebApplicationPostgres/Program.cs#L3-L10' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-UseDeltaPostgres' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\n\n### Add to a Route Group\n\nTo add to a specific [Route Group](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/route-handlers#route-groups):\n\n\u003c!-- snippet: UseDeltaMapGroup --\u003e\n\u003ca id='snippet-UseDeltaMapGroup'\u003e\u003c/a\u003e\n```cs\napp.MapGroup(\"/group\")\n    .UseDelta()\n    .MapGet(\"/\", () =\u003e \"Hello Group!\");\n```\n\u003csup\u003e\u003ca href='/src/WebApplicationSqlServer/Program.cs#L61-L67' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-UseDeltaMapGroup' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\n\n### ShouldExecute\n\nOptionally control what requests Delta is executed on.\n\n\u003c!-- snippet: ShouldExecute --\u003e\n\u003ca id='snippet-ShouldExecute'\u003e\u003c/a\u003e\n```cs\nvar app = builder.Build();\napp.UseDelta(\n    shouldExecute: httpContext =\u003e\n    {\n        var path = httpContext.Request.Path.ToString();\n        return path.Contains(\"match\");\n    });\n```\n\u003csup\u003e\u003ca href='/src/DeltaTests/Usage.cs#L19-L29' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-ShouldExecute' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\n\n### Custom Connection discovery\n\nBy default, Delta uses `HttpContext.RequestServices` to discover the SqlConnection and SqlTransaction:\n\n\u003c!-- snippet: DiscoverConnection --\u003e\n\u003ca id='snippet-DiscoverConnection'\u003e\u003c/a\u003e\n```cs\nstatic void InitConnectionTypes()\n{\n    var sqlConnectionType = Type.GetType(\"Microsoft.Data.SqlClient.SqlConnection, Microsoft.Data.SqlClient\");\n    if (sqlConnectionType != null)\n    {\n        connectionType = sqlConnectionType;\n        transactionType = sqlConnectionType.Assembly.GetType(\"Microsoft.Data.SqlClient.SqlTransaction\")!;\n        return;\n    }\n\n    var npgsqlConnection = Type.GetType(\"Npgsql.NpgsqlConnection, Npgsql\");\n    if (npgsqlConnection != null)\n    {\n        connectionType = npgsqlConnection;\n        transactionType = npgsqlConnection.Assembly.GetType(\"Npgsql.NpgsqlTransaction\")!;\n        return;\n    }\n\n    throw new(\"Could not find connection type. Tried Microsoft.Data.SqlClient.SqlConnection and Npgsql.NpgsqlTransaction\");\n}\n\nstatic Connection DiscoverConnection(HttpContext httpContext)\n{\n    var provider = httpContext.RequestServices;\n    var connection = (DbConnection) provider.GetRequiredService(connectionType);\n    var transaction = (DbTransaction?) provider.GetService(transactionType);\n    return new(connection, transaction);\n}\n```\n\u003csup\u003e\u003ca href='/src/Delta/DeltaExtensions_ConnectionDiscovery.cs#L10-L40' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-DiscoverConnection' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\nTo use custom connection discovery:\n\n\u003c!-- snippet: CustomDiscoveryConnection --\u003e\n\u003ca id='snippet-CustomDiscoveryConnection'\u003e\u003c/a\u003e\n```cs\nvar application = webApplicationBuilder.Build();\napplication.UseDelta(\n    getConnection: httpContext =\u003e httpContext.RequestServices.GetRequiredService\u003cSqlConnection\u003e());\n```\n\u003csup\u003e\u003ca href='/src/DeltaTests/Usage.cs#L335-L341' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-CustomDiscoveryConnection' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\nTo use custom connection and transaction discovery:\n\n\u003c!-- snippet: CustomDiscoveryConnectionAndTransaction --\u003e\n\u003ca id='snippet-CustomDiscoveryConnectionAndTransaction'\u003e\u003c/a\u003e\n```cs\nvar application = webApplicationBuilder.Build();\napplication.UseDelta(\n    getConnection: httpContext =\u003e\n    {\n        var provider = httpContext.RequestServices;\n        var connection = provider.GetRequiredService\u003cSqlConnection\u003e();\n        var transaction = provider.GetService\u003cSqlTransaction\u003e();\n        return new(connection, transaction);\n    });\n```\n\u003csup\u003e\u003ca href='/src/DeltaTests/Usage.cs#L346-L358' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-CustomDiscoveryConnectionAndTransaction' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\n\n## EF Usage\n\n\n### SqlServer DbContext using RowVersion\n\nEnable row versioning in Entity Framework\n\n\u003c!-- snippet: SampleSqlServerDbContext --\u003e\n\u003ca id='snippet-SampleSqlServerDbContext'\u003e\u003c/a\u003e\n```cs\npublic class SampleDbContext(DbContextOptions options) :\n    DbContext(options)\n{\n    public DbSet\u003cEmployee\u003e Employees { get; set; } = null!;\n    public DbSet\u003cCompany\u003e Companies { get; set; } = null!;\n\n    protected override void OnModelCreating(ModelBuilder builder)\n    {\n        var company = builder.Entity\u003cCompany\u003e();\n        company.HasKey(_ =\u003e _.Id);\n        company\n            .HasMany(_ =\u003e _.Employees)\n            .WithOne(_ =\u003e _.Company)\n            .IsRequired();\n        company\n            .Property(_ =\u003e _.RowVersion)\n            .IsRowVersion()\n            .HasConversion\u003cbyte[]\u003e();\n\n        var employee = builder.Entity\u003cEmployee\u003e();\n        employee.HasKey(_ =\u003e _.Id);\n        employee\n            .Property(_ =\u003e _.RowVersion)\n            .IsRowVersion()\n            .HasConversion\u003cbyte[]\u003e();\n    }\n}\n```\n\u003csup\u003e\u003ca href='/src/WebApplicationSqlServerEF/DataContext/SampleDbContext.cs#L1-L31' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-SampleSqlServerDbContext' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\n\n### Postgres DbContext\n\nEnable row versioning in Entity Framework\n\n\u003c!-- snippet: SamplePostgresDbContext --\u003e\n\u003ca id='snippet-SamplePostgresDbContext'\u003e\u003c/a\u003e\n```cs\npublic class SampleDbContext(DbContextOptions options) :\n    DbContext(options)\n{\n    public DbSet\u003cEmployee\u003e Employees { get; set; } = null!;\n    public DbSet\u003cCompany\u003e Companies { get; set; } = null!;\n    protected override void OnModelCreating(ModelBuilder builder)\n    {\n        var company = builder.Entity\u003cCompany\u003e();\n        company.HasKey(_ =\u003e _.Id);\n        company\n            .HasMany(_ =\u003e _.Employees)\n            .WithOne(_ =\u003e _.Company)\n            .IsRequired();\n\n        var employee = builder.Entity\u003cEmployee\u003e();\n        employee.HasKey(_ =\u003e _.Id);\n    }\n}\n```\n\u003csup\u003e\u003ca href='/src/WebApplicationPostgresEF/DataContext/SampleDbContext.cs#L1-L22' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-SamplePostgresDbContext' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\n\n### Add to WebApplicationBuilder\n\n\n#### SQL Server\n\n\u003c!-- snippet: UseDeltaSQLServerEF --\u003e\n\u003ca id='snippet-UseDeltaSQLServerEF'\u003e\u003c/a\u003e\n```cs\nvar builder = WebApplication.CreateBuilder();\nbuilder.Services.AddSqlServer\u003cSampleDbContext\u003e(connectionString);\n\nvar app = builder.Build();\napp.UseDelta\u003cSampleDbContext\u003e();\n```\n\u003csup\u003e\u003ca href='/src/WebApplicationSqlServerEF/Program.cs#L7-L15' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-UseDeltaSQLServerEF' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\n\n#### Postgres\n\n\u003c!-- snippet: UseDeltaPostgresEF --\u003e\n\u003ca id='snippet-UseDeltaPostgresEF'\u003e\u003c/a\u003e\n```cs\nvar builder = WebApplication.CreateBuilder();\nbuilder.Services.AddDbContext\u003cSampleDbContext\u003e(\n    _ =\u003e _.UseNpgsql(connectionString));\nvar app = builder.Build();\napp.UseDelta\u003cSampleDbContext\u003e();\n```\n\u003csup\u003e\u003ca href='/src/WebApplicationPostgresEF/Program.cs#L3-L11' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-UseDeltaPostgresEF' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\n\n### Add to a Route Group\n\nTo add to a specific [Route Group](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/route-handlers#route-groups):\n\n\u003c!-- snippet: UseDeltaMapGroupEF --\u003e\n\u003ca id='snippet-UseDeltaMapGroupEF'\u003e\u003c/a\u003e\n```cs\napp.MapGroup(\"/group\")\n    .UseDelta\u003cSampleDbContext\u003e()\n    .MapGet(\"/\", () =\u003e \"Hello Group!\");\n```\n\u003csup\u003e\u003ca href='/src/WebApplicationPostgresEF/Program.cs#L44-L50' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-UseDeltaMapGroupEF' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003ca id='snippet-UseDeltaMapGroupEF-1'\u003e\u003c/a\u003e\n```cs\napp.MapGroup(\"/group\")\n    .UseDelta\u003cSampleDbContext\u003e()\n    .MapGet(\"/\", () =\u003e \"Hello Group!\");\n```\n\u003csup\u003e\u003ca href='/src/WebApplicationSqlServerEF/Program.cs#L44-L50' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-UseDeltaMapGroupEF-1' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\n\n### ShouldExecute\n\nOptionally control what requests Delta is executed on.\n\n\u003c!-- snippet: ShouldExecuteEF --\u003e\n\u003ca id='snippet-ShouldExecuteEF'\u003e\u003c/a\u003e\n```cs\nvar app = builder.Build();\napp.UseDelta\u003cSampleDbContext\u003e(\n    shouldExecute: httpContext =\u003e\n    {\n        var path = httpContext.Request.Path.ToString();\n        return path.Contains(\"match\");\n    });\n```\n\u003csup\u003e\u003ca href='/src/Delta.EFTests/Usage.cs#L16-L26' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-ShouldExecuteEF' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\n\n## UseResponseDiagnostics\n\nResponse diagnostics is an opt-out feature that includes extra log information in the response headers.\n\nDisable by setting UseResponseDiagnostics to false at startup:\n\n\u003c!-- snippet: UseResponseDiagnostics --\u003e\n\u003ca id='snippet-UseResponseDiagnostics'\u003e\u003c/a\u003e\n```cs\nDeltaExtensions.UseResponseDiagnostics = false;\n```\n\u003csup\u003e\u003ca href='/src/DeltaTests/UseResponseDiagnosticsSnippet.cs#L4-L6' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-UseResponseDiagnostics' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\nResponse diagnostics headers are prefixed with `Delta-`.\n\nExample Response header when the Request has not `If-None-Match` header.\n\n\u003cimg src=\"/src/Delta-No304.png\"\u003e\n\n\n## Delta.SqlServer\n\nA set of helper methods for working with [SQL Server Change Tracking](https://learn.microsoft.com/en-us/sql/relational-databases/track-changes/track-data-changes-sql-server) and [SQL Server Row Versioning](https://learn.microsoft.com/en-us/sql/t-sql/data-types/rowversion-transact-sql)\n\nNuget: [Delta.SqlServer](https://www.nuget.org/packages/Delta.SqlServer)\n\n\n### GetLastTimeStamp\n\n\n#### For a `SqlConnection`:\n\n\u003c!-- snippet: GetLastTimeStampSqlConnection --\u003e\n\u003ca id='snippet-GetLastTimeStampSqlConnection'\u003e\u003c/a\u003e\n```cs\nvar timeStamp = await sqlConnection.GetLastTimeStamp();\n```\n\u003csup\u003e\u003ca href='/src/DeltaTests/Usage.cs#L188-L192' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-GetLastTimeStampSqlConnection' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\n\n#### For a `DbContext`:\n\n\u003c!-- snippet: GetLastTimeStampEF --\u003e\n\u003ca id='snippet-GetLastTimeStampEF'\u003e\u003c/a\u003e\n```cs\nvar timeStamp = await dbContext.GetLastTimeStamp();\n```\n\u003csup\u003e\u003ca href='/src/Delta.EFTests/Usage.cs#L41-L45' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-GetLastTimeStampEF' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\n\n### GetDatabasesWithTracking\n\nGet a list of all databases with change tracking enabled.\n\n\u003c!-- snippet: GetDatabasesWithTracking --\u003e\n\u003ca id='snippet-GetDatabasesWithTracking'\u003e\u003c/a\u003e\n```cs\nvar trackedDatabases = await sqlConnection.GetTrackedDatabases();\nforeach (var db in trackedDatabases)\n{\n    Trace.WriteLine(db);\n}\n```\n\u003csup\u003e\u003ca href='/src/DeltaTests/Usage.cs#L224-L232' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-GetDatabasesWithTracking' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\nUses the following SQL:\n\n\u003c!-- snippet: GetTrackedDatabasesSql --\u003e\n\u003ca id='snippet-GetTrackedDatabasesSql'\u003e\u003c/a\u003e\n```cs\nselect d.name\nfrom sys.databases as d inner join\n  sys.change_tracking_databases as t on\n  t.database_id = d.database_id\n```\n\u003csup\u003e\u003ca href='/src/Delta.SqlServer/DeltaExtensions_Sql.cs#L151-L156' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-GetTrackedDatabasesSql' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\n\n### GetTrackedTables\n\nGet a list of all tracked tables in database.\n\n\u003c!-- snippet: GetTrackedTables --\u003e\n\u003ca id='snippet-GetTrackedTables'\u003e\u003c/a\u003e\n```cs\nvar trackedTables = await sqlConnection.GetTrackedTables();\nforeach (var db in trackedTables)\n{\n    Trace.WriteLine(db);\n}\n```\n\u003csup\u003e\u003ca href='/src/DeltaTests/Usage.cs#L250-L258' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-GetTrackedTables' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\nUses the following SQL:\n\n\u003c!-- snippet: GetTrackedTablesSql --\u003e\n\u003ca id='snippet-GetTrackedTablesSql'\u003e\u003c/a\u003e\n```cs\nselect t.Name\nfrom sys.tables as t inner join\n  sys.change_tracking_tables as c on t.[object_id] = c.[object_id]\n```\n\u003csup\u003e\u003ca href='/src/Delta.SqlServer/DeltaExtensions_Sql.cs#L81-L85' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-GetTrackedTablesSql' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\n\n### IsTrackingEnabled\n\nDetermine if change tracking is enabled for a database.\n\n\u003c!-- snippet: IsTrackingEnabled --\u003e\n\u003ca id='snippet-IsTrackingEnabled'\u003e\u003c/a\u003e\n```cs\nvar isTrackingEnabled = await sqlConnection.IsTrackingEnabled();\n```\n\u003csup\u003e\u003ca href='/src/DeltaTests/Usage.cs#L324-L328' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-IsTrackingEnabled' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\nUses the following SQL:\n\n\u003c!-- snippet: IsTrackingEnabledSql --\u003e\n\u003ca id='snippet-IsTrackingEnabledSql'\u003e\u003c/a\u003e\n```cs\nselect count(d.name)\nfrom sys.databases as d inner join\n  sys.change_tracking_databases as t on\n  t.database_id = d.database_id\nwhere d.name = '{database}'\n```\n\u003csup\u003e\u003ca href='/src/Delta.SqlServer/DeltaExtensions_Sql.cs#L103-L109' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-IsTrackingEnabledSql' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\n\n### EnableTracking\n\nEnable change tracking for a database.\n\n\u003c!-- snippet: EnableTracking --\u003e\n\u003ca id='snippet-EnableTracking'\u003e\u003c/a\u003e\n```cs\nawait sqlConnection.EnableTracking();\n```\n\u003csup\u003e\u003ca href='/src/DeltaTests/Usage.cs#L318-L322' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-EnableTracking' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\nUses the following SQL:\n\n\u003c!-- snippet: EnableTrackingSql --\u003e\n\u003ca id='snippet-EnableTrackingSql'\u003e\u003c/a\u003e\n```cs\nalter database {database}\nset change_tracking = on\n(\n  change_retention = {retentionDays} days,\n  auto_cleanup = on\n)\n```\n\u003csup\u003e\u003ca href='/src/Delta.SqlServer/DeltaExtensions_Sql.cs#L64-L71' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-EnableTrackingSql' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\n\n### DisableTracking\n\nDisable change tracking for a database and all tables within that database.\n\n\u003c!-- snippet: DisableTracking --\u003e\n\u003ca id='snippet-DisableTracking'\u003e\u003c/a\u003e\n```cs\nawait sqlConnection.DisableTracking();\n```\n\u003csup\u003e\u003ca href='/src/DeltaTests/Usage.cs#L303-L307' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-DisableTracking' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\nUses the following SQL:\n\n\n#### For disabling tracking on a database:\n\n\u003c!-- snippet: DisableTrackingSqlDB --\u003e\n\u003ca id='snippet-DisableTrackingSqlDB'\u003e\u003c/a\u003e\n```cs\nalter database [{database}] set change_tracking = off;\n```\n\u003csup\u003e\u003ca href='/src/Delta.SqlServer/DeltaExtensions_Sql.cs#L135-L137' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-DisableTrackingSqlDB' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\n\n#### For disabling tracking on tables:\n\n\u003c!-- snippet: DisableTrackingSqlTable --\u003e\n\u003ca id='snippet-DisableTrackingSqlTable'\u003e\u003c/a\u003e\n```cs\nalter table [{table}] disable change_tracking;\n```\n\u003csup\u003e\u003ca href='/src/Delta.SqlServer/DeltaExtensions_Sql.cs#L126-L128' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-DisableTrackingSqlTable' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\n\n### SetTrackedTables\n\nEnables change tracking for all tables listed, and disables change tracking for all tables not listed.\n\n\u003c!-- snippet: SetTrackedTables --\u003e\n\u003ca id='snippet-SetTrackedTables'\u003e\u003c/a\u003e\n```cs\nawait sqlConnection.SetTrackedTables([\"Companies\"]);\n```\n\u003csup\u003e\u003ca href='/src/DeltaTests/Usage.cs#L244-L248' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-SetTrackedTables' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\nUses the following SQL:\n\n\n#### For enabling tracking on a database:\n\n\u003c!-- snippet: EnableTrackingSql --\u003e\n\u003ca id='snippet-EnableTrackingSql'\u003e\u003c/a\u003e\n```cs\nalter database {database}\nset change_tracking = on\n(\n  change_retention = {retentionDays} days,\n  auto_cleanup = on\n)\n```\n\u003csup\u003e\u003ca href='/src/Delta.SqlServer/DeltaExtensions_Sql.cs#L64-L71' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-EnableTrackingSql' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\n\n#### For enabling tracking on tables:\n\n\u003c!-- snippet: EnableTrackingTableSql --\u003e\n\u003ca id='snippet-EnableTrackingTableSql'\u003e\u003c/a\u003e\n```cs\nalter table [{table}] enable change_tracking\n```\n\u003csup\u003e\u003ca href='/src/Delta.SqlServer/DeltaExtensions_Sql.cs#L23-L25' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-EnableTrackingTableSql' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\n\n#### For disabling tracking on tables:\n\n\u003c!-- snippet: DisableTrackingTableSql --\u003e\n\u003ca id='snippet-DisableTrackingTableSql'\u003e\u003c/a\u003e\n```cs\nalter table [{table}] disable change_tracking;\n```\n\u003csup\u003e\u003ca href='/src/Delta.SqlServer/DeltaExtensions_Sql.cs#L34-L36' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-DisableTrackingTableSql' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\n\n## Verifying behavior\n\nThe behavior of Delta can be verified as follows:\n\n * Open a page in the site\n * Open the browser developer tools\n * Change to the Network tab\n * Refresh the page.\n\nCached responses will show as 304 in the `Status`:\n\n\u003cimg src=\"/src/network.png\"\u003e\n\nIn the headers `if-none-match` will show in the request and `etag` will show in the response:\n\n\u003cimg src=\"/src/network-details.png\"\u003e\n\n\n### Ensure cache is not disabled\n\nIf disable cache is checked, the browser will not send the `if-none-match` header. This will effectively cause a cache miss server side, and the full server pipeline will execute.\n\n\u003cimg src=\"/src/disable-cache.png\"\u003e\n\n\n### Certificates and Chromium\n\nChromium, and hence the Chrome and Edge browsers, are very sensitive to certificate problems when determining if an item should be cached. Specifically, if a request is done dynamically (type: xhr) and the server is using a self-signed certificate, then the browser will not send the `if-none-match` header. [Reference]( https://issues.chromium.org/issues/40666473). If self-signed certificates are required during development in lower environment, then use FireFox to test the caching behavior. \n\n\n## Programmatic client usage\n\nDelta is primarily designed to support web browsers as a client. All web browsers have the necessary 304 and caching functionally required.\n\nIn the scenario where web apis (that support using 304) are being consumed using .net as a client, consider using one of the below extensions to cache responses.\n\n * [Replicant](https://github.com/SimonCropp/Replicant)\n * [Tavis.HttpCache](https://github.com/tavis-software/Tavis.HttpCache)\n * [CacheCow](https://github.com/aliostad/CacheCow)\n * [Monkey Cache](https://github.com/jamesmontemagno/monkey-cache)\n\n\n## Icon\n\n[Estuary](https://thenounproject.com/term/estuary/1847616/) designed by [Daan](https://thenounproject.com/Asphaleia/) from [The Noun Project](https://thenounproject.com).\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsimoncropp%2Fdelta","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsimoncropp%2Fdelta","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsimoncropp%2Fdelta/lists"}