From 1274fbaefdf39230b96ad1b700b0af86557afcdb Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 28 Apr 2017 18:14:50 +1000 Subject: [PATCH] Fixes up the equality members for the event args types to include Gethash code and sequence equal checking, this is done by a base class used by event args that deal with collections. --- .../Events/CancellableObjectEventArgs.cs | 133 +++++++++++----- src/Umbraco.Core/Events/DeleteEventArgs.cs | 5 +- src/Umbraco.Core/Events/ImportEventArgs.cs | 2 +- .../Events/ImportPackageEventArgs.cs | 5 +- src/Umbraco.Core/Events/PublishEventArgs.cs | 2 +- src/Umbraco.Core/Events/SaveEventArgs.cs | 5 +- src/Umbraco.Core/HashCodeCombiner.cs | 149 +++++++++--------- src/Umbraco.Core/HashCodeHelper.cs | 101 ++++++++++++ src/Umbraco.Core/OrderedHashSet.cs | 2 +- src/Umbraco.Core/TypeHelper.cs | 3 +- src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../Scoping/ScopeEventDispatcherTests.cs | 73 ++++++++- 12 files changed, 348 insertions(+), 133 deletions(-) create mode 100644 src/Umbraco.Core/HashCodeHelper.cs diff --git a/src/Umbraco.Core/Events/CancellableObjectEventArgs.cs b/src/Umbraco.Core/Events/CancellableObjectEventArgs.cs index 6787e6aa7a..1c9b08f636 100644 --- a/src/Umbraco.Core/Events/CancellableObjectEventArgs.cs +++ b/src/Umbraco.Core/Events/CancellableObjectEventArgs.cs @@ -1,11 +1,13 @@ using System; +using System.Collections; using System.Collections.Generic; +using System.Linq; using System.Security.Permissions; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; namespace Umbraco.Core.Events -{ +{ /// /// Used as a base class for the generic type CancellableObjectEventArgs{T} so that we can get direct 'object' access to the underlying EventObject /// @@ -48,36 +50,36 @@ namespace Umbraco.Core.Events /// internal object EventObject { get; set; } - } - + } + /// /// Event args for a strongly typed object that can support cancellation /// /// - [HostProtection(SecurityAction.LinkDemand, SharedState = true)] - public class CancellableObjectEventArgs : CancellableObjectEventArgs, IEquatable> + [HostProtection(SecurityAction.LinkDemand, SharedState = true)] + public class CancellableObjectEventArgs : CancellableObjectEventArgs, IEquatable> { - public CancellableObjectEventArgs(T eventObject, bool canCancel, EventMessages messages, IDictionary additionalData) + public CancellableObjectEventArgs(T eventObject, bool canCancel, EventMessages messages, IDictionary additionalData) : base(eventObject, canCancel, messages, additionalData) { } - public CancellableObjectEventArgs(T eventObject, bool canCancel, EventMessages eventMessages) + public CancellableObjectEventArgs(T eventObject, bool canCancel, EventMessages eventMessages) : base(eventObject, canCancel, eventMessages) { } - public CancellableObjectEventArgs(T eventObject, EventMessages eventMessages) + public CancellableObjectEventArgs(T eventObject, EventMessages eventMessages) : base(eventObject, eventMessages) { } - public CancellableObjectEventArgs(T eventObject, bool canCancel) + public CancellableObjectEventArgs(T eventObject, bool canCancel) : base(eventObject, canCancel) { } - public CancellableObjectEventArgs(T eventObject) + public CancellableObjectEventArgs(T eventObject) : base(eventObject) { } @@ -90,41 +92,86 @@ namespace Umbraco.Core.Events /// protected new T EventObject { - get { return (T)base.EventObject; } + get { return (T) base.EventObject; } set { base.EventObject = value; } } - - public bool Equals(CancellableObjectEventArgs other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; + + public bool Equals(CancellableObjectEventArgs other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; return base.Equals(other) && EqualityComparer.Default.Equals(EventObject, other.EventObject); - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((CancellableObjectEventArgs)obj); - } - - public override int GetHashCode() - { - unchecked - { - return (base.GetHashCode() * 397) ^ EqualityComparer.Default.GetHashCode(EventObject); - } - } - - public static bool operator ==(CancellableObjectEventArgs left, CancellableObjectEventArgs right) - { - return Equals(left, right); - } - - public static bool operator !=(CancellableObjectEventArgs left, CancellableObjectEventArgs right) - { - return !Equals(left, right); - } + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((CancellableObjectEventArgs)obj); + } + + public override int GetHashCode() + { + unchecked + { + return (base.GetHashCode() * 397) ^ EqualityComparer.Default.GetHashCode(EventObject); + } + } + + public static bool operator ==(CancellableObjectEventArgs left, CancellableObjectEventArgs right) + { + return Equals(left, right); + } + + public static bool operator !=(CancellableObjectEventArgs left, CancellableObjectEventArgs right) + { + return !Equals(left, right); + } + } + + [HostProtection(SecurityAction.LinkDemand, SharedState = true)] + public class CancellableEnumerableObjectEventArgs : CancellableObjectEventArgs>, IEquatable> + { + public CancellableEnumerableObjectEventArgs(IEnumerable eventObject, bool canCancel, EventMessages messages, IDictionary additionalData) : base(eventObject, canCancel, messages, additionalData) + { + } + + public CancellableEnumerableObjectEventArgs(IEnumerable eventObject, bool canCancel, EventMessages eventMessages) : base(eventObject, canCancel, eventMessages) + { + } + + public CancellableEnumerableObjectEventArgs(IEnumerable eventObject, EventMessages eventMessages) : base(eventObject, eventMessages) + { + } + + public CancellableEnumerableObjectEventArgs(IEnumerable eventObject, bool canCancel) : base(eventObject, canCancel) + { + } + + public CancellableEnumerableObjectEventArgs(IEnumerable eventObject) : base(eventObject) + { + } + + public bool Equals(CancellableEnumerableObjectEventArgs other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + + return EventObject.SequenceEqual(other.EventObject); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((CancellableEnumerableObjectEventArgs)obj); + } + + public override int GetHashCode() + { + return HashCodeHelper.GetHashCode(EventObject); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/DeleteEventArgs.cs b/src/Umbraco.Core/Events/DeleteEventArgs.cs index 64cd4720cd..d0a4f024e1 100644 --- a/src/Umbraco.Core/Events/DeleteEventArgs.cs +++ b/src/Umbraco.Core/Events/DeleteEventArgs.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; namespace Umbraco.Core.Events { @@ -7,7 +8,7 @@ namespace Umbraco.Core.Events [SupersedeEvent(typeof(PublishEventArgs<>))] [SupersedeEvent(typeof(MoveEventArgs<>))] [SupersedeEvent(typeof(CopyEventArgs<>))] - public class DeleteEventArgs : CancellableObjectEventArgs>, IEquatable>, IDeletingMediaFilesEventArgs + public class DeleteEventArgs : CancellableEnumerableObjectEventArgs, IEquatable>, IDeletingMediaFilesEventArgs { /// /// Constructor accepting multiple entities that are used in the delete operation @@ -110,7 +111,7 @@ namespace Umbraco.Core.Events { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; - return base.Equals(other) && MediaFilesToDelete.Equals(other.MediaFilesToDelete); + return base.Equals(other) && MediaFilesToDelete.SequenceEqual(other.MediaFilesToDelete); } public override bool Equals(object obj) diff --git a/src/Umbraco.Core/Events/ImportEventArgs.cs b/src/Umbraco.Core/Events/ImportEventArgs.cs index dcecf5c36b..892149c0a2 100644 --- a/src/Umbraco.Core/Events/ImportEventArgs.cs +++ b/src/Umbraco.Core/Events/ImportEventArgs.cs @@ -4,7 +4,7 @@ using System.Xml.Linq; namespace Umbraco.Core.Events { - public class ImportEventArgs : CancellableObjectEventArgs>, IEquatable> + public class ImportEventArgs : CancellableEnumerableObjectEventArgs, IEquatable> { /// /// Constructor accepting an XElement with the xml being imported diff --git a/src/Umbraco.Core/Events/ImportPackageEventArgs.cs b/src/Umbraco.Core/Events/ImportPackageEventArgs.cs index 231e58c07e..4477faea50 100644 --- a/src/Umbraco.Core/Events/ImportPackageEventArgs.cs +++ b/src/Umbraco.Core/Events/ImportPackageEventArgs.cs @@ -4,7 +4,7 @@ using Umbraco.Core.Packaging.Models; namespace Umbraco.Core.Events { - internal class ImportPackageEventArgs : CancellableObjectEventArgs>, IEquatable> + internal class ImportPackageEventArgs : CancellableEnumerableObjectEventArgs, IEquatable> { private readonly MetaData _packageMetaData; @@ -32,7 +32,8 @@ namespace Umbraco.Core.Events public bool Equals(ImportPackageEventArgs other) { if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; + if (ReferenceEquals(this, other)) return true; + //TODO: MetaData for package metadata has no equality operators :/ return base.Equals(other) && _packageMetaData.Equals(other._packageMetaData); } diff --git a/src/Umbraco.Core/Events/PublishEventArgs.cs b/src/Umbraco.Core/Events/PublishEventArgs.cs index 1aa7c2308c..10bf94146c 100644 --- a/src/Umbraco.Core/Events/PublishEventArgs.cs +++ b/src/Umbraco.Core/Events/PublishEventArgs.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; namespace Umbraco.Core.Events { - public class PublishEventArgs : CancellableObjectEventArgs>, IEquatable> + public class PublishEventArgs : CancellableEnumerableObjectEventArgs, IEquatable> { /// /// Constructor accepting multiple entities that are used in the publish operation diff --git a/src/Umbraco.Core/Events/SaveEventArgs.cs b/src/Umbraco.Core/Events/SaveEventArgs.cs index e816a8f8bd..cd19038d8e 100644 --- a/src/Umbraco.Core/Events/SaveEventArgs.cs +++ b/src/Umbraco.Core/Events/SaveEventArgs.cs @@ -1,8 +1,9 @@ using System.Collections.Generic; +using System.Linq; namespace Umbraco.Core.Events { - public class SaveEventArgs : CancellableObjectEventArgs> + public class SaveEventArgs : CancellableEnumerableObjectEventArgs { /// /// Constructor accepting multiple entities that are used in the saving operation @@ -116,7 +117,5 @@ namespace Umbraco.Core.Events { get { return EventObject; } } - - } } \ No newline at end of file diff --git a/src/Umbraco.Core/HashCodeCombiner.cs b/src/Umbraco.Core/HashCodeCombiner.cs index b97a3cfacf..d3a55d5256 100644 --- a/src/Umbraco.Core/HashCodeCombiner.cs +++ b/src/Umbraco.Core/HashCodeCombiner.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; @@ -7,32 +6,32 @@ using System.Text; namespace Umbraco.Core { - /// - /// Used to create a hash code from multiple objects. - /// - /// - /// .Net has a class the same as this: System.Web.Util.HashCodeCombiner and of course it works for all sorts of things - /// which we've not included here as we just need a quick easy class for this in order to create a unique - /// hash of directories/files to see if they have changed. - /// - internal class HashCodeCombiner - { - private long _combinedHash = 5381L; + /// + /// Used to create a hash code from multiple objects. + /// + /// + /// .Net has a class the same as this: System.Web.Util.HashCodeCombiner and of course it works for all sorts of things + /// which we've not included here as we just need a quick easy class for this in order to create a unique + /// hash of directories/files to see if they have changed. + /// + internal class HashCodeCombiner + { + private long _combinedHash = 5381L; - internal void AddInt(int i) - { - _combinedHash = ((_combinedHash << 5) + _combinedHash) ^ i; - } + internal void AddInt(int i) + { + _combinedHash = ((_combinedHash << 5) + _combinedHash) ^ i; + } - internal void AddObject(object o) - { - AddInt(o.GetHashCode()); - } + internal void AddObject(object o) + { + AddInt(o.GetHashCode()); + } - internal void AddDateTime(DateTime d) - { - AddInt(d.GetHashCode()); - } + internal void AddDateTime(DateTime d) + { + AddInt(d.GetHashCode()); + } internal void AddString(string s) { @@ -40,61 +39,61 @@ namespace Umbraco.Core AddInt((StringComparer.InvariantCulture).GetHashCode(s)); } - internal void AddCaseInsensitiveString(string s) - { - if (s != null) - AddInt((StringComparer.InvariantCultureIgnoreCase).GetHashCode(s)); - } + internal void AddCaseInsensitiveString(string s) + { + if (s != null) + AddInt((StringComparer.InvariantCultureIgnoreCase).GetHashCode(s)); + } - internal void AddFileSystemItem(FileSystemInfo f) - { - //if it doesn't exist, don't proceed. - if (!f.Exists) - return; + internal void AddFileSystemItem(FileSystemInfo f) + { + //if it doesn't exist, don't proceed. + if (!f.Exists) + return; - AddCaseInsensitiveString(f.FullName); - AddDateTime(f.CreationTimeUtc); - AddDateTime(f.LastWriteTimeUtc); - - //check if it is a file or folder - var fileInfo = f as FileInfo; - if (fileInfo != null) - { - AddInt(fileInfo.Length.GetHashCode()); - } - - var dirInfo = f as DirectoryInfo; - if (dirInfo != null) - { - foreach (var d in dirInfo.GetFiles()) - { - AddFile(d); - } - foreach (var s in dirInfo.GetDirectories()) - { - AddFolder(s); - } - } - } + AddCaseInsensitiveString(f.FullName); + AddDateTime(f.CreationTimeUtc); + AddDateTime(f.LastWriteTimeUtc); - internal void AddFile(FileInfo f) - { - AddFileSystemItem(f); - } + //check if it is a file or folder + var fileInfo = f as FileInfo; + if (fileInfo != null) + { + AddInt(fileInfo.Length.GetHashCode()); + } - internal void AddFolder(DirectoryInfo d) - { - AddFileSystemItem(d); - } + var dirInfo = f as DirectoryInfo; + if (dirInfo != null) + { + foreach (var d in dirInfo.GetFiles()) + { + AddFile(d); + } + foreach (var s in dirInfo.GetDirectories()) + { + AddFolder(s); + } + } + } - /// - /// Returns the hex code of the combined hash code - /// - /// - internal string GetCombinedHashCode() - { - return _combinedHash.ToString("x", CultureInfo.InvariantCulture); - } + internal void AddFile(FileInfo f) + { + AddFileSystemItem(f); + } - } + internal void AddFolder(DirectoryInfo d) + { + AddFileSystemItem(d); + } + + /// + /// Returns the hex code of the combined hash code + /// + /// + internal string GetCombinedHashCode() + { + return _combinedHash.ToString("x", CultureInfo.InvariantCulture); + } + + } } diff --git a/src/Umbraco.Core/HashCodeHelper.cs b/src/Umbraco.Core/HashCodeHelper.cs new file mode 100644 index 0000000000..a3cedbdbb5 --- /dev/null +++ b/src/Umbraco.Core/HashCodeHelper.cs @@ -0,0 +1,101 @@ +using System.Collections.Generic; + +namespace Umbraco.Core +{ + /// + /// Borrowed from http://stackoverflow.com/a/2575444/694494 + /// + internal static class HashCodeHelper + { + public static int GetHashCode(T1 arg1, T2 arg2) + { + unchecked + { + return 31 * arg1.GetHashCode() + arg2.GetHashCode(); + } + } + + public static int GetHashCode(T1 arg1, T2 arg2, T3 arg3) + { + unchecked + { + int hash = arg1.GetHashCode(); + hash = 31 * hash + arg2.GetHashCode(); + return 31 * hash + arg3.GetHashCode(); + } + } + + public static int GetHashCode(T1 arg1, T2 arg2, T3 arg3, + T4 arg4) + { + unchecked + { + int hash = arg1.GetHashCode(); + hash = 31 * hash + arg2.GetHashCode(); + hash = 31 * hash + arg3.GetHashCode(); + return 31 * hash + arg4.GetHashCode(); + } + } + + public static int GetHashCode(T[] list) + { + unchecked + { + int hash = 0; + foreach (var item in list) + { + hash = 31 * hash + item.GetHashCode(); + } + return hash; + } + } + + public static int GetHashCode(IEnumerable list) + { + unchecked + { + int hash = 0; + foreach (var item in list) + { + hash = 31 * hash + item.GetHashCode(); + } + return hash; + } + } + + /// + /// Gets a hashcode for a collection for that the order of items + /// does not matter. + /// So {1, 2, 3} and {3, 2, 1} will get same hash code. + /// + public static int GetHashCodeForOrderNoMatterCollection( + IEnumerable list) + { + unchecked + { + int hash = 0; + int count = 0; + foreach (var item in list) + { + hash += item.GetHashCode(); + count++; + } + return 31 * hash + count.GetHashCode(); + } + } + + /// + /// Alternative way to get a hashcode is to use a fluent + /// interface like this:
+ /// return 0.CombineHashCode(field1).CombineHashCode(field2). + /// CombineHashCode(field3); + ///
+ public static int CombineHashCode(this int hashCode, T arg) + { + unchecked + { + return 31 * hashCode + arg.GetHashCode(); + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/OrderedHashSet.cs b/src/Umbraco.Core/OrderedHashSet.cs index 2fd545c915..801f1a9a41 100644 --- a/src/Umbraco.Core/OrderedHashSet.cs +++ b/src/Umbraco.Core/OrderedHashSet.cs @@ -11,7 +11,7 @@ namespace Umbraco.Core { private readonly bool _keepOldest; - public OrderedHashSet(bool keepOldest = true) + public OrderedHashSet(bool keepOldest = true) { _keepOldest = keepOldest; } diff --git a/src/Umbraco.Core/TypeHelper.cs b/src/Umbraco.Core/TypeHelper.cs index 1aabf3cef4..f8e10ff1fb 100644 --- a/src/Umbraco.Core/TypeHelper.cs +++ b/src/Umbraco.Core/TypeHelper.cs @@ -19,9 +19,8 @@ namespace Umbraco.Core private static readonly ConcurrentDictionary GetFieldsCache = new ConcurrentDictionary(); - private static readonly Assembly[] EmptyAssemblies = new Assembly[0]; + private static readonly Assembly[] EmptyAssemblies = new Assembly[0]; - /// /// Based on a type we'll check if it is IEnumerable{T} (or similar) and if so we'll return a List{T}, this will also deal with array types and return List{T} for those too. /// If it cannot be done, null is returned. diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index eae33101f8..270fbbc323 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -341,6 +341,7 @@ + diff --git a/src/Umbraco.Tests/Scoping/ScopeEventDispatcherTests.cs b/src/Umbraco.Tests/Scoping/ScopeEventDispatcherTests.cs index b3b1ab271f..825432756b 100644 --- a/src/Umbraco.Tests/Scoping/ScopeEventDispatcherTests.cs +++ b/src/Umbraco.Tests/Scoping/ScopeEventDispatcherTests.cs @@ -158,14 +158,14 @@ namespace Umbraco.Tests.Scoping content1.UpdateDate = now.AddMinutes(1); var content2 = MockedContent.CreateBasicContent(contentType); content2.Id = 123; - content1.UpdateDate = now.AddMinutes(2); + content2.UpdateDate = now.AddMinutes(2); var content3 = MockedContent.CreateBasicContent(contentType); content3.Id = 123; - content1.UpdateDate = now.AddMinutes(3); + content3.UpdateDate = now.AddMinutes(3); var scopeProvider = new ScopeProvider(Mock.Of()); using (var scope = scopeProvider.CreateScope(eventDispatcher: new PassiveEventDispatcher())) - { + { scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs(content1)); scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs(content2)); scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs(content3)); @@ -180,11 +180,78 @@ namespace Umbraco.Tests.Scoping foreach (var entity in args.SavedEntities) { Assert.AreEqual(content3, entity); + Assert.IsTrue(object.ReferenceEquals(content3, entity)); } } } } + [Test] + public void FirstIn() + { + DoSaveForContent += OnDoThingFail; + + var now = DateTime.Now; + var contentType = MockedContentTypes.CreateBasicContentType(); + var content1 = MockedContent.CreateBasicContent(contentType); + content1.Id = 123; + content1.UpdateDate = now.AddMinutes(1); + var content2 = MockedContent.CreateBasicContent(contentType); + content2.Id = 123; + content1.UpdateDate = now.AddMinutes(2); + var content3 = MockedContent.CreateBasicContent(contentType); + content3.Id = 123; + content1.UpdateDate = now.AddMinutes(3); + + var scopeProvider = new ScopeProvider(Mock.Of()); + using (var scope = scopeProvider.CreateScope(eventDispatcher: new PassiveEventDispatcher())) + { + scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs(content1)); + scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs(content2)); + scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs(content3)); + + // events have been queued + var events = scope.Events.GetEvents(EventDefinitionFilter.FirstIn).ToArray(); + Assert.AreEqual(1, events.Length); + Assert.AreEqual(content1, ((SaveEventArgs) events[0].Args).SavedEntities.First()); + Assert.IsTrue(object.ReferenceEquals(content1, ((SaveEventArgs)events[0].Args).SavedEntities.First())); + Assert.AreEqual(content1.UpdateDate, ((SaveEventArgs) events[0].Args).SavedEntities.First().UpdateDate); + } + } + + [Test] + public void LastIn() + { + DoSaveForContent += OnDoThingFail; + + var now = DateTime.Now; + var contentType = MockedContentTypes.CreateBasicContentType(); + var content1 = MockedContent.CreateBasicContent(contentType); + content1.Id = 123; + content1.UpdateDate = now.AddMinutes(1); + var content2 = MockedContent.CreateBasicContent(contentType); + content2.Id = 123; + content2.UpdateDate = now.AddMinutes(2); + var content3 = MockedContent.CreateBasicContent(contentType); + content3.Id = 123; + content3.UpdateDate = now.AddMinutes(3); + + var scopeProvider = new ScopeProvider(Mock.Of()); + using (var scope = scopeProvider.CreateScope(eventDispatcher: new PassiveEventDispatcher())) + { + scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs(content1)); + scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs(content2)); + scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs(content3)); + + // events have been queued + var events = scope.Events.GetEvents(EventDefinitionFilter.LastIn).ToArray(); + Assert.AreEqual(1, events.Length); + Assert.AreEqual(content3, ((SaveEventArgs)events[0].Args).SavedEntities.First()); + Assert.IsTrue(object.ReferenceEquals(content3, ((SaveEventArgs)events[0].Args).SavedEntities.First())); + Assert.AreEqual(content3.UpdateDate, ((SaveEventArgs)events[0].Args).SavedEntities.First().UpdateDate); + } + } + [TestCase(true)] [TestCase(false)] public void EventsDispatching_Passive(bool complete)