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 1 commit
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
Prev Previous commit
Next Next commit
Provides Many-to-Many logging
Adds in the ability to see when a many-to-many relationship is changed,
and provides several extra pieces of metadata on the properties object
to give details on what changed, how it changed, and the new endpoints
to look at
  • Loading branch information
Robert Petz committed May 2, 2013
commit 469f9962f2bae68dbadf362c528bde144a8b0988
37 changes: 21 additions & 16 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 = objectStateEntry.EntitySet.ElementType as EntityType;

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 All @@ -54,7 +59,7 @@ public bool IsDeleted
}
public bool IsModified
{
get { return AuditEntity.Action == AuditAction.Modified; }
get { return AuditEntity.Action == AuditAction.Modified || AdditionalModifiedProperty != null; }
}

private static AuditAction GetAction(ObjectStateEntry entity)
Expand Down
102 changes: 84 additions & 18 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
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; }
}
}
99 changes: 99 additions & 0 deletions Source/EntityFramework.Extended/Audit/Extensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Metadata.Edm;
using System.Data.Objects;
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;
var key = entry.GetEndEntityKey(end);
var entitySet = key.GetEntitySet(
entry.ObjectStateManager.MetadataWorkspace);
var property = entitySet.ElementType.NavigationProperties.Where(
p => p.RelationshipType == relationshipType &&
p.FromEndMember == end && p.ToEndMember == otherEnd)
.SingleOrDefault();
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));
}
}
}
}
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