{"id":1391,"date":"2024-10-26T00:00:00","date_gmt":"2024-10-25T22:00:00","guid":{"rendered":"https:\/\/demo.sidekick.be\/itenium\/db-audit-ef\/"},"modified":"2025-11-28T13:31:42","modified_gmt":"2025-11-28T12:31:42","slug":"db-audit-ef","status":"publish","type":"post","link":"https:\/\/demo.sidekick.be\/itenium\/blog\/dotnet\/db-audit-ef\/","title":{"rendered":"Database Auditing with EntityFramework and SQL Server"},"content":{"rendered":"<h1 id=\"option-1-created--modified-byon-triggers\">Option 1: Created &amp; Modified By\/On Triggers<\/h1>\n<p>Each table has 4 extra columns to keep track of by who\/when the record was created and by who\/when the record was last updated. Obviously little EF code is needed for this solution \ud83d\ude00<\/p>\n<pre><code class=\"language-sql\">CREATE TRIGGER TRG_Products_UPDATE\nON Products AFTER UPDATE\nAS\nBEGIN\n  SET NOCOUNT ON;\n\n  UPDATE Products\n  SET ModifiedBy = SYSTEM_USER, ModifiedOn = GETDATE()\n  WHERE Id IN (SELECT Id FROM inserted);\nEND;\n<\/code><\/pre>\n<p>The CreatedBy\/On columns can be set with a default value for which some EF configuration is needed:<\/p>\n<pre><code class=\"language-c#\">protected override void OnModelCreating(ModelBuilder modelBuilder)\n{\n  \/\/ Required since EF Core 7:\n  \/\/ Without this saving changes will fail\n  modelBuilder.Entity&lt;Product&gt;()\n    .ToTable(tb =&gt; tb.UseSqlOutputClause(false));\n\n  \/\/ Set default values for CreatedOn\/By\n  \/\/ Also ignore property changes made in code:\n  modelBuilder.Entity&lt;Product&gt;().Property(e =&gt; e.CreatedOn)\n    .HasDefaultValueSql(\"GETDATE()\")\n    .ValueGeneratedOnAdd()\n    .Metadata.SetBeforeSaveBehavior(PropertySaveBehavior.Ignore);\n\n  modelBuilder.Entity&lt;Product&gt;().Property(e =&gt; e.CreatedBy)\n    .HasDefaultValueSql(\"SYSTEM_USER\")\n    .ValueGeneratedOnAdd()\n    .Metadata.SetBeforeSaveBehavior(PropertySaveBehavior.Ignore);\n\n  \/\/ The ModifiedOn\/By are set by the trigger but still\n  \/\/ require some EF configuration:\n  modelBuilder.Entity&lt;Product&gt;().Property(e =&gt; e.ModifiedOn)\n    .ValueGeneratedOnAddOrUpdate()\n    .Metadata.SetBeforeSaveBehavior(PropertySaveBehavior.Ignore);\n\n  \/\/ We're not using ValueGeneratedOnUpdate() because that\n  \/\/ would allow a value to be set in code during insertion\n  modelBuilder.Entity&lt;Product&gt;().Property(e =&gt; e.ModifiedBy)\n    .ValueGeneratedOnAddOrUpdate()\n    .Metadata.SetBeforeSaveBehavior(PropertySaveBehavior.Ignore);\n}\n<\/code><\/pre>\n<h2 id=\"usesqloutputclausefalse\">UseSqlOutputClause(false)<\/h2>\n<p>Required for all tables that have triggers <a href=\"https:\/\/learn.microsoft.com\/en-us\/ef\/core\/what-is-new\/ef-core-7.0\/breaking-changes?tabs=v7#sqlserver-tables-with-triggers\">since EF Core 7<\/a>:<\/p>\n<pre><code class=\"language-c#\">modelBuilder.Entity&lt;Product&gt;().ToTable(tb =&gt; tb.UseSqlOutputClause(false));\n<\/code><\/pre>\n<p>This reverts the behavior of EF back to that of older versions of EF, a less performant way of capturing the generated Ids during insertion.<\/p>\n<h2 id=\"pros-vs-cons\">Pros vs Cons<\/h2>\n<p>\u2705 All changes to the table are captured, not just those done by the application<br \/> \u26a0\ufe0f Changes made by the application are typically all done with the same database user, not the actual user<br \/> \u26a0\ufe0f Need to revert to a less performant way of inserting records, which could be a problem for bulk inserts<br \/> \u26a0\ufe0f This approach just keeps track of the last modification and doesn\u2019t tell you <strong>what<\/strong> has changed<\/p>\n<h1 id=\"option-2-created--modified-byon-by-ef\">Option 2: Created &amp; Modified By\/On by EF<\/h1>\n<p>Doing exactly the same thing but this time we let EF do the heavy lifting.<\/p>\n<h2 id=\"abstraction-userprovider\">Abstraction: UserProvider<\/h2>\n<p>EF somehow needs to know who the current user is, so let\u2019s create a little abstraction and inject the correct implementation depending on the runtime context.<\/p>\n<pre><code class=\"language-c#\">public interface IUserProvider\n{\n  string UserName { get; }\n}\n\n\/\/\/ &lt;summary&gt;A simple ASP.NET implementation&lt;\/summary&gt;\npublic class HttpUserProvider(IHttpContextAccessor accessor) : IUserProvider\n{\n  public string UserName =&gt; accessor.HttpContext?.User?.Identity?.Name ?? \"???\";\n}\n<\/code><\/pre>\n<p>Our <code>DbContext<\/code> now needs this <code>IUserProvider<\/code>, something the <code>dotnet ef<\/code> CLI doesn\u2019t like.<\/p>\n<pre><code class=\"language-c#\">public class MyDbContextFactory : IDesignTimeDbContextFactory&lt;MyDbContext&gt;\n{\n  public MyDbContext CreateDbContext(string[] args)\n  {\n    return new MyDbContext(new FakeUserProvider());\n  }\n}\n<\/code><\/pre>\n<h2 id=\"abstraction-audit-owned-entity\">Abstraction: Audit Owned Entity<\/h2>\n<p>By having all our entities implement <code>IAudit<\/code> we can configure the <code>Audit<\/code><a href=\"https:\/\/martinfowler.com\/bliki\/ValueObject.html\">ValueObject<\/a> for all our entities as an <a href=\"https:\/\/learn.microsoft.com\/en-us\/ef\/core\/modeling\/owned-entities\">Owned entity<\/a>. This way we don\u2019t have 4 properties in all our entities cluttering our intellisense.<\/p>\n<pre><code class=\"language-c#\">public interface IAudit\n{\n  Audit Audit { get; }\n}\n\npublic class Audit\n{\n  public DateTime CreatedOn { get; set; }\n  [StringLength(50)]\n  public string CreatedBy { get; set; } = \"\";\n  public DateTime? ModifiedOn { get; set; }\n  [StringLength(50)]\n  public string? ModifiedBy { get; set; }\n}\n<\/code><\/pre>\n<h2 id=\"the-dbcontext-implementation\">The DbContext Implementation<\/h2>\n<pre><code class=\"language-c#\">public class MyDbContext(IUserProvider userProvider) : DbContext\n{\n  protected override void OnModelCreating(ModelBuilder modelBuilder)\n  {\n    var entities = modelBuilder.Model.GetEntityTypes()\n      .Where(entityType =&gt; typeof(IAudit).IsAssignableFrom(entityType.ClrType))\n      .ToArray();\n\n    foreach (var entityType in entities)\n    {\n      modelBuilder.Entity(entityType.ClrType)\n        .OwnsOne(typeof(Audit), nameof(IAudit.Audit), audit =&gt;\n        {\n            audit.Property(nameof(Audit.CreatedOn)).HasColumnName(nameof(Audit.CreatedOn));\n            audit.Property(nameof(Audit.CreatedBy)).HasColumnName(nameof(Audit.CreatedBy));\n            audit.Property(nameof(Audit.ModifiedOn)).HasColumnName(nameof(Audit.ModifiedOn));\n            audit.Property(nameof(Audit.ModifiedBy)).HasColumnName(nameof(Audit.ModifiedBy));\n        });\n    }\n  }\n\n  public override int SaveChanges()\n  {\n    \/\/ You may want to override SaveChangesAsync too!\n    SetAuditFields();\n    return base.SaveChanges();\n  }\n\n  private void SetAuditFields()\n  {\n    var entries = ChangeTracker\n      .Entries&lt;IAudit&gt;()\n      .Where(e =&gt; e.State is EntityState.Added or EntityState.Modified);\n\n    foreach (var entityEntry in entries)\n    {\n      Audit audit = entityEntry.Entity.Audit;\n      if (entityEntry.State == EntityState.Added)\n      {\n        audit.CreatedOn = DateTime.Now;\n        audit.CreatedBy = userProvider.UserName;\n        \/\/ Make sure a dev hasn't set these\n        \/\/ But really... You know, just don't set them...\n        audit.ModifiedOn = null;\n        audit.ModifiedBy = null;\n      }\n      else\n      {\n        audit.ModifiedOn = DateTime.Now;\n        audit.ModifiedBy = userProvider.UserName;\n\n        \/\/ Here we also revert any changes made in code to the CreatedBy\/On properties\n        \/\/ Again, this should go without saying and is probably just overhead...\n        var originalAudit = entityEntry.Reference(e =&gt; e.Audit).TargetEntry!;\n        audit.CreatedBy = originalAudit.OriginalValues.GetValue&lt;string&gt;(nameof(Audit.CreatedBy));\n        audit.CreatedOn = originalAudit.OriginalValues.GetValue&lt;DateTime&gt;(nameof(Audit.CreatedOn));\n      }\n    }\n  }\n}\n<\/code><\/pre>\n<p>If you really don\u2019t trust your team or something, the github repository also contains code where the <code>Audit<\/code> only has getters and everything is set using reflection.<\/p>\n<h2 id=\"pros-vs-cons-1\">Pros vs Cons<\/h2>\n<p>\u2705 Captures the actual user that made the request<br \/> \u26a0\ufe0f Changes done directly in the database are not tracked<br \/> \u26a0\ufe0f This approach just keeps track of the last modification and doesn\u2019t tell you <strong>what<\/strong> has changed<\/p>\n<h1 id=\"option-3-separate-audit-table-with-trigger\">Option 3: Separate Audit Table with Trigger<\/h1>\n<p>We create a separate table and track all changes to all our tables with a trigger.<\/p>\n<pre><code class=\"language-sql\">CREATE TRIGGER [dbo].[TRG_Products_ALL] ON [dbo].[Products]\nAFTER INSERT, UPDATE, DELETE\nAS\nBEGIN\nSET NOCOUNT ON;\n\n-- This trigger also updates the Products table ModifiedOn\/By\n-- But we don't want to create audit records for that!\nIF TRIGGER_NESTLEVEL(OBJECT_ID('dbo.TRG_Products_ALL')) &gt; 1\n  RETURN;\n\nIF NOT EXISTS(SELECT 1 FROM deleted) AND NOT EXISTS(SELECT 1 FROM inserted)\n  RETURN;\n\nDECLARE @ActionType CHAR(1)\nIF EXISTS (SELECT * FROM inserted)\n  IF EXISTS (SELECT * FROM deleted)\n    SELECT @ActionType = 'U'\n  ELSE\n    SELECT @ActionType = 'I'\nELSE\n  SELECT @ActionType = 'D'\n\nDECLARE @inserted XML, @deleted XML\nSET @inserted = (SELECT * FROM inserted FOR XML PATH)\nSET @deleted = (SELECT * FROM deleted FOR XML PATH)\n\nDECLARE @tableIds VARCHAR(MAX);\nSET @tableIds = (SELECT STRING_AGG(CAST(Id AS VARCHAR(MAX)), ',') FROM (SELECT DISTINCT Id FROM (SELECT Id FROM inserted UNION SELECT Id from deleted) ids) distinctIds)\n\nINSERT INTO [Audit] (TableName, TableIds, ActionType, OldValues, NewValues)\nSELECT 'Products', @tableIds, @ActionType, @deleted, @inserted;\n\nIF @ActionType = 'U'\n  UPDATE dbo.[Products]\n  SET ModifiedOn=CURRENT_TIMESTAMP, ModifiedBy=SUSER_SNAME()\n  WHERE [Id] IN (SELECT [Id] FROM inserted);\n\nEND;\n<\/code><\/pre>\n<p>This inserts XML with the changes in Audit.OldValues &amp; NewValues. If you want to use this approach for many tables, you probably want to abstract this in a function or something \ud83d\ude09<\/p>\n<h2 id=\"pros-vs-cons-2\">Pros vs Cons<\/h2>\n<p>\u2705 All changes to the table are captured, not just those done by the application<br \/> \u2705 This approach keeps track of all modifications<br \/> \u26a0\ufe0f Changes made by the application are typically all done with the same database user, not the actual user<br \/> \u26a0\ufe0f Need to revert to a less performant way of inserting records, which could be a problem for bulk inserts<br \/> \u26a0\ufe0f Huge update statements could overflow some of the Audit columns. This could be fixed by inserting a record for every single TableId<\/p>\n<h1 id=\"option-4-separate-audit-table-with-ef\">Option 4: Separate Audit Table with EF<\/h1>\n<p>We do exactly the same but with EF doing the heavy lifting. The Old\/NewValues are stored as JSON, not as XML.<\/p>\n<h2 id=\"abstraction-iid\">Abstraction: IId<\/h2>\n<p>We need to get the db ID to insert it into the Audit table!<br \/> All entities have to implement this interface.<\/p>\n<pre><code class=\"language-c#\">public interface IId\n{\n  int Id { get; set; }\n}\n<\/code><\/pre>\n<h2 id=\"the-dbcontext-implementation-1\">The DbContext Implementation<\/h2>\n<pre><code class=\"language-c#\">public class MyDbContext(IUserProvider userProvider) : DbContext\n{\n  public override int SaveChanges()\n  {\n    \/\/ and\/or override SaveChangesAsync\n\n    \/\/ Keep track of certain things before SaveChanges\n    \/\/ As we need to capture the generated IDs to insert it into our Audit table\n    var changes = ChangeTracker.Entries&lt;IId&gt;()\n      .Where(e =&gt; e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted)\n      .Select(e =&gt; new ChangedEntity(e))\n      .ToArray();\n\n    \/\/ Get the inserted DB ids\n    int rowsAffected = base.SaveChanges();\n\n    \/\/ Generate (&amp; save!) the Audit records\n    SaveAuditRecords(changes);\n    return rowsAffected;\n  }\n\n  private class ChangedEntity\n  {\n    public EntityEntry&lt;IId&gt; EntityEntry { get; }\n    public EntityState State { get; }\n    public (string oldValues, string newValues) Modifications { get; }\n\n    public ChangedEntity(EntityEntry&lt;IId&gt; entityEntry)\n    {\n      EntityEntry = entityEntry;\n\n      \/\/ After the first SaveChanges,\n      \/\/ the State is reset to Unchanged!\n      State = entityEntry.State;\n\n      if (State == EntityState.Modified)\n      {\n        \/\/ After the first SaveChanges,\n        \/\/ nothing is modified anymore!\n        string oldValues = GetValues(entityEntry, true);\n        string newValues = GetValues(entityEntry, false);\n        Modifications = (oldValues, newValues);\n      }\n    }\n\n    private static string GetValues(EntityEntry entry, bool originalValues)\n    {\n      var properties = entry.Properties\n        .Where(p =&gt; p.IsModified)\n        .ToDictionary(p =&gt; p.Metadata.Name, p =&gt; originalValues ? p.OriginalValue : p.CurrentValue);\n      return JsonSerializer.Serialize(properties);\n    }\n  }\n\n  private void SaveAuditRecords(ChangedEntity[] changes)\n  {\n    foreach (ChangedEntity change in changes)\n    {\n      var audit = new EfAudit()\n      {\n        ModifiedBy = userProvider.UserName,\n        ActionType = change.State,\n        ModifiedOn = DateTime.Now,\n        TableId = change.EntityEntry.Entity.Id,\n        TableName = change.EntityEntry.Metadata.GetTableName(),\n      };\n\n      switch (change.State)\n      {\n        case EntityState.Added:\n          \/\/ Explicit cast to object or it serializes just the IId.Id property\n          audit.NewValues = JsonSerializer.Serialize((object)change.EntityEntry.Entity);\n          break;\n\n        case EntityState.Deleted:\n          audit.OldValues = JsonSerializer.Serialize(change.EntityEntry.OriginalValues.ToObject());\n          break;\n\n        case EntityState.Modified:\n          audit.OldValues = change.Modifications.oldValues;\n          audit.NewValues = change.Modifications.newValues;\n          break;\n      }\n\n      Audit.Add(audit);\n    }\n\n    \/\/ Second save to insert the Audit records\n    base.SaveChanges();\n  }\n}\n<\/code><\/pre>\n<p>If you are doing EventSourcing, you already have this. It also allows you to implement this in different ways.<\/p>\n<h2 id=\"pros-vs-cons-3\">Pros vs Cons<\/h2>\n<p>\u2705 This approach keeps track of all modifications<br \/> \u2705 Captures the actual user that made the request<br \/> \u26a0\ufe0f Changes done directly in the database are not tracked<\/p>\n<h1 id=\"conclusion\">Conclusion<\/h1>\n<p>It depends? \ud83e\udd37<\/p>\n<ul>\n<li>Do you care who has changed what in your tables directly\/manually or using other applications?<\/li>\n<li>Do you care what exactly has changed over time and by whom?<\/li>\n<\/ul>\n<p>Of course, if you trust no one, you could also implement a combination of triggers and EF to really track everything \ud83d\ude00<\/p>\n<h1>Extras<\/h1>\n<p><b>Stuff that came into being during the making of this post<\/b><\/p>\n<ul>\n<li><a href=\"https:\/\/github.com\/itenium-be\/DbAuditWithEF\">SQL Server &#038; EntityFramework Core: how to audit<\/a><\/li>\n<\/ul>\n<h1> Package Versions <i class=\"fa fa-info-circle\" style=\"float: right; color: cornflowerblue; font-size: 28px\" title=\"Package versions used for this post and in the accompanying code on Github\"><\/i><\/h1>\n<div><a href=\"https:\/\/github.com\/dotnet\/efcore\" class=\"github\"><img decoding=\"async\" src=\"https:\/\/img.shields.io\/github\/stars\/dotnet\/efcore.svg?style=social&#038;label=Star\" \/><\/a><a href=\"https:\/\/github.com\/dotnet\/efcore\">dotnet\/efcore<\/a>: v8.0.10 <\/div>\n","protected":false},"excerpt":{"rendered":"<p>Keeping track of database changes in SQL Server with EntityFramework, let\u2019s go over your options with sample implementations.<\/p>\n<p><a href=\"https:\/\/github.com\/itenium-be\/DbAuditWithEF\">All code with some UnitTests<\/a> can be found at the Github repository.<\/p>\n","protected":false},"author":1,"featured_media":1981,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"inline_featured_image":false,"footnotes":""},"categories":[11],"tags":[24,6],"class_list":["post-1391","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dotnet","tag-sql","tag-tutorial"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v26.5 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Database Auditing with EntityFramework and SQL Server - Itenium<\/title>\n<meta name=\"robots\" content=\"noindex, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Database Auditing with EntityFramework and SQL Server - Itenium\" \/>\n<meta property=\"og:description\" content=\"Keeping track of database changes in SQL Server with EntityFramework, let\u2019s go over your options with sample implementations.All code with some UnitTests can be found at the Github repository.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/demo.sidekick.be\/itenium\/blog\/dotnet\/db-audit-ef\/\" \/>\n<meta property=\"og:site_name\" content=\"Itenium\" \/>\n<meta property=\"article:published_time\" content=\"2024-10-25T22:00:00+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2025-11-28T12:31:42+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/demo.sidekick.be\/itenium\/wp-content\/uploads\/2025\/11\/ef-audit.png\" \/>\n\t<meta property=\"og:image:width\" content=\"360\" \/>\n\t<meta property=\"og:image:height\" content=\"300\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"Usdevelop\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Usdevelop\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"8 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\/\/demo.sidekick.be\/itenium\/blog\/dotnet\/db-audit-ef\/\",\"url\":\"https:\/\/demo.sidekick.be\/itenium\/blog\/dotnet\/db-audit-ef\/\",\"name\":\"Database Auditing with EntityFramework and SQL Server - Itenium\",\"isPartOf\":{\"@id\":\"https:\/\/demo.sidekick.be\/itenium\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/demo.sidekick.be\/itenium\/blog\/dotnet\/db-audit-ef\/#primaryimage\"},\"image\":{\"@id\":\"https:\/\/demo.sidekick.be\/itenium\/blog\/dotnet\/db-audit-ef\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/demo.sidekick.be\/itenium\/wp-content\/uploads\/2025\/11\/ef-audit.png\",\"datePublished\":\"2024-10-25T22:00:00+00:00\",\"dateModified\":\"2025-11-28T12:31:42+00:00\",\"author\":{\"@id\":\"https:\/\/demo.sidekick.be\/itenium\/#\/schema\/person\/6ab401ce01e858184ee4a8667fe0f022\"},\"breadcrumb\":{\"@id\":\"https:\/\/demo.sidekick.be\/itenium\/blog\/dotnet\/db-audit-ef\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/demo.sidekick.be\/itenium\/blog\/dotnet\/db-audit-ef\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/demo.sidekick.be\/itenium\/blog\/dotnet\/db-audit-ef\/#primaryimage\",\"url\":\"https:\/\/demo.sidekick.be\/itenium\/wp-content\/uploads\/2025\/11\/ef-audit.png\",\"contentUrl\":\"https:\/\/demo.sidekick.be\/itenium\/wp-content\/uploads\/2025\/11\/ef-audit.png\",\"width\":360,\"height\":300,\"caption\":\"a thorough audit\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/demo.sidekick.be\/itenium\/blog\/dotnet\/db-audit-ef\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/demo.sidekick.be\/itenium\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Database Auditing with EntityFramework and SQL Server\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/demo.sidekick.be\/itenium\/#website\",\"url\":\"https:\/\/demo.sidekick.be\/itenium\/\",\"name\":\"Itenium\",\"description\":\"We build software. We build careers.\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/demo.sidekick.be\/itenium\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Person\",\"@id\":\"https:\/\/demo.sidekick.be\/itenium\/#\/schema\/person\/6ab401ce01e858184ee4a8667fe0f022\",\"name\":\"Usdevelop\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/demo.sidekick.be\/itenium\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/cc8cf9b9e9d5bbe7e8d247907cc9ac8457bbfa2854b52eaddf6529adfe88ceab?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/cc8cf9b9e9d5bbe7e8d247907cc9ac8457bbfa2854b52eaddf6529adfe88ceab?s=96&d=mm&r=g\",\"caption\":\"Usdevelop\"},\"sameAs\":[\"https:\/\/demo.sidekick.be\/itenium\"]}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Database Auditing with EntityFramework and SQL Server - Itenium","robots":{"index":"noindex","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"og_locale":"en_US","og_type":"article","og_title":"Database Auditing with EntityFramework and SQL Server - Itenium","og_description":"Keeping track of database changes in SQL Server with EntityFramework, let\u2019s go over your options with sample implementations.All code with some UnitTests can be found at the Github repository.","og_url":"https:\/\/demo.sidekick.be\/itenium\/blog\/dotnet\/db-audit-ef\/","og_site_name":"Itenium","article_published_time":"2024-10-25T22:00:00+00:00","article_modified_time":"2025-11-28T12:31:42+00:00","og_image":[{"width":360,"height":300,"url":"https:\/\/demo.sidekick.be\/itenium\/wp-content\/uploads\/2025\/11\/ef-audit.png","type":"image\/png"}],"author":"Usdevelop","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Usdevelop","Est. reading time":"8 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/demo.sidekick.be\/itenium\/blog\/dotnet\/db-audit-ef\/","url":"https:\/\/demo.sidekick.be\/itenium\/blog\/dotnet\/db-audit-ef\/","name":"Database Auditing with EntityFramework and SQL Server - Itenium","isPartOf":{"@id":"https:\/\/demo.sidekick.be\/itenium\/#website"},"primaryImageOfPage":{"@id":"https:\/\/demo.sidekick.be\/itenium\/blog\/dotnet\/db-audit-ef\/#primaryimage"},"image":{"@id":"https:\/\/demo.sidekick.be\/itenium\/blog\/dotnet\/db-audit-ef\/#primaryimage"},"thumbnailUrl":"https:\/\/demo.sidekick.be\/itenium\/wp-content\/uploads\/2025\/11\/ef-audit.png","datePublished":"2024-10-25T22:00:00+00:00","dateModified":"2025-11-28T12:31:42+00:00","author":{"@id":"https:\/\/demo.sidekick.be\/itenium\/#\/schema\/person\/6ab401ce01e858184ee4a8667fe0f022"},"breadcrumb":{"@id":"https:\/\/demo.sidekick.be\/itenium\/blog\/dotnet\/db-audit-ef\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/demo.sidekick.be\/itenium\/blog\/dotnet\/db-audit-ef\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/demo.sidekick.be\/itenium\/blog\/dotnet\/db-audit-ef\/#primaryimage","url":"https:\/\/demo.sidekick.be\/itenium\/wp-content\/uploads\/2025\/11\/ef-audit.png","contentUrl":"https:\/\/demo.sidekick.be\/itenium\/wp-content\/uploads\/2025\/11\/ef-audit.png","width":360,"height":300,"caption":"a thorough audit"},{"@type":"BreadcrumbList","@id":"https:\/\/demo.sidekick.be\/itenium\/blog\/dotnet\/db-audit-ef\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/demo.sidekick.be\/itenium\/"},{"@type":"ListItem","position":2,"name":"Database Auditing with EntityFramework and SQL Server"}]},{"@type":"WebSite","@id":"https:\/\/demo.sidekick.be\/itenium\/#website","url":"https:\/\/demo.sidekick.be\/itenium\/","name":"Itenium","description":"We build software. We build careers.","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/demo.sidekick.be\/itenium\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Person","@id":"https:\/\/demo.sidekick.be\/itenium\/#\/schema\/person\/6ab401ce01e858184ee4a8667fe0f022","name":"Usdevelop","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/demo.sidekick.be\/itenium\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/cc8cf9b9e9d5bbe7e8d247907cc9ac8457bbfa2854b52eaddf6529adfe88ceab?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/cc8cf9b9e9d5bbe7e8d247907cc9ac8457bbfa2854b52eaddf6529adfe88ceab?s=96&d=mm&r=g","caption":"Usdevelop"},"sameAs":["https:\/\/demo.sidekick.be\/itenium"]}]}},"_links":{"self":[{"href":"https:\/\/demo.sidekick.be\/itenium\/wp-json\/wp\/v2\/posts\/1391","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/demo.sidekick.be\/itenium\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/demo.sidekick.be\/itenium\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/demo.sidekick.be\/itenium\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/demo.sidekick.be\/itenium\/wp-json\/wp\/v2\/comments?post=1391"}],"version-history":[{"count":3,"href":"https:\/\/demo.sidekick.be\/itenium\/wp-json\/wp\/v2\/posts\/1391\/revisions"}],"predecessor-version":[{"id":2257,"href":"https:\/\/demo.sidekick.be\/itenium\/wp-json\/wp\/v2\/posts\/1391\/revisions\/2257"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/demo.sidekick.be\/itenium\/wp-json\/wp\/v2\/media\/1981"}],"wp:attachment":[{"href":"https:\/\/demo.sidekick.be\/itenium\/wp-json\/wp\/v2\/media?parent=1391"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/demo.sidekick.be\/itenium\/wp-json\/wp\/v2\/categories?post=1391"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/demo.sidekick.be\/itenium\/wp-json\/wp\/v2\/tags?post=1391"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}