Skip to content

Commit a48e5c0

Browse files
committed
adding support for nested properties in column values and ensured that column changes preserve the full complex-property path (#796)
1 parent 437cbbb commit a48e5c0

5 files changed

Lines changed: 166 additions & 13 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ All notable changes to Audit.NET and its extensions will be documented in this f
33

44
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
55

6+
## [32.3.0] - 2026-08-15:
7+
- **Audit.EntityFramework.Core**: Added support for nested properties in column values and ensured that column changes preserve the full complex-property path (#796)
8+
69
## [32.2.0] - 2026-06-12:
710
- **Audit.WebApi** / **Audit.WebApi.Core**: Making the existing `IncludeRequestBody` setting to control whether Form Variables are included (#792)
811
- **Audit.NET.MongoDB** / **Audit.MongoClient**: Upgrading MongoDB.Driver to latest version 3.9.0 (#793)

‎Directory.Build.props‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<Project>
22
<PropertyGroup>
3-
<Version>32.2.0</Version>
3+
<Version>32.3.0</Version>
44
<PackageReleaseNotes></PackageReleaseNotes>
55
<CheckEolTargetFramework>false</CheckEolTargetFramework>
66
<LangVersion>latest</LangVersion>

‎src/Audit.EntityFramework/DbContextHelper.Core.cs‎

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,26 +62,28 @@ private Dictionary<string, ColumnValueChange> GetChangesByColumn(IAuditDbContext
6262
/// <summary>
6363
/// Adds the change values from the complex properties recursively
6464
/// </summary>
65-
private void AddChangesFromComplexProperties(IAuditDbContext context, EntityEntry entry, IEnumerable<ComplexPropertyEntry> complexProperties, List<EventEntryChange> result)
65+
private void AddChangesFromComplexProperties(IAuditDbContext context, EntityEntry entry, IEnumerable<ComplexPropertyEntry> complexProperties, List<EventEntryChange> result, string prefix = null)
6666
{
6767
foreach (var complexEntry in complexProperties)
6868
{
69+
var complexPropertyPath = GetPropertyPath(prefix, complexEntry.Metadata.Name);
70+
6971
// Process the primitive properties
7072
foreach (var propEntry in complexEntry.Properties)
7173
{
7274
if (propEntry.IsModified && IncludeProperty(context, complexEntry.Metadata.ClrType, propEntry.Metadata.Name))
7375
{
7476
result.Add(new EventEntryChange()
7577
{
76-
ColumnName = GetColumnName(propEntry.Metadata),
78+
ColumnName = GetColumnNameFromComplexProperty(propEntry.Metadata, complexPropertyPath),
7779
NewValue = HasPropertyValue(context, entry, complexEntry.Metadata.ClrType, propEntry.Metadata.Name, propEntry.CurrentValue, out var overridenCurrentValue) ? overridenCurrentValue : propEntry.CurrentValue,
7880
OriginalValue = HasPropertyValue(context, entry, complexEntry.Metadata.ClrType, propEntry.Metadata.Name, propEntry.OriginalValue, out var overridenOriginalValue) ? overridenOriginalValue : propEntry.OriginalValue
7981
});
8082
}
8183
}
8284

8385
// Recursively process complex properties
84-
AddChangesFromComplexProperties(context, entry, complexEntry.ComplexProperties, result);
86+
AddChangesFromComplexProperties(context, entry, complexEntry.ComplexProperties, result, complexPropertyPath);
8587
}
8688
}
8789
#endif
@@ -117,10 +119,12 @@ private Dictionary<string, object> GetColumnValues(IAuditDbContext context, Enti
117119
/// <summary>
118120
/// Adds the column values from the complex properties recursively
119121
/// </summary>
120-
private void AddColumnValuesFromComplexProperties(IAuditDbContext context, EntityEntry entry, IEnumerable<ComplexPropertyEntry> complexProperties, Dictionary<string, object> result)
122+
private void AddColumnValuesFromComplexProperties(IAuditDbContext context, EntityEntry entry, IEnumerable<ComplexPropertyEntry> complexProperties, Dictionary<string, object> result, string prefix = null)
121123
{
122124
foreach (var complexEntry in complexProperties)
123125
{
126+
var complexPropertyPath = GetPropertyPath(prefix, complexEntry.Metadata.Name);
127+
124128
// Process the primitive properties
125129
foreach (var propEntry in complexEntry.Properties)
126130
{
@@ -132,13 +136,13 @@ private void AddColumnValuesFromComplexProperties(IAuditDbContext context, Entit
132136
value = overrideValue;
133137
}
134138

135-
var columnName = GetColumnName(propEntry.Metadata);
139+
var columnName = GetColumnNameFromComplexProperty(propEntry.Metadata, complexPropertyPath);
136140
result.Add(columnName, value);
137141
}
138142
}
139143

140144
// Recursively process complex properties
141-
AddColumnValuesFromComplexProperties(context, entry, complexEntry.ComplexProperties, result);
145+
AddColumnValuesFromComplexProperties(context, entry, complexEntry.ComplexProperties, result, complexPropertyPath);
142146
}
143147
}
144148
#endif
@@ -189,6 +193,16 @@ internal static string GetColumnName(IProperty prop, IEntityType metadata = null
189193
#endif
190194

191195
#if EF_CORE_8_OR_GREATER
196+
internal static string GetColumnNameFromComplexProperty(IProperty prop, string prefix)
197+
{
198+
return GetPropertyPath(prefix, GetFallbackColumnName(prop));
199+
}
200+
201+
private static string GetPropertyPath(string prefix, string name)
202+
{
203+
return string.IsNullOrEmpty(prefix) ? name : $"{prefix}.{name}";
204+
}
205+
192206
private static ITypeBase GetDeclaringType(IProperty prop)
193207
{
194208
return prop.DeclaringType;

‎test/Audit.EntityFramework.Core.UnitTest/Contexts.cs‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,53 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
6767
modelBuilder.Entity<AuditLog>().ComplexProperty(e => e.Address).ComplexProperty(a => a.Country);
6868
}
6969
}
70+
71+
[AuditDbContext(IncludeEntityObjects = true)]
72+
public class Context_ComplexTypes_DuplicateNames : AuditDbContext
73+
{
74+
public class Loan
75+
{
76+
[DatabaseGenerated(DatabaseGeneratedOption.None)]
77+
public int Id { get; set; }
78+
public string LoanType { get; set; }
79+
[Required]
80+
public required LoanData LoanData { get; set; }
81+
}
82+
83+
public record LoanData
84+
{
85+
public string LoanType { get; init; }
86+
[Required]
87+
public required BorrowerInfo PrimaryBorrower { get; init; }
88+
[Required]
89+
public required BorrowerInfo CoBorrower1 { get; init; }
90+
}
91+
92+
public record BorrowerInfo
93+
{
94+
public string Email { get; init; }
95+
}
96+
97+
public DbSet<Loan> Loans { get; set; }
98+
99+
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
100+
{
101+
if (!optionsBuilder.IsConfigured)
102+
{
103+
var cnnString = TestHelper.GetConnectionString(nameof(Context_ComplexTypes_DuplicateNames));
104+
optionsBuilder.UseSqlServer(cnnString).UseLazyLoadingProxies();
105+
}
106+
}
107+
108+
protected override void OnModelCreating(ModelBuilder modelBuilder)
109+
{
110+
modelBuilder.Entity<Loan>().ComplexProperty(e => e.LoanData, loanData =>
111+
{
112+
loanData.ComplexProperty(e => e.PrimaryBorrower);
113+
loanData.ComplexProperty(e => e.CoBorrower1);
114+
});
115+
}
116+
}
70117
#endif
71118

72119
#if EF_CORE_7_OR_GREATER

‎test/Audit.EntityFramework.Core.UnitTest/EfCoreTests.cs‎

Lines changed: 95 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,14 @@ public void Test_EF_ComplexType()
8787
Assert.That(evs[0].Entries[0].ColumnValues["Name"], Is.EqualTo("Development"));
8888
Assert.That(evs[1].Entries[0].ColumnValues["Name"], Is.EqualTo("New Name"));
8989

90-
Assert.That(evs[0].Entries[0].ColumnValues["Address_City"], Is.EqualTo("*Vienna*"));
91-
Assert.That(evs[1].Entries[0].ColumnValues["Address_City"], Is.EqualTo("*NewCity*"));
90+
Assert.That(evs[0].Entries[0].ColumnValues["Address.City"], Is.EqualTo("*Vienna*"));
91+
Assert.That(evs[1].Entries[0].ColumnValues["Address.City"], Is.EqualTo("*NewCity*"));
9292

93-
Assert.That(evs[0].Entries[0].ColumnValues["Address_Country_Alias"], Is.EqualTo("AU"));
94-
Assert.That(evs[1].Entries[0].ColumnValues["Address_Country_Alias"], Is.EqualTo("NEWALIAS"));
93+
Assert.That(evs[0].Entries[0].ColumnValues["Address.Country.Alias"], Is.EqualTo("AU"));
94+
Assert.That(evs[1].Entries[0].ColumnValues["Address.Country.Alias"], Is.EqualTo("NEWALIAS"));
9595

96-
Assert.That(evs[1].Entries[0].Changes.FirstOrDefault(ch => ch.ColumnName == "Address_Country_Alias")?.OriginalValue, Is.EqualTo("AU"));
97-
Assert.That(evs[1].Entries[0].Changes.FirstOrDefault(ch => ch.ColumnName == "Address_Country_Alias")?.NewValue, Is.EqualTo("NEWALIAS"));
96+
Assert.That(evs[1].Entries[0].Changes.FirstOrDefault(ch => ch.ColumnName == "Address.Country.Alias")?.OriginalValue, Is.EqualTo("AU"));
97+
Assert.That(evs[1].Entries[0].Changes.FirstOrDefault(ch => ch.ColumnName == "Address.Country.Alias")?.NewValue, Is.EqualTo("NEWALIAS"));
9898

9999
context.Database.EnsureDeleted();
100100
}
@@ -151,6 +151,95 @@ public void Test_EF_ComplexType_EntityFrameworkDataProvider_PropertyMatching()
151151
Assert.That(auditLogs[0].Address.Country.Alias, Is.EqualTo(alias));
152152
}
153153

154+
[Test]
155+
public void Test_EF_ComplexType_DuplicateLeafNames_ColumnValues()
156+
{
157+
using var context = new Context_ComplexTypes_DuplicateNames();
158+
Audit.Core.Configuration.Setup()
159+
.UseInMemoryProvider(out var dp);
160+
Audit.EntityFramework.Configuration.Setup()
161+
.ForContext<Context_ComplexTypes_DuplicateNames>(c => c.MapChangesByColumn());
162+
163+
context.Database.EnsureDeleted();
164+
context.Database.EnsureCreated();
165+
166+
context.Loans.Add(new Context_ComplexTypes_DuplicateNames.Loan()
167+
{
168+
Id = 1,
169+
LoanType = "FHA",
170+
LoanData = new Context_ComplexTypes_DuplicateNames.LoanData()
171+
{
172+
LoanType = "JsonFHA",
173+
PrimaryBorrower = new Context_ComplexTypes_DuplicateNames.BorrowerInfo() { Email = "primary@example.com" },
174+
CoBorrower1 = new Context_ComplexTypes_DuplicateNames.BorrowerInfo() { Email = "coborrower@example.com" }
175+
}
176+
});
177+
178+
Assert.DoesNotThrow(() => context.SaveChanges());
179+
180+
var evs = dp.GetAllEventsOfType<AuditEventEntityFramework>().Select(e => e.EntityFrameworkEvent).ToList();
181+
182+
Assert.That(evs.Count, Is.EqualTo(1));
183+
Assert.That(evs[0].Entries.Count, Is.EqualTo(1));
184+
Assert.That(evs[0].Entries[0].Action, Is.EqualTo("Insert"));
185+
Assert.That(evs[0].Entries[0].ColumnValues["LoanType"], Is.EqualTo("FHA"));
186+
Assert.That(evs[0].Entries[0].ColumnValues["LoanData.LoanType"], Is.EqualTo("JsonFHA"));
187+
Assert.That(evs[0].Entries[0].ColumnValues["LoanData.PrimaryBorrower.Email"], Is.EqualTo("primary@example.com"));
188+
Assert.That(evs[0].Entries[0].ColumnValues["LoanData.CoBorrower1.Email"], Is.EqualTo("coborrower@example.com"));
189+
190+
context.Database.EnsureDeleted();
191+
}
192+
193+
[Test]
194+
public void Test_EF_ComplexType_DuplicateLeafNames_Changes()
195+
{
196+
using var context = new Context_ComplexTypes_DuplicateNames();
197+
context.AuditDisabled = true;
198+
Audit.Core.Configuration.Setup()
199+
.UseInMemoryProvider(out var dp);
200+
201+
context.Database.EnsureDeleted();
202+
context.Database.EnsureCreated();
203+
204+
var loan = new Context_ComplexTypes_DuplicateNames.Loan()
205+
{
206+
Id = 1,
207+
LoanType = "FHA",
208+
LoanData = new Context_ComplexTypes_DuplicateNames.LoanData()
209+
{
210+
LoanType = "JsonFHA",
211+
PrimaryBorrower = new Context_ComplexTypes_DuplicateNames.BorrowerInfo() { Email = "primary@example.com" },
212+
CoBorrower1 = new Context_ComplexTypes_DuplicateNames.BorrowerInfo() { Email = "coborrower@example.com" }
213+
}
214+
};
215+
216+
context.Loans.Add(loan);
217+
context.SaveChanges();
218+
context.AuditDisabled = false;
219+
220+
loan.LoanType = "VA";
221+
loan.LoanData = loan.LoanData with
222+
{
223+
LoanType = "JsonVA",
224+
PrimaryBorrower = loan.LoanData.PrimaryBorrower with { Email = "updated-primary@example.com" },
225+
CoBorrower1 = loan.LoanData.CoBorrower1 with { Email = "updated-coborrower@example.com" }
226+
};
227+
228+
Assert.DoesNotThrow(() => context.SaveChanges());
229+
230+
var evs = dp.GetAllEventsOfType<AuditEventEntityFramework>().Select(e => e.EntityFrameworkEvent).ToList();
231+
232+
Assert.That(evs.Count, Is.EqualTo(1));
233+
Assert.That(evs[0].Entries.Count, Is.EqualTo(1));
234+
Assert.That(evs[0].Entries[0].Action, Is.EqualTo("Update"));
235+
Assert.That(evs[0].Entries[0].Changes.Any(ch => ch.ColumnName == "LoanType" && (string)ch.OriginalValue == "FHA" && (string)ch.NewValue == "VA"), Is.True);
236+
Assert.That(evs[0].Entries[0].Changes.Any(ch => ch.ColumnName == "LoanData.LoanType" && (string)ch.OriginalValue == "JsonFHA" && (string)ch.NewValue == "JsonVA"), Is.True);
237+
Assert.That(evs[0].Entries[0].Changes.Any(ch => ch.ColumnName == "LoanData.PrimaryBorrower.Email" && (string)ch.OriginalValue == "primary@example.com" && (string)ch.NewValue == "updated-primary@example.com"), Is.True);
238+
Assert.That(evs[0].Entries[0].Changes.Any(ch => ch.ColumnName == "LoanData.CoBorrower1.Email" && (string)ch.OriginalValue == "coborrower@example.com" && (string)ch.NewValue == "updated-coborrower@example.com"), Is.True);
239+
240+
context.Database.EnsureDeleted();
241+
}
242+
154243
#endif
155244

156245
#if EF_CORE_5_OR_GREATER

0 commit comments

Comments
 (0)