diff --git a/CHANGELOG.md b/CHANGELOG.md index e35c8a06..c77fe6ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to Audit.NET and its extensions will be documented in this f The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). +## [21.1.0] - 2023-10-09: +- Audit.EntityFramework.Core: Fixing bug in the `AuditCommandInterceptor` when multiple result sets are returned. (#521, #628) +- Audit.NET.MongoDB: Upgrade MongoDB.Driver reference to the latest version (#627) + ## [21.0.4] - 2023-09-15 - Audit.NET.PostgreSql: Fixing postgres double quote issue. (#623) diff --git a/Directory.Build.props b/Directory.Build.props index 579a2df7..7b0aa730 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 21.0.4 + 21.1.0 false diff --git a/src/Audit.EntityFramework/Interceptors/AuditCommandInterceptor.cs b/src/Audit.EntityFramework/Interceptors/AuditCommandInterceptor.cs index 54b2d9b4..22f012de 100644 --- a/src/Audit.EntityFramework/Interceptors/AuditCommandInterceptor.cs +++ b/src/Audit.EntityFramework/Interceptors/AuditCommandInterceptor.cs @@ -331,23 +331,30 @@ protected virtual void UpdateFailedEvent(CommandErrorEventData eventData) /// /// Serializes the result DB data reader and returns a new data reader to be overriden to the EF result. /// - protected virtual List> SerializeDataReader(DbDataReader reader, out DbDataReader newDataReader) + protected virtual Dictionary>> SerializeDataReader(DbDataReader reader, out DbDataReader newDataReader) { if (reader == null) { newDataReader = null; return null; } + + // Create the data table from the original reader + var dataSet = new DataSet(); + do + { + var table = new DataTable(); + table.Load(reader); + dataSet.Tables.Add(table); + } while (!reader.IsClosed); + + newDataReader = dataSet.CreateDataReader(); - var dataTable = new DataTable(); - dataTable.Load(reader); - newDataReader = dataTable.CreateDataReader(); + var resultData = dataSet.Tables.Cast().ToDictionary(k => k.TableName, t => + t.AsEnumerable() + .Select(row => t.Columns.Cast().ToDictionary(c => c.ColumnName, c => row[c])).ToList()); - return dataTable.AsEnumerable().Select( - row => dataTable.Columns.Cast().ToDictionary( - column => column.ColumnName, - column => row[column] - )).ToList(); + return resultData; } private Dictionary GetParameters(DbCommand command, CommandEventData eventData) diff --git a/src/Audit.EntityFramework/README.md b/src/Audit.EntityFramework/README.md index 24d1170b..1ea1bfa2 100644 --- a/src/Audit.EntityFramework/README.md +++ b/src/Audit.EntityFramework/README.md @@ -491,7 +491,7 @@ The following table describes the output fields for the low-level command interc | **CommandText** | string | The command text | | **Parameters** | Dictionary | The parameter values, if any, when `EnableSensitiveDataLogging` is enabled | | **IsAsync** | boolean | Indicates whether the call was asynchronous | -| **Result** | object | Result of the operation, only for Scalar and NonQuery methods. Reader methods does not include the result on the output. | +| **Result** | object | Result of the operation. Query results are only included when IncludeReaderResults is set to true. | | **Success** | boolean | Boolean to indicate if the operation was successful | | **ErrorMessage** | string | The exception thrown details (if any) | diff --git a/src/Audit.NET.MongoDB/Audit.NET.MongoDB.csproj b/src/Audit.NET.MongoDB/Audit.NET.MongoDB.csproj index ae63b7d7..a88b78cf 100644 --- a/src/Audit.NET.MongoDB/Audit.NET.MongoDB.csproj +++ b/src/Audit.NET.MongoDB/Audit.NET.MongoDB.csproj @@ -30,7 +30,7 @@ - + diff --git a/test/Audit.EntityFramework.Core.UnitTest/DbCommandInterceptorTests.cs b/test/Audit.EntityFramework.Core.UnitTest/DbCommandInterceptorTests.cs index 6d132faf..b79e636b 100644 --- a/test/Audit.EntityFramework.Core.UnitTest/DbCommandInterceptorTests.cs +++ b/test/Audit.EntityFramework.Core.UnitTest/DbCommandInterceptorTests.cs @@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore.Diagnostics; using NUnit.Framework; using System; +using System.Collections; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; @@ -20,6 +21,17 @@ public class DbCommandInterceptorTests { private static Random _rnd = new Random(); + [OneTimeSetUp] + public void OneTimeSetup() + { + using (var ctx = new DbCommandInterceptContext(new DbContextOptionsBuilder().Options)) + { + // not intercepted + ctx.Database.EnsureDeleted(); + ctx.Database.EnsureCreated(); + } + } + [SetUp] public void Setup() { @@ -46,12 +58,6 @@ public void Test_DbCommandInterceptor_HappyPath() .ForContext(_ => _ .IncludeEntityObjects(true)); - using (var ctx = new DbCommandInterceptContext(new DbContextOptionsBuilder().Options)) - { - // not intercepted - ctx.Database.EnsureDeleted(); - ctx.Database.EnsureCreated(); - } var interceptor = new AuditCommandInterceptor() { AuditEventType = "{context}:{database}:{method}" }; int id = _rnd.Next(); using (var ctx = new DbCommandInterceptContext(new DbContextOptionsBuilder().AddInterceptors(interceptor).Options)) @@ -109,12 +115,6 @@ public async Task Test_DbCommandInterceptor_HappyPathAsync() .ForContext(_ => _ .IncludeEntityObjects(true)); - using (var ctx = new DbCommandInterceptContext(new DbContextOptionsBuilder().Options)) - { - // not intercepted - await ctx.Database.EnsureDeletedAsync(); - await ctx.Database.EnsureCreatedAsync(); - } int id = _rnd.Next(); using (var ctx = new DbCommandInterceptContext(new DbContextOptionsBuilder().AddInterceptors(new AuditCommandInterceptor() { AuditEventType = "{context}:{database}:{method}" }).Options)) { @@ -250,28 +250,31 @@ public void Test_DbCommandInterceptor_IncludeReaderResult() .OnInsert(ev => inserted.Add(ev.GetCommandEntityFrameworkEvent()))) .WithCreationPolicy(EventCreationPolicy.InsertOnEnd); + int newId = 24; using (var ctx = new DbCommandInterceptContext(new DbContextOptionsBuilder().Options)) { - ctx.Database.EnsureCreated(); + // not intercepted + var dept = new DbCommandInterceptContext.Department() { Id = newId, Name = "Test", Comments = "Comment" }; + ctx.Departments.Add(dept); + ctx.SaveChanges(); } + var interceptor = new AuditCommandInterceptor() { LogParameterValues = true, IncludeReaderResults = true }; - DbCommandInterceptContext.Department dept; using (var ctx = new DbCommandInterceptContext(new DbContextOptionsBuilder().AddInterceptors(interceptor).Options)) { - int i = 22; - dept = ctx.Departments.FirstOrDefault(d => d.Id != i); + var dept = ctx.Departments.FirstOrDefault(d => d.Id == newId); } Assert.AreEqual(1, inserted.Count); Assert.IsNotNull(inserted[0].Parameters); Assert.IsTrue(inserted[0].Parameters.Any()); - Assert.AreEqual(22, inserted[0].Parameters.First().Value); + Assert.AreEqual(newId, inserted[0].Parameters.First().Value); Assert.IsNotNull(inserted[0].Result); - var resultList = inserted[0].Result as List>; + var resultList = inserted[0].Result as Dictionary>>; Assert.AreEqual(1, resultList.Count); - Assert.AreEqual(dept.Id, (int)resultList[0]["Id"]); - Assert.AreEqual(dept.Comments, resultList[0]["Comments"]); - Assert.AreEqual(dept.Name, resultList[0]["Name"]); + Assert.AreEqual(newId, (int)resultList.Values.First()[0]["Id"]); + Assert.AreEqual("Comment", resultList.Values.First()[0]["Comments"]); + Assert.AreEqual("Test", resultList.Values.First()[0]["Name"]); } [Test] @@ -283,28 +286,66 @@ public async Task Test_DbCommandInterceptor_IncludeReaderResultAsync() .OnInsert(ev => inserted.Add(ev.GetCommandEntityFrameworkEvent()))) .WithCreationPolicy(EventCreationPolicy.InsertOnEnd); + int newId = 23; using (var ctx = new DbCommandInterceptContext(new DbContextOptionsBuilder().Options)) { - ctx.Database.EnsureCreated(); + // not intercepted + var dept = new DbCommandInterceptContext.Department() { Id = newId, Name = "Test", Comments = "Comment" }; + await ctx.Departments.AddAsync(dept); + await ctx.SaveChangesAsync(); } + var interceptor = new AuditCommandInterceptor() { LogParameterValues = true, IncludeReaderResults = true }; - DbCommandInterceptContext.Department dept; using (var ctx = new DbCommandInterceptContext(new DbContextOptionsBuilder().AddInterceptors(interceptor).Options)) { - int i = 22; - dept = await ctx.Departments.FirstOrDefaultAsync(d => d.Id != i); + var dept = ctx.Departments.FirstOrDefault(d => d.Id == newId); } Assert.AreEqual(1, inserted.Count); Assert.IsNotNull(inserted[0].Parameters); Assert.IsTrue(inserted[0].Parameters.Any()); - Assert.AreEqual(22, inserted[0].Parameters.First().Value); + Assert.AreEqual(newId, inserted[0].Parameters.First().Value); Assert.IsNotNull(inserted[0].Result); - var resultList = inserted[0].Result as List>; + var resultList = inserted[0].Result as Dictionary>>; Assert.AreEqual(1, resultList.Count); - Assert.AreEqual(dept.Id, (int)resultList[0]["Id"]); - Assert.AreEqual(dept.Comments, resultList[0]["Comments"]); - Assert.AreEqual(dept.Name, resultList[0]["Name"]); + Assert.AreEqual(newId, (int)resultList.Values.First()[0]["Id"]); + Assert.AreEqual("Comment", resultList.Values.First()[0]["Comments"]); + Assert.AreEqual("Test", resultList.Values.First()[0]["Name"]); + } + + [Test] + public async Task Test_DbCommandInterceptor_IncludeReaderResult_MultipleResultSets_Async() + { + var inserted = new List(); + Audit.Core.Configuration.Setup() + .UseDynamicProvider(_ => _ + .OnInsert(ev => inserted.Add(ev.GetCommandEntityFrameworkEvent()))) + .WithCreationPolicy(EventCreationPolicy.InsertOnEnd); + + var interceptor = new AuditCommandInterceptor() { LogParameterValues = true, IncludeReaderResults = true }; + + using (var ctx = new DbCommandInterceptContext(new DbContextOptionsBuilder().AddInterceptors(interceptor).Options)) + { + var dept = new DbCommandInterceptContext.Department() + { + Name = "Dept1", + Address = new DbCommandInterceptContext.Address() + { + Text = "Addr1" + } + }; + await ctx.Departments.AddAsync(dept); + await ctx.SaveChangesAsync(); + inserted.Clear(); + + dept.Name = "DeptUpdated"; + dept.Address.Text = "AddrUpdated"; + await ctx.SaveChangesAsync(); + } + + Assert.AreEqual(1, inserted.Count); + + Assert.AreEqual(2, (inserted[0].Result as ICollection)?.Count); // Two result sets } #if EF_CORE_5_OR_GREATER @@ -333,12 +374,6 @@ public void Test_DbCommandInterceptor_CombineSaveChanges() .ForContext(_ => _ .IncludeEntityObjects(true)); - using (var ctx = new DbCommandInterceptContext(new DbContextOptionsBuilder().Options)) - { - // not intercepted - ctx.Database.EnsureCreated(); - } - var id = _rnd.Next(); var guid = Guid.NewGuid().ToString(); var dept = new DbCommandInterceptContext.Department() { Id = id, Name = guid, Comments = "test" }; @@ -384,10 +419,6 @@ public void Test_DbCommandInterceptor_IgnoreParams() .ForContext(_ => _ .IncludeEntityObjects(true)); - using (var ctx = new DbCommandInterceptContext(new DbContextOptionsBuilder().Options)) - { - ctx.Database.EnsureCreated(); - } var interceptor = new AuditCommandInterceptor() { LogParameterValues = false }; using (var ctx = new DbCommandInterceptContext(new DbContextOptionsBuilder().AddInterceptors(interceptor).Options)) { @@ -416,10 +447,6 @@ public void Test_DbCommandInterceptor_CreationPolicy() .IncludeEntityObjects(true)); int id = _rnd.Next(); - using (var ctx = new DbCommandInterceptContext(new DbContextOptionsBuilder().Options)) - { - ctx.Database.EnsureCreated(); - } using (var ctx = new DbCommandInterceptContext(new DbContextOptionsBuilder() .AddInterceptors(new AuditCommandInterceptor()).Options)) { @@ -448,12 +475,6 @@ public void Test_DbCommandInterceptor_DataProviderFromAuditDbContext() int id = _rnd.Next(); - // Use the default context to create the database - using (var ctx = new DbCommandInterceptContext(new DbContextOptionsBuilder().Options)) - { - ctx.Database.EnsureCreated(); - } - var optionsWithInterceptor = new DbContextOptionsBuilder() .AddInterceptors(new AuditCommandInterceptor()) .Options; @@ -490,12 +511,6 @@ public async Task Test_DbCommandInterceptor_DataProviderFromAuditDbContextAsync( int id = _rnd.Next(); - // Use the default context to create the database - using (var ctx = new DbCommandInterceptContext(new DbContextOptionsBuilder().Options)) - { - await ctx.Database.EnsureCreatedAsync(); - } - var optionsWithInterceptor = new DbContextOptionsBuilder() .AddInterceptors(new AuditCommandInterceptor()) .Options; @@ -530,12 +545,6 @@ public void Test_DbCommandInterceptor_AuditDisabledFromAuditDbContext() int id = _rnd.Next(); - // Use the default context to create the database - using (var ctx = new DbCommandInterceptContext(new DbContextOptionsBuilder().Options)) - { - ctx.Database.EnsureCreated(); - } - var optionsWithInterceptor = new DbContextOptionsBuilder() .AddInterceptors(new AuditCommandInterceptor()) .Options; @@ -569,12 +578,6 @@ public async Task Test_DbCommandInterceptor_AuditDisabledFromAuditDbContextAsync int id = _rnd.Next(); - // Use the default context to create the database - using (var ctx = new DbCommandInterceptContext(new DbContextOptionsBuilder().Options)) - { - await ctx.Database.EnsureCreatedAsync(); - } - var optionsWithInterceptor = new DbContextOptionsBuilder() .AddInterceptors(new AuditCommandInterceptor()) .Options; @@ -608,12 +611,6 @@ public void Test_DbCommandInterceptor_CustomFieldFromAuditDbContext() int id = _rnd.Next(); - // Use the default context to create the database - using (var ctx = new DbCommandInterceptContext(new DbContextOptionsBuilder().Options)) - { - ctx.Database.EnsureCreated(); - } - var optionsWithInterceptor = new DbContextOptionsBuilder() .AddInterceptors(new AuditCommandInterceptor()) .Options; @@ -649,12 +646,6 @@ public async Task Test_DbCommandInterceptor_CustomFieldFromAuditDbContextAsync() int id = _rnd.Next(); - // Use the default context to create the database - using (var ctx = new DbCommandInterceptContext(new DbContextOptionsBuilder().Options)) - { - await ctx.Database.EnsureCreatedAsync(); - } - var optionsWithInterceptor = new DbContextOptionsBuilder() .AddInterceptors(new AuditCommandInterceptor()) .Options; @@ -690,12 +681,6 @@ public void Test_DbCommandInterceptor_OnScopeXFromAuditDbContext() int id = _rnd.Next(); - // Use the default context to create the database - using (var ctx = new DbCommandInterceptContext(new DbContextOptionsBuilder().Options)) - { - ctx.Database.EnsureCreated(); - } - var optionsWithInterceptor = new DbContextOptionsBuilder() .AddInterceptors(new AuditCommandInterceptor()) .Options; @@ -736,12 +721,6 @@ public async Task Test_DbCommandInterceptor_OnScopeXFromAuditDbContextAsync() int id = _rnd.Next(); - // Use the default context to create the database - using (var ctx = new DbCommandInterceptContext(new DbContextOptionsBuilder().Options)) - { - await ctx.Database.EnsureCreatedAsync(); - } - var optionsWithInterceptor = new DbContextOptionsBuilder() .AddInterceptors(new AuditCommandInterceptor()) .Options; @@ -842,6 +821,15 @@ public class Department public int Id { get; set; } public string Name { get; set; } public string Comments { get; set; } + public Address Address { get; set; } + } + + public class Address + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int Id { get; set; } + public string Text { get; set; } } } }