Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented Many to Many relationship logging #50

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 20 additions & 15 deletions Source/EntityFramework.Extended/Audit/AuditEntryState.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Metadata.Edm;
using System.Data.Objects;
using EntityFramework.Extensions;
using EntityFramework.Reflection;

namespace EntityFramework.Audit
Expand All @@ -10,31 +12,34 @@ internal class AuditEntryState
{
public AuditEntryState(ObjectStateEntry objectStateEntry)
{
if (objectStateEntry == null)
throw new ArgumentNullException("objectStateEntry");
if (objectStateEntry == null)
throw new ArgumentNullException("objectStateEntry");

if (objectStateEntry.Entity == null)
throw new ArgumentException("The Entity property is null for the specified ObjectStateEntry.", "objectStateEntry");
if (objectStateEntry.Entity == null)
throw new ArgumentException("The Entity property is null for the specified ObjectStateEntry.", "objectStateEntry");

ObjectStateEntry = objectStateEntry;
Entity = objectStateEntry.Entity;
ObjectStateEntry = objectStateEntry;
Entity = objectStateEntry.Entity;

EntityType = objectStateEntry.EntitySet.ElementType as EntityType;
EntityType = Extensions.GetInheritedEntityType(objectStateEntry);

Type entityType = objectStateEntry.Entity.GetType();
entityType = ObjectContext.GetObjectType(entityType);
Type entityType = objectStateEntry.Entity.GetType();
entityType = ObjectContext.GetObjectType(entityType);

ObjectType = entityType;
EntityAccessor = TypeAccessor.GetAccessor(entityType);
ObjectType = entityType;
EntityAccessor = TypeAccessor.GetAccessor(entityType);

AuditEntity = new AuditEntity(objectStateEntry.Entity)
{
Action = GetAction(objectStateEntry),
};
AuditEntity = new AuditEntity(objectStateEntry.Entity)
{
Action = GetAction(objectStateEntry),
};
}

public ObjectContext ObjectContext { get; set; }
public AuditLog AuditLog { get; set; }
public NavigationProperty AdditionalModifiedProperty { get; set; }
public object AdditionalModifiedPropertyEnd { get; set; }
public bool AdditionalModifiedPropertyIsAdd { get; set; }

public object Entity { get; private set; }
public Type ObjectType { get; private set; }
Expand Down
113 changes: 92 additions & 21 deletions Source/EntityFramework.Extended/Audit/AuditLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,16 +150,73 @@ public AuditLog UpdateLog(AuditLog auditLog)
// must call to make sure changes are detected
ObjectContext.DetectChanges();

IEnumerable<ObjectStateEntry> changes = ObjectContext
.ObjectStateManager
.GetObjectStateEntries(EntityState.Added | EntityState.Deleted | EntityState.Modified);
IEnumerable<ObjectStateEntry> changes = ObjectContext
.ObjectStateManager
.GetObjectStateEntries(EntityState.Added | EntityState.Deleted | EntityState.Modified)
.Where(t => !t.IsRelationship);


foreach (ObjectStateEntry objectStateEntry in ObjectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Added | EntityState.Deleted).Where(t => t.IsRelationship))
{
ObjectStateEntry entryLeft = null;
ObjectStateEntry entryRight = null;
var associationEnds = objectStateEntry.GetAssociationEnds();
NavigationProperty navigationPropertyLeft = objectStateEntry.GetNavigationProperty(associationEnds[0]);
NavigationProperty navigationPropertyRight = objectStateEntry.GetNavigationProperty(associationEnds[1]);

if (objectStateEntry.State == EntityState.Added)
{
entryLeft = ObjectContext.ObjectStateManager.GetObjectStateEntry((EntityKey)objectStateEntry.CurrentValues[0]);
entryRight = ObjectContext.ObjectStateManager.GetObjectStateEntry((EntityKey)objectStateEntry.CurrentValues[1]);
}
else if (objectStateEntry.State == EntityState.Deleted)
{
entryLeft = ObjectContext.ObjectStateManager.GetObjectStateEntry((EntityKey)objectStateEntry.OriginalValues[0]);
entryRight = ObjectContext.ObjectStateManager.GetObjectStateEntry((EntityKey)objectStateEntry.OriginalValues[1]);
}

Type entityTypeLeft = entryLeft.Entity.GetType();
entityTypeLeft = ObjectContext.GetObjectType(entityTypeLeft);
if (Configuration.IsAuditable(entityTypeLeft))
{
var stateLeft = new AuditEntryState(entryLeft)
{
AuditLog = auditLog,
ObjectContext = ObjectContext,
AdditionalModifiedProperty = navigationPropertyLeft,
AdditionalModifiedPropertyEnd = ObjectContext.GetObjectByKey(entryRight.EntityKey),
AdditionalModifiedPropertyIsAdd = objectStateEntry.State == EntityState.Added
};

if (WriteEntity(stateLeft))
auditLog.Entities.Add(stateLeft.AuditEntity);
}

Type entityTypeRight = entryRight.Entity.GetType();
entityTypeRight = ObjectContext.GetObjectType(entityTypeRight);
if (Configuration.IsAuditable(entityTypeRight))
{
var stateRight = new AuditEntryState(entryRight)
{
AuditLog = auditLog,
ObjectContext = ObjectContext,
AdditionalModifiedProperty = navigationPropertyRight,
AdditionalModifiedPropertyEnd = ObjectContext.GetObjectByKey(entryLeft.EntityKey),
AdditionalModifiedPropertyIsAdd = objectStateEntry.State == EntityState.Added
};

if (WriteEntity(stateRight))
auditLog.Entities.Add(stateRight.AuditEntity);
}
}

