{"id":22065627,"url":"https://github.com/karenpayneoregon/efcore-shadow-properties","last_synced_at":"2026-05-05T12:34:08.049Z","repository":{"id":110840526,"uuid":"244486749","full_name":"karenpayneoregon/efcore-shadow-properties","owner":"karenpayneoregon","description":"Example for Entity Framework Core shadow properties","archived":false,"fork":false,"pushed_at":"2023-09-04T21:27:38.000Z","size":3844,"stargazers_count":1,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-03-23T18:16:55.878Z","etag":null,"topics":["csharp","entity-framework-core","shadow-properties"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/karenpayneoregon.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"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}},"created_at":"2020-03-02T22:13:13.000Z","updated_at":"2024-06-11T18:36:46.000Z","dependencies_parsed_at":null,"dependency_job_id":"5a166deb-0208-46d5-9a60-09f45a4c2bb2","html_url":"https://github.com/karenpayneoregon/efcore-shadow-properties","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/karenpayneoregon/efcore-shadow-properties","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/karenpayneoregon%2Fefcore-shadow-properties","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/karenpayneoregon%2Fefcore-shadow-properties/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/karenpayneoregon%2Fefcore-shadow-properties/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/karenpayneoregon%2Fefcore-shadow-properties/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/karenpayneoregon","download_url":"https://codeload.github.com/karenpayneoregon/efcore-shadow-properties/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/karenpayneoregon%2Fefcore-shadow-properties/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32649600,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-05T11:29:49.557Z","status":"ssl_error","status_checked_at":"2026-05-05T11:29:48.587Z","response_time":54,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":["csharp","entity-framework-core","shadow-properties"],"created_at":"2024-11-30T19:20:57.968Z","updated_at":"2026-05-05T12:34:08.028Z","avatar_url":"https://github.com/karenpayneoregon.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# EF Core Global Query Filters\n\n\n## Learn how to \n\n- Create [global query filters](https://learn.microsoft.com/en-us/ef/core/querying/filters#disabling-filters) which are LINQ query predicates applied to Entity Types, in this article, your models. A typical example, using soft delete, only show those records which are active which will be gone over in code.\n- Override global query filters [see also](https://learn.microsoft.com/en-us/ef/core/querying/filters#disabling-filters)\n- Override a DbContext SaveChangesAsync and SaveChanges to handle soft deletes.\n- Create an Excel file using [SpreadSheetLight](https://spreadsheetlight.com/) with [NuGet package](https://www.nuget.org/packages/SpreadsheetLight/3.5.0?_src=template).\n    - I included SpreadSheetLight chm file outside the solution under folder SpreadSheetLightHelp as some may find if hard to get from SpreadSheetLight site.\n- Using [FastMember.NetCore](https://www.nuget.org/packages/FastMember.NetCore/1.1.0?_src=template) to create a DataTable from a generic List for use with creating an Excel file.\n\n\n\n\n\n\n## Limitations\n\nFilters can only be defined for the root Entity Type of an inheritance hierarchy.\n\n## Core Projects\n\n- ShadowProperties, class project for data operations\n- HasQueryFilterRazorApp, Razor Pages project which\n    - Presents data\n    - Provides an interface to soft delete records\n    - Provides an interface to un-delete soft deletes.\n\n## Windows forms projects\n\nThere are two projects, Backend and DemoShadowProperties. These are from the following Microsoft TechNet article [Entity Framework Core shadow properties (C#)](https://social.technet.microsoft.com/wiki/contents/articles/53662.entity-framework-core-shadow-properties-c.aspx) which were done with .NET Framework 4.7 and have been updated to 4.8.\n\nThe Core projects were based off these projects which shows when writing decent code a we have a good start to port to ASP.NET Core and Razor Pages.\n\n## Database\n\nSchema for the table used to demonstrate using global query filters.\n\n![contact table schema](assets/contactTable.png)\n\n### Columns\n\n- **isDeleted** soft delete flag\n- **CreatedBy** user which added the record\n- **CreatedAt** when the record was added date time\n- **LastUser** user to last modify a record\n- **LastUpdated** last updated date time\n\n### SQL to examine data in SSMS\n\n```sql\nSELECT ContactId,\n       FirstName,\n       LastName,\n       LastUser,\n       CreatedBy,\n\t   FORMAT(CreatedAt, 'MM/dd/yyyy') AS CreatedAt,\n\t   FORMAT(LastUpdated, 'MM/dd/yyyy') AS LastUpdated,\n       IIF(isDeleted = 'TRUE' , 'Y','N') AS Deleted\nFROM dbo.Contact1;\n```\n\n## Setup a global query filter\n\nIn the DbContext OnModelCreating method, after the entities have been configured add.\n\n```csharp\nmodelBuilder.Entity\u003cContact\u003e()\n    .HasQueryFilter(contact =\u003e\n        EF.Property\u003cbool\u003e(contact, \"isDeleted\") \n        == false);\n```\n\nThat's it. In the Index page to get contacts.\n\n### Read filtered records\n\n```csharp\npublic async Task OnGetAsync()\n{\n\n    if (_context.Contacts != null)\n    {\n        Contacts = await _context.Contacts.ToListAsync();\n    }\n}\n```\n\nGenerated SQL, note the WHERE clause.\n\n```csharp\nSELECT [c].[ContactId], [c].[CreatedAt], [c].[CreatedBy], [c].[FirstName], [c].[LastName], [c].[LastUpdated], [c].[LastUser], [c].[isDeleted]\nFROM [Contact1] AS [c]\nWHERE [c].[isDeleted] = CAST(0 AS bit)\n```\n\n:stop_sign: did you notice in the database the table name is Contact1 but in the above code we are using Contact model. This is done via [Table name annotations](https://learn.microsoft.com/en-us/ef/core/modeling/entity-types?tabs=data-annotations#table-name).\n\n```csharp\n[Table(\"Contact1\")]\npublic partial class Contact : INotifyPropertyChanged\n```\n\n### Ignore filters\n\n[IgnoreQueryFilters extension](https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.entityframeworkqueryableextensions.ignorequeryfilters?view=efcore-7.0) Specifies that the current Entity Framework LINQ query should not have any model-level entity query filters applied.\n\n```csharp\npublic async Task OnGetAsync()\n{\n    if (_context.Contacts != null)\n    {\n        Contacts = await _context\n            .Contacts\n            .IgnoreQueryFilters() // IMPORTANT\n            .ToListAsync();\n    }\n}\n```\n\n## Handling soft deletes\n\nWorking in DeleteContactPage.\n\n- An id is passed from the index page\n- Find and validate the contact exists still\n- Begin tracking the contact marked as deleted.\n\n```csharp\npublic async Task\u003cIActionResult\u003e OnPostAsync(int? id)\n{\n    if (id == null || _context.Contacts == null)\n    {\n        return NotFound();\n    }\n    var contact = await _context.Contacts.FindAsync(id);\n\n    if (contact != null)\n    {\n        Contact = contact;\n        _context.Contacts.Remove(Contact);\n        await _context.SaveChangesAsync();\n    }\n\n    return RedirectToPage(\"./Index\");\n}\n```\n\nBack in the DbContext we override SaveChangesAsync.\n\n```csharp\npublic override Task\u003cint\u003e SaveChangesAsync(CancellationToken cancellationToken = new ())\n{\n    HandleChanges();\n    return base.SaveChangesAsync(cancellationToken);\n}\n```\n\n**HandleChanges method**\n\n- Traverse entities, if the state is `EntityState.Deleted`, set its state to Modfied followed by setting `isDeleted` to `true`.\n\n```csharp\nprivate void HandleChanges()\n{\n    foreach (var entry in ChangeTracker.Entries())\n    {\n        // take care of date time created and updated\n        if (entry.State is EntityState.Added or EntityState.Modified)\n        {\n            entry.Property(\"LastUpdated\").CurrentValue = DateTime.Now;\n            entry.Property(\"LastUser\").CurrentValue = Environment.UserName;\n\n            if (entry.Entity is Contact \u0026\u0026 entry.State == EntityState.Added)\n            {\n                entry.Property(\"CreatedAt\").CurrentValue = DateTime.Now;\n                entry.Property(\"CreatedBy\").CurrentValue = Environment.UserName;\n            }\n        }\n        else if (entry.State == EntityState.Deleted)\n        {\n            // Change state to modified and set delete flag\n            entry.State = EntityState.Modified;\n            entry.Property(\"isDeleted\").CurrentValue = true;\n        }\n    }\n}\n```\n\n\n\n\n## WCAG Accessibility\n\nAll pages conform to WCAG AA standard.\n\nNote that the checkbox on the admin page needed aria-label attribute for screen readers to properly identify the checkbox purpose.\n\n## Admin page\n\nThis page allows the user to perform deletions and un delete operations which are respected by the global in place.\n\n\n\n\n## Soure code\n\nClone the following [GitHub repository](https://github.com/karenpayneoregon/efcore-shadow-properties)\n\n## Next step\n\nIs to work with an Interceptor and [interface](https://learn.microsoft.com/en-us/ef/core/logging-events-diagnostics/interceptors).\n\nFor this see [How to Implement a Soft Delete Strategy with Entity Framework Core](https://blog.jetbrains.com/dotnet/2023/06/14/how-to-implement-a-soft-delete-strategy-with-entity-framework-core/?utm_campaign=rider\u0026utm_content=nonprod\u0026utm_medium=referral\u0026utm_source=twitter). Going this way is a personal choice and in this article moving in this direction will be easy as this code and mine are compatible.\n\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkarenpayneoregon%2Fefcore-shadow-properties","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkarenpayneoregon%2Fefcore-shadow-properties","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkarenpayneoregon%2Fefcore-shadow-properties/lists"}