├── Directory.Build.props ├── Source ├── ChangeTracking.Tests │ ├── AssemblyInfo.cs │ ├── Lead.cs │ ├── OrderDetail.cs │ ├── Address.cs │ ├── IRevertibleChangeTrackingTests.cs │ ├── InventoryUpdate.cs │ ├── INotifyCollectionChangedTests.cs │ ├── ChangeTracking.Tests.csproj │ ├── GraphTests.cs │ ├── Order.cs │ ├── Helper.cs │ ├── InternalTests.cs │ ├── SpeedTest.cs │ ├── DoNoTrackTests.cs │ ├── IEditableObjectTests.cs │ ├── IBindingListTests.cs │ ├── ProxyTests.cs │ ├── INotifyPropertyChangedTests.cs │ ├── IChangeTrackableCollectionTests.cs │ └── IChangeTrackableTests.cs ├── ChangeTracking │ ├── IInterceptorSettings.cs │ ├── IChangeTrackingManager.cs │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── ICollectionPropertyTrackable.cs │ ├── IComplexPropertyTrackable.cs │ ├── Internal │ │ ├── IChangeTrackableInternal.cs │ │ ├── IRevertibleChangeTrackingInternal.cs │ │ ├── IEditableObjectInternal.cs │ │ ├── ChangeTrackingSettings.cs │ │ ├── Extensions.cs │ │ ├── Utils.cs │ │ └── ProxyTargetMap.cs │ ├── DoNoTrackAttribute.cs │ ├── ChangeStatus.cs │ ├── ChangeTrackingInterceptorSelector.cs │ ├── IChangeTrackingFactory.cs │ ├── IChangeTrackableCollection.cs │ ├── Extensions.cs │ ├── ChangeTracking.csproj │ ├── ChangeTrackingProxyGenerationHook.cs │ ├── IChangeTrackable.cs │ ├── Core.cs │ ├── ChangeTrackingBindingList.cs │ ├── EditableObjectInterceptor.cs │ ├── NotifyPropertyChangedInterceptor.cs │ ├── ChangeTrackingCollectionInterceptor.cs │ ├── CollectionPropertyInterceptor.cs │ ├── ComplexPropertyInterceptor.cs │ ├── ChangeTrackingFactory.cs │ └── ChangeTrackingInterceptor.cs └── ChangeTracking.sln ├── .gitattributes ├── appveyor.yml ├── License.md ├── .gitignore └── README.md /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | latest 4 | 5 | -------------------------------------------------------------------------------- /Source/ChangeTracking.Tests/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] -------------------------------------------------------------------------------- /Source/ChangeTracking.Tests/Lead.cs: -------------------------------------------------------------------------------- 1 | namespace ChangeTracking.Tests 2 | { 3 | [DoNoTrack] 4 | public class Lead 5 | { 6 | public virtual int LeadId { get; set; } 7 | } 8 | } -------------------------------------------------------------------------------- /Source/ChangeTracking/IInterceptorSettings.cs: -------------------------------------------------------------------------------- 1 | namespace ChangeTracking 2 | { 3 | internal interface IInterceptorSettings 4 | { 5 | bool IsInitialized { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Source/ChangeTracking/IChangeTrackingManager.cs: -------------------------------------------------------------------------------- 1 | namespace ChangeTracking 2 | { 3 | internal interface IChangeTrackingManager 4 | { 5 | bool Delete(); 6 | bool UnDelete(); 7 | } 8 | } -------------------------------------------------------------------------------- /Source/ChangeTracking/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] 4 | [assembly: InternalsVisibleTo("ChangeTracking.Tests")] -------------------------------------------------------------------------------- /Source/ChangeTracking/ICollectionPropertyTrackable.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace ChangeTracking 4 | { 5 | internal interface ICollectionPropertyTrackable 6 | { 7 | IEnumerable CollectionPropertyTrackables { get; } 8 | } 9 | } -------------------------------------------------------------------------------- /Source/ChangeTracking/IComplexPropertyTrackable.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace ChangeTracking 4 | { 5 | internal interface IComplexPropertyTrackable 6 | { 7 | IEnumerable ComplexPropertyTrackables { get; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Source/ChangeTracking/Internal/IChangeTrackableInternal.cs: -------------------------------------------------------------------------------- 1 | namespace ChangeTracking.Internal 2 | { 3 | internal interface IChangeTrackableInternal 4 | { 5 | object GetOriginal(UnrollGraph unrollGraph); 6 | object GetCurrent(UnrollGraph unrollGraph); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Source/ChangeTracking/DoNoTrackAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ChangeTracking 4 | { 5 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, Inherited = false, AllowMultiple = false)] 6 | public sealed class DoNoTrackAttribute : Attribute 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Source/ChangeTracking.Tests/OrderDetail.cs: -------------------------------------------------------------------------------- 1 | namespace ChangeTracking.Tests 2 | { 3 | public class OrderDetail 4 | { 5 | public virtual int OrderDetailId { get; set; } 6 | public virtual string ItemNo { get; set; } 7 | 8 | public virtual Order Order { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Source/ChangeTracking/ChangeStatus.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace ChangeTracking 7 | { 8 | public enum ChangeStatus 9 | { 10 | Unchanged, 11 | Added, 12 | Changed, 13 | Deleted 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Source/ChangeTracking.Tests/Address.cs: -------------------------------------------------------------------------------- 1 | namespace ChangeTracking.Tests 2 | { 3 | public class Address 4 | { 5 | public virtual int AddressId { get; set; } 6 | public virtual string City { get; set; } 7 | [DoNoTrack] 8 | public virtual string State { get; set; } 9 | public virtual string Zip => "12345"; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Source/ChangeTracking/Internal/IRevertibleChangeTrackingInternal.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace ChangeTracking.Internal 4 | { 5 | internal interface IRevertibleChangeTrackingInternal 6 | { 7 | void AcceptChanges(List parents); 8 | void RejectChanges(List parents); 9 | bool IsChanged(List parents); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Source/ChangeTracking/ChangeTrackingInterceptorSelector.cs: -------------------------------------------------------------------------------- 1 | using Castle.DynamicProxy; 2 | using System; 3 | 4 | namespace ChangeTracking 5 | { 6 | internal class ChangeTrackingInterceptorSelector : IInterceptorSelector 7 | { 8 | IInterceptor[] IInterceptorSelector.SelectInterceptors(Type type, System.Reflection.MethodInfo method, IInterceptor[] interceptors) => interceptors; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Source/ChangeTracking/Internal/IEditableObjectInternal.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.ComponentModel; 3 | 4 | namespace ChangeTracking.Internal 5 | { 6 | internal interface IEditableObjectInternal : IEditableObject 7 | { 8 | void BeginEdit(List parents); 9 | void CancelEdit(List parents); 10 | void EndEdit(List parents); 11 | } 12 | } -------------------------------------------------------------------------------- /Source/ChangeTracking/IChangeTrackingFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace ChangeTracking 4 | { 5 | public interface IChangeTrackingFactory 6 | { 7 | T AsTrackable(T target, ChangeStatus status = ChangeStatus.Unchanged) where T : class; 8 | ICollection AsTrackableCollection(ICollection target) where T : class; 9 | bool MakeComplexPropertiesTrackable { get; set; } 10 | bool MakeCollectionPropertiesTrackable { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Source/ChangeTracking/IChangeTrackableCollection.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace ChangeTracking 4 | { 5 | public interface IChangeTrackableCollection : System.ComponentModel.IRevertibleChangeTracking, IEnumerable 6 | { 7 | IEnumerable UnchangedItems { get; } 8 | IEnumerable AddedItems { get; } 9 | IEnumerable ChangedItems { get; } 10 | IEnumerable DeletedItems { get; } 11 | 12 | bool UnDelete(T item); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /Source/ChangeTracking.Tests/IRevertibleChangeTrackingTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Xunit; 3 | 4 | namespace ChangeTracking.Tests 5 | { 6 | public class IRevertibleChangeTrackingTests 7 | { 8 | [Fact] 9 | public void AsTrackable_Should_Make_Object_Implement_IRevertibleChangeTracking() 10 | { 11 | var order = Helper.GetOrder(); 12 | 13 | Order trackable = order.AsTrackable(); 14 | 15 | trackable.Should().BeAssignableTo(); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Source/ChangeTracking/Internal/ChangeTrackingSettings.cs: -------------------------------------------------------------------------------- 1 | namespace ChangeTracking.Internal 2 | { 3 | internal readonly struct ChangeTrackingSettings 4 | { 5 | internal ChangeTrackingSettings(bool makeComplexPropertiesTrackable, bool makeCollectionPropertiesTrackable) 6 | { 7 | MakeComplexPropertiesTrackable = makeComplexPropertiesTrackable; 8 | MakeCollectionPropertiesTrackable = makeCollectionPropertiesTrackable; 9 | } 10 | 11 | internal bool MakeComplexPropertiesTrackable { get; } 12 | internal bool MakeCollectionPropertiesTrackable { get; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Source/ChangeTracking.Tests/InventoryUpdate.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace ChangeTracking.Tests 4 | { 5 | public class InventoryUpdate 6 | { 7 | public InventoryUpdate() 8 | { 9 | UpdateInfos = new List(); 10 | } 11 | 12 | public virtual int InventoryUpdateId { get; set; } 13 | public virtual int? LinkedToInventoryUpdateId { get; set; } 14 | public virtual InventoryUpdate LinkedToInventoryUpdate { get; set; } 15 | public virtual InventoryUpdate LinkedInventoryUpdate { get; set; } 16 | 17 | public virtual IList UpdateInfos { get; set; } 18 | } 19 | 20 | public class UpdateInfo 21 | { 22 | public virtual int UpdateInfoId { get; set; } 23 | public virtual InventoryUpdate InventoryUpdate { get; set; } 24 | } 25 | } -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 2.2.{build} 2 | configuration: Release 3 | pull_requests: 4 | do_not_increment_build_number: true 5 | image: 6 | - Visual Studio 2017 7 | - Ubuntu 8 | dotnet_csproj: 9 | patch: true 10 | file: '**\ChangeTracking.csproj' 11 | version: '{version}' 12 | package_version: '{version}' 13 | before_build: 14 | - ps: dotnet restore Source\ChangeTracking.sln 15 | build: 16 | project: Source\ChangeTracking.sln 17 | publish_nuget: true 18 | verbosity: minimal 19 | test_script: 20 | - ps: dotnet test Source\ChangeTracking.Tests\ChangeTracking.Tests.csproj 21 | for: 22 | - 23 | matrix: 24 | only: 25 | - image: Ubuntu 26 | build_script: 27 | - ps: dotnet build Source\ChangeTracking\ChangeTracking.csproj --framework netstandard2.0 28 | test_script: 29 | - ps: dotnet test Source\ChangeTracking.Tests\ChangeTracking.Tests.csproj --framework netcoreapp2.0 30 | -------------------------------------------------------------------------------- /Source/ChangeTracking/Internal/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | 4 | namespace ChangeTracking.Internal 5 | { 6 | public static class Extensions 7 | { 8 | public static bool IsSetter(this MethodInfo method) 9 | { 10 | return method.IsSpecialName && method.Name.StartsWith("set_", StringComparison.Ordinal); 11 | } 12 | 13 | public static bool IsGetter(this MethodInfo method) 14 | { 15 | return method.IsSpecialName && method.Name.StartsWith("get_", StringComparison.Ordinal); 16 | } 17 | 18 | public static bool IsProperty(this MethodInfo method) 19 | { 20 | return method.IsSpecialName && (method.Name.StartsWith("get_", StringComparison.Ordinal) || method.Name.StartsWith("set_", StringComparison.Ordinal)); 21 | } 22 | 23 | public static string PropertyName(this MethodInfo method) 24 | { 25 | return method.Name.StartsWith("set_") ? method.Name.Substring("set_".Length) : method.Name.Substring("get_".Length); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /License.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) [2004] [Joel Weiss] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Source/ChangeTracking.Tests/INotifyCollectionChangedTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using System.Collections.Generic; 3 | using System.Collections.Specialized; 4 | using Xunit; 5 | 6 | namespace ChangeTracking.Tests 7 | { 8 | public class INotifyCollectionChangedTests 9 | { 10 | [Fact] 11 | public void AsTrackable_On_Collection_Should_Make_It_INotifyCollectionChangedTests() 12 | { 13 | var orders = Helper.GetOrdersIList(); 14 | 15 | var trackable = orders.AsTrackable(); 16 | 17 | trackable.Should().BeAssignableTo(); 18 | } 19 | 20 | [Fact] 21 | public void AsTrackable_On_Collection_Add_Should_Raise_CollectionChanged() 22 | { 23 | IList orders = Helper.GetOrdersIList(); 24 | 25 | IList trackable = orders.AsTrackable(); 26 | INotifyCollectionChanged collection = (INotifyCollectionChanged)trackable; 27 | var monitor = collection.Monitor(); 28 | 29 | trackable.Add(new Order()); 30 | 31 | monitor.Should().Raise(nameof(INotifyCollectionChanged.CollectionChanged)); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Source/ChangeTracking.Tests/ChangeTracking.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0;net452 5 | ChangeTracking.Tests 6 | ChangeTracking.Tests 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | all 15 | runtime; build; native; contentfiles; analyzers 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /Source/ChangeTracking/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | 5 | namespace ChangeTracking 6 | { 7 | public static class Extensions 8 | { 9 | public static IChangeTrackable CastToIChangeTrackable(this T target) where T : class 10 | { 11 | return (IChangeTrackable)target; 12 | } 13 | 14 | public static IChangeTrackableCollection CastToIChangeTrackableCollection(this ICollection target) where T : class 15 | { 16 | return (IChangeTrackableCollection)target; 17 | } 18 | 19 | public static IChangeTrackableCollection CastToIChangeTrackableCollection(this IList target) where T : class 20 | { 21 | return (IChangeTrackableCollection)target; 22 | } 23 | 24 | public static IChangeTrackableCollection CastToIChangeTrackableCollection(this IList target) where T : class 25 | { 26 | return (IChangeTrackableCollection)target; 27 | } 28 | 29 | public static IChangeTrackableCollection CastToIChangeTrackableCollection(this IBindingList target) where T : class 30 | { 31 | return (IChangeTrackableCollection)target; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Source/ChangeTracking.Tests/GraphTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using ChangeTracking.Internal; 5 | using FluentAssertions; 6 | using Xunit; 7 | 8 | namespace ChangeTracking.Tests 9 | { 10 | public class GraphTests 11 | { 12 | [Fact] 13 | public async Task Add_To_Graph_OnOther_Thread_Should_Not_Throw() 14 | { 15 | Graph graph = new Graph(); 16 | CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); 17 | 18 | _ = Task.Run(() => 19 | { 20 | int i = 0; 21 | while (!cancellationTokenSource.IsCancellationRequested) 22 | { 23 | graph.Add(new ProxyWeakTargetMap(i, i)); 24 | i++; 25 | } 26 | }); 27 | 28 | try 29 | { 30 | for (int i = 0; i < 10; i++) 31 | { 32 | graph.Invoking(g => g.GetExistingProxyForTarget(int.MaxValue)).Should().NotThrow(); 33 | await Task.Delay(i); 34 | } 35 | } 36 | finally 37 | { 38 | cancellationTokenSource.Cancel(); 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Source/ChangeTracking.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26621.2 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChangeTracking.Tests", "ChangeTracking.Tests\ChangeTracking.Tests.csproj", "{A9D772B8-D98D-4AD8-942E-E873B09406A1}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChangeTracking", "ChangeTracking\ChangeTracking.csproj", "{6A5E8568-5A20-47A8-9716-1545F04473C1}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {A9D772B8-D98D-4AD8-942E-E873B09406A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {A9D772B8-D98D-4AD8-942E-E873B09406A1}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {A9D772B8-D98D-4AD8-942E-E873B09406A1}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {A9D772B8-D98D-4AD8-942E-E873B09406A1}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {6A5E8568-5A20-47A8-9716-1545F04473C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {6A5E8568-5A20-47A8-9716-1545F04473C1}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {6A5E8568-5A20-47A8-9716-1545F04473C1}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {6A5E8568-5A20-47A8-9716-1545F04473C1}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {BFDE85DF-7533-4874-B7D0-088CD0220BE2} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /Source/ChangeTracking/ChangeTracking.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0;net452 5 | ChangeTracking 6 | ChangeTracking 7 | 2.2.0 8 | Joel Weiss 9 | 10 | Change Tracking 11 | 12 | Track changes in your POCO objects, and in your collections. 13 | 14 | By using Castle Dynamic Proxy to create proxies of your classes at runtime, you can use your objects just like you used do, and just by calling the AsTrackable() extension method, you get automatic change tracking, and cancellation. 15 | 16 | https://github.com/joelweiss/ChangeTracking/blob/master/License.md</licenseUrl 17 | https://github.com/joelweiss/ChangeTracking 18 | https://github.com/joelweiss/ChangeTracking 19 | ChangeTracking DynamicProxy Change Tracking POCO 20 | false 21 | true 22 | embedded 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 4.4.1 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /Source/ChangeTracking.Tests/Order.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace ChangeTracking.Tests 5 | { 6 | public class Order 7 | { 8 | public Order() 9 | { 10 | OrderDetails = new List(); 11 | Leads = new List(); 12 | } 13 | 14 | public virtual int Id { get; set; } 15 | public virtual string CustomerNumber { get; set; } 16 | public virtual Address Address { get; set; } 17 | public virtual IList OrderDetails { get; set; } 18 | public virtual int OrderDetailsCount => OrderDetails != null ? OrderDetails.Count : 0; 19 | public virtual OrderDetail OrderDetail => OrderDetails?.FirstOrDefault(od => od.OrderDetailId == Id); 20 | public virtual Order LinkedToOrder { get; set; } 21 | public virtual Order LinkedOrder { get; set; } 22 | [DoNoTrack] 23 | public int LeadId { get; set; } 24 | [DoNoTrack] 25 | public virtual IList DoNotTrackOrderDetails { get; set; } 26 | [DoNoTrack] 27 | public virtual Address DoNotTrackAddress { get; set; } 28 | public virtual Lead Lead { get; set; } 29 | public virtual IList Leads { get; set; } 30 | 31 | public Order CreateOrder() 32 | { 33 | return new Order(); 34 | } 35 | 36 | public virtual void VirtualModifier() 37 | { 38 | CustomerNumber = "ChangedInVirtualModifier"; 39 | } 40 | 41 | public void NonVirtualModifier() 42 | { 43 | CustomerNumber = "ChangedInNonVirtualModifier"; 44 | } 45 | 46 | private string _Name; 47 | public virtual void SetNameVirtual(string name) => _Name = name; 48 | public virtual string GetNameVirtual() => _Name; 49 | public void SetNameNonVirtual(string name) => _Name = name; 50 | public string GetNameNonVirtual() => _Name; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Source/ChangeTracking/Internal/Utils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | 6 | namespace ChangeTracking.Internal 7 | { 8 | internal static class Utils 9 | { 10 | internal static IEnumerable GetChildren(object proxy, List parents) 11 | { 12 | IEnumerable children = ((ICollectionPropertyTrackable)proxy).CollectionPropertyTrackables 13 | .Concat(((IComplexPropertyTrackable)proxy).ComplexPropertyTrackables); 14 | List result; 15 | if (parents is null) 16 | { 17 | result = children.OfType().ToList(); 18 | } 19 | else 20 | { 21 | result = children.Except(parents).OfType().ToList(); 22 | parents.AddRange(result.Cast()); 23 | } 24 | return result; 25 | } 26 | 27 | internal static bool IsMarkedDoNotTrack(PropertyInfo propertyInfo) 28 | { 29 | Type doNoTrackAttribute = typeof(DoNoTrackAttribute); 30 | return propertyInfo.GetCustomAttribute(doNoTrackAttribute) != null 31 | ? true 32 | : IsMarkedDoNotTrack(propertyInfo.PropertyType); 33 | } 34 | 35 | internal static bool IsMarkedDoNotTrack(Type type) 36 | { 37 | Type doNoTrackAttribute = typeof(DoNoTrackAttribute); 38 | if (type.GetCustomAttribute(doNoTrackAttribute) != null) 39 | { 40 | return true; 41 | } 42 | if (type.IsInterface && type.GetGenericArguments().FirstOrDefault() is Type genericCollectionArgumentType && typeof(ICollection<>).MakeGenericType(genericCollectionArgumentType).IsAssignableFrom(type) && genericCollectionArgumentType.GetCustomAttribute(doNoTrackAttribute) != null) 43 | { 44 | return true; 45 | } 46 | 47 | return false; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Source/ChangeTracking/ChangeTrackingProxyGenerationHook.cs: -------------------------------------------------------------------------------- 1 | using Castle.DynamicProxy; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Reflection; 6 | using ChangeTracking.Internal; 7 | 8 | namespace ChangeTracking 9 | { 10 | internal class ChangeTrackingProxyGenerationHook : IProxyGenerationHook 11 | { 12 | private static HashSet _MethodsToSkip; 13 | private readonly Type _Type; 14 | private HashSet _InstanceMethodsToSkip; 15 | 16 | static ChangeTrackingProxyGenerationHook() 17 | { 18 | _MethodsToSkip = new HashSet { "Equals", "GetType", "ToString", "GetHashCode" }; 19 | } 20 | 21 | public ChangeTrackingProxyGenerationHook(Type type) 22 | { 23 | _Type = type; 24 | const BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly; 25 | _InstanceMethodsToSkip = new HashSet(type 26 | .GetMethods(bindingFlags) 27 | .Where(mi => !mi.IsSpecialName || (mi.IsProperty() && type.GetProperty(mi.PropertyName(), bindingFlags) is PropertyInfo pi && (!pi.CanWrite || Utils.IsMarkedDoNotTrack(pi))))); 28 | } 29 | 30 | public void MethodsInspected() { } 31 | 32 | public void NonProxyableMemberNotification(Type type, MemberInfo memberInfo) 33 | { 34 | if (memberInfo is MethodInfo methodInfo && methodInfo.IsProperty() && !_InstanceMethodsToSkip.Contains(methodInfo)) 35 | { 36 | throw new InvalidOperationException($"Property {methodInfo.Name.Substring("set_".Length)} is not virtual. Can't track classes with non-virtual properties."); 37 | } 38 | } 39 | 40 | public bool ShouldInterceptMethod(Type type, System.Reflection.MethodInfo methodInfo) => !_MethodsToSkip.Contains(methodInfo.Name) && !_InstanceMethodsToSkip.Contains(methodInfo); 41 | 42 | public override bool Equals(object obj) => (obj as ChangeTrackingProxyGenerationHook)?._Type == _Type; 43 | 44 | public override int GetHashCode() => _Type.GetHashCode(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Source/ChangeTracking.Tests/Helper.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace ChangeTracking.Tests 5 | { 6 | internal static class Helper 7 | { 8 | internal static Order GetOrder(int? orderId = null) 9 | { 10 | int id = orderId ?? 1; 11 | Order order = new Order 12 | { 13 | Id = id, 14 | CustomerNumber = "Test", 15 | Address = new Address 16 | { 17 | AddressId = 1, 18 | City = "New York" 19 | }, 20 | OrderDetails = new List 21 | { 22 | new OrderDetail 23 | { 24 | OrderDetailId = 1, 25 | ItemNo = "Item123" 26 | }, 27 | new OrderDetail 28 | { 29 | OrderDetailId = 2, 30 | ItemNo = "Item369" 31 | } 32 | }, 33 | DoNotTrackOrderDetails = new List 34 | { 35 | new OrderDetail 36 | { 37 | OrderDetailId = 20, 38 | ItemNo = "Item20" 39 | } 40 | }, 41 | LeadId = id + 500, 42 | Lead = new Lead 43 | { 44 | LeadId = id + 600 45 | }, 46 | Leads = new List 47 | { 48 | new Lead 49 | { 50 | LeadId = id + 700 51 | } 52 | }, 53 | DoNotTrackAddress = new Address 54 | { 55 | AddressId = id + 800, 56 | City = "London" 57 | } 58 | }; 59 | Order linkedOrder = new Order 60 | { 61 | Id = id + 1000, 62 | LinkedToOrder = order 63 | }; 64 | order.LinkedOrder = linkedOrder; 65 | 66 | foreach (OrderDetail orderDetail in order.OrderDetails) 67 | { 68 | orderDetail.Order = order; 69 | } 70 | return order; 71 | } 72 | 73 | internal static IList GetOrdersIList() => Enumerable.Range(1, 10).Select(i => GetOrder(i)).ToList(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Source/ChangeTracking.Tests/InternalTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using System.Collections.Generic; 3 | using Xunit; 4 | 5 | namespace ChangeTracking.Tests 6 | { 7 | public class InternalTests 8 | { 9 | public class Home 10 | { 11 | internal Home() 12 | { 13 | Town = new Town 14 | { 15 | Name = "Prague" 16 | }; 17 | Rooms = new List 18 | { 19 | new Room 20 | { 21 | Name = "Dining" 22 | } 23 | }; 24 | } 25 | 26 | public virtual ICollection Rooms { get; set; } 27 | public virtual Town Town { get; set; } 28 | } 29 | 30 | public class Room 31 | { 32 | internal Room() { } 33 | public virtual string Name { get; set; } 34 | } 35 | 36 | public class Town 37 | { 38 | internal Town() { } 39 | public virtual string Name { get; set; } 40 | } 41 | 42 | [Fact] 43 | public void GetOriginal_OnInternal_Should_Not_Throw() 44 | { 45 | Home home = new Home(); 46 | 47 | Home trackable = home.AsTrackable(); 48 | 49 | trackable.CastToIChangeTrackable().Invoking(t => t.GetOriginal()).Should().NotThrow(); 50 | } 51 | 52 | [Fact] 53 | public void GetCurrent_OnInternal_Should_Not_Throw() 54 | { 55 | Home home = new Home(); 56 | 57 | Home trackable = home.AsTrackable(); 58 | 59 | trackable.CastToIChangeTrackable().Invoking(t => t.GetCurrent()).Should().NotThrow(); 60 | } 61 | 62 | [Fact] 63 | public void Internal_ComplexProperty_Should_Be_Trackable() 64 | { 65 | Home home = new Home(); 66 | 67 | Home trackable = home.AsTrackable(); 68 | 69 | trackable.Town.Should().BeAssignableTo>(); 70 | } 71 | 72 | [Fact] 73 | public void BindingList_AddNew_With_Items_With_Internal_Constructor_Should_Not_Throw() 74 | { 75 | Home home = new Home(); 76 | 77 | Home trackable = home.AsTrackable(); 78 | var bindingList = (System.ComponentModel.IBindingList)trackable.Rooms; 79 | 80 | bindingList.Invoking(bl => bl.AddNew()).Should().NotThrow(); 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /Source/ChangeTracking/IChangeTrackable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Linq.Expressions; 5 | 6 | namespace ChangeTracking 7 | { 8 | public interface IChangeTrackable : IChangeTrackable 9 | { 10 | /// 11 | /// Gets the original value of a given property. 12 | /// 13 | /// The type of the result. 14 | /// The selector. 15 | /// 16 | /// 17 | /// A property selector expression has an incorrect format 18 | /// or 19 | /// A selected member is not a property 20 | /// 21 | TResult GetOriginalValue(Expression> selector); 22 | 23 | /// 24 | /// Gets the original value of a given property. 25 | /// 26 | /// The type of the result. 27 | /// Name of the property. 28 | /// 29 | TResult GetOriginalValue(string propertyName); 30 | 31 | /// 32 | /// Gets the original value of a given property. 33 | /// 34 | /// Name of the property. 35 | /// 36 | object GetOriginalValue(string propertyName); 37 | 38 | /// 39 | /// Gets the original object as a poco without the current changes. 40 | /// 41 | /// 42 | /// The type that is specified for T does not have a parameterless constructor. 43 | T GetOriginal(); 44 | 45 | /// 46 | /// Gets the current object as a poco with the current changes. 47 | /// 48 | /// 49 | /// The type that is specified for T does not have a parameterless constructor. 50 | T GetCurrent(); 51 | } 52 | 53 | public interface IChangeTrackable : INotifyPropertyChanged, IChangeTracking, IRevertibleChangeTracking, IEditableObject 54 | { 55 | event EventHandler StatusChanged; 56 | ChangeStatus ChangeTrackingStatus { get; } 57 | IEnumerable ChangedProperties { get; } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Source/ChangeTracking.Tests/SpeedTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using Xunit; 5 | using Xunit.Abstractions; 6 | 7 | namespace ChangeTracking.Tests 8 | { 9 | public class SpeedTest 10 | { 11 | private readonly ITestOutputHelper _Output; 12 | 13 | public SpeedTest(ITestOutputHelper output) 14 | { 15 | _Output = output; 16 | } 17 | 18 | [Fact] 19 | public void TestSpeed() 20 | { 21 | int reps = 100000; 22 | var lists = new[] { new List(reps), new List(reps) }; 23 | for (int i = 0; i < 2; i++) 24 | { 25 | var list = lists[i]; 26 | for (int j = 0; j < reps; j++) 27 | { 28 | list.Add(new Order 29 | { 30 | Id = 1, 31 | CustomerNumber = "Test" 32 | }); 33 | } 34 | } 35 | var trackedList = lists[0]; 36 | var swAsTrackable = new Stopwatch(); 37 | swAsTrackable.Start(); 38 | for (int i = 0; i < trackedList.Count; i++) 39 | { 40 | trackedList[i] = trackedList[i].AsTrackable(); 41 | } 42 | swAsTrackable.Stop(); 43 | _Output.WriteLine("Call AsTrackable on {0:N0} objects: {1} ms", reps, swAsTrackable.ElapsedMilliseconds); 44 | 45 | var noneTrackedList = lists[1]; 46 | var swNotTracked = new Stopwatch(); 47 | GC.Collect(); 48 | swNotTracked.Start(); 49 | for (int i = 0; i < noneTrackedList.Count; i++) 50 | { 51 | var order = noneTrackedList[i]; 52 | order.Id = 2; 53 | order.CustomerNumber = "Test2"; 54 | var id = order.Id; 55 | var cust = order.CustomerNumber; 56 | } 57 | swNotTracked.Stop(); 58 | _Output.WriteLine("Write and Read {0:N0} none tracked objects: {1} ms", reps, swNotTracked.ElapsedMilliseconds); 59 | GC.Collect(); 60 | var swTracked = new Stopwatch(); 61 | swTracked.Start(); 62 | for (int i = 0; i < trackedList.Count; i++) 63 | { 64 | var order = trackedList[i]; 65 | order.Id = 2; 66 | order.CustomerNumber = "Test2"; 67 | var id = order.Id; 68 | var cust = order.CustomerNumber; 69 | } 70 | swTracked.Stop(); 71 | _Output.WriteLine("Write and Read {0:N0} tracked objects: {1} ms", reps, swTracked.ElapsedMilliseconds); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Source/ChangeTracking/Core.cs: -------------------------------------------------------------------------------- 1 | using ChangeTracking.Internal; 2 | using System.Collections.Generic; 3 | 4 | namespace ChangeTracking 5 | { 6 | public static class Core 7 | { 8 | public static T AsTrackable(this T target) where T : class 9 | { 10 | return ChangeTrackingFactory.Default.AsTrackable(target); 11 | } 12 | 13 | public static T AsTrackable(this T target, ChangeStatus status = ChangeStatus.Unchanged, bool makeComplexPropertiesTrackable = true, bool makeCollectionPropertiesTrackable = true) where T : class 14 | { 15 | return ChangeTrackingFactory.Default.AsTrackable(target, new ChangeTrackingSettings(makeComplexPropertiesTrackable, makeCollectionPropertiesTrackable), status); 16 | } 17 | 18 | public static ICollection AsTrackable(this System.Collections.ObjectModel.Collection target) where T : class 19 | { 20 | return ChangeTrackingFactory.Default.AsTrackableCollection(target); 21 | } 22 | 23 | public static ICollection AsTrackable(this System.Collections.ObjectModel.Collection target, bool makeComplexPropertiesTrackable = true, bool makeCollectionPropertiesTrackable = true) where T : class 24 | { 25 | return ChangeTrackingFactory.Default.AsTrackableCollection(target, new ChangeTrackingSettings(makeComplexPropertiesTrackable, makeCollectionPropertiesTrackable)); 26 | } 27 | 28 | public static ICollection AsTrackable(this ICollection target) where T : class 29 | { 30 | return ChangeTrackingFactory.Default.AsTrackableCollection(target); 31 | } 32 | 33 | public static ICollection AsTrackable(this ICollection target, bool makeComplexPropertiesTrackable, bool makeCollectionPropertiesTrackable) where T : class 34 | { 35 | return ChangeTrackingFactory.Default.AsTrackableCollection(target, new ChangeTrackingSettings(makeComplexPropertiesTrackable, makeCollectionPropertiesTrackable)); 36 | } 37 | 38 | public static IList AsTrackable(this List target) where T : class 39 | { 40 | return (IList)ChangeTrackingFactory.Default.AsTrackableCollection(target); 41 | } 42 | 43 | public static IList AsTrackable(this List target, bool makeComplexPropertiesTrackable, bool makeCollectionPropertiesTrackable) where T : class 44 | { 45 | return (IList)ChangeTrackingFactory.Default.AsTrackableCollection(target, new ChangeTrackingSettings(makeComplexPropertiesTrackable, makeCollectionPropertiesTrackable)); 46 | } 47 | 48 | public static IList AsTrackable(this IList target) where T : class 49 | { 50 | return (IList)ChangeTrackingFactory.Default.AsTrackableCollection(target); 51 | } 52 | 53 | public static IList AsTrackable(this IList target, bool makeComplexPropertiesTrackable, bool makeCollectionPropertiesTrackable) where T : class 54 | { 55 | return (IList)ChangeTrackingFactory.Default.AsTrackableCollection(target, new ChangeTrackingSettings(makeComplexPropertiesTrackable, makeCollectionPropertiesTrackable)); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # PDT-specific 29 | .buildpath 30 | 31 | 32 | ################# 33 | ## Visual Studio 34 | ################# 35 | 36 | ## Ignore Visual Studio temporary files, build results, and 37 | ## files generated by popular Visual Studio add-ons. 38 | 39 | # User-specific files 40 | *.suo 41 | *.user 42 | *.sln.docstates 43 | 44 | # Build results 45 | 46 | [Dd]ebug/ 47 | [Rr]elease/ 48 | x64/ 49 | build/ 50 | [Bb]in/ 51 | [Oo]bj/ 52 | 53 | # MSTest test Results 54 | [Tt]est[Rr]esult*/ 55 | [Bb]uild[Ll]og.* 56 | 57 | *_i.c 58 | *_p.c 59 | *.ilk 60 | *.meta 61 | *.obj 62 | *.pch 63 | *.pdb 64 | *.pgc 65 | *.pgd 66 | *.rsp 67 | *.sbr 68 | *.tlb 69 | *.tli 70 | *.tlh 71 | *.tmp 72 | *.tmp_proj 73 | *.log 74 | *.vspscc 75 | *.vssscc 76 | .builds 77 | *.pidb 78 | *.log 79 | *.scc 80 | 81 | # Visual C++ cache files 82 | ipch/ 83 | *.aps 84 | *.ncb 85 | *.opensdf 86 | *.sdf 87 | *.cachefile 88 | 89 | # Visual Studio profiler 90 | *.psess 91 | *.vsp 92 | *.vspx 93 | 94 | # Guidance Automation Toolkit 95 | *.gpState 96 | 97 | # ReSharper is a .NET coding add-in 98 | _ReSharper*/ 99 | *.[Rr]e[Ss]harper 100 | 101 | # TeamCity is a build add-in 102 | _TeamCity* 103 | 104 | # DotCover is a Code Coverage Tool 105 | *.dotCover 106 | 107 | # NCrunch 108 | *.ncrunch* 109 | .*crunch*.local.xml 110 | 111 | # Installshield output folder 112 | [Ee]xpress/ 113 | 114 | # DocProject is a documentation generator add-in 115 | DocProject/buildhelp/ 116 | DocProject/Help/*.HxT 117 | DocProject/Help/*.HxC 118 | DocProject/Help/*.hhc 119 | DocProject/Help/*.hhk 120 | DocProject/Help/*.hhp 121 | DocProject/Help/Html2 122 | DocProject/Help/html 123 | 124 | # Click-Once directory 125 | publish/ 126 | 127 | # Publish Web Output 128 | *.Publish.xml 129 | *.pubxml 130 | 131 | # NuGet Packages Directory 132 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 133 | #packages/ 134 | 135 | # Windows Azure Build Output 136 | csx 137 | *.build.csdef 138 | 139 | # Windows Store app package directory 140 | AppPackages/ 141 | 142 | # Others 143 | sql/ 144 | *.Cache 145 | ClientBin/ 146 | [Ss]tyle[Cc]op.* 147 | ~$* 148 | *~ 149 | *.dbmdl 150 | *.[Pp]ublish.xml 151 | *.pfx 152 | *.publishsettings 153 | 154 | # RIA/Silverlight projects 155 | Generated_Code/ 156 | 157 | # Backup & report files from converting an old project file to a newer 158 | # Visual Studio version. Backup files are not needed, because we have git ;-) 159 | _UpgradeReport_Files/ 160 | Backup*/ 161 | UpgradeLog*.XML 162 | UpgradeLog*.htm 163 | 164 | # SQL Server files 165 | App_Data/*.mdf 166 | App_Data/*.ldf 167 | 168 | ############# 169 | ## Windows detritus 170 | ############# 171 | 172 | # Windows image file caches 173 | Thumbs.db 174 | ehthumbs.db 175 | 176 | # Folder config file 177 | Desktop.ini 178 | 179 | # Recycle Bin used on file shares 180 | $RECYCLE.BIN/ 181 | 182 | # Mac crap 183 | .DS_Store 184 | 185 | 186 | ############# 187 | ## Python 188 | ############# 189 | 190 | *.py[co] 191 | 192 | # Packages 193 | *.egg 194 | *.egg-info 195 | dist/ 196 | build/ 197 | eggs/ 198 | parts/ 199 | var/ 200 | sdist/ 201 | develop-eggs/ 202 | .installed.cfg 203 | 204 | # Installer logs 205 | pip-log.txt 206 | 207 | # Unit test / coverage reports 208 | .coverage 209 | .tox 210 | 211 | #Translations 212 | *.mo 213 | 214 | #Mr Developer 215 | .mr.developer.cfg 216 | 217 | */packages/* 218 | !*/packages/repositories.config 219 | 220 | */.cr/* 221 | 222 | */.vs/* -------------------------------------------------------------------------------- /Source/ChangeTracking.Tests/DoNoTrackTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Xunit; 3 | 4 | namespace ChangeTracking.Tests 5 | { 6 | public class DoNoTrackTests 7 | { 8 | 9 | [Fact] 10 | public void AsTrackable_Should_Not_Track_Properties_That_Are_Marked_DoNotTrack_And_Be_Ok_That_It_Is_Not_Virtual() 11 | { 12 | var order = Helper.GetOrder(); 13 | 14 | Order trackable = order.AsTrackable(); 15 | trackable.LeadId = 123; 16 | 17 | trackable.CastToIChangeTrackable().ChangeTrackingStatus.Should().Be(ChangeStatus.Unchanged); 18 | trackable.CastToIChangeTrackable().ChangedProperties.Should().NotContain(nameof(Order.LeadId)); 19 | } 20 | 21 | [Fact] 22 | public void AsTrackable_Should_Not_Track_ComplexProperties_That_The_PropertyType_Is_Marked_DoNotTrack() 23 | { 24 | var order = Helper.GetOrder(); 25 | 26 | Order trackable = order.AsTrackable(); 27 | 28 | trackable.Lead.Should().NotBeAssignableTo>(); 29 | } 30 | 31 | [Fact] 32 | public void AsTrackable_Should_Not_Track_ComplexProperties_That_Are_Marked_DoNotTrack() 33 | { 34 | var order = Helper.GetOrder(); 35 | 36 | Order trackable = order.AsTrackable(); 37 | 38 | trackable.DoNotTrackAddress.Should().NotBeAssignableTo>(); 39 | } 40 | 41 | [Fact] 42 | public void AsTrackable_Should_Not_Track_CollectionProperties_That_The_PropertyType_Is_Marked_DoNotTrack() 43 | { 44 | var order = Helper.GetOrder(); 45 | 46 | Order trackable = order.AsTrackable(); 47 | 48 | trackable.Leads.Should().NotBeAssignableTo>(); 49 | } 50 | 51 | [Fact] 52 | public void AsTrackable_Should_Not_Track_CollectionProperties_That_Are_Marked_DoNotTrack() 53 | { 54 | var order = Helper.GetOrder(); 55 | 56 | Order trackable = order.AsTrackable(); 57 | trackable.LeadId = 123; 58 | 59 | trackable.CastToIChangeTrackable().ChangeTrackingStatus.Should().Be(ChangeStatus.Unchanged); 60 | trackable.CastToIChangeTrackable().ChangedProperties.Should().NotContain(nameof(Order.LeadId)); 61 | } 62 | 63 | [Fact] 64 | public void AsTrackable_Properties_That_Are_DoNotTrack_Should_Still_Be_Copied_To_Trackable() 65 | { 66 | var order = Helper.GetOrder(); 67 | 68 | Order trackable = order.AsTrackable(); 69 | 70 | trackable.LeadId.Should().Be(order.LeadId); 71 | trackable.Lead.Should().Be(order.Lead); 72 | trackable.DoNotTrackAddress.Should().Be(order.DoNotTrackAddress); 73 | trackable.DoNotTrackOrderDetails.Should().BeEquivalentTo(order.DoNotTrackOrderDetails); 74 | trackable.Leads.Should().BeEquivalentTo(order.Leads); 75 | } 76 | 77 | [Fact] 78 | public void AsTrackable_DoNotTrack_Should_Not_Change_Status() 79 | { 80 | var order = Helper.GetOrder(); 81 | 82 | Order trackable = order.AsTrackable(); 83 | const string stateValue = "State"; 84 | trackable.Address.State = stateValue; 85 | 86 | trackable.CastToIChangeTrackable().ChangeTrackingStatus.Should().Equals(ChangeStatus.Unchanged); 87 | trackable.CastToIChangeTrackable().RejectChanges(); 88 | trackable.Address.State.Should().Be(stateValue); 89 | } 90 | 91 | [Fact] 92 | public void AsTrackable_DoNotTrack_Should_Not_RaiseEvents() 93 | { 94 | var order = Helper.GetOrder(); 95 | 96 | Order trackable = order.AsTrackable(); 97 | using (var monitor = ((IChangeTrackable)trackable).Monitor()) 98 | { 99 | trackable.DoNotTrackOrderDetails.Add(new OrderDetail 100 | { 101 | OrderDetailId = 123, 102 | ItemNo = "Item123" 103 | }); 104 | trackable.Address.State = "State"; 105 | trackable.LeadId = 999; 106 | trackable.Lead = new Lead(); 107 | trackable.Leads.Add(new Lead()); 108 | trackable.Leads = null; 109 | 110 | monitor.Should().NotRaise(nameof(IChangeTrackable.StatusChanged)); 111 | monitor.Should().NotRaise(nameof(IChangeTrackable.PropertyChanged)); 112 | } 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /Source/ChangeTracking/Internal/ProxyTargetMap.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Linq; 5 | using System.Threading; 6 | 7 | namespace ChangeTracking.Internal 8 | { 9 | internal class ProxyWeakTargetMap 10 | { 11 | private readonly WeakReference _Target; 12 | 13 | public ProxyWeakTargetMap(object target, object proxy) 14 | { 15 | _Target = new WeakReference(target); 16 | Proxy = proxy; 17 | } 18 | 19 | public object Target => _Target.Target; 20 | 21 | public object Proxy { get; } 22 | } 23 | 24 | internal class Graph : Collection 25 | { 26 | private readonly ReaderWriterLockSlim _ClearLock; 27 | private int _InvokeCount; 28 | 29 | public Graph() 30 | { 31 | _ClearLock = new ReaderWriterLockSlim(); 32 | } 33 | 34 | internal ProxyWeakTargetMap GetExistingProxyForTarget(object target) 35 | { 36 | Interlocked.Increment(ref _InvokeCount); 37 | int invokeCount = Interlocked.CompareExchange(ref _InvokeCount, 0, 100); 38 | if (invokeCount == 100) 39 | { 40 | _ClearLock.EnterWriteLock(); 41 | try 42 | { 43 | for (int i = Count - 1; i >= 0; i--) 44 | { 45 | if (this[i].Target is null) 46 | { 47 | RemoveAt(i); 48 | } 49 | } 50 | } 51 | finally 52 | { 53 | _ClearLock.ExitWriteLock(); 54 | } 55 | } 56 | _ClearLock.EnterReadLock(); 57 | try 58 | { 59 | return Items.FirstOrDefault(t => ReferenceEquals(target, t.Target)); 60 | } 61 | finally 62 | { 63 | _ClearLock.ExitReadLock(); 64 | } 65 | } 66 | 67 | protected override void InsertItem(int index, ProxyWeakTargetMap item) 68 | { 69 | try 70 | { 71 | _ClearLock.EnterWriteLock(); 72 | base.InsertItem(index, item); 73 | } 74 | finally 75 | { 76 | _ClearLock.ExitWriteLock(); 77 | } 78 | } 79 | } 80 | 81 | internal class ProxyTargetMap 82 | { 83 | public ProxyTargetMap(object target, object proxy) 84 | { 85 | Target = target; 86 | Proxy = proxy; 87 | } 88 | 89 | public object Target { get; } 90 | public object Proxy { get; } 91 | } 92 | 93 | internal class UnrollGraph 94 | { 95 | private readonly List _ProxyTargetMaps; 96 | private readonly List _RegisteredProxies; 97 | private readonly List _PocoSetters; 98 | private readonly List>> _ListSetters; 99 | 100 | public UnrollGraph() 101 | { 102 | _ProxyTargetMaps = new List(); 103 | _RegisteredProxies = new List(); 104 | _PocoSetters = new List(); 105 | _ListSetters = new List>>(); 106 | } 107 | 108 | internal bool RegisterOrScheduleAssignPocoInsteadOfProxy(object proxy, Action pocoSetter) 109 | { 110 | object map = _RegisteredProxies.FirstOrDefault(p => ReferenceEquals(p, proxy)) ?? _ProxyTargetMaps.FirstOrDefault(m => ReferenceEquals(m.Proxy, proxy)); 111 | if (map is null) 112 | { 113 | _RegisteredProxies.Add(proxy); 114 | return true; 115 | } 116 | if (pocoSetter != null) 117 | { 118 | _PocoSetters.Add(() => pocoSetter(_ProxyTargetMaps.First(m => ReferenceEquals(m.Proxy, proxy)).Target)); 119 | } 120 | return false; 121 | } 122 | 123 | internal void AddMap(ProxyTargetMap map) 124 | { 125 | _RegisteredProxies.Remove(map.Proxy); 126 | _ProxyTargetMaps.Add(map); 127 | } 128 | 129 | internal void AddListSetter(Action> listSetter) => _ListSetters.Add(listSetter); 130 | 131 | internal void FinishWireUp() 132 | { 133 | foreach (Action action in _PocoSetters) 134 | { 135 | action(); 136 | } 137 | foreach (Action> action in _ListSetters) 138 | { 139 | action(proxy => _ProxyTargetMaps.First(m => ReferenceEquals(m.Proxy, proxy)).Target); 140 | } 141 | } 142 | } 143 | } -------------------------------------------------------------------------------- /Source/ChangeTracking/ChangeTrackingBindingList.cs: -------------------------------------------------------------------------------- 1 | using ChangeTracking.Internal; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Collections.Specialized; 5 | using System.ComponentModel; 6 | 7 | namespace ChangeTracking 8 | { 9 | internal sealed class ChangeTrackingBindingList : BindingList, INotifyCollectionChanged where T : class 10 | { 11 | private readonly Action _ItemCanceled; 12 | private readonly Action _DeleteItem; 13 | private readonly ChangeTrackingSettings _ChangeTrackingSettings; 14 | private readonly Graph _Graph; 15 | 16 | public event NotifyCollectionChangedEventHandler CollectionChanged; 17 | 18 | public ChangeTrackingBindingList(IList list, Action deleteItem, Action itemCanceled, ChangeTrackingSettings changeTrackingSettings, Graph graph) 19 | : base(list) 20 | { 21 | _DeleteItem = deleteItem; 22 | _ItemCanceled = itemCanceled; 23 | _ChangeTrackingSettings = changeTrackingSettings; 24 | _Graph = graph; 25 | var bindingListType = typeof(ChangeTrackingBindingList).BaseType; 26 | bindingListType.GetField("raiseItemChangedEvents", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic).SetValue(this, true); 27 | var hookMethod = bindingListType.GetMethod("HookPropertyChanged", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); 28 | foreach (var item in list) 29 | { 30 | hookMethod.Invoke(this, new object[] { item }); 31 | } 32 | } 33 | 34 | protected override void InsertItem(int index, T item) 35 | { 36 | object trackable = item as IChangeTrackable; 37 | if (trackable == null) 38 | { 39 | trackable = ChangeTrackingFactory.Default.AsTrackable(item, ChangeStatus.Added, _ItemCanceled, _ChangeTrackingSettings, _Graph); 40 | } 41 | base.InsertItem(index, (T)trackable); 42 | CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index)); 43 | } 44 | 45 | protected override void SetItem(int index, T item) 46 | { 47 | object trackable = item as IChangeTrackable; 48 | if (trackable == null) 49 | { 50 | trackable = ChangeTrackingFactory.Default.AsTrackable(item, ChangeStatus.Added, _ItemCanceled, _ChangeTrackingSettings, _Graph); 51 | } 52 | T originalItem = this[index]; 53 | base.SetItem(index, (T)trackable); 54 | CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, item, originalItem, index)); 55 | } 56 | 57 | protected override void RemoveItem(int index) 58 | { 59 | T removedItem = this[index]; 60 | _DeleteItem?.Invoke(removedItem); 61 | base.RemoveItem(index); 62 | CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removedItem, index)); 63 | } 64 | 65 | protected override void ClearItems() 66 | { 67 | for (int index = 0; index < Count; index++) 68 | { 69 | T removedItem = this[index]; 70 | _DeleteItem?.Invoke(removedItem); 71 | } 72 | base.ClearItems(); 73 | CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); 74 | } 75 | 76 | protected override void OnListChanged(ListChangedEventArgs e) 77 | { 78 | base.OnListChanged(e); 79 | switch (e.ListChangedType) 80 | { 81 | case ListChangedType.Reset: 82 | CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); 83 | break; 84 | default: 85 | return; 86 | } 87 | } 88 | 89 | protected override object AddNewCore() 90 | { 91 | AddingNewEventArgs e = new AddingNewEventArgs(null); 92 | OnAddingNew(e); 93 | T newItem = (T)e.NewObject; 94 | 95 | if (newItem == null) 96 | { 97 | newItem = (T)Activator.CreateInstance(typeof(T), nonPublic: true); 98 | } 99 | 100 | object trackable = newItem as IChangeTrackable; 101 | if (trackable == null) 102 | { 103 | trackable = ChangeTrackingFactory.Default.AsTrackable(newItem, ChangeStatus.Added, _ItemCanceled, _ChangeTrackingSettings, _Graph); 104 | var editable = (IEditableObject)trackable; 105 | editable.BeginEdit(); 106 | } 107 | Add((T)trackable); 108 | 109 | return trackable; 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Source/ChangeTracking/EditableObjectInterceptor.cs: -------------------------------------------------------------------------------- 1 | using Castle.DynamicProxy; 2 | using ChangeTracking.Internal; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Reflection; 7 | 8 | namespace ChangeTracking 9 | { 10 | internal sealed class EditableObjectInterceptor : IInterceptor, IInterceptorSettings 11 | { 12 | private static Dictionary _Properties; 13 | private readonly Dictionary _BeforeEditValues; 14 | private readonly Action _NotifyParentItemCanceled; 15 | private bool _IsEditing; 16 | 17 | public bool IsInitialized { get; set; } 18 | 19 | static EditableObjectInterceptor() 20 | { 21 | _Properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance).ToDictionary(pi => pi.Name); 22 | } 23 | 24 | internal EditableObjectInterceptor() 25 | { 26 | _BeforeEditValues = new Dictionary(); 27 | } 28 | 29 | internal EditableObjectInterceptor(Action notifyParentItemCanceled) 30 | : this() 31 | { 32 | _NotifyParentItemCanceled = notifyParentItemCanceled; 33 | } 34 | 35 | 36 | public void Intercept(IInvocation invocation) 37 | { 38 | if (!IsInitialized) 39 | { 40 | return; 41 | } 42 | if (_IsEditing == true) 43 | { 44 | if (invocation.Method.IsSetter()) 45 | { 46 | string propName = invocation.Method.PropertyName(); 47 | if (!_BeforeEditValues.ContainsKey(propName)) 48 | { 49 | var oldValue = _Properties[propName].GetValue(invocation.Proxy, null); 50 | invocation.Proceed(); 51 | var newValue = _Properties[propName].GetValue(invocation.Proxy, null); 52 | if (!Equals(oldValue, newValue)) 53 | { 54 | _BeforeEditValues.Add(propName, oldValue); 55 | } 56 | } 57 | else 58 | { 59 | var originalValue = _BeforeEditValues[propName]; 60 | invocation.Proceed(); 61 | var newValue = _Properties[propName].GetValue(invocation.Proxy, null); 62 | if (Equals(originalValue, newValue)) 63 | { 64 | _BeforeEditValues.Remove(propName); 65 | } 66 | } 67 | return; 68 | } 69 | else if (invocation.Method.IsGetter()) 70 | { 71 | invocation.Proceed(); 72 | return; 73 | } 74 | } 75 | switch (invocation.Method.Name) 76 | { 77 | case "BeginEdit": 78 | BeginEdit(invocation.Proxy, invocation.Arguments.Length == 0 ? null : (List)invocation.Arguments[0]); 79 | break; 80 | case "CancelEdit": 81 | CancelEdit(invocation.Proxy, invocation.Arguments.Length == 0 ? null : (List)invocation.Arguments[0]); 82 | break; 83 | case "EndEdit": 84 | EndEdit(invocation.Proxy, invocation.Arguments.Length == 0 ? null : (List)invocation.Arguments[0]); 85 | break; 86 | default: 87 | invocation.Proceed(); 88 | break; 89 | } 90 | } 91 | 92 | private void BeginEdit(object proxy, List parents) 93 | { 94 | parents = parents ?? new List(20) { proxy }; 95 | foreach (var child in Utils.GetChildren(proxy, parents)) 96 | { 97 | child.BeginEdit(parents); 98 | } 99 | _IsEditing = true; 100 | } 101 | 102 | private void CancelEdit(object proxy, List parents) 103 | { 104 | parents = parents ?? new List(20) { proxy }; 105 | foreach (var child in Utils.GetChildren(proxy, parents)) 106 | { 107 | child.CancelEdit(parents); 108 | } 109 | if (_IsEditing) 110 | { 111 | _IsEditing = false; 112 | foreach (var oldValue in _BeforeEditValues) 113 | { 114 | _Properties[oldValue.Key].SetValue(proxy, oldValue.Value, null); 115 | } 116 | _NotifyParentItemCanceled?.Invoke((T)proxy); 117 | } 118 | } 119 | 120 | private void EndEdit(object proxy, List parents) 121 | { 122 | parents = parents ?? new List(20) { proxy }; 123 | foreach (var child in Utils.GetChildren(proxy, parents)) 124 | { 125 | child.EndEdit(parents); 126 | } 127 | if (_IsEditing == true) 128 | { 129 | _IsEditing = false; 130 | _BeforeEditValues.Clear(); 131 | } 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /Source/ChangeTracking.Tests/IEditableObjectTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using System; 3 | using Xunit; 4 | 5 | namespace ChangeTracking.Tests 6 | { 7 | public class IEditableObjectTests 8 | { 9 | [Fact] 10 | public void AsTrackable_Should_Make_Object_Implement_IEditableObject() 11 | { 12 | var order = Helper.GetOrder(); 13 | 14 | Order trackable = order.AsTrackable(); 15 | 16 | trackable.Should().BeAssignableTo(); 17 | } 18 | 19 | [Fact] 20 | public void CancelEdit_On_Item_Should_Revert_Changes() 21 | { 22 | var order = Helper.GetOrder(); 23 | 24 | var trackable = order.AsTrackable(); 25 | var editableObject = (System.ComponentModel.IEditableObject)trackable; 26 | 27 | editableObject.BeginEdit(); 28 | trackable.CustomerNumber = "Testing"; 29 | editableObject.CancelEdit(); 30 | 31 | trackable.CustomerNumber.Should().Be("Test", because: "item was canceled"); 32 | } 33 | 34 | [Fact] 35 | public void CancelEdit_On_Item_After_EndEdit_Should_Not_Revert_Changes() 36 | { 37 | var order = Helper.GetOrder(); 38 | 39 | var trackable = order.AsTrackable(); 40 | var editableObject = (System.ComponentModel.IEditableObject)trackable; 41 | 42 | editableObject.BeginEdit(); 43 | trackable.CustomerNumber = "Testing"; 44 | editableObject.EndEdit(); 45 | editableObject.CancelEdit(); 46 | 47 | trackable.CustomerNumber.Should().Be("Testing", because: "item was canceled after calling EndEdit"); 48 | } 49 | 50 | [Fact] 51 | public void With_Out_BeginEdit_CancelEdit_Should_Do_Nothing() 52 | { 53 | var order = Helper.GetOrder(); 54 | 55 | var trackable = order.AsTrackable(); 56 | var editableObject = (System.ComponentModel.IEditableObject)trackable; 57 | 58 | trackable.CustomerNumber = "Testing"; 59 | editableObject.CancelEdit(); 60 | 61 | trackable.CustomerNumber.Should().Be("Testing", because: "item was canceled after calling EndEdit"); 62 | } 63 | 64 | [Fact] 65 | public void AsTrackable_Should_Make_Object_Complex_Property_Implement_IEditableObject() 66 | { 67 | var order = Helper.GetOrder(); 68 | 69 | Order trackable = order.AsTrackable(); 70 | 71 | trackable.Address.Should().BeAssignableTo(); 72 | } 73 | 74 | [Fact] 75 | public void AsTrackable_Should_Not_Make_Object_Complex_Property_Implement_IEditableObject_If_Passed_False() 76 | { 77 | var order = Helper.GetOrder(); 78 | 79 | Order trackable = order.AsTrackable(makeComplexPropertiesTrackable: false); 80 | 81 | (trackable.Address as System.ComponentModel.IEditableObject).Should().BeNull(); 82 | } 83 | 84 | [Fact] 85 | public void CancelEdit_On_Item_Should_Revert_Changes_On_Complex_Property() 86 | { 87 | var order = Helper.GetOrder(); 88 | 89 | var trackable = order.AsTrackable(); 90 | var editableObject = (System.ComponentModel.IEditableObject)trackable; 91 | 92 | editableObject.BeginEdit(); 93 | trackable.Address.City = "Chicago"; 94 | editableObject.CancelEdit(); 95 | 96 | trackable.Address.City.Should().Be("New York", because: "parent item was canceled"); 97 | } 98 | 99 | [Fact] 100 | public void BeginEdit_On_Circular_Reference_Should_Not_Throw_OverflowException() 101 | { 102 | var update0 = new InventoryUpdate 103 | { 104 | InventoryUpdateId = 0 105 | }; 106 | var update1 = new InventoryUpdate 107 | { 108 | InventoryUpdateId = 1, 109 | LinkedToInventoryUpdate = update0 110 | }; 111 | update0.LinkedInventoryUpdate = update1; 112 | 113 | var trackable = update0.AsTrackable(); 114 | 115 | trackable.Invoking(t => ((System.ComponentModel.IEditableObject)t).BeginEdit()).Should().NotThrow(); 116 | } 117 | 118 | [Fact] 119 | public void CancelEdit_On_Circular_Reference_Should_Not_Throw_OverflowException() 120 | { 121 | var update0 = new InventoryUpdate 122 | { 123 | InventoryUpdateId = 0 124 | }; 125 | var update1 = new InventoryUpdate 126 | { 127 | InventoryUpdateId = 1, 128 | LinkedToInventoryUpdate = update0 129 | }; 130 | update0.LinkedInventoryUpdate = update1; 131 | 132 | var trackable = update0.AsTrackable(); 133 | 134 | trackable.Invoking(t => ((System.ComponentModel.IEditableObject)t).CancelEdit()).Should().NotThrow(); 135 | } 136 | 137 | [Fact] 138 | public void EndEdit_On_Circular_Reference_Should_Not_Throw_OverflowException() 139 | { 140 | var update0 = new InventoryUpdate 141 | { 142 | InventoryUpdateId = 0 143 | }; 144 | var update1 = new InventoryUpdate 145 | { 146 | InventoryUpdateId = 1, 147 | LinkedToInventoryUpdate = update0 148 | }; 149 | update0.LinkedInventoryUpdate = update1; 150 | 151 | var trackable = update0.AsTrackable(); 152 | 153 | trackable.Invoking(t => ((System.ComponentModel.IEditableObject)t).EndEdit()).Should().NotThrow(); 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /Source/ChangeTracking.Tests/IBindingListTests.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using FluentAssertions; 3 | using Xunit; 4 | 5 | namespace ChangeTracking.Tests 6 | { 7 | public class IBindingListTests 8 | { 9 | [Fact] 10 | public void AsTrackable_On_Collection_Should_Make_It_ICancelAddNew() 11 | { 12 | var orders = Helper.GetOrdersIList(); 13 | 14 | var trackable = orders.AsTrackable(); 15 | 16 | trackable.Should().BeAssignableTo(); 17 | } 18 | 19 | [Fact] 20 | public void AsTrackable_On_Collection_Should_Make_It_IBindingList() 21 | { 22 | var orders = Helper.GetOrdersIList(); 23 | 24 | var trackable = orders.AsTrackable(); 25 | 26 | trackable.Should().BeAssignableTo(); 27 | } 28 | 29 | [Fact] 30 | public void AsTrackable_On_Collection_AddNew_Should_Raise_ListChanged() 31 | { 32 | var orders = Helper.GetOrdersIList(); 33 | 34 | var trackable = orders.AsTrackable(); 35 | var bindingList = (System.ComponentModel.IBindingList)trackable; 36 | var monitor = bindingList.Monitor(); 37 | 38 | bindingList.AddNew(); 39 | 40 | monitor.Should().Raise(nameof(System.ComponentModel.IBindingList.ListChanged)); 41 | } 42 | 43 | [Fact] 44 | public void AsTrackable_On_Collection_Remove_Should_Raise_ListChanged() 45 | { 46 | var orders = Helper.GetOrdersIList(); 47 | 48 | var trackable = orders.AsTrackable(); 49 | var bindingList = (System.ComponentModel.IBindingList)trackable; 50 | var monitor = bindingList.Monitor(); 51 | 52 | trackable.Remove(trackable[0]); 53 | 54 | monitor.Should().Raise(nameof(System.ComponentModel.IBindingList.ListChanged)); 55 | } 56 | 57 | [Fact] 58 | public void CancelEdit_On_Item_Should_Remove_From_Collection() 59 | { 60 | var orders = Helper.GetOrdersIList(); 61 | 62 | var trackable = orders.AsTrackable(); 63 | var bindingList = (System.ComponentModel.IBindingList)trackable; 64 | 65 | bindingList.AddNew(); 66 | var withAddedCount = bindingList.Count; 67 | var addedItem = bindingList.Cast().Single(o => o.CustomerNumber == null); 68 | var editableObject = (System.ComponentModel.IEditableObject)addedItem; 69 | editableObject.CancelEdit(); 70 | 71 | bindingList.Count.Should().Be(withAddedCount - 1, because: "item was canceled"); 72 | } 73 | 74 | [Fact] 75 | public void Change_Property_On_Item_That_Implements_INotifyPropertyChanged_In_Collection_Should_Raise_ListChanged() 76 | { 77 | var orders = Helper.GetOrdersIList(); 78 | 79 | var trackable = orders.AsTrackable(); 80 | var bindingList = (System.ComponentModel.IBindingList)trackable; 81 | var monitor = bindingList.Monitor(); 82 | 83 | ((Order)bindingList[0]).Id = 123; 84 | 85 | monitor.Should().Raise(nameof(System.ComponentModel.IBindingList.ListChanged)); 86 | } 87 | 88 | [Fact] 89 | public void AcceptChanges_On_Collection_Should_Raise_ListChanged() 90 | { 91 | var orders = Helper.GetOrdersIList(); 92 | var trackable = orders.AsTrackable(); 93 | 94 | var first = trackable.First(); 95 | var bl = trackable as System.ComponentModel.IBindingList; 96 | bl.ListChanged += (o, e) => 97 | { 98 | ; 99 | }; 100 | first.Id = 963; 101 | var bindingList = (System.ComponentModel.IBindingList)trackable; 102 | var monitor = bindingList.Monitor(); 103 | 104 | trackable.CastToIChangeTrackableCollection().AcceptChanges(); 105 | 106 | monitor.Should().Raise(nameof(System.ComponentModel.IBindingList.ListChanged)); 107 | } 108 | 109 | [Fact] 110 | public void AcceptChanges_On_Collection_If_No_Changes_Should_Not_Raise_ListChanged() 111 | { 112 | var orders = Helper.GetOrdersIList(); 113 | var trackable = orders.AsTrackable(); 114 | var bindingList = (System.ComponentModel.IBindingList)trackable; 115 | var monitor = bindingList.Monitor(); 116 | 117 | trackable.CastToIChangeTrackableCollection().AcceptChanges(); 118 | 119 | monitor.Should().NotRaise(nameof(System.ComponentModel.IBindingList.ListChanged)); 120 | } 121 | 122 | [Fact] 123 | public void RejectChanges_On_Collection_Should_Raise_ListChanged() 124 | { 125 | var orders = Helper.GetOrdersIList(); 126 | var trackable = orders.AsTrackable(); 127 | 128 | var first = trackable.First(); 129 | first.Id = 963; 130 | var bindingList = (System.ComponentModel.IBindingList)trackable; 131 | var monitor = bindingList.Monitor(); 132 | 133 | trackable.CastToIChangeTrackableCollection().RejectChanges(); 134 | 135 | monitor.Should().Raise(nameof(System.ComponentModel.IBindingList.ListChanged)); 136 | } 137 | 138 | [Fact] 139 | public void RejectChanges_On_Collection_If_No_Changes_Should_Not_Raise_ListChanged() 140 | { 141 | var orders = Helper.GetOrdersIList(); 142 | var trackable = orders.AsTrackable(); 143 | var bindingList = (System.ComponentModel.IBindingList)trackable; 144 | var monitor = bindingList.Monitor(); 145 | 146 | trackable.CastToIChangeTrackableCollection().RejectChanges(); 147 | 148 | monitor.Should().NotRaise(nameof(System.ComponentModel.IBindingList.ListChanged)); 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /Source/ChangeTracking.Tests/ProxyTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.ComponentModel; 5 | using System.Linq; 6 | using Xunit; 7 | 8 | namespace ChangeTracking.Tests 9 | { 10 | public class ProxyTests 11 | { 12 | [Fact] 13 | public void Change_Property_Should_Raise_PropertyChanged_Event_if_non_virtual_method() 14 | { 15 | var order = Helper.GetOrder(); 16 | var trackable = order.AsTrackable(); 17 | var monitor = ((INotifyPropertyChanged)trackable).Monitor(); 18 | 19 | trackable.NonVirtualModifier(); 20 | 21 | monitor.Should().RaisePropertyChangeFor(o => ((Order)o).CustomerNumber); 22 | } 23 | 24 | [Fact] 25 | public void Change_Property_Should_Raise_PropertyChanged_Event_if_method_virtual() 26 | { 27 | var order = Helper.GetOrder(); 28 | var trackable = order.AsTrackable(); 29 | var monitor = ((INotifyPropertyChanged)trackable).Monitor(); 30 | 31 | trackable.VirtualModifier(); 32 | 33 | monitor.Should().RaisePropertyChangeFor(o => ((Order)o).CustomerNumber); 34 | } 35 | 36 | [Fact] 37 | public void Set_On_Field_Virtual_Should_Return_Correct_Value() 38 | { 39 | var order = Helper.GetOrder(); 40 | order.SetNameVirtual("MyName"); 41 | var trackable = order.AsTrackable(); 42 | 43 | var name = trackable.GetNameVirtual(); 44 | 45 | name.Should().Be("MyName"); 46 | } 47 | 48 | [Fact] 49 | public void Set_On_Field_Virtual_When_In_Collection_Should_Return_Correct_Value() 50 | { 51 | var order = Helper.GetOrder(); 52 | order.SetNameVirtual("MyName"); 53 | var list = new List { order }; 54 | var trackableList = list.AsTrackable(); 55 | 56 | var name = trackableList[0].GetNameVirtual(); 57 | 58 | name.Should().Be("MyName"); 59 | } 60 | 61 | 62 | [Fact] 63 | public void Set_On_Field_NonVirtual_Should_Return_Correct_Value() 64 | { 65 | var order = Helper.GetOrder(); 66 | order.SetNameNonVirtual("MyName"); 67 | var trackable = order.AsTrackable(); 68 | 69 | var name = trackable.GetNameNonVirtual(); 70 | 71 | name.Should().Be("MyName"); 72 | } 73 | 74 | [Fact] 75 | public void Set_On_Field_NonVirtual_When_In_Collection_Should_Return_Correct_Value() 76 | { 77 | var order = Helper.GetOrder(); 78 | order.SetNameNonVirtual("MyName"); 79 | var list = new List { order }; 80 | var trackableList = list.AsTrackable(); 81 | 82 | var name = trackableList[0].GetNameNonVirtual(); 83 | 84 | name.Should().Be("MyName"); 85 | } 86 | 87 | [Fact] 88 | public void Fields_Should_Copy_Over() 89 | { 90 | Game game = new Game("ReadOnly") 91 | { 92 | Property = "Test", 93 | _Int = 333, 94 | _String = "Testing", 95 | _ListInt = new List { 1, 2, 3, 4 }, 96 | _CustomStruct = new CustomStruct 97 | { 98 | _Int = 30, 99 | _String = "StructString" 100 | } 101 | }; 102 | 103 | Game trackableGame = game.AsTrackable(); 104 | 105 | trackableGame._ReadOnly.Should().Be("ReadOnly"); 106 | trackableGame.Property.Should().Be("Test"); 107 | trackableGame._Int.Should().Be(333); 108 | trackableGame._String.Should().Be("Testing"); 109 | trackableGame._ListInt.Should().BeEquivalentTo(new List { 1, 2, 3, 4 }); 110 | trackableGame._CustomStruct.Should().Be(new CustomStruct 111 | { 112 | _Int = 30, 113 | _String = "StructString" 114 | }); 115 | } 116 | 117 | [Fact] 118 | public void Complex_PropertyWith_Field_Should_Copy_Over() 119 | { 120 | Game game = new Game("ReadOnly") 121 | { 122 | Player = new Player() 123 | }; 124 | game.Player.SetName("PlayerName"); 125 | 126 | Game trackableGame = game.AsTrackable(); 127 | 128 | trackableGame.Player.GetName().Should().Be("PlayerName"); 129 | } 130 | 131 | [Fact] 132 | public void Events_Should_Copy_Over() 133 | { 134 | Game game = new Game("ReadOnly"); 135 | bool raised = false; 136 | game.OnClicked += (o, ef) => raised = true; 137 | 138 | Game trackableGame = game.AsTrackable(); 139 | 140 | trackableGame.Raise(); 141 | 142 | raised.Should().BeTrue(); 143 | } 144 | 145 | public class Game 146 | { 147 | public Game(string readOnly) => _ReadOnly = readOnly; 148 | public Game() { } 149 | 150 | public virtual string Property { get; set; } 151 | public virtual Player Player { get; set; } 152 | public readonly string _ReadOnly; 153 | public string _String; 154 | public int _Int; 155 | public List _ListInt; 156 | public CustomStruct _CustomStruct; 157 | public event EventHandler OnClicked; 158 | 159 | public void Raise() => OnClicked?.Invoke(this, EventArgs.Empty); 160 | } 161 | 162 | public struct CustomStruct 163 | { 164 | public int _Int; 165 | public string _String; 166 | } 167 | 168 | public class Player 169 | { 170 | // this property is here to make the class proxyable 171 | public virtual int UserId { get; set; } 172 | private string _Name; 173 | public virtual void SetName(string name) => _Name = name; 174 | public virtual string GetName() => _Name; 175 | } 176 | 177 | [Fact] 178 | public void ReadOnlyProperty_Should_Not_BeCaptured_As_A_Trackable() 179 | { 180 | Order order = Helper.GetOrder(); 181 | Order trackable = order.AsTrackable(); 182 | 183 | OrderDetail orderDetail = trackable.OrderDetail; 184 | trackable.Id = 2; 185 | 186 | trackable.OrderDetail.ItemNo.Should().Be("Item369"); 187 | trackable.OrderDetail.OrderDetailId.Should().Be(2); 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build status](https://ci.appveyor.com/api/projects/status/n9j44hcpe2wmkkgd/branch/master?svg=true)](https://ci.appveyor.com/project/joelweiss/changetracking/branch/master) 2 | [![NuGet Badge](https://buildstats.info/nuget/ChangeTracking?includePreReleases=true)](https://www.nuget.org/packages/ChangeTracking/) 3 | [![Test status](https://img.shields.io/appveyor/tests/JoelWeiss/ChangeTracking.svg)](https://ci.appveyor.com/project/JoelWeiss/ChangeTracking/branch/master) 4 | 5 | # ChangeTracking 6 | 7 | Track changes in your POCO objects, and in your collections. 8 | By using [Castle Dynamic Proxy](http://www.castleproject.org/projects/dynamicproxy/) to create proxies of your classes at runtime, you can use your objects just like you used to, and just by calling the `AsTrackable()` extension method, you get automatic change tracking, and cancellation. 9 | 10 | All trackable POCOs implement [`IChangeTrackable`](https://github.com/joelweiss/ChangeTracking/blob/master/Source/ChangeTracking/IChangeTrackable.cs), [`IRevertibleChangeTracking`](http://msdn.microsoft.com/en-us/library/vstudio/system.componentmodel.irevertiblechangetracking.aspx), [`IChangeTracking`](http://msdn.microsoft.com/en-us/library/vstudio/system.componentmodel.ichangetracking.aspx), [`IEditableObject`](http://msdn.microsoft.com/en-us/library/system.componentmodel.ieditableobject.aspx) and [`INotifyPropertyChanged`](http://msdn.microsoft.com/en-us/library/system.componentmodel.inotifypropertychanged.aspx). 11 | 12 | And all trackable collections implement [`IChangeTrackableCollection`](https://github.com/joelweiss/ChangeTracking/blob/master/Source/ChangeTracking/IChangeTrackableCollection.cs), [`IBindingList`](http://msdn.microsoft.com/en-us/library/vstudio/system.componentmodel.ibindinglist.aspx) [`ICancelAddNew`](http://msdn.microsoft.com/en-us/library/vstudio/system.componentmodel.icanceladdnew.aspx), [`INotifyCollectionChanged `](https://msdn.microsoft.com/en-us/library/system.collections.specialized.inotifycollectionchanged(v=vs.110).aspx), `IList`, `IList`, `ICollection`, `ICollection`, `IEnumerable` and `IEnumerable` 13 | 14 | # Installation 15 | ``` 16 | PM> Install-Package ChangeTracking 17 | ``` 18 | 19 | Example 20 | --------- 21 | 22 | ### To make an object trackable 23 | ```csharp 24 | using ChangeTracking; 25 | //... 26 | Order order = new Order 27 | { 28 | Id = 1, 29 | CustumerNumber = "Test", 30 | Address = new Address 31 | { 32 | AddressId = 1, 33 | City = "New York" 34 | }, 35 | OrderDetails = new List 36 | { 37 | new OrderDetail 38 | { 39 | OrderDetailId = 1, 40 | ItemNo = "Item123" 41 | }, 42 | new OrderDetail 43 | { 44 | OrderDetailId = 2, 45 | ItemNo = "Item369" 46 | } 47 | } 48 | }; 49 | Order trackedOrder = order.AsTrackable(); 50 | ``` 51 | And here is how you get to the tracked info. 52 | ```csharp 53 | var trackable = (IChangeTrackable)trackedOrder; 54 | // same as 55 | var trackable = trackedOrder.CastToIChangeTrackable(); 56 | ``` 57 | And here is what's available on `trackable`. 58 | ```csharp 59 | //Can be Unchanged, Added, Changed, Deleted 60 | ChangeStatus status = trackable.ChangeTrackingStatus; 61 | 62 | //Will be true if ChangeTrackingStatus is not Unchanged 63 | bool isChanged = trackable.IsChanged; 64 | 65 | //Will retrieve the original value of a property 66 | string originalCustNumber = trackable.GetOriginalValue(o => o.CustumerNumber); 67 | 68 | //Will retrieve a copy of the original item 69 | var originalOrder = trackable.GetOriginal(); 70 | 71 | //Calling RejectChanges will reject all the changes you made, reset all properties to their original values and set ChangeTrackingStatus to Unchanged 72 | trackable.RejectChanges(); 73 | 74 | //Calling AcceptChanges will accept all the changes you made, clears the original values and set ChangeTrackingStatus to Unchanged 75 | trackable.AcceptChanges(); 76 | 77 | //If ChangeTrackingStatus is Changed it returns all changed property names, if ChangeTrackingStatus is Added or Deleted it returns all properties 78 | trackable.ChangedProperties; 79 | ``` 80 | By default complex properties and collection properties will be tracked (if it can be made trackable). 81 | 82 | You can change the default by setting 83 | ```csharp 84 | ChangeTrackingFactory.Default.MakeCollectionPropertiesTrackable = false; 85 | 86 | ChangeTrackingFactory.Default.MakeComplexPropertiesTrackable = false; 87 | ``` 88 | You can override the default when creating the trackable. 89 | ```csharp 90 | var trackable = order.AsTrackable(makeComplexPropertiesTrackable: false); 91 | ``` 92 | or 93 | ```csharp 94 | var trackable = order.AsTrackable(makeCollectionPropertiesTrackable: false); 95 | ``` 96 | ### And on a collection 97 | ```csharp 98 | var orders = new List{new Order { Id = 1, CustumerNumber = "Test" } }; 99 | IList trackableOrders = orders.AsTrackable(); 100 | ``` 101 | And here is how you get to the tracked info. 102 | ```csharp 103 | var trackable = (IChangeTrackableCollection)trackableOrders; 104 | // Same as 105 | var trackable = trackableOrders.CastToIChangeTrackableCollection(); 106 | ``` 107 | And here is what's available on `trackable`. 108 | ```csharp 109 | // Will be true if there are any changed items, added items or deleted items in the collection. 110 | bool isChanged = trackable.IsChanged; 111 | 112 | // Will return all items with ChangeTrackingStatus of Unchanged 113 | IEnumerable unchangedOrders = trackable.UnchangedItems; 114 | // Will return all items that were added to the collection - with ChangeTrackingStatus of Added 115 | IEnumerable addedOrders = trackable.AddedItems; 116 | // Will return all items with ChangeTrackingStatus of Changed 117 | IEnumerable changedOrders = trackable.ChangedItems; 118 | // Will return all items that were removed from the collection - with ChangeTrackingStatus of Deleted 119 | IEnumerable deletedOrders = trackable.DeletedItems; 120 | 121 | // Will Accept all the changes in the collection and its items, deleted items will be cleared and all items ChangeTrackingStatus will be Unchanged 122 | trackable.AcceptChanges(); 123 | 124 | // Will Reject all the changes in the collection and its items, deleted items will be moved back to the collection, added items removed and all items ChangeTrackingStatus will be Unchanged 125 | trackable.RejectChanges(); 126 | ``` 127 | 128 | ### Exlude Properties 129 | To exlude a property from being tracked, apply the `DoNoTrack` attribute to the property or to the the property class. 130 | 131 | 132 | ```csharp 133 | public class Order 134 | { 135 | [DoNoTrack] 136 | public virtual Address Address { get; set; } 137 | 138 | //will not be tracked because the Lead class is marked [DoNotTrack]. 139 | public virtual Lead Lead { get; set; } 140 | } 141 | 142 | [DoNoTrack] 143 | public class Lead 144 | { 145 | public virtual int LeadId { get; set; } 146 | } 147 | ``` 148 | 149 | Requirements and restrictions 150 | -------------------------------- 151 | 152 | * .net 4.5.2 and above 153 | * netstandard 2.0 154 | 155 | ##### For Plain objects 156 | * Your class must not be `sealed` and all members in your class must be `public virtual` 157 | 158 | ```csharp 159 | public class Order 160 | { 161 | public virtual int Id { get; set; } 162 | public virtual string CustumerNumber { get; set; } 163 | public virtual Address Address { get; set; } 164 | public virtual IList OrderDetails { get; set; } 165 | } 166 | ``` 167 | 168 | ##### For Collections 169 | * You can only assign the created proxy to one of the implemented interfaces, i.e. `ICollection`, `IList` and `IBindingList`, and the `AsTrackable()` will choose the correct extennsion method only if called on `IList`, `IList`, `ICollection` and `ICollection`. 170 | 171 | ```csharp 172 | IList orders = new List().AsTrackable(); 173 | ``` 174 | -------------------------------------------------------------------------------- /Source/ChangeTracking.Tests/INotifyPropertyChangedTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using System; 3 | using System.ComponentModel; 4 | using Xunit; 5 | 6 | namespace ChangeTracking.Tests 7 | { 8 | public class INotifyPropertyChangedTests 9 | { 10 | [Fact] 11 | public void AsTrackable_Should_Make_Object_Implement_INotifyPropertyChanged() 12 | { 13 | var order = Helper.GetOrder(); 14 | 15 | Order trackable = order.AsTrackable(); 16 | 17 | trackable.Should().BeAssignableTo(); 18 | } 19 | 20 | [Fact] 21 | public void Change_Property_Should_Raise_PropertyChanged_Event() 22 | { 23 | var order = Helper.GetOrder(); 24 | var trackable = order.AsTrackable(); 25 | var monitor = ((INotifyPropertyChanged)trackable).Monitor(); 26 | 27 | trackable.CustomerNumber = "Test1"; 28 | 29 | monitor.Should().RaisePropertyChangeFor(o => ((Order)o).CustomerNumber); 30 | } 31 | 32 | [Fact] 33 | public void RejectChanges_Should_Raise_PropertyChanged() 34 | { 35 | var order = Helper.GetOrder(); 36 | 37 | var trackable = order.AsTrackable(); 38 | trackable.Id = 963; 39 | var monitor = ((INotifyPropertyChanged)trackable).Monitor(); 40 | var intf = trackable.CastToIChangeTrackable(); 41 | intf.RejectChanges(); 42 | 43 | monitor.Should().RaisePropertyChangeFor(o => ((Order)o).Id); 44 | } 45 | 46 | [Fact] 47 | public void When_PropertyChanged_Raised_Property_Should_Be_Changed() 48 | { 49 | var order = Helper.GetOrder(); 50 | var trackable = order.AsTrackable(); 51 | var inpc = (System.ComponentModel.INotifyPropertyChanged)trackable; 52 | int newValue = 0; 53 | inpc.PropertyChanged += (o, e) => newValue = order.Id; 54 | 55 | trackable.Id = 1234; 56 | 57 | newValue.Should().Be(1234); 58 | } 59 | 60 | [Fact] 61 | public void When_CollectionProperty_Children_Trackable_Change_Property_On_Item_In_Collection_Should_Raise_PropertyChanged_Event() 62 | { 63 | var order = Helper.GetOrder(); 64 | var trackable = order.AsTrackable(); 65 | var monitor = ((INotifyPropertyChanged)trackable).Monitor(); 66 | 67 | trackable.OrderDetails[0].ItemNo = "Testing"; 68 | 69 | monitor.Should().RaisePropertyChangeFor(o => ((Order)o).OrderDetails); 70 | } 71 | 72 | [Fact] 73 | public void When_CollectionProperty_Children_Not_Trackable_Change_Property_On_Item_In_Collection_Should_Not_Raise_PropertyChanged_Event() 74 | { 75 | var order = Helper.GetOrder(); 76 | var trackable = order.AsTrackable(makeCollectionPropertiesTrackable: false); 77 | var monitor = ((INotifyPropertyChanged)trackable).Monitor(); 78 | 79 | trackable.OrderDetails[0].ItemNo = "Testing"; 80 | 81 | monitor.Should().NotRaisePropertyChangeFor(o => ((Order)o).OrderDetails); 82 | } 83 | 84 | [Fact] 85 | public void When_CollectionProperty_Children_Trackable_Change_CollectionProperty_Should_Raise_PropertyChanged_Event() 86 | { 87 | var order = Helper.GetOrder(); 88 | var trackable = order.AsTrackable(); 89 | var monitor = ((INotifyPropertyChanged)trackable).Monitor(); 90 | 91 | trackable.OrderDetails.Add(new OrderDetail 92 | { 93 | OrderDetailId = 123, 94 | ItemNo = "Item123" 95 | }); 96 | 97 | monitor.Should().RaisePropertyChangeFor(o => ((Order)o).OrderDetails); 98 | } 99 | 100 | [Fact] 101 | public void When_CollectionProperty_Children_Not_Trackable_Change_CollectionProperty_Should_Not_Raise_PropertyChanged_Event() 102 | { 103 | var order = Helper.GetOrder(); 104 | var trackable = order.AsTrackable(makeCollectionPropertiesTrackable: false); 105 | var monitor = ((INotifyPropertyChanged)trackable).Monitor(); 106 | 107 | trackable.OrderDetails.Add(new OrderDetail 108 | { 109 | OrderDetailId = 123, 110 | ItemNo = "Item123" 111 | }); 112 | 113 | monitor.Should().NotRaisePropertyChangeFor(o => ((Order)o).OrderDetails); 114 | } 115 | 116 | [Fact] 117 | public void When_ComplexProperty_Children_Trackable_Change_Property_On_Complex_Property_Should_Raise_PropertyChanged_Event() 118 | { 119 | var order = Helper.GetOrder(); 120 | var trackable = order.AsTrackable(); 121 | var monitor = ((INotifyPropertyChanged)trackable).Monitor(); 122 | 123 | trackable.Address.City = "Chicago"; 124 | 125 | monitor.Should().RaisePropertyChangeFor(o => ((Order)o).Address); 126 | } 127 | 128 | [Fact] 129 | public void When_Not_ComplexProperty_Children_Trackable_Change_Property_On_Complex_Property_Should_Not_Raise_PropertyChanged_Event() 130 | { 131 | var order = Helper.GetOrder(); 132 | var trackable = order.AsTrackable(makeComplexPropertiesTrackable: false); 133 | var monitor = ((INotifyPropertyChanged)trackable).Monitor(); 134 | 135 | trackable.Address.City = "Chicago"; 136 | 137 | monitor.Should().NotRaisePropertyChangeFor(o => ((Order)o).Address); 138 | } 139 | 140 | [Fact] 141 | public void Change_Property_Should_Raise_PropertyChanged_On_ChangeTrackingStatus_Event() 142 | { 143 | var order = Helper.GetOrder(); 144 | var trackable = order.AsTrackable(); 145 | var monitor = ((INotifyPropertyChanged)trackable).Monitor(); 146 | 147 | trackable.CustomerNumber = "Test1"; 148 | IChangeTrackable changeTrackable = trackable.CastToIChangeTrackable(); 149 | 150 | monitor.Should().RaisePropertyChangeFor(ct => ct.CastToIChangeTrackable().ChangeTrackingStatus); 151 | } 152 | 153 | [Fact] 154 | public void Change_Property_Should_Raise_PropertyChanged_On_ChangedProperties() 155 | { 156 | var order = Helper.GetOrder(); 157 | var trackable = order.AsTrackable(); 158 | var monitor = ((INotifyPropertyChanged)trackable).Monitor(); 159 | 160 | trackable.CustomerNumber = "Test1"; 161 | IChangeTrackable changeTrackable = trackable.CastToIChangeTrackable(); 162 | 163 | monitor.Should().RaisePropertyChangeFor(ct => ct.CastToIChangeTrackable().ChangedProperties); 164 | } 165 | 166 | [Fact] 167 | public void Change_Property_From_Value_To_Null_Should_Stop_Notification() 168 | { 169 | Order trackable = new Order { Id = 321, Address = new Address { AddressId = 0 } }.AsTrackable(); 170 | Address trackableAddress = trackable.Address; 171 | trackable.Address = null; 172 | var monitor = ((INotifyPropertyChanged)trackable).Monitor(); 173 | 174 | trackableAddress.AddressId = 2; 175 | 176 | monitor.Should().NotRaisePropertyChangeFor(o => ((Order)o).Address); 177 | } 178 | 179 | [Fact] 180 | public void PropertyChanged_On_Circular_Reference_Should_Not_Throw_OverflowException() 181 | { 182 | var update0 = new InventoryUpdate 183 | { 184 | InventoryUpdateId = 0 185 | }; 186 | var update1 = new InventoryUpdate 187 | { 188 | InventoryUpdateId = 1, 189 | LinkedToInventoryUpdate = update0 190 | }; 191 | update0.LinkedInventoryUpdate = update1; 192 | 193 | var trackable = update0.AsTrackable(); 194 | //read these properties to force event wire up 195 | _ = trackable.LinkedInventoryUpdate.LinkedToInventoryUpdate; 196 | trackable.InventoryUpdateId = 3; 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /Source/ChangeTracking/NotifyPropertyChangedInterceptor.cs: -------------------------------------------------------------------------------- 1 | using Castle.DynamicProxy; 2 | using ChangeTracking.Internal; 3 | using System.Collections.Generic; 4 | using System.ComponentModel; 5 | using System.Linq; 6 | using System.Reflection; 7 | 8 | namespace ChangeTracking 9 | { 10 | internal class NotifyPropertyChangedInterceptor : IInterceptor, IInterceptorSettings where T : class 11 | { 12 | private static Dictionary _Properties; 13 | private readonly Dictionary _PropertyChangedEventHandlers; 14 | private readonly object _PropertyChangedEventHandlersLock; 15 | private readonly Dictionary _ListChangedEventHandlers; 16 | private readonly object _ListChangedEventHandlersLock; 17 | private readonly HashSet _CurrentlyExecutingPropertyChangedEvents; 18 | private readonly object _CurrentlyExecutingPropertyChangedEventsLock; 19 | 20 | public bool IsInitialized { get; set; } 21 | 22 | static NotifyPropertyChangedInterceptor() 23 | { 24 | _Properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance).ToDictionary(pi => pi.Name); 25 | } 26 | 27 | internal NotifyPropertyChangedInterceptor(ChangeTrackingInterceptor changeTrackingInterceptor) 28 | { 29 | _PropertyChangedEventHandlers = new Dictionary(); 30 | _PropertyChangedEventHandlersLock = new object(); 31 | _ListChangedEventHandlers = new Dictionary(); 32 | _ListChangedEventHandlersLock = new object(); 33 | _CurrentlyExecutingPropertyChangedEvents = new HashSet(); 34 | _CurrentlyExecutingPropertyChangedEventsLock = new object(); 35 | changeTrackingInterceptor._StatusChanged += (o, e) => RaisePropertyChanged(o, nameof(IChangeTrackable.ChangeTrackingStatus)); 36 | changeTrackingInterceptor._ChangedPropertiesChanged += (o, e) => RaisePropertyChanged(o, nameof(IChangeTrackable.ChangedProperties)); 37 | PropertyChanged += delegate { }; 38 | } 39 | 40 | public void Intercept(IInvocation invocation) 41 | { 42 | if (!IsInitialized) 43 | { 44 | return; 45 | } 46 | if (invocation.Method.IsSetter()) 47 | { 48 | string propertyName = invocation.Method.PropertyName(); 49 | var previousValue = _Properties[propertyName].GetValue(invocation.Proxy, null); 50 | invocation.Proceed(); 51 | var newValue = _Properties[propertyName].GetValue(invocation.Proxy, null); 52 | if (!Equals(previousValue, newValue)) 53 | { 54 | RaisePropertyChanged(invocation.Proxy, propertyName); 55 | RaisePropertyChanged(invocation.Proxy, nameof(IChangeTrackable.ChangedProperties)); 56 | } 57 | if (!ReferenceEquals(previousValue, newValue)) 58 | { 59 | UnsubscribeFromChildPropertyChanged(propertyName, previousValue); 60 | SubscribeToChildPropertyChanged(invocation, propertyName, newValue); 61 | UnsubscribeFromChildListChanged(propertyName, previousValue); 62 | SubscribeToChildListChanged(invocation, propertyName, newValue); 63 | } 64 | return; 65 | } 66 | if (invocation.Method.IsGetter()) 67 | { 68 | invocation.Proceed(); 69 | string propertyName = invocation.Method.PropertyName(); 70 | SubscribeToChildPropertyChanged(invocation, propertyName, invocation.ReturnValue); 71 | SubscribeToChildListChanged(invocation, propertyName, invocation.ReturnValue); 72 | return; 73 | } 74 | switch (invocation.Method.Name) 75 | { 76 | case "add_PropertyChanged": 77 | PropertyChanged += (PropertyChangedEventHandler)invocation.Arguments[0]; 78 | break; 79 | case "remove_PropertyChanged": 80 | PropertyChanged -= (PropertyChangedEventHandler)invocation.Arguments[0]; 81 | break; 82 | default: 83 | invocation.Proceed(); 84 | break; 85 | } 86 | } 87 | 88 | private void UnsubscribeFromChildPropertyChanged(string propertyName, object oldChild) 89 | { 90 | if (oldChild is INotifyPropertyChanged trackable) 91 | { 92 | lock (_PropertyChangedEventHandlersLock) 93 | { 94 | if (_PropertyChangedEventHandlers.TryGetValue(propertyName, out PropertyChangedEventHandler handler)) 95 | { 96 | trackable.PropertyChanged -= handler; 97 | _PropertyChangedEventHandlers.Remove(propertyName); 98 | } 99 | } 100 | } 101 | } 102 | 103 | private void SubscribeToChildPropertyChanged(IInvocation invocation, string propertyName, object newValue) 104 | { 105 | if (newValue is INotifyPropertyChanged newChild) 106 | { 107 | lock (_PropertyChangedEventHandlersLock) 108 | { 109 | if (!_PropertyChangedEventHandlers.ContainsKey(propertyName)) 110 | { 111 | void newHandler(object sender, PropertyChangedEventArgs e) => RaisePropertyChanged(invocation.Proxy, propertyName); 112 | newChild.PropertyChanged += newHandler; 113 | _PropertyChangedEventHandlers.Add(propertyName, newHandler); 114 | } 115 | } 116 | } 117 | } 118 | 119 | private void UnsubscribeFromChildListChanged(string propertyName, object oldChild) 120 | { 121 | if (oldChild is IBindingList trackable) 122 | { 123 | lock (_ListChangedEventHandlersLock) 124 | { 125 | if (_ListChangedEventHandlers.TryGetValue(propertyName, out ListChangedEventHandler handler)) 126 | { 127 | trackable.ListChanged -= handler; 128 | _ListChangedEventHandlers.Remove(propertyName); 129 | } 130 | } 131 | } 132 | } 133 | 134 | private void SubscribeToChildListChanged(IInvocation invocation, string propertyName, object newValue) 135 | { 136 | if (newValue is IBindingList newChild) 137 | { 138 | lock (_ListChangedEventHandlersLock) 139 | { 140 | if (!_ListChangedEventHandlers.ContainsKey(propertyName)) 141 | { 142 | void newHandler(object sender, ListChangedEventArgs e) => RaisePropertyChanged(invocation.Proxy, propertyName); 143 | newChild.ListChanged += newHandler; 144 | _ListChangedEventHandlers.Add(propertyName, newHandler); 145 | } 146 | } 147 | } 148 | } 149 | 150 | private event PropertyChangedEventHandler PropertyChanged; 151 | 152 | private void RaisePropertyChanged(object proxy, string propertyName) 153 | { 154 | lock (_CurrentlyExecutingPropertyChangedEventsLock) 155 | { 156 | if (!_CurrentlyExecutingPropertyChangedEvents.Add(propertyName)) 157 | { 158 | return; 159 | } 160 | } 161 | try 162 | { 163 | PropertyChanged(proxy, new PropertyChangedEventArgs(propertyName)); 164 | } 165 | finally 166 | { 167 | lock (_CurrentlyExecutingPropertyChangedEventsLock) 168 | { 169 | _CurrentlyExecutingPropertyChangedEvents.Remove(propertyName); 170 | } 171 | } 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /Source/ChangeTracking/ChangeTrackingCollectionInterceptor.cs: -------------------------------------------------------------------------------- 1 | using Castle.DynamicProxy; 2 | using ChangeTracking.Internal; 3 | using System.Collections.Generic; 4 | using System.Collections.Specialized; 5 | using System.Linq; 6 | using System.Reflection; 7 | 8 | namespace ChangeTracking 9 | { 10 | internal sealed class ChangeTrackingCollectionInterceptor : IInterceptor, IChangeTrackableCollection, IInterceptorSettings where T : class 11 | { 12 | private readonly ChangeTrackingBindingList _WrappedTarget; 13 | private readonly IList _DeletedItems; 14 | private readonly static HashSet _ImplementedMethods; 15 | private readonly static HashSet _BindingListImplementedMethods; 16 | private readonly static HashSet _IBindingListImplementedMethods; 17 | private readonly static HashSet _INotifyCollectionChangedImplementedMethods; 18 | private readonly ChangeTrackingSettings _ChangeTrackingSettings; 19 | private readonly Graph _Graph; 20 | 21 | public bool IsInitialized { get; set; } 22 | 23 | static ChangeTrackingCollectionInterceptor() 24 | { 25 | _ImplementedMethods = new HashSet(typeof(ChangeTrackingCollectionInterceptor).GetMethods(BindingFlags.Instance | BindingFlags.Public).Select(m => m.Name)); 26 | _BindingListImplementedMethods = new HashSet(typeof(ChangeTrackingBindingList).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy).Select(m => m.Name)); 27 | _IBindingListImplementedMethods = new HashSet(typeof(ChangeTrackingBindingList).GetInterfaceMap(typeof(System.ComponentModel.IBindingList)).TargetMethods.Where(mi => mi.IsPrivate).Select(mi => mi.Name.Substring(mi.Name.LastIndexOf('.') + 1))); 28 | _INotifyCollectionChangedImplementedMethods = new HashSet(typeof(INotifyCollectionChanged).GetMethods(BindingFlags.Instance | BindingFlags.Public).Select(m => m.Name)); 29 | } 30 | 31 | internal ChangeTrackingCollectionInterceptor(IList target, ChangeTrackingSettings changeTrackingSettings, Graph graph) 32 | { 33 | _ChangeTrackingSettings = changeTrackingSettings; 34 | _Graph = graph; 35 | for (int i = 0; i < target.Count; i++) 36 | { 37 | target[i] = ChangeTrackingFactory.Default.AsTrackable(target[i], ChangeStatus.Unchanged, ItemCanceled, _ChangeTrackingSettings, _Graph); 38 | } 39 | _WrappedTarget = new ChangeTrackingBindingList(target, DeleteItem, ItemCanceled, _ChangeTrackingSettings, _Graph); 40 | _DeletedItems = new List(); 41 | } 42 | 43 | public void Intercept(IInvocation invocation) 44 | { 45 | switch (invocation.Method.Name) 46 | { 47 | case nameof(IRevertibleChangeTrackingInternal.AcceptChanges): 48 | AcceptChanges(invocation.Proxy, invocation.Arguments.Length == 0 ? null : (List)invocation.Arguments[0]); 49 | break; 50 | case nameof(IRevertibleChangeTrackingInternal.RejectChanges): 51 | RejectChanges(invocation.Proxy, invocation.Arguments.Length == 0 ? null : (List)invocation.Arguments[0]); 52 | break; 53 | default: 54 | if (_ImplementedMethods.Contains(invocation.Method.Name)) 55 | { 56 | invocation.ReturnValue = invocation.Method.Invoke(this, invocation.Arguments); 57 | return; 58 | } 59 | if (_BindingListImplementedMethods.Contains(invocation.Method.Name)) 60 | { 61 | invocation.ReturnValue = invocation.Method.Invoke(_WrappedTarget, invocation.Arguments); 62 | return; 63 | } 64 | if (_IBindingListImplementedMethods.Contains(invocation.Method.Name)) 65 | { 66 | invocation.ReturnValue = invocation.Method.Invoke(_WrappedTarget, invocation.Arguments); 67 | return; 68 | } 69 | if (_INotifyCollectionChangedImplementedMethods.Contains(invocation.Method.Name)) 70 | { 71 | invocation.ReturnValue = invocation.Method.Invoke(_WrappedTarget, invocation.Arguments); 72 | return; 73 | } 74 | if (invocation.Method.Name == nameof(IRevertibleChangeTrackingInternal.IsChanged)) 75 | { 76 | invocation.ReturnValue = IsChanged; 77 | return; 78 | } 79 | invocation.Proceed(); 80 | break; 81 | } 82 | } 83 | 84 | private void DeleteItem(T item) 85 | { 86 | var currentStatus = item.CastToIChangeTrackable().ChangeTrackingStatus; 87 | var manager = (IChangeTrackingManager)item; 88 | bool deleteSuccess = manager.Delete(); 89 | if (deleteSuccess && currentStatus != ChangeStatus.Added) 90 | { 91 | _DeletedItems.Add(item); 92 | } 93 | } 94 | 95 | private void ItemCanceled(T item) => _WrappedTarget.CancelNew(_WrappedTarget.IndexOf(item)); 96 | 97 | public IEnumerable UnchangedItems => _WrappedTarget.Cast>().Where(ct => ct.ChangeTrackingStatus == ChangeStatus.Unchanged).Cast(); 98 | 99 | public IEnumerable AddedItems => _WrappedTarget.Cast>().Where(ct => ct.ChangeTrackingStatus == ChangeStatus.Added).Cast(); 100 | 101 | public IEnumerable ChangedItems => _WrappedTarget.Cast>().Where(ct => ct.ChangeTrackingStatus == ChangeStatus.Changed).Cast(); 102 | 103 | public IEnumerable DeletedItems => _DeletedItems.Select(i => i); 104 | 105 | public bool UnDelete(T item) 106 | { 107 | var manager = (IChangeTrackingManager)item; 108 | bool unDeleteSuccess = manager.UnDelete(); 109 | if (unDeleteSuccess) 110 | { 111 | bool removeSuccess = _DeletedItems.Remove(item); 112 | if (removeSuccess) 113 | { 114 | _WrappedTarget.Add(item); 115 | return true; 116 | } 117 | } 118 | return false; 119 | } 120 | 121 | public void AcceptChanges() => throw new System.InvalidOperationException("Invalid call you must call the overload with proxy and parents arguments"); 122 | 123 | private void AcceptChanges(object proxy, List parents) 124 | { 125 | parents = parents ?? new List(20) { proxy }; 126 | foreach (var item in _WrappedTarget.Cast()) 127 | { 128 | item.AcceptChanges(parents); 129 | if (item is IEditableObjectInternal editable) 130 | { 131 | editable.EndEdit(parents); 132 | } 133 | } 134 | _DeletedItems.Clear(); 135 | } 136 | 137 | public void RejectChanges() => throw new System.InvalidOperationException("Invalid call you must call the overload with proxy and parents arguments"); 138 | 139 | private void RejectChanges(object proxy, List parents) 140 | { 141 | AddedItems.ToList().ForEach(i => _WrappedTarget.Remove(i)); 142 | parents = parents ?? new List(20) { proxy }; 143 | foreach (var item in _WrappedTarget.Cast()) 144 | { 145 | item.RejectChanges(parents); 146 | } 147 | foreach (var item in _DeletedItems) 148 | { 149 | ((IRevertibleChangeTrackingInternal)item).RejectChanges(parents); 150 | _WrappedTarget.Add(item); 151 | } 152 | _DeletedItems.Clear(); 153 | } 154 | 155 | public bool IsChanged => ChangedItems.Any() || AddedItems.Any() || DeletedItems.Any(); 156 | 157 | public IEnumerator GetEnumerator() => _WrappedTarget.GetEnumerator(); 158 | 159 | System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /Source/ChangeTracking/CollectionPropertyInterceptor.cs: -------------------------------------------------------------------------------- 1 | using Castle.DynamicProxy; 2 | using ChangeTracking.Internal; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Threading; 8 | 9 | namespace ChangeTracking 10 | { 11 | internal class CollectionPropertyInterceptor : IInterceptor, IInterceptorSettings 12 | { 13 | private static readonly List _Properties; 14 | private static readonly Dictionary, object, ChangeTrackingSettings, Graph>> _Actions; 15 | private readonly Dictionary _Trackables; 16 | private readonly object _TrackablesLock; 17 | private readonly ChangeTrackingSettings _ChangeTrackingSettings; 18 | private readonly Graph _Graph; 19 | private bool _AreAllPropertiesTrackable; 20 | 21 | public bool IsInitialized { get; set; } 22 | 23 | static CollectionPropertyInterceptor() 24 | { 25 | _Actions = new Dictionary, object, ChangeTrackingSettings, Graph>>(); 26 | _Properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance).ToList(); 27 | var getters = _Properties.Where(pi => pi.CanRead).Select(pi => new KeyValuePair, object, ChangeTrackingSettings, Graph>>(pi.Name, GetGetterAction(pi))); 28 | foreach (var getter in getters) 29 | { 30 | _Actions.Add("get_" + getter.Key, getter.Value); 31 | } 32 | var setters = _Properties.Where(pi => pi.CanWrite).Select(pi => new KeyValuePair, object, ChangeTrackingSettings, Graph>>(pi.Name, GetSetterAction(pi))); 33 | foreach (var setter in setters) 34 | { 35 | _Actions.Add("set_" + setter.Key, setter.Value); 36 | } 37 | } 38 | 39 | public CollectionPropertyInterceptor(ChangeTrackingSettings changeTrackingSettings, Graph graph) 40 | { 41 | _ChangeTrackingSettings = changeTrackingSettings; 42 | _Graph = graph; 43 | _Trackables = new Dictionary(); 44 | _TrackablesLock = new object(); 45 | } 46 | 47 | private static Action, object, ChangeTrackingSettings, Graph> GetGetterAction(PropertyInfo propertyInfo) 48 | { 49 | if (CanCollectionBeTrackable(propertyInfo)) 50 | { 51 | return (invocation, trackables, trackablesLock, changeTrackingSettings, graph) => 52 | { 53 | string propertyName = invocation.Method.PropertyName(); 54 | lock (trackablesLock) 55 | { 56 | if (!trackables.ContainsKey(propertyName)) 57 | { 58 | object childTarget = propertyInfo.GetValue(invocation.InvocationTarget, null); 59 | if (childTarget == null) 60 | { 61 | return; 62 | } 63 | trackables.Add(propertyName, ChangeTrackingFactory.Default.AsTrackableCollectionChild(propertyInfo.PropertyType, childTarget, changeTrackingSettings, graph)); 64 | } 65 | invocation.ReturnValue = trackables[propertyName]; 66 | } 67 | }; 68 | } 69 | return (invocation, _, __, ___, ____) => 70 | { 71 | invocation.Proceed(); 72 | }; 73 | } 74 | 75 | private static Action, object, ChangeTrackingSettings, Graph> GetSetterAction(PropertyInfo propertyInfo) 76 | { 77 | if (CanCollectionBeTrackable(propertyInfo)) 78 | { 79 | return (invocation, trackables, trackablesLock, changeTrackingSettings, graph) => 80 | { 81 | string parentPropertyName = invocation.Method.PropertyName(); 82 | invocation.Proceed(); 83 | 84 | bool lockWasTaken = false; 85 | try 86 | { 87 | object childTarget = invocation.Arguments[0]; 88 | object newValue; 89 | if (invocation.Arguments[0] == null) 90 | { 91 | newValue = null; 92 | } 93 | else if (childTarget is IProxyTargetAccessor && childTarget.GetType().GetInterfaces().FirstOrDefault(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IChangeTrackableCollection<>)) != null) 94 | { 95 | newValue = invocation.Arguments[0]; 96 | } 97 | else 98 | { 99 | Monitor.Enter(trackablesLock, ref lockWasTaken); 100 | newValue = ChangeTrackingFactory.Default.AsTrackableCollectionChild(propertyInfo.PropertyType, childTarget, changeTrackingSettings, graph); 101 | } 102 | if (!lockWasTaken) 103 | { 104 | Monitor.Enter(trackablesLock, ref lockWasTaken); 105 | } 106 | trackables[parentPropertyName] = newValue; 107 | } 108 | finally 109 | { 110 | if (lockWasTaken) 111 | { 112 | Monitor.Exit(trackablesLock); 113 | } 114 | } 115 | }; 116 | } 117 | return (invocation, _, __, ___, ____) => 118 | { 119 | invocation.Proceed(); 120 | }; 121 | } 122 | 123 | private static bool CanCollectionBeTrackable(PropertyInfo propertyInfo) 124 | { 125 | Type propertyType = propertyInfo.PropertyType; 126 | Type genericCollectionArgumentType = propertyType.GetGenericArguments().FirstOrDefault(); 127 | return genericCollectionArgumentType != null && propertyType.IsInterface && typeof(ICollection<>).MakeGenericType(genericCollectionArgumentType).IsAssignableFrom(propertyType) && !Utils.IsMarkedDoNotTrack(propertyType); 128 | } 129 | 130 | public void Intercept(IInvocation invocation) 131 | { 132 | if (!IsInitialized) 133 | { 134 | return; 135 | } 136 | if (invocation.Method.Name == "get_CollectionPropertyTrackables") 137 | { 138 | invocation.ReturnValue = CollectionPropertyTrackables(invocation.Proxy); 139 | return; 140 | } 141 | if (_ChangeTrackingSettings.MakeCollectionPropertiesTrackable && _Actions.TryGetValue(invocation.Method.Name, out Action, object, ChangeTrackingSettings, Graph> action)) 142 | { 143 | action(invocation, _Trackables, _TrackablesLock, _ChangeTrackingSettings, _Graph); 144 | } 145 | else 146 | { 147 | invocation.Proceed(); 148 | } 149 | } 150 | 151 | private IEnumerable CollectionPropertyTrackables(object proxy) 152 | { 153 | if (!_ChangeTrackingSettings.MakeCollectionPropertiesTrackable) 154 | { 155 | return Enumerable.Empty(); 156 | } 157 | if (!_AreAllPropertiesTrackable) 158 | { 159 | MakeAllPropertiesTrackable(proxy); 160 | } 161 | lock (_TrackablesLock) 162 | { 163 | return _Trackables.Values.ToArray(); 164 | } 165 | } 166 | 167 | private void MakeAllPropertiesTrackable(object proxy) 168 | { 169 | var notTrackedProperties = _Properties.Where(pi => !_Trackables.ContainsKey(pi.Name) && CanCollectionBeTrackable(pi)); 170 | foreach (var property in notTrackedProperties) 171 | { 172 | property.GetValue(proxy, null); 173 | } 174 | _AreAllPropertiesTrackable = true; 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /Source/ChangeTracking/ComplexPropertyInterceptor.cs: -------------------------------------------------------------------------------- 1 | using Castle.DynamicProxy; 2 | using ChangeTracking.Internal; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Threading; 8 | 9 | namespace ChangeTracking 10 | { 11 | internal class ComplexPropertyInterceptor : IInterceptor, IInterceptorSettings 12 | { 13 | private static readonly List _Properties; 14 | private static readonly Dictionary, object, ChangeTrackingSettings, Graph>> _Actions; 15 | private readonly Dictionary _Trackables; 16 | private readonly object _TrackablesLock; 17 | private readonly ChangeTrackingSettings _ChangeTrackingSettings; 18 | private readonly Graph _Graph; 19 | private bool _AreAllPropertiesTrackable; 20 | 21 | public bool IsInitialized { get; set; } 22 | 23 | static ComplexPropertyInterceptor() 24 | { 25 | _Actions = new Dictionary, object, ChangeTrackingSettings, Graph>>(); 26 | _Properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance).ToList(); 27 | var getters = _Properties.Where(pi => pi.CanRead).Select(pi => new KeyValuePair, object, ChangeTrackingSettings, Graph>>(pi.Name, GetGetterAction(pi))); 28 | foreach (var getter in getters) 29 | { 30 | _Actions.Add("get_" + getter.Key, getter.Value); 31 | } 32 | var setters = _Properties.Where(pi => pi.CanWrite).Select(pi => new KeyValuePair, object, ChangeTrackingSettings, Graph>>(pi.Name, GetSetterAction(pi))); 33 | foreach (var setter in setters) 34 | { 35 | _Actions.Add("set_" + setter.Key, setter.Value); 36 | } 37 | } 38 | 39 | internal ComplexPropertyInterceptor(ChangeTrackingSettings changeTrackingSettings, Graph graph) 40 | { 41 | _ChangeTrackingSettings = changeTrackingSettings; 42 | _Graph = graph; 43 | _Trackables = new Dictionary(); 44 | _TrackablesLock = new object(); 45 | } 46 | 47 | private static Action, object, ChangeTrackingSettings, Graph> GetGetterAction(PropertyInfo propertyInfo) 48 | { 49 | if (CanComplexPropertyBeTrackable(propertyInfo)) 50 | { 51 | return (invocation, trackables, trackablesLock, changeTrackingSettings, graph) => 52 | { 53 | string propertyName = invocation.Method.PropertyName(); 54 | lock (trackablesLock) 55 | { 56 | if (!trackables.ContainsKey(propertyName)) 57 | { 58 | object childTarget = propertyInfo.GetValue(invocation.InvocationTarget, null); 59 | if (childTarget == null) 60 | { 61 | return; 62 | } 63 | trackables.Add(propertyName, ChangeTrackingFactory.Default.AsTrackableChild(propertyInfo.PropertyType, childTarget, null, changeTrackingSettings, graph)); 64 | } 65 | invocation.ReturnValue = trackables[propertyName]; 66 | } 67 | }; 68 | } 69 | return (invocation, _, __, ___, ____) => 70 | { 71 | invocation.Proceed(); 72 | }; 73 | } 74 | 75 | private static Action, object, ChangeTrackingSettings, Graph> GetSetterAction(PropertyInfo propertyInfo) 76 | { 77 | if (CanComplexPropertyBeTrackable(propertyInfo)) 78 | { 79 | return (invocation, trackables, trackablesLock, changeTrackingSettings, graph) => 80 | { 81 | string parentPropertyName = invocation.Method.PropertyName(); 82 | invocation.Proceed(); 83 | 84 | bool lockWasTaken = false; 85 | try 86 | { 87 | object childTarget = invocation.Arguments[0]; 88 | object newValue; 89 | if (childTarget == null) 90 | { 91 | newValue = null; 92 | } 93 | else if (childTarget is IChangeTrackableInternal) 94 | { 95 | newValue = invocation.Arguments[0]; 96 | } 97 | else 98 | { 99 | Monitor.Enter(trackablesLock, ref lockWasTaken); 100 | newValue = ChangeTrackingFactory.Default.AsTrackableChild(propertyInfo.PropertyType, childTarget, null, changeTrackingSettings, graph); 101 | } 102 | if (!lockWasTaken) 103 | { 104 | Monitor.Enter(trackablesLock, ref lockWasTaken); 105 | } 106 | trackables[parentPropertyName] = newValue; 107 | } 108 | finally 109 | { 110 | if (lockWasTaken) 111 | { 112 | Monitor.Exit(trackablesLock); 113 | } 114 | } 115 | }; 116 | } 117 | return (invocation, _, __, ___, ____) => 118 | { 119 | invocation.Proceed(); 120 | }; 121 | } 122 | 123 | private static bool CanComplexPropertyBeTrackable(PropertyInfo propertyInfo) 124 | { 125 | if (!propertyInfo.CanWrite || Utils.IsMarkedDoNotTrack(propertyInfo)) 126 | { 127 | return false; 128 | } 129 | Type propertyType = propertyInfo.PropertyType; 130 | return propertyType.IsClass && 131 | !propertyType.IsSealed && 132 | propertyType.GetConstructor(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, binder: null, Type.EmptyTypes, modifiers: null) != null && 133 | propertyType.GetProperties(BindingFlags.Public | BindingFlags.Instance).All(pi => Utils.IsMarkedDoNotTrack(pi) || pi.GetAccessors()[0].IsVirtual); 134 | } 135 | 136 | public void Intercept(IInvocation invocation) 137 | { 138 | if (!IsInitialized) 139 | { 140 | return; 141 | } 142 | if (invocation.Method.Name == "get_ComplexPropertyTrackables") 143 | { 144 | invocation.ReturnValue = ComplexPropertyTrackables(invocation.Proxy); 145 | return; 146 | } 147 | if (_ChangeTrackingSettings.MakeComplexPropertiesTrackable && _Actions.TryGetValue(invocation.Method.Name, out Action, object, ChangeTrackingSettings, Graph> action)) 148 | { 149 | action(invocation, _Trackables, _TrackablesLock, _ChangeTrackingSettings, _Graph); 150 | } 151 | else 152 | { 153 | invocation.Proceed(); 154 | } 155 | } 156 | 157 | private IEnumerable ComplexPropertyTrackables(object proxy) 158 | { 159 | if (!_ChangeTrackingSettings.MakeComplexPropertiesTrackable) 160 | { 161 | return Enumerable.Empty(); 162 | } 163 | if (!_AreAllPropertiesTrackable) 164 | { 165 | MakeAllPropertiesTrackable(proxy); 166 | } 167 | lock (_TrackablesLock) 168 | { 169 | return _Trackables.Values.ToArray(); 170 | } 171 | } 172 | 173 | private void MakeAllPropertiesTrackable(object proxy) 174 | { 175 | var notTrackedProperties = _Properties.Where(pi => !_Trackables.ContainsKey(pi.Name) && CanComplexPropertyBeTrackable(pi)); 176 | foreach (var property in notTrackedProperties) 177 | { 178 | property.GetValue(proxy, null); 179 | } 180 | _AreAllPropertiesTrackable = true; 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /Source/ChangeTracking/ChangeTrackingFactory.cs: -------------------------------------------------------------------------------- 1 | using Castle.DynamicProxy; 2 | using ChangeTracking.Internal; 3 | using System; 4 | using System.Collections; 5 | using System.Collections.Concurrent; 6 | using System.Collections.Generic; 7 | using System.Collections.Specialized; 8 | using System.ComponentModel; 9 | using System.Linq; 10 | using System.Linq.Expressions; 11 | using System.Reflection; 12 | 13 | namespace ChangeTracking 14 | { 15 | public class ChangeTrackingFactory : IChangeTrackingFactory 16 | { 17 | private readonly ProxyGenerator _ProxyGenerator; 18 | private readonly IInterceptorSelector _Selector; 19 | private readonly ConcurrentDictionary _Options; 20 | private readonly MethodInfo _SetValueMethodInfo; 21 | private readonly ConcurrentDictionary> _FieldCopiers; 22 | 23 | static ChangeTrackingFactory() => Default = new ChangeTrackingFactory(); 24 | 25 | public static ChangeTrackingFactory Default { get; } 26 | 27 | public ChangeTrackingFactory() : this(makeComplexPropertiesTrackable: true, makeCollectionPropertiesTrackable: true) { } 28 | 29 | public ChangeTrackingFactory(bool makeComplexPropertiesTrackable, bool makeCollectionPropertiesTrackable) 30 | { 31 | MakeComplexPropertiesTrackable = makeComplexPropertiesTrackable; 32 | MakeCollectionPropertiesTrackable = makeCollectionPropertiesTrackable; 33 | 34 | _ProxyGenerator = new ProxyGenerator(); 35 | _Selector = new ChangeTrackingInterceptorSelector(); 36 | _Options = new ConcurrentDictionary(); 37 | _SetValueMethodInfo = typeof(FieldInfo).GetMethod(nameof(FieldInfo.SetValue), new[] { typeof(object), typeof(object) }); 38 | _FieldCopiers = new ConcurrentDictionary>(); 39 | } 40 | 41 | public T AsTrackable(T target, ChangeStatus status = ChangeStatus.Unchanged) where T : class 42 | { 43 | return AsTrackable(target, new ChangeTrackingSettings(MakeComplexPropertiesTrackable, MakeCollectionPropertiesTrackable), status); 44 | } 45 | 46 | public ICollection AsTrackableCollection(ICollection target) where T : class 47 | { 48 | return AsTrackableCollection(target, new ChangeTrackingSettings(MakeComplexPropertiesTrackable, MakeCollectionPropertiesTrackable)); 49 | } 50 | 51 | public bool MakeComplexPropertiesTrackable { get; set; } 52 | public bool MakeCollectionPropertiesTrackable { get; set; } 53 | 54 | internal T AsTrackable(T target, ChangeStatus status, Action notifyParentListItemCanceled, ChangeTrackingSettings changeTrackingSettings, Graph graph) where T : class 55 | { 56 | ThrowIfTargetIsProxy(target); 57 | ProxyWeakTargetMap existing = graph.GetExistingProxyForTarget(target); 58 | if (existing != null) 59 | { 60 | return (T)existing.Proxy; 61 | } 62 | 63 | //if T was ICollection it would of gone to one of the other overloads 64 | if (target as ICollection != null) 65 | { 66 | throw new InvalidOperationException("Only IList, List and ICollection are supported"); 67 | } 68 | 69 | var changeTrackingInterceptor = new ChangeTrackingInterceptor(status); 70 | var notifyPropertyChangedInterceptor = new NotifyPropertyChangedInterceptor(changeTrackingInterceptor); 71 | var editableObjectInterceptor = new EditableObjectInterceptor(notifyParentListItemCanceled); 72 | var complexPropertyInterceptor = new ComplexPropertyInterceptor(changeTrackingSettings, graph); 73 | var collectionPropertyInterceptor = new CollectionPropertyInterceptor(changeTrackingSettings, graph); 74 | object proxy = _ProxyGenerator.CreateClassProxyWithTarget(typeof(T), 75 | new[] { typeof(IChangeTrackableInternal), typeof(IRevertibleChangeTrackingInternal), typeof(IChangeTrackable), typeof(IChangeTrackingManager), typeof(IComplexPropertyTrackable), typeof(ICollectionPropertyTrackable), typeof(IEditableObjectInternal), typeof(INotifyPropertyChanged) }, 76 | target, 77 | GetOptions(typeof(T)), 78 | notifyPropertyChangedInterceptor, 79 | changeTrackingInterceptor, 80 | editableObjectInterceptor, 81 | complexPropertyInterceptor, 82 | collectionPropertyInterceptor); 83 | CopyFieldsAndProperties(source: target, target: proxy); 84 | notifyPropertyChangedInterceptor.IsInitialized = true; 85 | changeTrackingInterceptor.IsInitialized = true; 86 | editableObjectInterceptor.IsInitialized = true; 87 | complexPropertyInterceptor.IsInitialized = true; 88 | collectionPropertyInterceptor.IsInitialized = true; 89 | graph.Add(new ProxyWeakTargetMap(target, proxy)); 90 | return (T)proxy; 91 | } 92 | 93 | internal ICollection AsTrackableCollection(ICollection target, ChangeTrackingSettings changeTrackingSettings) where T : class 94 | { 95 | if (!(target is IList list)) 96 | { 97 | list = target.ToList(); 98 | } 99 | ThrowIfTargetIsProxy(list); 100 | if (list.OfType>().Any(ct => ct.ChangeTrackingStatus != ChangeStatus.Unchanged)) 101 | { 102 | throw new InvalidOperationException("some items in the collection are already being tracked"); 103 | } 104 | 105 | Graph graph = new Graph(); 106 | object proxy = _ProxyGenerator.CreateInterfaceProxyWithTarget(typeof(IList), 107 | new[] { typeof(IChangeTrackableCollection), typeof(IRevertibleChangeTrackingInternal), typeof(IBindingList), typeof(ICancelAddNew), typeof(INotifyCollectionChanged) }, list, GetOptions(typeof(T)), new ChangeTrackingCollectionInterceptor(list, changeTrackingSettings, graph)); 108 | graph.Add(new ProxyWeakTargetMap(list, proxy)); 109 | return (ICollection)proxy; 110 | } 111 | 112 | internal T AsTrackable(T target, ChangeTrackingSettings changeTrackingSettings, ChangeStatus status = ChangeStatus.Unchanged) where T : class 113 | { 114 | return AsTrackable(target, status, null, changeTrackingSettings, new Graph()); 115 | } 116 | 117 | private ProxyGenerationOptions GetOptions(Type type) 118 | { 119 | ProxyGenerationOptions CreateOptions(Type createOptionsForType) => new ProxyGenerationOptions 120 | { 121 | Hook = new ChangeTrackingProxyGenerationHook(createOptionsForType), 122 | Selector = _Selector 123 | }; 124 | return _Options.GetOrAdd(type, CreateOptions); 125 | } 126 | 127 | private void CopyFieldsAndProperties(T source, object target) => CopyFieldsAndProperties(typeof(T), source, target); 128 | 129 | private void CopyFieldsAndProperties(Type type, object source, object target) 130 | { 131 | Action copier = _FieldCopiers.GetOrAdd(type, typeCopying => 132 | { 133 | List fieldInfosToCopy = typeCopying 134 | .GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) 135 | .Where(fi => !(fi.Name.StartsWith("<") && fi.Name.EndsWith(">k__BackingField"))) 136 | .ToList(); 137 | List propertyInfosToCopy = typeCopying 138 | .GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) 139 | .Where(pi => pi.CanRead && pi.CanWrite && Utils.IsMarkedDoNotTrack(pi)) 140 | .ToList(); 141 | if (fieldInfosToCopy.Count == 0 && propertyInfosToCopy.Count == 0) 142 | { 143 | return null; 144 | } 145 | 146 | ParameterExpression sourceParameter = Expression.Parameter(typeof(object), "source"); 147 | ParameterExpression targetParameter = Expression.Parameter(typeof(object), "target"); 148 | UnaryExpression sourceAsType = Expression.Convert(sourceParameter, typeCopying); 149 | UnaryExpression targetAsType = Expression.Convert(targetParameter, typeCopying); 150 | 151 | IEnumerable setFieldsExpressions = fieldInfosToCopy.Select(fi => 152 | { 153 | if (fi.IsInitOnly) 154 | { 155 | return Expression.Call(Expression.Constant(fi), _SetValueMethodInfo, targetAsType, Expression.Field(sourceAsType, fi)); 156 | } 157 | return Expression.Assign(Expression.Field(targetAsType, fi), Expression.Field(sourceAsType, fi)); 158 | }); 159 | IEnumerable setPropertiesExpressions = propertyInfosToCopy.Select(pi => 160 | { 161 | return Expression.Assign(Expression.Property(targetAsType, pi), Expression.Property(sourceAsType, pi)); 162 | }); 163 | BlockExpression block = Expression.Block(setFieldsExpressions.Concat(setPropertiesExpressions)); 164 | 165 | return Expression.Lambda>(block, sourceParameter, targetParameter).Compile(); 166 | }); 167 | copier?.Invoke(source, target); 168 | } 169 | 170 | internal object AsTrackableCollectionChild(Type type, object target, ChangeTrackingSettings changeTrackingSettings, Graph graph) 171 | { 172 | ThrowIfTargetIsProxy(target); 173 | ProxyWeakTargetMap existing = graph.GetExistingProxyForTarget(target); 174 | if (existing != null) 175 | { 176 | return existing.Proxy; 177 | } 178 | Type genericArgument = type.GetGenericArguments().First(); 179 | object proxy = _ProxyGenerator.CreateInterfaceProxyWithTarget(typeof(IList<>).MakeGenericType(genericArgument), 180 | new[] { typeof(IChangeTrackableCollection<>).MakeGenericType(genericArgument), typeof(IRevertibleChangeTrackingInternal), typeof(IBindingList), typeof(ICancelAddNew), typeof(INotifyCollectionChanged) }, 181 | target, 182 | GetOptions(genericArgument), 183 | CreateInterceptor(typeof(ChangeTrackingCollectionInterceptor<>).MakeGenericType(genericArgument), target, changeTrackingSettings, graph)); 184 | graph.Add(new ProxyWeakTargetMap(target, proxy)); 185 | return proxy; 186 | } 187 | 188 | private static void ThrowIfTargetIsProxy(object target) 189 | { 190 | if (target is IRevertibleChangeTrackingInternal) 191 | { 192 | throw new InvalidOperationException("The target is already a Trackable Proxy"); 193 | } 194 | } 195 | 196 | internal object AsTrackableChild(Type type, object target, Action notifyParentItemCanceled, ChangeTrackingSettings changeTrackingSettings, Internal.Graph graph) 197 | { 198 | ThrowIfTargetIsProxy(target); 199 | ProxyWeakTargetMap existing = graph.GetExistingProxyForTarget(target); 200 | if (existing != null) 201 | { 202 | return existing.Proxy; 203 | } 204 | IInterceptor changeTrackingInterceptor = CreateInterceptor(typeof(ChangeTrackingInterceptor<>).MakeGenericType(type), ChangeStatus.Unchanged); 205 | IInterceptor notifyPropertyChangedInterceptor = CreateInterceptor(typeof(NotifyPropertyChangedInterceptor<>).MakeGenericType(type), changeTrackingInterceptor); 206 | IInterceptor editableObjectInterceptor = CreateInterceptor(typeof(EditableObjectInterceptor<>).MakeGenericType(type), notifyParentItemCanceled); 207 | IInterceptor complexPropertyInterceptor = CreateInterceptor(typeof(ComplexPropertyInterceptor<>).MakeGenericType(type), changeTrackingSettings, graph); 208 | IInterceptor collectionPropertyInterceptor = CreateInterceptor(typeof(CollectionPropertyInterceptor<>).MakeGenericType(type), changeTrackingSettings, graph); 209 | object proxy = _ProxyGenerator.CreateClassProxyWithTarget(type, 210 | new[] { typeof(IChangeTrackableInternal), typeof(IRevertibleChangeTrackingInternal), typeof(IChangeTrackable<>).MakeGenericType(type), typeof(IChangeTrackingManager), typeof(IComplexPropertyTrackable), typeof(ICollectionPropertyTrackable), typeof(IEditableObjectInternal), typeof(INotifyPropertyChanged) }, 211 | target, 212 | GetOptions(type), 213 | notifyPropertyChangedInterceptor, 214 | changeTrackingInterceptor, 215 | editableObjectInterceptor, 216 | complexPropertyInterceptor, 217 | collectionPropertyInterceptor); 218 | CopyFieldsAndProperties(type: type, source: target, target: proxy); 219 | ((IInterceptorSettings)notifyPropertyChangedInterceptor).IsInitialized = true; 220 | ((IInterceptorSettings)changeTrackingInterceptor).IsInitialized = true; 221 | ((IInterceptorSettings)editableObjectInterceptor).IsInitialized = true; 222 | ((IInterceptorSettings)complexPropertyInterceptor).IsInitialized = true; 223 | ((IInterceptorSettings)collectionPropertyInterceptor).IsInitialized = true; 224 | graph.Add(new ProxyWeakTargetMap(target, proxy)); 225 | return proxy; 226 | } 227 | 228 | private static IInterceptor CreateInterceptor(Type type, params object[] args) 229 | { 230 | return (IInterceptor)Activator.CreateInstance(type, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, null, args, null); 231 | } 232 | } 233 | } -------------------------------------------------------------------------------- /Source/ChangeTracking.Tests/IChangeTrackableCollectionTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using FluentAssertions; 5 | using Xunit; 6 | 7 | namespace ChangeTracking.Tests 8 | { 9 | public class IChangeTrackableCollectionTests 10 | { 11 | [Fact] 12 | public void AsTrackable_On_Collection_Should_Make_Object_Implement_IChangeTrackableCollection() 13 | { 14 | var orders = Helper.GetOrdersIList(); 15 | 16 | ICollection trackable = orders.AsTrackable(); 17 | 18 | trackable.Should().BeAssignableTo>(); 19 | } 20 | 21 | [Fact] 22 | public void When_AsTrackable_On_Collection_CastToIChangeTrackableCollection_Should_Not_Throw_InvalidCastException() 23 | { 24 | var orders = Helper.GetOrdersIList(); 25 | 26 | IList trackable = orders.AsTrackable(); 27 | 28 | trackable.Invoking(o => o.CastToIChangeTrackableCollection()).Should().NotThrow(); 29 | } 30 | 31 | [Fact] 32 | public void When_Not_AsTrackable_On_Collection_CastToIChangeTrackableCollection_Should_Throw_InvalidCastException() 33 | { 34 | var orders = Helper.GetOrdersIList(); 35 | 36 | orders.Invoking(o => o.CastToIChangeTrackableCollection()).Should().Throw(); 37 | } 38 | 39 | [Fact] 40 | public void When_Calling_AsTrackable_On_Collection_Already_Tracking_Should_Throw() 41 | { 42 | var orders = Helper.GetOrdersIList(); 43 | 44 | orders[0] = orders[0].AsTrackable(); 45 | orders[0].CustomerNumber = "Test1"; 46 | 47 | orders.Invoking(list => list.AsTrackable()).Should().Throw(); 48 | } 49 | 50 | [Fact] 51 | public void When_Calling_AsTrackable_On_Collection_All_Items_Should_Become_Trackable() 52 | { 53 | var orders = Helper.GetOrdersIList(); 54 | 55 | orders.AsTrackable(); 56 | 57 | orders.Should().ContainItemsAssignableTo>(); 58 | } 59 | 60 | [Fact] 61 | public void When_Adding_To_Collection_Should_Be_IChangeTracableTrackable() 62 | { 63 | var orders = Helper.GetOrdersIList(); 64 | 65 | var trackable = orders.AsTrackable(); 66 | trackable.Add(new Order { Id = 999999999, CustomerNumber = "Customer" }); 67 | 68 | trackable.Single(o => o.Id == 999999999).Should().BeAssignableTo>(); 69 | } 70 | 71 | [Fact] 72 | public void When_Adding_To_Collection_Status_Should_Be_Added() 73 | { 74 | var orders = Helper.GetOrdersIList(); 75 | 76 | var trackable = orders.AsTrackable(); 77 | trackable.Add(new Order { Id = 999999999, CustomerNumber = "Customer" }); 78 | 79 | trackable.Single(o => o.Id == 999999999).CastToIChangeTrackable().ChangeTrackingStatus.Should().Be(ChangeStatus.Added); 80 | } 81 | 82 | [Fact] 83 | public void When_Adding_To_Collection_Via_Indexer_Status_Should_Be_Added() 84 | { 85 | IList list = Helper.GetOrdersIList(); 86 | 87 | var trackable = list.AsTrackable(); 88 | trackable[0] = new Order { Id = 999999999, CustomerNumber = "Customer" }; 89 | 90 | trackable.Single(o => o.Id == 999999999).CastToIChangeTrackable().ChangeTrackingStatus.Should().Be(ChangeStatus.Added); 91 | } 92 | 93 | [Fact] 94 | public void When_Deleting_From_Collection_Status_Should_Be_Deleted() 95 | { 96 | var orders = Helper.GetOrdersIList(); 97 | 98 | var trackable = orders.AsTrackable(); 99 | var first = trackable.First(); 100 | trackable.Remove(first); 101 | 102 | first.CastToIChangeTrackable().ChangeTrackingStatus.Should().Be(ChangeStatus.Deleted); 103 | } 104 | 105 | [Fact] 106 | public void When_Deleting_From_Collection_Should_Be_Added_To_DeletedItems() 107 | { 108 | var orders = Helper.GetOrdersIList(); 109 | 110 | var trackable = orders.AsTrackable(); 111 | var first = trackable.First(); 112 | trackable.Remove(first); 113 | 114 | trackable.CastToIChangeTrackableCollection().DeletedItems.Should().HaveCount(1) 115 | .And.OnlyContain(o => o.Id == first.Id && o.CustomerNumber == first.CustomerNumber); 116 | } 117 | 118 | [Fact] 119 | public void When_Deleting_From_Collection_Item_That_Status_Is_Added_Should_Not_Be_Added_To_DeletedItems() 120 | { 121 | var orders = Helper.GetOrdersIList(); 122 | 123 | var trackable = orders.AsTrackable(); 124 | var first = trackable.First(); 125 | trackable.Remove(first); 126 | var order = Helper.GetOrder(); 127 | order.Id = 999; 128 | trackable.Add(order); 129 | trackable.Remove(trackable.Single(o => o.Id == 999)); 130 | 131 | trackable.CastToIChangeTrackableCollection().DeletedItems.Should().HaveCount(1) 132 | .And.OnlyContain(o => o.Id == first.Id && o.CustomerNumber == first.CustomerNumber); 133 | } 134 | 135 | [Fact] 136 | public void When_Using_Not_On_List_Of_T_Or_Collection_Of_T_Should_Throw() 137 | { 138 | var orders = Helper.GetOrdersIList().ToArray(); 139 | 140 | orders.Invoking(o => o.AsTrackable()).Should().Throw(); 141 | } 142 | 143 | [Fact] 144 | public void AsTrackable_On_Collection_Should_Make_It_IRevertibleChangeTracking() 145 | { 146 | var orders = Helper.GetOrdersIList(); 147 | 148 | var trackable = orders.AsTrackable(); 149 | 150 | trackable.Should().BeAssignableTo(); 151 | } 152 | 153 | [Fact] 154 | public void AcceptChanges_On_Collection_Should_All_Items_Status_Be_Unchanged() 155 | { 156 | var orders = Helper.GetOrdersIList(); 157 | 158 | var trackable = orders.AsTrackable(); 159 | var first = trackable.First(); 160 | first.Id = 963; 161 | first.CustomerNumber = "Testing"; 162 | var collectionintf = trackable.CastToIChangeTrackableCollection(); 163 | collectionintf.AcceptChanges(); 164 | 165 | trackable.All(o => o.CastToIChangeTrackable().ChangeTrackingStatus == ChangeStatus.Unchanged).Should().BeTrue(); 166 | } 167 | 168 | [Fact] 169 | public void AcceptChanges_On_Collection_Should_AcceptChanges() 170 | { 171 | var orders = Helper.GetOrdersIList(); 172 | 173 | var trackable = orders.AsTrackable(); 174 | var first = trackable.First(); 175 | first.Id = 963; 176 | first.CustomerNumber = "Testing"; 177 | var itemIntf = first.CastToIChangeTrackable(); 178 | var collectionintf = trackable.CastToIChangeTrackableCollection(); 179 | int oldChangeStatusCount = collectionintf.ChangedItems.Count(); 180 | collectionintf.AcceptChanges(); 181 | 182 | itemIntf.GetOriginalValue(c => c.CustomerNumber).Should().Be("Testing"); 183 | itemIntf.GetOriginalValue(c => c.Id).Should().Be(963); 184 | oldChangeStatusCount.Should().Be(1); 185 | collectionintf.ChangedItems.Count().Should().Be(0); 186 | } 187 | 188 | [Fact] 189 | public void AcceptChanges_On_Collection_Should_Clear_DeletedItems() 190 | { 191 | var orders = Helper.GetOrdersIList(); 192 | 193 | var trackable = orders.AsTrackable(); 194 | var first = trackable.First(); 195 | trackable.Remove(first); 196 | var intf = trackable.CastToIChangeTrackableCollection(); 197 | int oldDeleteStatusCount = intf.DeletedItems.Count(); 198 | intf.AcceptChanges(); 199 | 200 | 201 | oldDeleteStatusCount.Should().Be(1); 202 | intf.DeletedItems.Count().Should().Be(0); 203 | trackable.All(o => o.CastToIChangeTrackable().ChangeTrackingStatus == ChangeStatus.Unchanged).Should().BeTrue(); 204 | } 205 | 206 | [Fact] 207 | public void RejectChanges_On_Collection_Should_All_Items_Status_Be_Unchanged() 208 | { 209 | var orders = Helper.GetOrdersIList(); 210 | var trackable = orders.AsTrackable(); 211 | 212 | var first = trackable.First(); 213 | first.Id = 963; 214 | first.CustomerNumber = "Testing"; 215 | var intf = trackable.CastToIChangeTrackableCollection(); 216 | var oldAnythingUnchanged = intf.ChangedItems.Any(); 217 | intf.RejectChanges(); 218 | 219 | oldAnythingUnchanged.Should().BeTrue(); 220 | intf.ChangedItems.Count().Should().Be(0); 221 | } 222 | 223 | [Fact] 224 | public void RejectChanges_On_Collection_Should_RejectChanges() 225 | { 226 | var orders = Helper.GetOrdersIList(); 227 | var trackable = orders.AsTrackable(); 228 | 229 | var first = trackable.First(); 230 | first.Id = 963; 231 | first.CustomerNumber = "Testing"; 232 | var newOrder = Helper.GetOrder(); 233 | newOrder.Id = 999; 234 | trackable.Add(newOrder); 235 | trackable.RemoveAt(5); 236 | var intf = trackable.CastToIChangeTrackableCollection(); 237 | intf.RejectChanges(); 238 | var ordersToMatch = Helper.GetOrdersIList().AsTrackable(); 239 | 240 | intf.UnchangedItems.Should().Contain(i => ordersToMatch.SingleOrDefault(o => 241 | o.Id == i.Id && 242 | i.CustomerNumber == o.CustomerNumber && 243 | i.CastToIChangeTrackable().ChangeTrackingStatus == o.CastToIChangeTrackable().ChangeTrackingStatus) != null); 244 | intf.UnchangedItems.Count().Should().Be(ordersToMatch.Count); 245 | intf.UnchangedItems.Count().Should().Be(intf.Count()); 246 | } 247 | 248 | [Fact] 249 | public void RejectChanges_On_Collection_Should_Move_DeletedItems_Back_To_Unchanged() 250 | { 251 | var orders = Helper.GetOrdersIList(); 252 | var trackable = orders.AsTrackable(); 253 | 254 | var first = orders.First(); 255 | trackable.Remove(first); 256 | var intf = trackable.CastToIChangeTrackableCollection(); 257 | intf.RejectChanges(); 258 | 259 | trackable.Count.Should().Be(10); 260 | intf.UnchangedItems.Count().Should().Be(10); 261 | } 262 | 263 | [Fact] 264 | public void RejectChanges_On_Collection_Should_RejectChanges_Only_After_Last_AcceptChanges() 265 | { 266 | var orders = Helper.GetOrdersIList(); 267 | var trackable = orders.AsTrackable(); 268 | 269 | var first = orders.First(); 270 | first.Id = 963; 271 | first.CustomerNumber = "Testing"; 272 | var collectionIntf = trackable.CastToIChangeTrackableCollection(); 273 | collectionIntf.AcceptChanges(); 274 | first.Id = 999; 275 | first.CustomerNumber = "Testing 123"; 276 | collectionIntf.RejectChanges(); 277 | var intf = first.CastToIChangeTrackable(); 278 | var orderToMatch = Helper.GetOrder(); 279 | orderToMatch.Id = 963; 280 | orderToMatch.CustomerNumber = "Testing"; 281 | Order originalOrder = intf.GetOriginal(); 282 | 283 | originalOrder.Should().BeEquivalentTo(orderToMatch, options => options.IgnoringCyclicReferences()); 284 | intf.GetOriginalValue(o => o.Id).Should().Be(963); 285 | } 286 | 287 | [Fact] 288 | public void UnDelete_Should_Move_Back_Item_From_DeletedItems_And_Change_Back_Status() 289 | { 290 | var orders = Helper.GetOrdersIList(); 291 | var trackable = orders.AsTrackable(); 292 | 293 | Order first = trackable.First(); 294 | trackable.Remove(first); 295 | trackable.CastToIChangeTrackableCollection().UnDelete(first); 296 | 297 | trackable.Should().Contain(first); 298 | trackable.CastToIChangeTrackableCollection().DeletedItems.Should().NotContain(first).And.BeEmpty(); 299 | first.CastToIChangeTrackable().ChangeTrackingStatus.Should().Be(ChangeStatus.Unchanged); 300 | } 301 | 302 | [Fact] 303 | public void Can_Enumerate_IChangeTrackableCollection() 304 | { 305 | var orders = Helper.GetOrdersIList(); 306 | var trackable = orders.AsTrackable(); 307 | 308 | trackable.Invoking(t => t.FirstOrDefault()).Should().NotThrow(); 309 | } 310 | 311 | [Fact] 312 | public void AsTrackable_On_ICollection_Should_Convert_ToIList_Internally() 313 | { 314 | IList orders = Helper.GetOrdersIList(); 315 | ICollection ordersSet = new System.Collections.ObjectModel.Collection(orders); 316 | 317 | ICollection trackable = ordersSet.AsTrackable(); 318 | 319 | trackable.Should().BeAssignableTo>(); 320 | } 321 | 322 | [Fact] 323 | public void AsTrackable_Should_Take_MakeCollectionPropertiesTrackable_From_Default() 324 | { 325 | ChangeTrackingFactory.Default.MakeCollectionPropertiesTrackable = false; 326 | Order order = Helper.GetOrder(); 327 | 328 | Order trackable = order.AsTrackable(); 329 | 330 | trackable.Should().BeAssignableTo>(); 331 | trackable.OrderDetails.Should().NotBeAssignableTo>(); 332 | ChangeTrackingFactory.Default.MakeCollectionPropertiesTrackable = true; 333 | } 334 | 335 | [Fact] 336 | public void AsTrackable_Should_Take_MakeComplexPropertiesTrackable_From_Default() 337 | { 338 | ChangeTrackingFactory.Default.MakeComplexPropertiesTrackable = false; 339 | Order order = Helper.GetOrder(); 340 | 341 | Order trackable = order.AsTrackable(); 342 | 343 | trackable.Should().BeAssignableTo>(); 344 | trackable.Address.Should().NotBeAssignableTo>(); 345 | ChangeTrackingFactory.Default.MakeComplexPropertiesTrackable = true; 346 | } 347 | 348 | [Fact] 349 | public void When_Clear_Collection_Should_Work() 350 | { 351 | IList orders = Helper.GetOrdersIList(); 352 | IList trackable = orders.AsTrackable(); 353 | 354 | trackable.Clear(); 355 | 356 | IChangeTrackableCollection trackableCollection = trackable.CastToIChangeTrackableCollection(); 357 | trackableCollection.IsChanged.Should().BeTrue(); 358 | trackableCollection.DeletedItems.Should().NotBeEmpty(); 359 | trackableCollection.UnchangedItems.Should().BeEmpty(); 360 | } 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /Source/ChangeTracking/ChangeTrackingInterceptor.cs: -------------------------------------------------------------------------------- 1 | using Castle.DynamicProxy; 2 | using ChangeTracking.Internal; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Linq.Expressions; 7 | using System.Reflection; 8 | using System.Text; 9 | 10 | namespace ChangeTracking 11 | { 12 | internal sealed class ChangeTrackingInterceptor : IInterceptor, IInterceptorSettings where T : class 13 | { 14 | private static readonly Dictionary _Properties; 15 | private readonly Dictionary _OriginalValueDictionary; 16 | private readonly HashSet _ChangedComplexOrCollectionProperties; 17 | private readonly Dictionary _StatusChangedEventHandlers; 18 | private readonly object _StatusChangedEventHandlersLock; 19 | private ChangeStatus _ChangeTrackingStatus; 20 | private bool _InRejectChanges; 21 | internal EventHandler _StatusChanged = delegate { }; 22 | internal EventHandler _ChangedPropertiesChanged = delegate { }; 23 | 24 | public bool IsInitialized { get; set; } 25 | 26 | static ChangeTrackingInterceptor() 27 | { 28 | _Properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.CanWrite).ToDictionary(pi => pi.Name); 29 | } 30 | 31 | internal ChangeTrackingInterceptor(ChangeStatus status) 32 | { 33 | _OriginalValueDictionary = new Dictionary(); 34 | _ChangedComplexOrCollectionProperties = new HashSet(); 35 | _StatusChangedEventHandlers = new Dictionary(); 36 | _StatusChangedEventHandlersLock = new object(); 37 | _ChangeTrackingStatus = status; 38 | } 39 | 40 | public void Intercept(IInvocation invocation) 41 | { 42 | if (!IsInitialized) 43 | { 44 | return; 45 | } 46 | if (invocation.Method.IsSetter() && !_InRejectChanges) 47 | { 48 | if (_ChangeTrackingStatus == ChangeStatus.Deleted) 49 | { 50 | throw new InvalidOperationException("Can not modify deleted object"); 51 | } 52 | string propertyName = invocation.Method.PropertyName(); 53 | 54 | object previousValue = _Properties[propertyName].GetValue(invocation.Proxy, null); 55 | 56 | invocation.Proceed(); 57 | object newValue = _Properties[propertyName].GetValue(invocation.Proxy, null); 58 | 59 | if (!ReferenceEquals(previousValue, newValue)) 60 | { 61 | UnsubscribeFromChildStatusChanged(propertyName, previousValue); 62 | SubscribeToChildStatusChanged(invocation.Proxy, propertyName, newValue); 63 | } 64 | bool originalValueFound = _OriginalValueDictionary.TryGetValue(propertyName, out object originalValue); 65 | if (!originalValueFound && !Equals(previousValue, newValue)) 66 | { 67 | _OriginalValueDictionary.Add(propertyName, previousValue); 68 | RaiseChangePropertiesChanged(invocation.Proxy); 69 | SetAndRaiseStatusChanged(invocation.Proxy, parents: new List { invocation.Proxy }, setStatusEvenIfStatsAddedOrDeleted: false); 70 | } 71 | else if (originalValueFound && Equals(originalValue, newValue)) 72 | { 73 | _OriginalValueDictionary.Remove(propertyName); 74 | RaiseChangePropertiesChanged(invocation.Proxy); 75 | SetAndRaiseStatusChanged(invocation.Proxy, parents: new List { invocation.Proxy }, setStatusEvenIfStatsAddedOrDeleted: false); 76 | } 77 | return; 78 | } 79 | else if (invocation.Method.IsGetter()) 80 | { 81 | string propertyName = invocation.Method.PropertyName(); 82 | if (propertyName == nameof(IChangeTrackable.ChangeTrackingStatus)) 83 | { 84 | invocation.ReturnValue = _ChangeTrackingStatus; 85 | } 86 | else if (propertyName == nameof(System.ComponentModel.IChangeTracking.IsChanged)) 87 | { 88 | invocation.ReturnValue = _ChangeTrackingStatus != ChangeStatus.Unchanged; 89 | } 90 | else if (propertyName == nameof(IChangeTrackable.ChangedProperties)) 91 | { 92 | invocation.ReturnValue = GetChangedProperties(); 93 | } 94 | else 95 | { 96 | invocation.Proceed(); 97 | SubscribeToChildStatusChanged(invocation.Proxy, propertyName, invocation.ReturnValue); 98 | } 99 | return; 100 | } 101 | switch (invocation.Method.Name) 102 | { 103 | case nameof(IChangeTrackable.GetOriginalValue): 104 | invocation.ReturnValue = ((dynamic)this).GetOriginalValue((T)invocation.Proxy, (dynamic)invocation.Arguments[0]); 105 | break; 106 | case nameof(IChangeTrackable.GetOriginal): 107 | case nameof(IChangeTrackable.GetCurrent): 108 | object poco; 109 | Func getPropertyProxy; 110 | Func getPropertyPoco; 111 | if (invocation.Method.Name == nameof(IChangeTrackable.GetOriginal)) 112 | { 113 | getPropertyProxy = (property, proxy) => _OriginalValueDictionary.TryGetValue(property.Name, out object value) ? value : property.GetValue(proxy, null); 114 | getPropertyPoco = (proxy, g) => proxy.GetOriginal(g); 115 | } 116 | else 117 | { 118 | getPropertyProxy = (property, proxy) => property.GetValue(proxy, null); 119 | getPropertyPoco = (proxy, g) => proxy.GetCurrent(g); 120 | } 121 | 122 | bool isRootCall = invocation.Arguments.Length == 0; 123 | if (isRootCall) 124 | { 125 | UnrollGraph unrollGraph = new UnrollGraph(); 126 | poco = GetPoco((T)invocation.Proxy, unrollGraph, getPropertyProxy, getPropertyPoco); 127 | unrollGraph.FinishWireUp(); 128 | } 129 | else 130 | { 131 | poco = GetPoco((T)invocation.Proxy, (UnrollGraph)invocation.Arguments[0], getPropertyProxy, getPropertyPoco); 132 | } 133 | invocation.ReturnValue = poco; 134 | break; 135 | case "add_StatusChanged": 136 | _StatusChanged += (EventHandler)invocation.Arguments[0]; 137 | break; 138 | case "remove_StatusChanged": 139 | _StatusChanged -= (EventHandler)invocation.Arguments[0]; 140 | break; 141 | case "Delete": 142 | invocation.ReturnValue = Delete(invocation.Proxy); 143 | break; 144 | case "UnDelete": 145 | invocation.ReturnValue = UnDelete(invocation.Proxy); 146 | break; 147 | case nameof(IRevertibleChangeTrackingInternal.AcceptChanges): 148 | AcceptChanges(invocation.Proxy, invocation.Arguments.Length == 0 ? null : (List)invocation.Arguments[0]); 149 | break; 150 | case nameof(IRevertibleChangeTrackingInternal.RejectChanges): 151 | RejectChanges(invocation.Proxy, invocation.Arguments.Length == 0 ? null : (List)invocation.Arguments[0]); 152 | break; 153 | case nameof(IRevertibleChangeTrackingInternal.IsChanged): 154 | invocation.ReturnValue = GetNewChangeStatus(invocation.Proxy, (List)invocation.Arguments[0]) != ChangeStatus.Unchanged; 155 | break; 156 | default: 157 | invocation.Proceed(); 158 | break; 159 | } 160 | } 161 | 162 | private object GetOriginalValue(T target, string propertyName) 163 | { 164 | if (!_OriginalValueDictionary.TryGetValue(propertyName, out object value)) 165 | { 166 | try 167 | { 168 | value = _Properties[propertyName].GetValue(target, null); 169 | } 170 | catch (KeyNotFoundException ex) 171 | { 172 | throw new ArgumentOutOfRangeException($"\"{propertyName}\" is not a valid property name of type \"{typeof(T)}\"", ex); 173 | } 174 | } 175 | return value; 176 | } 177 | 178 | private TResult GetOriginalValue(T target, Expression> selector) 179 | { 180 | var propertyInfo = GetPropertyInfo(selector); 181 | 182 | string propName = propertyInfo.Name; 183 | TResult originalValue = _OriginalValueDictionary.TryGetValue(propName, out object value) ? 184 | (TResult)value : 185 | selector.Compile()(target); 186 | if (originalValue is IChangeTrackableInternal trackable) 187 | { 188 | UnrollGraph unrollGraph = new UnrollGraph(); 189 | originalValue = (TResult)trackable.GetOriginal(unrollGraph); 190 | unrollGraph.FinishWireUp(); 191 | } 192 | return originalValue; 193 | } 194 | 195 | public PropertyInfo GetPropertyInfo(Expression> propertyLambda) 196 | { 197 | MemberExpression member = propertyLambda.Body as MemberExpression; 198 | if (member == null) 199 | { 200 | throw new ArgumentException(string.Format("Expression '{0}' refers to a method, not a property.", propertyLambda)); 201 | } 202 | 203 | PropertyInfo propInfo = member.Member as PropertyInfo; 204 | if (propInfo == null) 205 | { 206 | throw new ArgumentException(string.Format("Expression '{0}' refers to a field, not a property.", propertyLambda)); 207 | } 208 | 209 | Type type = typeof(TSource); 210 | if (type != propInfo.ReflectedType && !type.IsSubclassOf(propInfo.ReflectedType)) 211 | { 212 | throw new ArgumentException(string.Format("Expression '{0}' refers to a property that is not from type {1}.", propertyLambda, type)); 213 | } 214 | return propInfo; 215 | } 216 | 217 | private T GetPoco(T proxy, UnrollGraph unrollGraph, Func getPropertyProxy, Func getPropertyPoco) 218 | { 219 | T original = (T)Activator.CreateInstance(typeof(T), nonPublic: true); 220 | foreach (var property in _Properties.Values) 221 | { 222 | object oldPropertyProxyValue = getPropertyProxy(property, proxy); 223 | if (oldPropertyProxyValue != null) 224 | { 225 | if (oldPropertyProxyValue is IChangeTrackableInternal trackable) 226 | { 227 | if (unrollGraph.RegisterOrScheduleAssignPocoInsteadOfProxy(trackable, pocoValue => property.SetValue(original, pocoValue, null))) 228 | { 229 | object newPropertyValue; 230 | newPropertyValue = getPropertyPoco(trackable, unrollGraph); 231 | unrollGraph.AddMap(new ProxyTargetMap(newPropertyValue, trackable)); 232 | property.SetValue(original, newPropertyValue, null); 233 | } 234 | } 235 | else if (oldPropertyProxyValue.GetType().GetInterface("IChangeTrackableCollection`1") != null) 236 | { 237 | if (unrollGraph.RegisterOrScheduleAssignPocoInsteadOfProxy(oldPropertyProxyValue, pocoValue => property.SetValue(original, pocoValue, null))) 238 | { 239 | System.Collections.IEnumerable originalPropertyValueEnumerable = (System.Collections.IEnumerable)oldPropertyProxyValue; 240 | int listCount = originalPropertyValueEnumerable is IEnumerable e ? e.Count() : originalPropertyValueEnumerable.Cast().Count(); 241 | var list = (System.Collections.IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(property.PropertyType.GetGenericArguments().First()), listCount); 242 | 243 | foreach (var proxyListItem in originalPropertyValueEnumerable) 244 | { 245 | if (unrollGraph.RegisterOrScheduleAssignPocoInsteadOfProxy(proxyListItem, pocoSetter: null)) 246 | { 247 | object listItem = getPropertyPoco((IChangeTrackableInternal)proxyListItem, unrollGraph); 248 | unrollGraph.AddMap(new ProxyTargetMap(listItem, proxyListItem)); 249 | } 250 | } 251 | 252 | unrollGraph.AddListSetter(map => 253 | { 254 | foreach (var proxyListItem in originalPropertyValueEnumerable) 255 | { 256 | list.Add(map(proxyListItem)); 257 | } 258 | }); 259 | unrollGraph.AddMap(new ProxyTargetMap(list, oldPropertyProxyValue)); 260 | property.SetValue(original, list, null); 261 | } 262 | } 263 | else 264 | { 265 | property.SetValue(original, oldPropertyProxyValue, null); 266 | } 267 | } 268 | } 269 | return original; 270 | } 271 | 272 | private bool Delete(object proxy) 273 | { 274 | if (_ChangeTrackingStatus != ChangeStatus.Deleted) 275 | { 276 | _ChangeTrackingStatus = ChangeStatus.Deleted; 277 | _StatusChanged(proxy, EventArgs.Empty); 278 | return true; 279 | } 280 | return false; 281 | } 282 | 283 | private bool UnDelete(object proxy) 284 | { 285 | if (_ChangeTrackingStatus == ChangeStatus.Deleted) 286 | { 287 | SetAndRaiseStatusChanged(proxy, parents: new List { proxy }, setStatusEvenIfStatsAddedOrDeleted: true); 288 | return true; 289 | } 290 | return false; 291 | } 292 | 293 | private void AcceptChanges(object proxy, List parents) 294 | { 295 | if (_ChangeTrackingStatus == ChangeStatus.Deleted) 296 | { 297 | throw new InvalidOperationException("Can not call AcceptChanges on deleted object"); 298 | } 299 | ChangeStatus changeTrackingStatusWhenStarted = _ChangeTrackingStatus; 300 | parents = parents ?? new List(20) { proxy }; 301 | foreach (var child in Utils.GetChildren(proxy, parents)) 302 | { 303 | if (child is IRevertibleChangeTrackingInternal childInternal) 304 | { 305 | childInternal.AcceptChanges(parents); 306 | } 307 | else 308 | { 309 | child.AcceptChanges(); 310 | } 311 | } 312 | _OriginalValueDictionary.Clear(); 313 | SetAndRaiseStatusChanged(proxy, parents, setStatusEvenIfStatsAddedOrDeleted: true); 314 | bool anythingChanged = changeTrackingStatusWhenStarted != _ChangeTrackingStatus; 315 | if (anythingChanged) 316 | { 317 | RaiseChangePropertiesChanged(proxy); 318 | } 319 | } 320 | 321 | private void RejectChanges(object proxy, List parents) 322 | { 323 | ChangeStatus changeTrackingStatusWhenStarted = _ChangeTrackingStatus; 324 | parents = parents ?? new List(20) { proxy }; 325 | foreach (var child in Utils.GetChildren(proxy, parents)) 326 | { 327 | if (child is IRevertibleChangeTrackingInternal childInternal) 328 | { 329 | childInternal.RejectChanges(parents); 330 | } 331 | else 332 | { 333 | child.RejectChanges(); 334 | } 335 | } 336 | if (_OriginalValueDictionary.Count > 0) 337 | { 338 | _InRejectChanges = true; 339 | foreach (var changedProperty in _OriginalValueDictionary) 340 | { 341 | _Properties[changedProperty.Key].SetValue(proxy, changedProperty.Value, null); 342 | } 343 | _OriginalValueDictionary.Clear(); 344 | _InRejectChanges = false; 345 | } 346 | SetAndRaiseStatusChanged(proxy, parents, setStatusEvenIfStatsAddedOrDeleted: true); 347 | bool anythingChanged = changeTrackingStatusWhenStarted != _ChangeTrackingStatus; 348 | if (anythingChanged) 349 | { 350 | RaiseChangePropertiesChanged(proxy); 351 | } 352 | } 353 | 354 | private void UnsubscribeFromChildStatusChanged(string propertyName, object oldChild) 355 | { 356 | lock (_StatusChangedEventHandlersLock) 357 | { 358 | if (_StatusChangedEventHandlers.TryGetValue(propertyName, out Delegate handler)) 359 | { 360 | if (oldChild is IChangeTrackable trackable) 361 | { 362 | trackable.StatusChanged -= (EventHandler)handler; 363 | _StatusChangedEventHandlers.Remove(propertyName); 364 | return; 365 | } 366 | if (oldChild is System.ComponentModel.IBindingList collectionTrackable) 367 | { 368 | collectionTrackable.ListChanged -= (System.ComponentModel.ListChangedEventHandler)handler; 369 | _StatusChangedEventHandlers.Remove(propertyName); 370 | } 371 | } 372 | } 373 | } 374 | 375 | private void SubscribeToChildStatusChanged(object proxy, string propertyName, object newValue) 376 | { 377 | void Handler(object sender) 378 | { 379 | SetAndRaiseStatusChanged(proxy, parents: new List { proxy }, setStatusEvenIfStatsAddedOrDeleted: false); 380 | if (sender is IRevertibleChangeTrackingInternal trackable && trackable.IsChanged(new List { proxy }) is bool isChanged && (isChanged && _ChangedComplexOrCollectionProperties.Add(propertyName) || !isChanged && _ChangedComplexOrCollectionProperties.Remove(propertyName))) 381 | { 382 | RaiseChangePropertiesChanged(proxy); 383 | } 384 | } 385 | 386 | lock (_StatusChangedEventHandlersLock) 387 | { 388 | if (!_StatusChangedEventHandlers.ContainsKey(propertyName)) 389 | { 390 | if (newValue is IChangeTrackable newChild) 391 | { 392 | EventHandler newHandler = (sender, e) => Handler(newValue); 393 | newChild.StatusChanged += newHandler; 394 | _StatusChangedEventHandlers.Add(propertyName, newHandler); 395 | return; 396 | } 397 | if (newValue is System.ComponentModel.IBindingList newCollectionChild) 398 | { 399 | System.ComponentModel.ListChangedEventHandler newHandler = (sender, e) => Handler(newValue); 400 | newCollectionChild.ListChanged += newHandler; 401 | _StatusChangedEventHandlers.Add(propertyName, newHandler); 402 | } 403 | } 404 | } 405 | } 406 | 407 | private void SetAndRaiseStatusChanged(object proxy, List parents, bool setStatusEvenIfStatsAddedOrDeleted) 408 | { 409 | if (_ChangeTrackingStatus == ChangeStatus.Changed || _ChangeTrackingStatus == ChangeStatus.Unchanged || setStatusEvenIfStatsAddedOrDeleted) 410 | { 411 | var newChangeStatus = GetNewChangeStatus(proxy, parents); 412 | if (_ChangeTrackingStatus != newChangeStatus) 413 | { 414 | _ChangeTrackingStatus = newChangeStatus; 415 | _StatusChanged(proxy, EventArgs.Empty); 416 | } 417 | } 418 | } 419 | 420 | private ChangeStatus GetNewChangeStatus(object proxy, List parents) => _OriginalValueDictionary.Count == 0 && Utils.GetChildren(proxy, parents).All(c => !c.IsChanged(parents)) ? ChangeStatus.Unchanged : ChangeStatus.Changed; 421 | 422 | private IEnumerable GetChangedProperties() 423 | { 424 | switch (_ChangeTrackingStatus) 425 | { 426 | case ChangeStatus.Unchanged: 427 | return Enumerable.Empty(); 428 | case ChangeStatus.Added: 429 | case ChangeStatus.Deleted: 430 | return _Properties.Keys; 431 | case ChangeStatus.Changed: 432 | return _Properties.Keys.Where(propertyName => _OriginalValueDictionary.Keys.Concat(_ChangedComplexOrCollectionProperties).Contains(propertyName)); 433 | default: throw null; 434 | } 435 | } 436 | 437 | private void RaiseChangePropertiesChanged(object sender) => _ChangedPropertiesChanged(sender, EventArgs.Empty); 438 | } 439 | } 440 | -------------------------------------------------------------------------------- /Source/ChangeTracking.Tests/IChangeTrackableTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using FluentAssertions; 6 | using FluentAssertions.Events; 7 | using Xunit; 8 | 9 | namespace ChangeTracking.Tests 10 | { 11 | public class IChangeTrackableTests 12 | { 13 | [Fact] 14 | public void AsTrackable_Should_Make_Object_Implement_IChangeTrackable() 15 | { 16 | var order = Helper.GetOrder(); 17 | 18 | Order trackable = order.AsTrackable(); 19 | 20 | trackable.Should().BeAssignableTo>(); 21 | } 22 | 23 | [Fact] 24 | public void When_AsTrackable_CastToIChangeTrackable_Should_Not_Throw_InvalidCastException() 25 | { 26 | var order = Helper.GetOrder(); 27 | 28 | Order trackable = order.AsTrackable(); 29 | 30 | trackable.Invoking(o => o.CastToIChangeTrackable()).Should().NotThrow(); 31 | } 32 | 33 | [Fact] 34 | public void When_Not_AsTrackable_CastToIChangeTrackable_Should_Throw_InvalidCastException() 35 | { 36 | var order = Helper.GetOrder(); 37 | 38 | order.Invoking(o => o.CastToIChangeTrackable()).Should().Throw(); 39 | } 40 | 41 | [Fact] 42 | public void Change_Property_Should_Raise_StatusChanged_Event() 43 | { 44 | var order = Helper.GetOrder(); 45 | var trackable = order.AsTrackable(); 46 | var monitor = ((IChangeTrackable)trackable).Monitor(); 47 | 48 | trackable.CustomerNumber = "Test1"; 49 | 50 | monitor.Should().Raise(nameof(IChangeTrackable.StatusChanged)); 51 | } 52 | 53 | [Fact] 54 | public void Change_Property_To_Same_Value_Should_Not_Raise_StatusChanged_Event() 55 | { 56 | var order = Helper.GetOrder(); 57 | var trackable = order.AsTrackable(); 58 | var monitor = ((IChangeTrackable)trackable).Monitor(); 59 | 60 | trackable.CustomerNumber = "Test"; 61 | 62 | monitor.Should().NotRaise(nameof(IChangeTrackable.StatusChanged)); 63 | } 64 | 65 | [Fact] 66 | public void Change_Property_From_Null_To_Value_Should_Not_Throw() 67 | { 68 | var trackable = new Order { Id = 321, CustomerNumber = null }.AsTrackable(); 69 | 70 | trackable.Invoking(o => o.CustomerNumber = "Test").Should().NotThrow(); 71 | } 72 | 73 | [Fact] 74 | public void GetOriginalValue_Should_Return_Original_Value() 75 | { 76 | var order = Helper.GetOrder(); 77 | var trackable = order.AsTrackable(); 78 | 79 | trackable.CustomerNumber = "Test1"; 80 | 81 | trackable.CastToIChangeTrackable().GetOriginalValue(o => o.CustomerNumber).Should().Be("Test"); 82 | } 83 | 84 | [Fact] 85 | public void GetOriginalValue_Generic_By_Property_Name_Should_Return_Original_Value() 86 | { 87 | var order = Helper.GetOrder(); 88 | var trackable = order.AsTrackable(); 89 | 90 | trackable.CustomerNumber = "Test1"; 91 | 92 | trackable.CastToIChangeTrackable().GetOriginalValue("CustomerNumber").Should().Be("Test"); 93 | } 94 | 95 | 96 | [Fact] 97 | public void GetOriginalValue_By_Property_Name_Should_Return_Original_Value() 98 | { 99 | var order = Helper.GetOrder(); 100 | var trackable = order.AsTrackable(); 101 | 102 | trackable.CustomerNumber = "Test1"; 103 | 104 | trackable.CastToIChangeTrackable().GetOriginalValue("CustomerNumber").Should().Be("Test"); 105 | } 106 | 107 | [Fact] 108 | public void GetOriginal_Should_Return_Original() 109 | { 110 | var order = Helper.GetOrder(); 111 | var trackable = order.AsTrackable(); 112 | 113 | trackable.Id = 124; 114 | trackable.CustomerNumber = "Test1"; 115 | 116 | var original = trackable.CastToIChangeTrackable().GetOriginal(); 117 | var newOne = Helper.GetOrder(); 118 | original.Should().BeEquivalentTo(newOne, options => options.IgnoringCyclicReferences()); 119 | (original is IChangeTrackable).Should().BeFalse(); 120 | } 121 | 122 | [Fact] 123 | public void When_Setting_Status_Should_Be_That_Status() 124 | { 125 | var order = Helper.GetOrder(); 126 | 127 | var trackable = order.AsTrackable(ChangeStatus.Added); 128 | 129 | trackable.CastToIChangeTrackable().ChangeTrackingStatus.Should().Be(ChangeStatus.Added); 130 | } 131 | 132 | [Fact] 133 | public void When_Status_Added_And_Change_Value_Status_Should_Still_Be_Added() 134 | { 135 | var order = Helper.GetOrder(); 136 | 137 | var trackable = order.AsTrackable(ChangeStatus.Added); 138 | trackable.CustomerNumber = "Test1"; 139 | 140 | trackable.CastToIChangeTrackable().ChangeTrackingStatus.Should().Be(ChangeStatus.Added); 141 | } 142 | 143 | [Fact] 144 | public void When_Status_Is_Deleted_And_Change_Value_Should_Throw() 145 | { 146 | var order = Helper.GetOrder(); 147 | 148 | var trackable = order.AsTrackable(ChangeStatus.Deleted); 149 | 150 | trackable.Invoking(o => o.CustomerNumber = "Test1").Should().Throw(); 151 | } 152 | 153 | [Fact] 154 | public void AcceptChanges_Should_Status_Be_Unchanged() 155 | { 156 | var order = Helper.GetOrder(); 157 | 158 | var trackable = order.AsTrackable(); 159 | trackable.Id = 963; 160 | trackable.CustomerNumber = "Testing"; 161 | var intf = trackable.CastToIChangeTrackable(); 162 | 163 | var oldChangeStatus = intf.ChangeTrackingStatus; 164 | intf.AcceptChanges(); 165 | 166 | oldChangeStatus.Should().Be(ChangeStatus.Changed); 167 | intf.ChangeTrackingStatus.Should().Be(ChangeStatus.Unchanged); 168 | } 169 | 170 | [Fact] 171 | public void AcceptChanges_Should_AcceptChanges() 172 | { 173 | var order = Helper.GetOrder(); 174 | 175 | var trackable = order.AsTrackable(); 176 | trackable.Id = 963; 177 | trackable.CustomerNumber = "Testing"; 178 | var intf = trackable.CastToIChangeTrackable(); 179 | intf.AcceptChanges(); 180 | 181 | intf.Should().BeEquivalentTo(intf.GetOriginal().AsTrackable(), options => options.IgnoringCyclicReferences()); 182 | intf.GetOriginalValue(o => o.Id).Should().Be(963); 183 | } 184 | 185 | [Fact] 186 | public void RejectChanges_Should_Status_Be_Unchanged() 187 | { 188 | var order = Helper.GetOrder(); 189 | 190 | var trackable = order.AsTrackable(); 191 | trackable.Id = 963; 192 | trackable.CustomerNumber = "Testing"; 193 | var intf = trackable.CastToIChangeTrackable(); 194 | var oldChangeStatus = intf.ChangeTrackingStatus; 195 | intf.RejectChanges(); 196 | 197 | oldChangeStatus.Should().Be(ChangeStatus.Changed); 198 | intf.ChangeTrackingStatus.Should().Be(ChangeStatus.Unchanged); 199 | } 200 | 201 | [Fact] 202 | public void RejectChanges_Should_RejectChanges() 203 | { 204 | var order = Helper.GetOrder(); 205 | 206 | var trackable = order.AsTrackable(); 207 | trackable.Id = 963; 208 | trackable.CustomerNumber = "Testing"; 209 | var intf = trackable.CastToIChangeTrackable(); 210 | intf.RejectChanges(); 211 | 212 | trackable.Should().BeEquivalentTo(Helper.GetOrder().AsTrackable(), options => options.IgnoringCyclicReferences()); 213 | } 214 | 215 | [Fact] 216 | public void AcceptChanges_Should_AcceptChanges_On_Complex_Property() 217 | { 218 | var order = Helper.GetOrder(); 219 | 220 | var trackable = order.AsTrackable(); 221 | trackable.Address.AddressId = 963; 222 | trackable.Address.City = "Chicago"; 223 | var intf = trackable.CastToIChangeTrackable(); 224 | intf.AcceptChanges(); 225 | 226 | trackable.Address.Should().BeEquivalentTo(trackable.Address.CastToIChangeTrackable().GetOriginal().AsTrackable()); 227 | } 228 | 229 | [Fact] 230 | public void RejectChanges_Should_Status_Be_Unchanged_On_Complex_Property() 231 | { 232 | var order = Helper.GetOrder(); 233 | 234 | var trackable = order.AsTrackable(); 235 | trackable.Address.AddressId = 963; 236 | trackable.Address.City = "Chicago"; 237 | var oldChangeStatus = trackable.Address.CastToIChangeTrackable().ChangeTrackingStatus; 238 | trackable.CastToIChangeTrackable().RejectChanges(); 239 | 240 | oldChangeStatus.Should().Be(ChangeStatus.Changed); 241 | trackable.Address.CastToIChangeTrackable().ChangeTrackingStatus.Should().Be(ChangeStatus.Unchanged); 242 | } 243 | 244 | [Fact] 245 | public void RejectChanges_Should_RejectChanges_On_Complex_Property() 246 | { 247 | var order = Helper.GetOrder(); 248 | 249 | var trackable = order.AsTrackable(); 250 | trackable.Address.AddressId = 963; 251 | trackable.Address.City = "Chicago"; 252 | var intf = trackable.CastToIChangeTrackable(); 253 | intf.RejectChanges(); 254 | 255 | trackable.Address.Should().BeEquivalentTo(Helper.GetOrder().AsTrackable().Address); 256 | } 257 | 258 | [Fact] 259 | public void AcceptChanges_Should_AcceptChanges_On_Collection_Property() 260 | { 261 | var order = Helper.GetOrder(); 262 | 263 | var trackable = order.AsTrackable(); 264 | trackable.OrderDetails[0].ItemNo = "ItemTesting"; 265 | var intf = trackable.CastToIChangeTrackable(); 266 | intf.AcceptChanges(); 267 | 268 | trackable.OrderDetails[0].ItemNo.Should().Be("ItemTesting"); 269 | trackable.OrderDetails[0].CastToIChangeTrackable().GetOriginalValue(i => i.ItemNo).Should().Be("ItemTesting"); 270 | } 271 | 272 | [Fact] 273 | public void RejectChanges_Should_Status_Be_Unchanged_On_Collection_Property() 274 | { 275 | var order = Helper.GetOrder(); 276 | 277 | var trackable = order.AsTrackable(); 278 | trackable.OrderDetails[0].ItemNo = "ItemTesting"; 279 | var intf = trackable.CastToIChangeTrackable(); 280 | var oldChangeStatus = trackable.OrderDetails[0].CastToIChangeTrackable().ChangeTrackingStatus; 281 | intf.RejectChanges(); 282 | 283 | oldChangeStatus.Should().Be(ChangeStatus.Changed); 284 | trackable.OrderDetails[0].CastToIChangeTrackable().ChangeTrackingStatus.Should().Be(ChangeStatus.Unchanged); 285 | } 286 | 287 | [Fact] 288 | public void RejectChanges_Should_RejectChanges_On_Collection_Property() 289 | { 290 | var order = Helper.GetOrder(); 291 | 292 | var trackable = order.AsTrackable(); 293 | trackable.OrderDetails[0].ItemNo = "ItemTesting"; 294 | var intf = trackable.CastToIChangeTrackable(); 295 | intf.RejectChanges(); 296 | 297 | trackable.OrderDetails[0].Should().BeEquivalentTo(Helper.GetOrder().OrderDetails[0].AsTrackable(), options => options.IgnoringCyclicReferences()); 298 | } 299 | 300 | [Fact] 301 | public void AsTrackable_Should_ComplexProperty_Children_Be_Trackable() 302 | { 303 | var order = Helper.GetOrder(); 304 | var trackable = order.AsTrackable(); 305 | 306 | trackable.Address.Should().BeAssignableTo>(); 307 | } 308 | 309 | [Fact] 310 | public void AsTrackable_When_ComplexProperty_Children_Trackable_Child_Change_Should_Change_Parent() 311 | { 312 | var order = Helper.GetOrder(); 313 | var trackable = order.AsTrackable(); 314 | 315 | trackable.Address.AddressId = 999; 316 | 317 | trackable.Address.CastToIChangeTrackable().ChangeTrackingStatus.Should().Be(ChangeStatus.Changed); 318 | trackable.CastToIChangeTrackable().ChangeTrackingStatus.Should().Be(ChangeStatus.Changed); 319 | } 320 | 321 | [Fact] 322 | public void AsTrackable_When_ComplexProperty_Children_Trackable_AcceptChanges_Should_Accept_On_Children() 323 | { 324 | var order = Helper.GetOrder(); 325 | var trackable = order.AsTrackable(); 326 | 327 | trackable.Address.AddressId = 999; 328 | trackable.CastToIChangeTrackable().AcceptChanges(); 329 | 330 | trackable.Address.CastToIChangeTrackable().GetOriginalValue(a => a.AddressId).Should().Be(999); 331 | } 332 | 333 | [Fact] 334 | public void AsTrackable_When_Passed_False_Should_Not_ComplexProperty_Children_Be_Trackable() 335 | { 336 | var order = Helper.GetOrder(); 337 | var trackable = order.AsTrackable(makeComplexPropertiesTrackable: false); 338 | 339 | trackable.Address.AddressId = 99; 340 | Action action = () => { _ = (IChangeTrackable
)trackable.Address; }; 341 | 342 | action.Should().Throw(); 343 | } 344 | 345 | [Fact] 346 | public void AsTrackable_When_Not_ComplexProperty_Children_Trackable_AcceptChanges_Should_Work() 347 | { 348 | var order = Helper.GetOrder(); 349 | var trackable = order.AsTrackable(makeComplexPropertiesTrackable: false); 350 | 351 | trackable.Id = 999; 352 | trackable.Address.AddressId = 999; 353 | trackable.CastToIChangeTrackable().AcceptChanges(); 354 | var intf = trackable.CastToIChangeTrackable(); 355 | 356 | intf.GetOriginalValue(o => o.Id).Should().Be(999); 357 | } 358 | 359 | [Fact] 360 | public void AsTrackable_When_Not_ComplexProperty_Children_Trackable_RejectChanges_Should_Work() 361 | { 362 | var order = Helper.GetOrder(); 363 | var trackable = order.AsTrackable(makeComplexPropertiesTrackable: false); 364 | 365 | trackable.Id = 999; 366 | trackable.Address.AddressId = 999; 367 | trackable.CastToIChangeTrackable().RejectChanges(); 368 | var intf = trackable.CastToIChangeTrackable(); 369 | 370 | intf.GetOriginalValue(o => o.Id).Should().Be(1); 371 | trackable.Address.AddressId.Should().Be(999); 372 | } 373 | 374 | [Fact] 375 | public void AsTrackable_Should_CollectionProperty_Children_Be_Trackable() 376 | { 377 | var order = Helper.GetOrder(); 378 | var trackable = order.AsTrackable(); 379 | 380 | trackable.OrderDetails.Should().BeAssignableTo>(); 381 | } 382 | 383 | [Fact] 384 | public void AsTrackable_When_CollectionProperty_Children_Trackable_Child_Change_Property_Should_Change_Parent() 385 | { 386 | var order = Helper.GetOrder(); 387 | var trackable = order.AsTrackable(); 388 | 389 | trackable.OrderDetails[0].OrderDetailId = 999; 390 | 391 | trackable.OrderDetails.CastToIChangeTrackableCollection().IsChanged.Should().BeTrue(); 392 | trackable.CastToIChangeTrackable().ChangeTrackingStatus.Should().Be(ChangeStatus.Changed); 393 | } 394 | 395 | [Fact] 396 | public void AsTrackable_When_CollectionProperty_Children_Trackable_Child_Change_Collection_Should_Change_Parent() 397 | { 398 | var order = Helper.GetOrder(); 399 | var trackable = order.AsTrackable(); 400 | 401 | trackable.OrderDetails.Add(new OrderDetail 402 | { 403 | OrderDetailId = 123, 404 | ItemNo = "Item123" 405 | }); 406 | 407 | trackable.OrderDetails.CastToIChangeTrackableCollection().IsChanged.Should().BeTrue(); 408 | trackable.CastToIChangeTrackable().ChangeTrackingStatus.Should().Be(ChangeStatus.Changed); 409 | } 410 | 411 | [Fact] 412 | public void AsTrackable_When_CollectionProperty_Children_Trackable_AcceptChanges_Should_Accept_On_Children() 413 | { 414 | var order = Helper.GetOrder(); 415 | var trackable = order.AsTrackable(); 416 | 417 | trackable.OrderDetails[0].OrderDetailId = 999; 418 | trackable.CastToIChangeTrackable().AcceptChanges(); 419 | 420 | trackable.OrderDetails[0].CastToIChangeTrackable().GetOriginalValue(o => o.OrderDetailId).Should().Be(999); 421 | } 422 | 423 | [Fact] 424 | public void AsTrackable_When_Passed_False_Should_Not_CollectionProperty_Children_Be_Trackable() 425 | { 426 | var order = Helper.GetOrder(); 427 | var trackable = order.AsTrackable(makeCollectionPropertiesTrackable: false); 428 | 429 | Action action = () => { _ = (IChangeTrackableCollection)trackable.OrderDetails; }; 430 | 431 | action.Should().Throw(); 432 | } 433 | 434 | [Fact] 435 | public void AsTrackable_When_Not_CollectionProperty_Children_Trackable_AcceptChanges_Should_Work() 436 | { 437 | var order = Helper.GetOrder(); 438 | var trackable = order.AsTrackable(makeCollectionPropertiesTrackable: false); 439 | 440 | trackable.Id = 999; 441 | trackable.OrderDetails[0].OrderDetailId = 999; 442 | trackable.CastToIChangeTrackable().AcceptChanges(); 443 | var intf = trackable.CastToIChangeTrackable(); 444 | 445 | intf.GetOriginalValue(o => o.Id).Should().Be(999); 446 | } 447 | 448 | [Fact] 449 | public void AsTrackable_When_Not_CollectionProperty_Children_Trackable_RejectChanges_Should_Work() 450 | { 451 | var order = Helper.GetOrder(); 452 | var trackable = order.AsTrackable(makeCollectionPropertiesTrackable: false); 453 | 454 | trackable.Id = 999; 455 | trackable.OrderDetails[0].OrderDetailId = 999; 456 | trackable.CastToIChangeTrackable().RejectChanges(); 457 | var intf = trackable.CastToIChangeTrackable(); 458 | 459 | intf.GetOriginalValue(o => o.Id).Should().Be(1); 460 | trackable.OrderDetails[0].OrderDetailId.Should().Be(999); 461 | } 462 | 463 | [Fact] 464 | public void AcceptChanges_Should_Raise_StatusChanged() 465 | { 466 | var order = Helper.GetOrder(); 467 | 468 | var trackable = order.AsTrackable(); 469 | trackable.Id = 963; 470 | var monitor = ((IChangeTrackable)trackable).Monitor(); 471 | var intf = trackable.CastToIChangeTrackable(); 472 | intf.AcceptChanges(); 473 | 474 | monitor.Should().Raise(nameof(IChangeTrackable.StatusChanged)); 475 | } 476 | 477 | [Fact] 478 | public void RejectChanges_Should_Raise_StatusChanged() 479 | { 480 | var order = Helper.GetOrder(); 481 | 482 | var trackable = order.AsTrackable(); 483 | trackable.Id = 963; 484 | var monitor = ((IChangeTrackable)trackable).Monitor(); 485 | var intf = trackable.CastToIChangeTrackable(); 486 | 487 | intf.RejectChanges(); 488 | 489 | monitor.Should().Raise(nameof(IChangeTrackable.StatusChanged)); 490 | } 491 | 492 | [Fact] 493 | public void When_StatusChanged_Raised_Property_Should_Be_Changed() 494 | { 495 | var order = Helper.GetOrder(); 496 | var trackable = order.AsTrackable(); 497 | var intf = trackable.CastToIChangeTrackable(); 498 | int newValue = 0; 499 | intf.StatusChanged += (o, e) => newValue = order.Id; 500 | 501 | trackable.Id = 1234; 502 | 503 | newValue.Should().Be(1234); 504 | } 505 | 506 | [Fact] 507 | public void When_CollectionProperty_Children_Trackable_Change_Property_On_Item_In_Collection_Should_Raise_StatusChanged_Event() 508 | { 509 | var order = Helper.GetOrder(); 510 | var trackable = order.AsTrackable(); 511 | var monitor = ((IChangeTrackable)trackable).Monitor(); 512 | 513 | trackable.OrderDetails[0].ItemNo = "Testing"; 514 | 515 | monitor.Should().Raise(nameof(IChangeTrackable.StatusChanged)); 516 | } 517 | 518 | [Fact] 519 | public void When_CollectionProperty_Children_Not_Trackable_Change_Property_On_Item_In_Collection_Should_Not_Raise_StatusChanged_Event() 520 | { 521 | var order = Helper.GetOrder(); 522 | var trackable = order.AsTrackable(makeCollectionPropertiesTrackable: false); 523 | var monitor = ((IChangeTrackable)trackable).Monitor(); 524 | 525 | trackable.OrderDetails[0].ItemNo = "Testing"; 526 | 527 | monitor.Should().NotRaise(nameof(IChangeTrackable.StatusChanged)); 528 | } 529 | 530 | [Fact] 531 | public void When_CollectionProperty_Children_Trackable_Change_CollectionProperty_Should_Raise_StatusChanged_Event() 532 | { 533 | var order = Helper.GetOrder(); 534 | var trackable = order.AsTrackable(); 535 | var monitor = ((IChangeTrackable)trackable).Monitor(); 536 | 537 | trackable.OrderDetails.Add(new OrderDetail 538 | { 539 | OrderDetailId = 123, 540 | ItemNo = "Item123" 541 | }); 542 | 543 | monitor.Should().Raise(nameof(IChangeTrackable.StatusChanged)); 544 | } 545 | 546 | [Fact] 547 | public void When_CollectionProperty_Children_Not_Trackable_Change_CollectionProperty_Should_Not_Raise_StatusChanged_Event() 548 | { 549 | var order = Helper.GetOrder(); 550 | var trackable = order.AsTrackable(makeCollectionPropertiesTrackable: false); 551 | var monitor = ((IChangeTrackable)trackable).Monitor(); 552 | 553 | trackable.OrderDetails.Add(new OrderDetail 554 | { 555 | OrderDetailId = 123, 556 | ItemNo = "Item123" 557 | }); 558 | 559 | monitor.Should().NotRaise(nameof(IChangeTrackable.StatusChanged)); 560 | } 561 | 562 | [Fact] 563 | public void When_ComplexProperty_Children_Trackable_Change_Property_On_Complex_Property_Should_Raise_StatusChanged_Event() 564 | { 565 | var order = Helper.GetOrder(); 566 | var trackable = order.AsTrackable(); 567 | var monitor = ((IChangeTrackable)trackable).Monitor(); 568 | 569 | trackable.Address.City = "Chicago"; 570 | 571 | monitor.Should().Raise(nameof(IChangeTrackable.StatusChanged)); 572 | } 573 | 574 | [Fact] 575 | public void When_Not_ComplexProperty_Children_Trackable_Change_Property_On_Complex_Property_Should_Not_Raise_StatusChanged_Event() 576 | { 577 | var order = Helper.GetOrder(); 578 | var trackable = order.AsTrackable(makeComplexPropertiesTrackable: false); 579 | var monitor = ((IChangeTrackable)trackable).Monitor(); 580 | 581 | trackable.Address.City = "Chicago"; 582 | 583 | monitor.Should().NotRaise(nameof(IChangeTrackable.StatusChanged)); 584 | } 585 | 586 | [Fact] 587 | public void When_CollectionProperty_Children_Trackable_Set_CollectionProperty_And_Change_Collection_Should_Raise_StatusChanged_Event() 588 | { 589 | var order = Helper.GetOrder(); 590 | var trackable = order.AsTrackable(); 591 | trackable.OrderDetails = new List(); 592 | trackable.CastToIChangeTrackable().AcceptChanges(); 593 | var monitor = ((IChangeTrackable)trackable).Monitor(); 594 | 595 | trackable.OrderDetails.Add(new OrderDetail()); 596 | 597 | monitor.Should().Raise(nameof(IChangeTrackable.StatusChanged)); 598 | } 599 | 600 | [Fact] 601 | public void When_CollectionProperty_Children_Trackable_Set_CollectionProperty_And_Change_Collection_Item_Property_Should_Raise_StatusChanged_Event() 602 | { 603 | var order = Helper.GetOrder(); 604 | var trackable = order.AsTrackable(); 605 | trackable.OrderDetails = new List { new OrderDetail() }; 606 | trackable.CastToIChangeTrackable().AcceptChanges(); 607 | var monitor = ((IChangeTrackable)trackable).Monitor(); 608 | 609 | trackable.OrderDetails[0].OrderDetailId = 123; 610 | 611 | monitor.Should().Raise(nameof(IChangeTrackable.StatusChanged)); 612 | } 613 | 614 | [Fact] 615 | public void When_ComplexProperty_Children_Trackable_Set_ComplexProperty_And_Change_Property_Should_Raise_StatusChanged_Event() 616 | { 617 | var order = Helper.GetOrder(); 618 | var trackable = order.AsTrackable(); 619 | 620 | trackable.Address = new Address(); 621 | trackable.CastToIChangeTrackable().AcceptChanges(); 622 | var monitor = ((IChangeTrackable)trackable).Monitor(); 623 | trackable.Address.AddressId = 123; 624 | 625 | monitor.Should().Raise(nameof(IChangeTrackable.StatusChanged)); 626 | } 627 | 628 | [Fact] 629 | public void When_Nothing_Is_Changed_Should_Be_Unchanged() 630 | { 631 | Order order = Helper.GetOrder(); 632 | var trackable = order.AsTrackable(); 633 | 634 | trackable.CastToIChangeTrackable().IsChanged.Should().BeFalse(); 635 | trackable.CastToIChangeTrackable().ChangeTrackingStatus.Should().Be(ChangeStatus.Unchanged); 636 | } 637 | 638 | [Fact] 639 | public void When_Nothing_Is_Changed_ChangedProperties_Should_BeEmpty() 640 | { 641 | Order order = Helper.GetOrder(); 642 | var trackable = order.AsTrackable(); 643 | 644 | trackable.CastToIChangeTrackable().ChangedProperties.Should().BeEmpty(); 645 | } 646 | 647 | [Fact] 648 | public void When_Changed_ChangedProperties_Should_ReturnChangedProperties() 649 | { 650 | Order order = Helper.GetOrder(); 651 | var trackable = order.AsTrackable(); 652 | trackable.CustomerNumber = "Change"; 653 | 654 | trackable.CastToIChangeTrackable().ChangedProperties.Should().BeEquivalentTo(nameof(Order.CustomerNumber)); 655 | } 656 | 657 | [Fact] 658 | public void When_Changed_ComplexProperty_ChangedProperties_Should_ReturnChangedProperties() 659 | { 660 | Order order = Helper.GetOrder(); 661 | var trackable = order.AsTrackable(); 662 | trackable.Address.City = "Hanoi"; 663 | 664 | trackable.CastToIChangeTrackable().ChangedProperties.Should().BeEquivalentTo(nameof(Order.Address)); 665 | } 666 | 667 | [Fact] 668 | public void When_Changed_ComplexProperty_PropertyChanged_Should_Raise_For_ChangedProperties() 669 | { 670 | Order order = Helper.GetOrder(); 671 | var trackable = order.AsTrackable(); 672 | 673 | IMonitor monitor = ((System.ComponentModel.INotifyPropertyChanged)trackable).Monitor(); 674 | trackable.Address.City = "Karachi"; 675 | 676 | monitor.Should().RaisePropertyChangeFor(o => ((IChangeTrackable)o).ChangedProperties); 677 | monitor.OccurredEvents.Count(i => i.Parameters.OfType().SingleOrDefault()?.PropertyName == nameof(IChangeTrackable.ChangedProperties)).Should().Be(1); 678 | } 679 | 680 | [Fact] 681 | public void When_Changed_ComplexProperty_PropertyChanged_Should_Raise_Once_If_Changing_Again_For_ChangedProperties() 682 | { 683 | Order order = Helper.GetOrder(); 684 | var trackable = order.AsTrackable(); 685 | 686 | IMonitor monitor = ((System.ComponentModel.INotifyPropertyChanged)trackable).Monitor(); 687 | trackable.Address.City = "Milan"; 688 | trackable.Address.City = "Osaka"; 689 | 690 | monitor.Should().RaisePropertyChangeFor(o => ((IChangeTrackable)o).ChangedProperties); 691 | monitor.OccurredEvents.Count(i => i.Parameters.OfType().SingleOrDefault()?.PropertyName == nameof(IChangeTrackable.ChangedProperties)).Should().Be(1); 692 | } 693 | 694 | [Fact] 695 | public void When_Changed_ComplexProperty_PropertyChanged_Should_Raise_Twice_If_Changing_Back_To_Original_For_ChangedProperties() 696 | { 697 | Order order = Helper.GetOrder(); 698 | var trackable = order.AsTrackable(); 699 | 700 | IMonitor monitor = ((System.ComponentModel.INotifyPropertyChanged)trackable).Monitor(); 701 | string originalCiti = trackable.Address.City; 702 | trackable.Address.City = "Phoenix"; 703 | trackable.Address.City = originalCiti; 704 | 705 | monitor.Should().RaisePropertyChangeFor(o => ((IChangeTrackable)o).ChangedProperties); 706 | monitor.OccurredEvents.Count(i => i.Parameters.OfType().SingleOrDefault()?.PropertyName == nameof(IChangeTrackable.ChangedProperties)).Should().Be(2); 707 | } 708 | 709 | [Fact] 710 | public void When_Changed_ComplexProperty_PropertyChanged_Should_Raise_Twice_If_Reverting_Back_To_Original_For_ChangedProperties() 711 | { 712 | Order order = Helper.GetOrder(); 713 | var trackable = order.AsTrackable(); 714 | 715 | IMonitor monitor = ((System.ComponentModel.INotifyPropertyChanged)trackable).Monitor(); 716 | trackable.Address.City = "Hiroshima"; 717 | trackable.Address.CastToIChangeTrackable().RejectChanges(); 718 | 719 | trackable.CastToIChangeTrackable().ChangedProperties.Should().BeEmpty(); 720 | monitor.Should().RaisePropertyChangeFor(o => ((IChangeTrackable)o).ChangedProperties); 721 | monitor.OccurredEvents.Count(i => i.Parameters.OfType().SingleOrDefault()?.PropertyName == nameof(IChangeTrackable.ChangedProperties)).Should().Be(2); 722 | } 723 | 724 | [Fact] 725 | public void When_Changed_CollectionProperty_PropertyChanged_Should_Raise_Twice_If_AcceptChanges_For_ChangedProperties() 726 | { 727 | Order order = Helper.GetOrder(); 728 | var trackable = order.AsTrackable(); 729 | 730 | IMonitor monitor = ((System.ComponentModel.INotifyPropertyChanged)trackable).Monitor(); 731 | trackable.OrderDetails.Clear(); 732 | 733 | trackable.CastToIChangeTrackable().ChangedProperties.Count().Should().Be(1); 734 | trackable.CastToIChangeTrackable().AcceptChanges(); 735 | trackable.CastToIChangeTrackable().ChangedProperties.Should().BeEmpty(); 736 | monitor.Should().RaisePropertyChangeFor(o => ((IChangeTrackable)o).ChangedProperties); 737 | monitor.OccurredEvents.Count(i => i.Parameters.OfType().SingleOrDefault()?.PropertyName == nameof(IChangeTrackable.ChangedProperties)).Should().Be(2); 738 | } 739 | 740 | [Fact] 741 | public void When_Changed_CollectionProperty_ChangedProperties_Should_ReturnChangedProperties() 742 | { 743 | Order order = Helper.GetOrder(); 744 | var trackable = order.AsTrackable(); 745 | trackable.OrderDetails.RemoveAt(0); 746 | 747 | trackable.CastToIChangeTrackable().ChangedProperties.Should().BeEquivalentTo(nameof(Order.OrderDetails)); 748 | } 749 | 750 | [Fact] 751 | public void Change_Property_From_Null_To_Null_Should_Not_Throw() 752 | { 753 | var trackable = new Order { Id = 321, }.AsTrackable(); 754 | 755 | trackable.Invoking(o => o.Address = null).Should().NotThrow(); 756 | } 757 | 758 | [Fact] 759 | public void Change_Property_From_Value_To_Null_Should_Not_Throw() 760 | { 761 | var trackable = new Order { Id = 321, Address = new Address() }.AsTrackable(); 762 | 763 | trackable.Invoking(o => o.Address = null).Should().NotThrow(); 764 | } 765 | 766 | [Fact] 767 | public void GetCurrent_Should_Return_Original() 768 | { 769 | var order = Helper.GetOrder(); 770 | var trackable = order.AsTrackable(); 771 | 772 | trackable.Id = 124; 773 | trackable.CustomerNumber = "Test1"; 774 | 775 | var current = trackable.CastToIChangeTrackable().GetCurrent(); 776 | 777 | current.Should().BeEquivalentTo(trackable, options => options.IgnoringCyclicReferences()); 778 | (current is IChangeTrackable).Should().BeFalse(); 779 | } 780 | 781 | [Fact] 782 | public void Insert_On_Child_Collection_Should_Be_Intercepted() 783 | { 784 | var order = Helper.GetOrder(); 785 | var trackable = order.AsTrackable(); 786 | 787 | var monitor = ((IChangeTrackable)trackable).Monitor(); 788 | 789 | trackable.OrderDetails.Insert(0, new OrderDetail()); 790 | 791 | monitor.Should().Raise(nameof(IChangeTrackable.StatusChanged)); 792 | } 793 | 794 | [Fact] 795 | public void Complex_Property_Should_Be_Trackable_Even_Not_All_Properties_Are_Read_Write() 796 | { 797 | var order = Helper.GetOrder(); 798 | var trackable = order.AsTrackable(); 799 | 800 | trackable.Address.Should().BeAssignableTo>(); 801 | } 802 | 803 | [Fact] 804 | public async Task Concurrent_Access_To_ComplexProperty_Should_Not_Throw() 805 | { 806 | Order order = Helper.GetOrder(); 807 | Order trackable = order.AsTrackable(); 808 | 809 | int count = 2; 810 | Address[] addresses = new Address[count]; 811 | Task[] tasks = new Task[count]; 812 | 813 | for (int i = 0; i < count; i++) 814 | { 815 | int index = i; 816 | tasks[index] = Task.Run(() => 817 | { 818 | addresses[index] = trackable.Address; 819 | }); 820 | } 821 | await Task.WhenAll(tasks); 822 | 823 | Address firstAddress = addresses[0]; 824 | addresses.All(a => a != null && ReferenceEquals(firstAddress, a)).Should().BeTrue(); 825 | } 826 | 827 | [Fact] 828 | public async Task Concurrent_Access_To_CollectionProperty_Should_Not_Throw() 829 | { 830 | Order order = Helper.GetOrder(); 831 | Order trackable = order.AsTrackable(); 832 | 833 | int count = 2; 834 | IList[] orderDetails = new IList[count]; 835 | Task[] tasks = new Task[count]; 836 | 837 | for (int i = 0; i < count; i++) 838 | { 839 | int index = i; 840 | tasks[index] = Task.Run(() => 841 | { 842 | orderDetails[index] = trackable.OrderDetails; 843 | }); 844 | } 845 | await Task.WhenAll(tasks); 846 | 847 | IList firstOrderDetails = orderDetails[0]; 848 | orderDetails.All(od => od != null && ReferenceEquals(firstOrderDetails, od)).Should().BeTrue(); 849 | } 850 | 851 | [Fact] 852 | public void AcceptChanges_On_Circular_Reference_Should_Not_Throw_OverflowException() 853 | { 854 | InventoryUpdate update = GetObjectGraph(); 855 | 856 | var trackable = update.AsTrackable(); 857 | trackable.InventoryUpdateId = 3; 858 | 859 | trackable.Invoking(t => t.CastToIChangeTrackable().AcceptChanges()).Should().NotThrow(); 860 | } 861 | 862 | [Fact] 863 | public void RejectChanges_On_Circular_Reference_Should_Not_Throw_OverflowException() 864 | { 865 | InventoryUpdate update = GetObjectGraph(); 866 | 867 | var trackable = update.AsTrackable(); 868 | trackable.InventoryUpdateId = 3; 869 | 870 | trackable.Invoking(t => t.CastToIChangeTrackable().RejectChanges()).Should().NotThrow(); 871 | } 872 | 873 | [Fact] 874 | public void Circular_Reference_Should_Be_Same_Reference() 875 | { 876 | InventoryUpdate update = GetObjectGraph(); 877 | 878 | var trackable = update.AsTrackable(); 879 | 880 | trackable.Should().BeSameAs(trackable.LinkedInventoryUpdate.LinkedToInventoryUpdate); 881 | trackable.LinkedInventoryUpdate.Should().BeSameAs(trackable.LinkedInventoryUpdate.LinkedToInventoryUpdate.LinkedInventoryUpdate); 882 | } 883 | 884 | private static InventoryUpdate GetObjectGraph() 885 | { 886 | var update0 = new InventoryUpdate 887 | { 888 | InventoryUpdateId = 0, 889 | UpdateInfos = new List 890 | { 891 | new UpdateInfo 892 | { 893 | UpdateInfoId = 1 894 | } 895 | } 896 | }; 897 | update0.UpdateInfos[0].InventoryUpdate = update0; 898 | var update1 = new InventoryUpdate 899 | { 900 | InventoryUpdateId = 1, 901 | LinkedToInventoryUpdate = update0 902 | }; 903 | update0.LinkedInventoryUpdate = update1; 904 | return update0; 905 | } 906 | 907 | [Fact] 908 | public void When_Changed_Back_Should_Be_Unchanged() 909 | { 910 | Order order = Helper.GetOrder(); 911 | var trackable = order.AsTrackable(); 912 | 913 | trackable.Id++; 914 | trackable.Id--; 915 | trackable.LinkedOrder.Id++; 916 | trackable.LinkedOrder.Id--; 917 | 918 | trackable.CastToIChangeTrackable().IsChanged.Should().BeFalse(); 919 | trackable.CastToIChangeTrackable().ChangeTrackingStatus.Should().Be(ChangeStatus.Unchanged); 920 | } 921 | 922 | [Fact] 923 | public void Call_AsTrackable_On_Trackable_Should_Throw_InvalidOperationException() 924 | { 925 | IList orders = Helper.GetOrdersIList(); 926 | IList trackable = orders.AsTrackable(); 927 | 928 | trackable.Invoking(t => t.AsTrackable()).Should().Throw(); 929 | } 930 | 931 | public class Phone 932 | { 933 | public virtual CallerId CallerId { get; set; } 934 | public virtual CallerId TheCallerId => CallerId; 935 | } 936 | 937 | public class CallerId 938 | { 939 | public virtual string Name { get; set; } 940 | } 941 | 942 | [Fact] 943 | public void ReadOnly_Property_Should_Not_Be_Intercepted() 944 | { 945 | Phone phone = new Phone 946 | { 947 | CallerId = new CallerId 948 | { 949 | Name = "Caller" 950 | } 951 | }; 952 | Phone trackable = phone.AsTrackable(); 953 | 954 | trackable.TheCallerId.Name = "ChangedCaller"; 955 | 956 | trackable.CallerId.CastToIChangeTrackable().IsChanged.Should().BeTrue(); 957 | trackable.TheCallerId.Should().BeSameAs(trackable.CallerId); 958 | } 959 | 960 | [Fact] 961 | public void When_Setting_All_Back_Status_IsChanged_Should_Be_False() 962 | { 963 | var update0 = new InventoryUpdate 964 | { 965 | InventoryUpdateId = 0 966 | }; 967 | 968 | var update1 = new InventoryUpdate 969 | { 970 | InventoryUpdateId = 1, 971 | LinkedToInventoryUpdateId = update0.InventoryUpdateId, 972 | LinkedToInventoryUpdate = update0 973 | }; 974 | update0.LinkedInventoryUpdate = update1; 975 | 976 | var update3 = new InventoryUpdate 977 | { 978 | InventoryUpdateId = 3, 979 | LinkedToInventoryUpdateId = update1.InventoryUpdateId, 980 | LinkedToInventoryUpdate = update1 981 | }; 982 | update1.LinkedInventoryUpdate = update3; 983 | 984 | var update2 = new InventoryUpdate 985 | { 986 | InventoryUpdateId = 2 987 | }; 988 | 989 | IList updates = new List 990 | { 991 | update0, 992 | update1, 993 | update3, 994 | update2 995 | }; 996 | 997 | IList trackableUpdates = updates.AsTrackable(); 998 | 999 | List updatesToLink = trackableUpdates.OrderBy(iu => iu.InventoryUpdateId).ToList(); 1000 | InventoryUpdate linkedToInventoryUpdate = null; 1001 | foreach (InventoryUpdate inventoryUpdate in updatesToLink) 1002 | { 1003 | inventoryUpdate.LinkedToInventoryUpdateId = linkedToInventoryUpdate?.InventoryUpdateId; 1004 | inventoryUpdate.LinkedToInventoryUpdate = linkedToInventoryUpdate; 1005 | inventoryUpdate.LinkedInventoryUpdate = null; 1006 | if (linkedToInventoryUpdate != null) 1007 | { 1008 | linkedToInventoryUpdate.LinkedInventoryUpdate = inventoryUpdate; 1009 | } 1010 | linkedToInventoryUpdate = inventoryUpdate; 1011 | } 1012 | 1013 | InventoryUpdate inventoryUpdateToUnlink = trackableUpdates[3]; 1014 | linkedToInventoryUpdate = inventoryUpdateToUnlink.LinkedToInventoryUpdate; 1015 | InventoryUpdate linkedInventoryUpdate = inventoryUpdateToUnlink.LinkedInventoryUpdate; 1016 | 1017 | inventoryUpdateToUnlink.LinkedToInventoryUpdateId = null; 1018 | inventoryUpdateToUnlink.LinkedToInventoryUpdate = null; 1019 | inventoryUpdateToUnlink.LinkedInventoryUpdate = null; 1020 | 1021 | if (linkedToInventoryUpdate != null) 1022 | { 1023 | linkedToInventoryUpdate.LinkedInventoryUpdate = linkedInventoryUpdate; 1024 | } 1025 | if (linkedInventoryUpdate != null) 1026 | { 1027 | linkedInventoryUpdate.LinkedToInventoryUpdateId = linkedToInventoryUpdate?.InventoryUpdateId; 1028 | linkedInventoryUpdate.LinkedToInventoryUpdate = linkedToInventoryUpdate; 1029 | } 1030 | 1031 | trackableUpdates.CastToIChangeTrackableCollection().IsChanged.Should().BeFalse(); 1032 | trackableUpdates[0].CastToIChangeTrackable().IsChanged.Should().BeFalse(); 1033 | trackableUpdates[1].CastToIChangeTrackable().IsChanged.Should().BeFalse(); 1034 | trackableUpdates[2].CastToIChangeTrackable().IsChanged.Should().BeFalse(); 1035 | trackableUpdates[3].CastToIChangeTrackable().IsChanged.Should().BeFalse(); 1036 | } 1037 | } 1038 | } --------------------------------------------------------------------------------