foreach (ObjectStateEntry objectStateEntry in changes)
{
if (objectStateEntry.Entity == null)
continue;

Type entityType = objectStateEntry.Entity.GetType();

foreach (ObjectStateEntry objectStateEntry in changes)
{
if (objectStateEntry.Entity == null)
continue;

Type entityType = objectStateEntry.Entity.GetType();
entityType = ObjectContext.GetObjectType(entityType);
if (!Configuration.IsAuditable(entityType))
continue;
Expand Down Expand Up @@ -308,15 +365,18 @@ private void WriteRelationships(AuditEntryState state)
if (!Configuration.IncludeRelationships)
return;

var properties = state.EntityType.NavigationProperties;
var properties = state.EntityType.NavigationProperties.ToList();
if (properties.Count == 0)
return;

var modifiedMembers = state.ObjectStateEntry
.GetModifiedProperties()
.ToList();

var type = state.ObjectType;
if (state.AdditionalModifiedProperty != null)
modifiedMembers.Add(state.AdditionalModifiedProperty.Name);

var type = state.ObjectType;

var currentValues = state.IsDeleted
? state.ObjectStateEntry.OriginalValues
Expand All @@ -328,10 +388,6 @@ private void WriteRelationships(AuditEntryState state)

foreach (NavigationProperty navigationProperty in properties)
{
if (navigationProperty.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many
|| navigationProperty.FromEndMember.RelationshipMultiplicity != RelationshipMultiplicity.Many)
continue;

string name = navigationProperty.Name;
if (Configuration.IsNotAudited(type, name))
continue;
Expand All @@ -343,12 +399,16 @@ private void WriteRelationships(AuditEntryState state)

bool isModified = IsModifed(navigationProperty, modifiedMembers);

if (navigationProperty == state.AdditionalModifiedProperty) isModified = true;

if (state.IsModified && !isModified
&& !Configuration.IsAlwaysAudited(type, name))
continue; // this means the property was not changed, skip it

bool isLoaded = IsLoaded(state, navigationProperty, accessor);
if (!isLoaded && !Configuration.LoadRelationships)
bool isLoaded = navigationProperty == state.AdditionalModifiedProperty ||
IsLoaded(state, navigationProperty, accessor);

if (!isLoaded && !Configuration.LoadRelationships)
continue;

var auditProperty = new AuditProperty();
Expand All @@ -362,7 +422,13 @@ private void WriteRelationships(AuditEntryState state)

object currentValue;

if (isLoaded)
if (isLoaded && navigationProperty == state.AdditionalModifiedProperty)
{
currentValue = state.AdditionalModifiedPropertyEnd;
auditProperty.IsManyToMany = true;
auditProperty.IsManyToManyAdd = state.AdditionalModifiedPropertyIsAdd;
}
else if (isLoaded)
{
// get value directly from instance to save db call
object valueInstance = accessor.GetValue(state.Entity);
Expand Down Expand Up @@ -503,7 +569,7 @@ private static object GetDisplayValue(AuditEntryState state, NavigationProperty
return null;

var edmType = referentialConstraint
.FromProperties
.ToProperties
.Select(p => p.DeclaringType)
.FirstOrDefault();

Expand Down Expand Up @@ -568,13 +634,18 @@ private static bool IsLoaded(AuditEntryState state, NavigationProperty navigatio
{
var relationshipManager = state.ObjectStateEntry.RelationshipManager;
var getEntityReference = _relatedAccessor.Value.MakeGenericMethod(accessor.MemberType);
var parameters = new[]
var parameters = new object[]
{
navigationProperty.RelationshipType.FullName,
navigationProperty.ToEndMember.Name
};

var entityReference = getEntityReference.Invoke(relationshipManager, parameters) as EntityReference;
EntityReference entityReference = null;
try
{
entityReference = getEntityReference.Invoke(relationshipManager, parameters) as EntityReference;
}
catch(TargetInvocationException){}
return (entityReference != null && entityReference.IsLoaded);
}

Expand Down
14 changes: 14 additions & 0 deletions Source/EntityFramework.Extended/Audit/AuditProperty.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,19 @@ public class AuditProperty
/// <value>The original value of the property.</value>
[XmlElement("original")]
public object Original { get; set; }

/// <summary>
/// Gets or sets the is many to many value of the property
/// </summary>
/// <value>True if this represents a change to a many to many relationship</value>
[XmlElement("isManyToMany")]
public bool IsManyToMany { get; set; }

/// <summary>
/// Gets or sets the is many to many value of the property
/// </summary>
/// <value>True if this many to many change represents an insert, false if a delete</value>
[XmlElement("isManyToManyAdd")]
public bool IsManyToManyAdd { get; set; }
}
}
118 changes: 118 additions & 0 deletions Source/EntityFramework.Extended/Audit/Extensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Metadata.Edm;
using System.Data.Objects;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace EntityFramework.Audit
{
public static class Extensions
{
public static IExtendedDataRecord UsableValues(this ObjectStateEntry entry)
{
switch (entry.State)
{
case EntityState.Added:
case EntityState.Detached:
case EntityState.Unchanged:
case EntityState.Modified:
return (IExtendedDataRecord)entry.CurrentValues;
case EntityState.Deleted:
return (IExtendedDataRecord)entry.OriginalValues;
default:
throw new InvalidOperationException("This entity state should not exist.");
}
}

public static AssociationEndMember[] GetAssociationEnds(this ObjectStateEntry entry)
{
var fieldMetadata =
entry.UsableValues().DataRecordInfo.FieldMetadata;

return fieldMetadata.Select(
m => m.FieldType as AssociationEndMember).ToArray();
}

public static AssociationEndMember GetOtherAssociationEnd(
this ObjectStateEntry entry, AssociationEndMember end)
{
end.ValidateBelongsTo(entry);
AssociationEndMember[] ends = entry.GetAssociationEnds();
if (ends[0] == end)
{
return ends[1];
}
return ends[0];
}

public static EntityKey GetEndEntityKey(this ObjectStateEntry entry, AssociationEndMember end)
{
end.ValidateBelongsTo(entry);

AssociationEndMember[] ends = entry.GetAssociationEnds();

if (ends[0] == end)
{
return entry.UsableValues()[0] as EntityKey;
}

return entry.UsableValues()[1] as EntityKey;
}

public static NavigationProperty GetNavigationProperty(this ObjectStateEntry entry, AssociationEndMember end)
{
end.ValidateBelongsTo(entry);

var otherEnd = entry.GetOtherAssociationEnd(end);
var relationshipType = entry.EntitySet.ElementType;

EntityType elementType = GetInheritedEntityTypeByEntityName(entry, end.Name);
NavigationProperty property =
elementType.NavigationProperties.SingleOrDefault(p => p.RelationshipType == relationshipType &&
p.FromEndMember == end && p.ToEndMember == otherEnd);
return property;
}

static void ValidateBelongsTo(this AssociationEndMember end, ObjectStateEntry entry)
{
if (!entry.IsRelationship)
{
throw new ArgumentException("is not a relationship entry", "entry");
}

var fieldMetadata =
entry.UsableValues().DataRecordInfo.FieldMetadata;
if (fieldMetadata[0].FieldType as AssociationEndMember != end &&
fieldMetadata[1].FieldType as AssociationEndMember != end)
{
throw new InvalidOperationException(string.Format(
"association end {0} does not participate in the " +
"relationship {1}", end, entry));
}
}

public static EntityType GetInheritedEntityType(ObjectStateEntry entry)
{
if (entry.Entity == null)
{
throw new ArgumentException("entity is null", "entry");
}
string entityName = entry.Entity.GetType().Name;
return GetInheritedEntityTypeByEntityName(entry, entityName);
}

static EntityType GetInheritedEntityTypeByEntityName(ObjectStateEntry entry, string entityName)
{
var stateEntryEdmType =
(EntityType)
entry.ObjectStateManager.MetadataWorkspace.GetType(entityName, entry.EntitySet.ElementType.NamespaceName,
DataSpace.CSpace);

return stateEntryEdmType;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,7 @@
<AssemblyOriginatorKeyFile>..\EntityFramework.Extended.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>
<ItemGroup>
<Reference Include="EntityFramework, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<Reference Include="EntityFramework">
<HintPath>..\packages\EntityFramework.5.0.0\lib\net45\EntityFramework.dll</HintPath>
</Reference>
<Reference Include="System" />
Expand Down Expand Up @@ -73,6 +72,7 @@
<Compile Include="Audit\AuditKeyCollection.cs" />
<Compile Include="Audit\AuditLog.cs" />
<Compile Include="Audit\AuditLogger.cs" />
<Compile Include="Audit\Extensions.cs" />
<Compile Include="Batch\IBatchRunner.cs" />
<Compile Include="Batch\SqlServerBatchRunner.cs" />
<Compile Include="Caching\CacheExpirationMode.cs" />
Expand Down
Loading