public void GenerateAndApplyReversePatch()
        {
            var initial = new PhoneBook()
            {
                Entries = new Dictionary <string, TestPerson>()
                {
                    ["0800JOHNDOE"] = new TestPerson()
                    {
                        FirstName = "John",
                        LastName  = "Doe",
                        IsAdult   = true,
                    },
                    ["0800JANEDOE"] = new TestPerson()
                    {
                        FirstName = "Jane",
                        LastName  = "Doe",
                        IsAdult   = true,
                    },
                },
            };

            using DraftScope scope = (DraftScope)DraftExtensions.CreateDraft(initial, out PhoneBook draft);

            var patchGenerator = new DictionaryPatchGenerator();
            var patches        = new JsonPatchDocument();
            var inversePatches = new JsonPatchDocument();

            draft.Entries.Remove("0800JANEDOE");
            draft.Entries.Add("0800BABYDOE", new TestPerson()
            {
                FirstName = "Baby",
                LastName  = "Doe",
            });

            // trick the scope into thinking that is finishing and should not create proxies anymore.
            scope.IsFinishing = true;

            patchGenerator.Generate((IDraft)draft.Entries, "/Entries", patches, inversePatches);

            // inverse order of inverse patches.
            inversePatches.Operations.Reverse();

            var final = scope.FinishDraft <PhoneBook, IPhoneBook>(draft);

            var result = IPhoneBook.Produce(initial, p =>
            {
                patches.ApplyTo(p);
            });

            result = result.Produce(p =>
            {
                inversePatches.ApplyTo(p);
            });

            Assert.Equal(2, result.Entries.Count);
            Assert.Same(initial.Entries["0800JOHNDOE"], result.Entries["0800JOHNDOE"]);
            Assert.Same(initial.Entries["0800JANEDOE"], result.Entries["0800JANEDOE"]);
        }
        public void GenerateDictionaryPatch()
        {
            var initial = new PhoneBook()
            {
                Entries = new Dictionary <string, TestPerson>()
                {
                    ["0800JOHNDOE"] = new TestPerson()
                    {
                        FirstName = "John",
                        LastName  = "Doe",
                        IsAdult   = true,
                    },
                    ["0800JANEDOE"] = new TestPerson()
                    {
                        FirstName = "Jane",
                        LastName  = "Doe",
                        IsAdult   = true,
                    },
                },
            };

            using var scope = DraftExtensions.CreateDraft(initial, out PhoneBook draft);

            var patchGenerator = new DictionaryPatchGenerator();
            var patches        = new JsonPatchDocument();
            var inversePatches = new JsonPatchDocument();

            draft.Entries.Remove("0800JANEDOE");
            draft.Entries.Add("0800BABYDOE", new TestPerson()
            {
                FirstName = "Baby",
                LastName  = "Doe",
            });

            patchGenerator.Generate((IDraft)draft.Entries, "/Entries", patches, inversePatches);

            // inverse order of inverse patches.
            inversePatches.Operations.Reverse();

            JsonAssert.Equal(
                @"
            [
              {
                'path': '/Entries/0800JANEDOE',
                'op': 'remove'
              },
              {
                'value': {
                  'Cars': null,
                  'FirstName': 'Baby',
                  'LastName': 'Doe',
                  'IsAdult': false,
                  'FirstChild': null,
                  'SecondChild': null
                },
                'path': '/Entries/0800BABYDOE',
                'op': 'add'
              }
            ]
            ", JsonConvert.SerializeObject(patches));

            JsonAssert.Equal(
                @"
            [
              {
                'path': '/Entries/0800BABYDOE',
                'op': 'remove'
              },
              {
                'value': {
                  'FirstName': 'Jane',
                  'LastName': 'Doe',
                  'IsAdult': true,
                  'FirstChild': null,
                  'SecondChild': null,
                  'Cars': null
                },
                'path': '/Entries/0800JANEDOE',
                'op': 'add'
              }
            ]
            ", JsonConvert.SerializeObject(inversePatches));
        }
예제 #3
0
        /// <summary>
        /// Finishes an instance.
        /// </summary>
        /// <param name="draft">The instance to finish.</param>
        /// <returns>The immutable variant of the instance.</returns>
        private object?FinishInstance(object?draft)
        {
            ObjectPatchGenerator?    objectPatchGenerator     = null;
            DictionaryPatchGenerator?dictionaryPatchGenerator = null;
            CollectionPatchGenerator?collectionPatchGenerator = null;
            JsonPatchDocument?       patches        = null;
            JsonPatchDocument?       inversePatches = null;

            if (this.Parent?.Patches != null && this.Patches == null)
            {
                patches        = new JsonPatchDocument();
                inversePatches = new JsonPatchDocument();
            }
            else
            {
                patches        = this.Patches;
                inversePatches = this.InversePatches;
            }

            if (patches != null && inversePatches != null)
            {
                objectPatchGenerator     = new ObjectPatchGenerator();
                dictionaryPatchGenerator = new DictionaryPatchGenerator();
                collectionPatchGenerator = new CollectionPatchGenerator(new DynamicLargestCommonSubsequence());
            }

            object?Reconcile(object?draft)
            {
                if (draft == null)
                {
                    return(null);
                }

                var draftType = draft.GetType();

                if (draftType.IsValueType || this.AllowedImmutableReferenceTypes.Contains(draftType))
                {
                    return(draft);
                }

                var proxyType = GetProxyType(draft);

                if (proxyType == null)
                {
                    throw new DraftException(draft, $"The object of type {draftType} cannot be made immutable.");
                }

                if (draft is IDraft idraft && this.drafts.Contains(draft))
                {
                    var delayedOperations = new List <Action>();

                    if (idraft.DraftState is ObjectDraftState objectDraftState)
                    {
                        foreach ((string propertyName, object child) in objectDraftState.ChildDrafts)
                        {
                            var immutable = Reconcile(child);

                            if (ReferenceEquals(immutable, child))
                            {
                                delayedOperations.Add(() =>
                                {
                                    // use reflection to set the property and trigger changed on the parent.
                                    draftType.GetProperty(propertyName).SetValue(draft, immutable);
                                });
                            }
                        }

                        objectPatchGenerator?.Generate(idraft, idraft.DraftState !.Path !.ToString(), patches !, inversePatches !);
                    }
                    else if (idraft.DraftState is CollectionDraftState collectionDraftState)
                    {
                        if (draft is IDictionary dictionary)
                        {
                            foreach (DictionaryEntry entry in dictionary)
                            {
                                if (InternalIsDraft(entry.Value) && this.drafts.Contains(entry.Value))
                                {
                                    var immutable = Reconcile(entry.Value);

                                    delayedOperations.Add(() =>
                                    {
                                        // draft turned into immutable.
                                        if (ReferenceEquals(immutable, entry.Value))
                                        {
                                            idraft.DraftState !.Changed = true;
                                        }

                                        // draft reverted to original.
                                        else
                                        {
                                            dictionary[entry.Key] = immutable;
                                        }
                                    });
                                }
                            }

                            dictionaryPatchGenerator?.Generate(idraft, idraft.DraftState !.Path !.ToString(), patches !, inversePatches !);
                        }
                        else if (draft is IList list)
                        {
                            // todo: handle sets.
                            for (int i = 0; i < list.Count; i++)
                            {
                                object?child = list[i];
                                if (InternalIsDraft(child) && this.drafts.Contains(child))
                                {
                                    var immutable = Reconcile(child);

                                    // capture i
                                    int captured = i;

                                    delayedOperations.Add(() =>
                                    {
                                        // draft turned into immutable.
                                        if (ReferenceEquals(immutable, child))
                                        {
                                            idraft.DraftState !.Changed = true;
                                        }

                                        // draft reverted to original.
                                        else
                                        {
                                            list[captured] = immutable;
                                        }
                                    });
                                }
                            }

                            collectionPatchGenerator?.Generate(idraft, idraft.DraftState !.Path !.ToString(), patches !, inversePatches !);
                        }
                    }

                    foreach (var toExecute in delayedOperations)
                    {
                        toExecute();
                    }

                    // not changed, return the original.
                    if (!idraft.DraftState !.Changed)
                    {
                        draft = idraft.DraftState.GetOriginal <object?>();
                    }
                }

                return(draft);
            }

            this.IsFinishing = true;
            try
            {
                draft = Reconcile(draft);

                if (draft is ILockable lockable)
                {
                    lockable.Lock();
                }

                if (this.Parent?.Patches != null)
                {
                    this.Parent.Patches.Operations.AddRange(patches !.Operations);
                    this.Parent.InversePatches !.Operations.AddRange(inversePatches !.Operations);
                    if (draft != null)
                    {
                        this.Parent.HasPatches.Add(draft);
                    }
                }
                else
                {
                    this.producerOptions.InversePatches?.Operations.Reverse();
                }

                return(draft);
            }
            finally
            {
                this.IsFinishing = false;
                this.Dispose();
            }
        }