Skip to content

Commit

Permalink
Adding support for Complex Types (#638)
Browse files Browse the repository at this point in the history
  • Loading branch information
Federico Colombo authored and Federico Colombo committed Nov 16, 2023
1 parent 0f01586 commit c00949f
Show file tree
Hide file tree
Showing 6 changed files with 226 additions and 26 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/).

## [22.0.1] - 2023-11-15:
- Audit.EntityFramework.Core: Adding support for [Complex Types](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-8.0/whatsnew#value-objects-using-complex-types) in EF Core 8.
Complex types are now included in the `ColumnValues` and `Changes` collections of the EF audit entity.

## [22.0.0] - 2023-11-14:
- Audit.NET / Audit.EntityFramework.Core / Audit.NET.SqlServer: Adding support for NET8 (#632)

Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<Version>22.0.0</Version>
<Version>22.0.1</Version>
<PackageReleaseNotes></PackageReleaseNotes>
<CheckEolTargetFramework>false</CheckEolTargetFramework>
</PropertyGroup>
Expand Down
129 changes: 106 additions & 23 deletions src/Audit.EntityFramework/DbContextHelper.Core.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;

namespace Audit.EntityFramework
{
Expand All @@ -32,7 +30,6 @@ private List<EventEntryChange> GetChanges(IAuditDbContext context, EntityEntry e
PropertyEntry propEntry = entry.Property(prop.Name);
if (propEntry.IsModified)
{

if (IncludeProperty(context, entry, prop.Name))
{
result.Add(new EventEntryChange()
Expand All @@ -44,30 +41,41 @@ private List<EventEntryChange> GetChanges(IAuditDbContext context, EntityEntry e
}
}
}

#if EF_CORE_8_OR_GREATER
AddChangesFromComplexProperties(context, entry, entry.ComplexProperties, result);
#endif

return result;
}

#if EF_CORE_8_OR_GREATER
/// <summary>
/// Gets the name of the column.
/// Adds the change values from the complex properties recursively
/// </summary>
internal static string GetColumnName(IProperty prop)
private void AddChangesFromComplexProperties(IAuditDbContext context, EntityEntry entry, IEnumerable<ComplexPropertyEntry> complexProperties, List<EventEntryChange> result)
{
#if EF_CORE_7_OR_GREATER
var storeObjectIdentifier = StoreObjectIdentifier.Create(prop.DeclaringEntityType, StoreObjectType.Table);
return storeObjectIdentifier.HasValue
? (prop.GetColumnName(storeObjectIdentifier.Value) ?? prop.GetDefaultColumnName())
: prop.GetDefaultColumnName();
#elif EF_CORE_5_OR_GREATER
var storeObjectIdentifier = StoreObjectIdentifier.Create(prop.DeclaringEntityType, StoreObjectType.Table);
return storeObjectIdentifier.HasValue
? prop.GetColumnName(storeObjectIdentifier.Value)
: prop.GetDefaultColumnBaseName();
#elif EF_CORE_3
return prop.GetColumnName();
#else
return prop.Relational().ColumnName ?? prop.Name;
#endif
foreach (var complexEntry in complexProperties)
{
// Process the primitive properties
foreach (var propEntry in complexEntry.Properties)
{
if (propEntry.IsModified && IncludeProperty(context, complexEntry.Metadata.ClrType, propEntry.Metadata.Name))
{
result.Add(new EventEntryChange()
{
ColumnName = GetColumnName(propEntry.Metadata),
NewValue = HasPropertyValue(context, entry, complexEntry.Metadata.ClrType, propEntry.Metadata.Name, propEntry.CurrentValue, out var overridenCurrentValue) ? overridenCurrentValue : propEntry.CurrentValue,
OriginalValue = HasPropertyValue(context, entry, complexEntry.Metadata.ClrType, propEntry.Metadata.Name, propEntry.OriginalValue, out var overridenOriginalValue) ? overridenOriginalValue : propEntry.OriginalValue
});
}
}

// Recursively process complex properties
AddChangesFromComplexProperties(context, entry, complexEntry.ComplexProperties, result);
}
}
#endif

/// <summary>
/// Gets the column values for an insert/delete operation.
Expand All @@ -78,7 +86,7 @@ private Dictionary<string, object> GetColumnValues(IAuditDbContext context, Enti
var props = entry.Metadata.GetProperties();
foreach (var prop in props)
{
PropertyEntry propEntry = entry.Property(prop.Name);
var propEntry = entry.Property(prop.Name);
if (IncludeProperty(context, entry, prop.Name))
{
object value = entry.State != EntityState.Deleted ? propEntry.CurrentValue : propEntry.OriginalValue;
Expand All @@ -89,8 +97,69 @@ private Dictionary<string, object> GetColumnValues(IAuditDbContext context, Enti
result.Add(GetColumnName(prop), value);
}
}

#if EF_CORE_8_OR_GREATER
AddColumnValuesFromComplexProperties(context, entry, entry.ComplexProperties, result);
#endif
return result;
}

#if EF_CORE_8_OR_GREATER
/// <summary>
/// Adds the column values from the complex properties recursively
/// </summary>
private void AddColumnValuesFromComplexProperties(IAuditDbContext context, EntityEntry entry, IEnumerable<ComplexPropertyEntry> complexProperties, Dictionary<string, object> result)
{
foreach (var complexEntry in complexProperties)
{
// Process the primitive properties
foreach (var propEntry in complexEntry.Properties)
{
if (IncludeProperty(context, complexEntry.Metadata.ClrType, propEntry.Metadata.Name))
{
var value = propEntry.CurrentValue;
if (HasPropertyValue(context, entry, complexEntry.Metadata.ClrType, propEntry.Metadata.Name, value, out object overrideValue))
{
value = overrideValue;
}

var columnName = GetColumnName(propEntry.Metadata);
result.Add(columnName, value);
}
}

// Recursively process complex properties
AddColumnValuesFromComplexProperties(context, entry, complexEntry.ComplexProperties, result);
}
}
#endif

/// <summary>
/// Gets the name of the column.
/// </summary>
internal static string GetColumnName(IProperty prop)
{
#if EF_CORE_8_OR_GREATER
var storeObjectIdentifier = StoreObjectIdentifier.Create(prop.DeclaringType, StoreObjectType.Table);
return storeObjectIdentifier.HasValue
? (prop.GetColumnName(storeObjectIdentifier.Value) ?? prop.GetDefaultColumnName())
: prop.GetDefaultColumnName();
#elif EF_CORE_7_OR_GREATER
var storeObjectIdentifier = StoreObjectIdentifier.Create(prop.DeclaringEntityType, StoreObjectType.Table);
return storeObjectIdentifier.HasValue
? (prop.GetColumnName(storeObjectIdentifier.Value) ?? prop.GetDefaultColumnName())
: prop.GetDefaultColumnName();
#elif EF_CORE_5_OR_GREATER
var storeObjectIdentifier = StoreObjectIdentifier.Create(prop.DeclaringEntityType, StoreObjectType.Table);
return storeObjectIdentifier.HasValue
? prop.GetColumnName(storeObjectIdentifier.Value)
: prop.GetDefaultColumnBaseName();
#elif EF_CORE_3
return prop.GetColumnName();
#else
return prop.Relational().ColumnName ?? prop.Name;
#endif
}

// Determines if the property should be included or is ignored
private bool IncludeProperty(IAuditDbContext context, EntityEntry entry, string propName)
Expand All @@ -100,7 +169,14 @@ private bool IncludeProperty(IAuditDbContext context, EntityEntry entry, string
{
return true;
}
var ignoredProperties = EnsurePropertiesIgnoreAttrCache(entityType);

return IncludeProperty(context, entityType, propName);
}

// Determines if the property should be included or is ignored
private bool IncludeProperty(IAuditDbContext context, Type entityType, string propName)
{
var ignoredProperties = EnsurePropertiesIgnoreAttrCache(entityType);
if (ignoredProperties != null && ignoredProperties.Contains(propName))
{
// Property marked with AuditIgnore attribute
Expand All @@ -123,6 +199,13 @@ private bool HasPropertyValue(IAuditDbContext context, EntityEntry entry, string
{
return false;
}

return HasPropertyValue(context, entry, entityType, propName, currentValue, out value);
}

private bool HasPropertyValue(IAuditDbContext context, EntityEntry entry, Type entityType, string propName, object currentValue, out object value)
{
value = null;
var overrideProperties = EnsurePropertiesOverrideAttrCache(entityType);
if (overrideProperties != null && overrideProperties.TryGetValue(propName, out var property))
{
Expand All @@ -147,7 +230,7 @@ private bool HasPropertyValue(IAuditDbContext context, EntityEntry entry, string
}
return false;
}

/// <summary>
/// Gets the name of the entity.
/// </summary>
Expand Down
4 changes: 2 additions & 2 deletions src/Audit.WebApi/Audit.WebApi.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,15 @@
</ItemGroup>

<ItemGroup Condition=" '$(TargetFramework)' == 'net45' ">
<PackageReference Include="Microsoft.AspNet.WebApi.Core" Version="5.2.6" />
<PackageReference Include="Microsoft.AspNet.WebApi.Core" Version="5.3.0" />
<PackageReference Include="Microsoft.AspNet.WebApi.Owin" Version="5.3.0" />
<Reference Include="System.Web" />
<Reference Include="System" />
<Reference Include="Microsoft.CSharp" />
</ItemGroup>

<ItemGroup Condition=" '$(TargetFramework)' == 'net461' ">
<PackageReference Include="Microsoft.AspNet.WebApi.Core" Version="5.2.6" />
<PackageReference Include="Microsoft.AspNet.WebApi.Core" Version="5.3.0" />
<PackageReference Include="Microsoft.AspNet.WebApi.Owin" Version="5.3.0" />
<Reference Include="System.Web" />
</ItemGroup>
Expand Down
51 changes: 51 additions & 0 deletions test/Audit.EntityFramework.Core.UnitTest/Contexts.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,60 @@
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace Audit.EntityFramework.Core.UnitTest
{
#if EF_CORE_8_OR_GREATER
[AuditDbContext(IncludeEntityObjects = true)]
public class Context_ComplexTypes : AuditDbContext
{
public class Person
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int Id { get; set; }
public string Name { get; set; }
[Required]
public required Address Address { get; set; }
}

//[ComplexType]
public record Address
{
public string Line1 { get; init; }
[AuditIgnore]
public string Line2 { get; init; }
public string City { get; init; }
[Required]
public required Country Country { get; init; }
public string PostCode { get; init; }
}

//[ComplexType]
public record Country
{
public string Name { get; init; }
public string Alias { get; init; }
}

public DbSet<Person> People { get; set; }

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
var cnnString = TestHelper.GetConnectionString(nameof(Context_ComplexTypes));
optionsBuilder.UseSqlServer(cnnString).UseLazyLoadingProxies();
}
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Person>().ComplexProperty(e => e.Address).ComplexProperty(a => a.Country);
}
}
#endif

#if EF_CORE_7_OR_GREATER
[AuditDbContext(IncludeEntityObjects = true)]
public class Context_OwnedEntity_ToJson : AuditDbContext
Expand Down
62 changes: 62 additions & 0 deletions test/Audit.EntityFramework.Core.UnitTest/EfCoreTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Audit.Core;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Internal;
using System.Threading.Tasks;

namespace Audit.EntityFramework.Core.UnitTest
{
Expand All @@ -26,6 +27,67 @@ public void Setup()
new DemoContext().Database.EnsureCreated();
}

#if EF_CORE_8_OR_GREATER
[Test]
public void Test_EF_ComplexType()
{
Audit.EntityFramework.Configuration.Setup()
.ForContext<Context_ComplexTypes>(c => c
.ForEntity<Context_ComplexTypes.Address>(a => a.Format(p => p.City, city => $"*{city}*"))
.ForEntity<Context_ComplexTypes.Country>(a => a.Format(p => p.Alias, alias => alias?.ToUpperInvariant())));

using var context = new Context_ComplexTypes();
var evs = new List<EntityFrameworkEvent>();
Audit.Core.Configuration.Setup()
.UseDynamicProvider(_ => _.OnInsertAndReplace(ev =>
{
evs.Add(ev.GetEntityFrameworkEvent());
}));

context.Database.EnsureDeleted();
context.Database.EnsureCreated();

var person = new Context_ComplexTypes.Person()
{
Id = 1,
Name = "Development",
Address = new Context_ComplexTypes.Address()
{
Country = new Context_ComplexTypes.Country() { Name = "Austria", Alias = "Au" },
City = "Vienna",
Line1 = "Street",
PostCode = "1234"
}
};

context.People.Add(person);
context.SaveChanges();

person.Name = "New Name";
person.Address = person.Address with { City = "NewCity", Country = person.Address.Country with { Alias = "newalias" } };
context.SaveChanges();

Assert.AreEqual(2, evs.Count);
Assert.AreEqual(1, evs[0].Entries.Count);
Assert.AreEqual(1, evs[1].Entries.Count);

Assert.AreEqual("Insert", evs[0].Entries[0].Action);
Assert.AreEqual("Update", evs[1].Entries[0].Action);

Assert.AreEqual("Development", evs[0].Entries[0].ColumnValues["Name"]);
Assert.AreEqual("New Name", evs[1].Entries[0].ColumnValues["Name"]);

Assert.AreEqual("*Vienna*", evs[0].Entries[0].ColumnValues["Address_City"]);
Assert.AreEqual("*NewCity*", evs[1].Entries[0].ColumnValues["Address_City"]);

Assert.AreEqual("AU", evs[0].Entries[0].ColumnValues["Address_Country_Alias"]);
Assert.AreEqual("NEWALIAS", evs[1].Entries[0].ColumnValues["Address_Country_Alias"]);

Assert.AreEqual("AU", evs[1].Entries[0].Changes.FirstOrDefault(ch => ch.ColumnName == "Address_Country_Alias")?.OriginalValue);
Assert.AreEqual("NEWALIAS", evs[1].Entries[0].Changes.FirstOrDefault(ch => ch.ColumnName == "Address_Country_Alias")?.NewValue);
}
#endif

#if EF_CORE_5_OR_GREATER

[Test]
Expand Down

0 comments on commit c00949f

Please sign in to comment.