diff --git a/CHANGELOG.md b/CHANGELOG.md index 21fb5cb7e4..de0045eeaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,27 @@ * Added automatic serialization and deserialization of Realm classes when using methods on `MongoClient.Collection`, without the need to annotate classes with `MongoDB.Bson`attributes. This feature required to change the default serialization for various types (including `DateTimeOffset`). If you prefer to use the previous serialization, you need to call `Realm.SetLegacySerialization` before any kind of serialization is done, otherwise it may not work as epxected. [#3459](https://github.com/realm/realm-dotnet/pull/3459) ### Enhancements +* Add support for passing a key paths collection (`KeyPathsCollection`) when using `IRealmCollection.SubscribeForNotifications`. Passing a `KeyPathsCollection` allows to specify which changes in properties should raise a notification. + + A `KeyPathsCollection` can be obtained by: + - building it explicitly by using the method `KeyPathsCollection.Of`; + - building it implicitly with the conversion from a `List` or array of `KeyPath` or strings; + - getting one of the static values `Full` and `Shallow` for full and shallow notifications respectively. + + + For example: + ```csharp + var query = realm.All(); + + KeyPathsCollection kpc; + + //Equivalent declarations + kpc = KeyPathsCollection.Of("Email", "Name"); + kpc = new List {"Email", "Name"}; + + query.SubscribeForNotifications(NotificationCallback, kpc); + ``` + (PR [#3501 ](https://github.com/realm/realm-dotnet/pull/3501)) * Added the `MongoClient.GetCollection` method to get a collection of documents from MongoDB that can be deserialized in Realm objects. This methods works the same as `MongoClient.GetDatabase(dbName).GetCollection(collectionName)`, but the database name and collection name are automatically derived from the Realm object class. [#3414](https://github.com/realm/realm-dotnet/pull/3414) ### Fixed diff --git a/Realm/Realm/DatabaseTypes/Accessors/ManagedAccessor.cs b/Realm/Realm/DatabaseTypes/Accessors/ManagedAccessor.cs index 3ce6b112d5..917b1ce86a 100644 --- a/Realm/Realm/DatabaseTypes/Accessors/ManagedAccessor.cs +++ b/Realm/Realm/DatabaseTypes/Accessors/ManagedAccessor.cs @@ -209,8 +209,10 @@ public void UnsubscribeFromNotifications() } /// - void INotifiable.NotifyCallbacks(NotifiableObjectHandleBase.CollectionChangeSet? changes, bool shallow) + void INotifiable.NotifyCallbacks(NotifiableObjectHandleBase.CollectionChangeSet? changes, + KeyPathsCollectionType type, Delegate? callback) { + Debug.Assert(callback == null, "Object notifications don't support keypaths, so callback should always be null"); if (changes.HasValue) { foreach (var propertyIndex in changes.Value.Properties) diff --git a/Realm/Realm/DatabaseTypes/INotifiable.cs b/Realm/Realm/DatabaseTypes/INotifiable.cs index 51a736dead..8220db146a 100644 --- a/Realm/Realm/DatabaseTypes/INotifiable.cs +++ b/Realm/Realm/DatabaseTypes/INotifiable.cs @@ -33,8 +33,9 @@ internal interface INotifiable /// Method called when there are changes to report for that object. /// /// The changes that occurred. - /// Whether the changes are coming from a shallow notifier or not. - void NotifyCallbacks(TChangeset? changes, bool shallow); + /// The type of the key paths collection related to the notification. + /// The eventual callback to call for the notification (if type == Explicit). + void NotifyCallbacks(TChangeset? changes, KeyPathsCollectionType type, Delegate? callback); } internal class NotificationToken : IDisposable diff --git a/Realm/Realm/DatabaseTypes/KeyPathCollection.cs b/Realm/Realm/DatabaseTypes/KeyPathCollection.cs new file mode 100644 index 0000000000..d33aef4f93 --- /dev/null +++ b/Realm/Realm/DatabaseTypes/KeyPathCollection.cs @@ -0,0 +1,166 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2023 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace Realms; + +/// +/// Represents a collection of that can be used when subscribing to notifications with . +/// +/// +/// A can be obtained by: +/// +/// +/// building it explicitly by using the method ; +/// +/// +/// building it implicitly with the conversion from a or array of or strings; +/// +/// +/// getting one of the static values and for full and shallow notifications respectively. +/// +/// +/// +/// +/// +public class KeyPathsCollection : IEnumerable +{ + private IEnumerable _collection; + + private static readonly KeyPathsCollection _shallow = new(KeyPathsCollectionType.Shallow); + private static readonly KeyPathsCollection _full = new(KeyPathsCollectionType.Full); + + internal KeyPathsCollectionType Type { get; } + + private KeyPathsCollection(KeyPathsCollectionType type, ICollection? collection = null) + { + Debug.Assert(type == KeyPathsCollectionType.Explicit == (collection?.Any() == true), + $"If collection isn't empty, then the type must be {nameof(KeyPathsCollectionType.Explicit)}"); + + Type = type; + _collection = collection ?? Enumerable.Empty(); + + VerifyKeyPaths(); + } + + /// + /// Builds a from an array of . + /// + /// The array of to use for the . + /// The built from the input array of . + public static KeyPathsCollection Of(params KeyPath[] paths) + { + if (paths.Length == 0) + { + return new KeyPathsCollection(KeyPathsCollectionType.Shallow); + } + + return new KeyPathsCollection(KeyPathsCollectionType.Explicit, paths); + } + + /// + /// Gets a value for shallow notifications, that will raise notifications only for changes to the collection itself (for example when an element is added or removed), + /// but not for changes to any of the properties of the elements of the collection. + /// + public static KeyPathsCollection Shallow => _shallow; + + /// + /// Gets a value for full notifications, for which changes to all top-level properties and 4 nested levels will raise a notification. This is the default value. + /// + public static KeyPathsCollection Full => _full; + + public static implicit operator KeyPathsCollection(List paths) => + new(KeyPathsCollectionType.Explicit, paths.Select(path => (KeyPath)path).ToArray()); + + public static implicit operator KeyPathsCollection(List paths) => new(KeyPathsCollectionType.Explicit, paths); + + public static implicit operator KeyPathsCollection(string[] paths) => + new(KeyPathsCollectionType.Explicit, paths.Select(path => (KeyPath)path).ToArray()); + + public static implicit operator KeyPathsCollection(KeyPath[] paths) => new(KeyPathsCollectionType.Explicit, paths); + + internal IEnumerable GetStrings() => _collection.Select(x => x.Path); + + internal void VerifyKeyPaths() + { + foreach (var item in _collection) + { + if (string.IsNullOrWhiteSpace(item.Path)) + { + throw new ArgumentException("A key path cannot be null, empty, or consisting only of white spaces"); + } + } + } + + /// + public IEnumerator GetEnumerator() + { + return _collection.GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } +} + +/// +/// Represents a key path that can be used as a part of a when subscribing for notifications. +/// A can be implicitly built from a string, where the string is the name of a property (e.g "FirstName"), eventually dotted to indicated nested properties. +/// (e.g "Dog.Name"). Wildcards can also be used in key paths to capture all properties at a given level (e.g "*", "Friends.*" or "*.FirstName"). +/// +public readonly struct KeyPath +{ + internal string Path { get; } + + private KeyPath(string path) + { + Path = path; + } + + public static implicit operator KeyPath(string s) => new(s); + + /// + public override bool Equals(object? obj) => obj is KeyPath path && Path == path.Path; + + /// + public override int GetHashCode() => Path.GetHashCode(); + + public static bool operator ==(KeyPath left, KeyPath right) + { + return left.Equals(right); + } + + public static bool operator !=(KeyPath left, KeyPath right) + { + return !(left == right); + } +} + +internal enum KeyPathsCollectionType +{ + Full, + Shallow, + Explicit +} diff --git a/Realm/Realm/DatabaseTypes/RealmCollectionBase.cs b/Realm/Realm/DatabaseTypes/RealmCollectionBase.cs index e8c1c09666..1ba7570d87 100644 --- a/Realm/Realm/DatabaseTypes/RealmCollectionBase.cs +++ b/Realm/Realm/DatabaseTypes/RealmCollectionBase.cs @@ -30,6 +30,7 @@ using Realms.Exceptions; using Realms.Helpers; using Realms.Schema; +using static Realms.NotifiableObjectHandleBase; namespace Realms { @@ -189,15 +190,41 @@ public IRealmCollection Freeze() return CreateCollection(frozenRealm, frozenHandle); } - public IDisposable SubscribeForNotifications(NotificationCallbackDelegate callback) - => SubscribeForNotificationsImpl(callback, shallow: false); + public IDisposable SubscribeForNotifications(NotificationCallbackDelegate callback, KeyPathsCollection? keyPathsCollection = null) + { + keyPathsCollection ??= KeyPathsCollection.Full; + + if (keyPathsCollection.Type == KeyPathsCollectionType.Explicit && !ContainsRealmObjects()) + { + throw new InvalidOperationException("Key paths can be used only with collections of Realm objects"); + } + + return SubscribeForNotificationsImpl(callback, keyPathsCollection); + } - internal IDisposable SubscribeForNotificationsImpl(NotificationCallbackDelegate callback, bool shallow) + internal IDisposable SubscribeForNotificationsImpl(NotificationCallbackDelegate callback, KeyPathsCollection keyPathsCollection) { Argument.NotNull(callback, nameof(callback)); - _notificationCallbacks.Value.Add(callback, shallow); - return NotificationToken.Create(callback, c => UnsubscribeFromNotifications(c, shallow)); + if (keyPathsCollection.Type == KeyPathsCollectionType.Explicit) + { + var managedResultsHandle = GCHandle.Alloc(this, GCHandleType.Weak); + var callbackHandle = GCHandle.Alloc(callback, GCHandleType.Weak); + + var token = Handle.Value.AddNotificationCallback(GCHandle.ToIntPtr(managedResultsHandle), keyPathsCollection, + GCHandle.ToIntPtr(callbackHandle)); + + return NotificationToken.Create(callback, _ => token.Dispose()); + } + + // For notifications with type Full or Shallow we cache the callbacks on the managed level, to avoid creating multiple notifications in core + _notificationCallbacks.Value.Add(callback, keyPathsCollection); + return NotificationToken.Create(callback, c => UnsubscribeFromNotifications(c, keyPathsCollection.Type)); + } + + protected virtual bool ContainsRealmObjects() + { + return typeof(IRealmObjectBase).IsAssignableFrom(typeof(T)); } protected abstract T GetValueAtIndex(int index); @@ -257,9 +284,9 @@ protected static IEmbeddedObject EnsureUnmanagedEmbedded(in RealmValue value) return result; } - private void UnsubscribeFromNotifications(NotificationCallbackDelegate callback, bool shallow) + private void UnsubscribeFromNotifications(NotificationCallbackDelegate callback, KeyPathsCollectionType type) { - _notificationCallbacks.Value.Remove(callback, shallow); + _notificationCallbacks.Value.Remove(callback, type); } #region INotifyCollectionChanged @@ -396,18 +423,18 @@ private void UpdateCollectionChangedSubscriptionIfNecessary(bool isSubscribed) { if (isSubscribed) { - SubscribeForNotificationsImpl(OnChange, shallow: true); + SubscribeForNotificationsImpl(OnChange, KeyPathsCollection.Shallow); } else { - UnsubscribeFromNotifications(OnChange, shallow: true); + UnsubscribeFromNotifications(OnChange, KeyPathsCollectionType.Shallow); } } } #endregion INotifyCollectionChanged - void INotifiable.NotifyCallbacks(NotifiableObjectHandleBase.CollectionChangeSet? changes, bool shallow) + void INotifiable.NotifyCallbacks(CollectionChangeSet? changes, KeyPathsCollectionType type, Delegate? callback) { ChangeSet? changeset = null; if (changes != null) @@ -422,7 +449,13 @@ private void UpdateCollectionChangedSubscriptionIfNecessary(bool isSubscribed) cleared: actualChanges.Cleared); } - _notificationCallbacks.Value.Notify(changeset, shallow); + if (callback is NotificationCallbackDelegate notificationCallback) + { + notificationCallback(this, changeset); + return; + } + + _notificationCallbacks.Value.Notify(changeset, type); } public IEnumerator GetEnumerator() => new Enumerator(this); @@ -611,19 +644,17 @@ internal class NotificationCallbacks { private readonly RealmCollectionBase _parent; - private readonly Dictionary> Callbacks)> _subscriptions = new(); + private readonly Dictionary> Callbacks)> _subscriptions = new(); public NotificationCallbacks(RealmCollectionBase parent) { _parent = parent; } - // Shallow here and everywhere else is a bit of a shortcut we're taking until we have proper keypath filtering. - // Once we do, we probably want this to be some keypath identifier that we can pass between managed and native. - public void Add(NotificationCallbackDelegate callback, bool shallow) + public void Add(NotificationCallbackDelegate callback, KeyPathsCollection keyPathsCollection) { - var keyPath = shallow ? KeyPath.Empty : KeyPath.Full; - if (_subscriptions.TryGetValue(keyPath, out var subscription)) + var kpcType = keyPathsCollection.Type; + if (_subscriptions.TryGetValue(kpcType, out var subscription)) { if (subscription.DeliveredInitialNotification) { @@ -638,29 +669,28 @@ public void Add(NotificationCallbackDelegate callback, bool shallow) else { // If this is a new subscription, we store it in the backing dictionary, then we subscribe outside of transaction - _subscriptions[keyPath] = (null, false, new() { callback }); + _subscriptions[kpcType] = (null, false, new() { callback }); _parent.Realm.ExecuteOutsideTransaction(() => { // It's possible that we unsubscribed in the meantime, so only add a notification callback if we still have callbacks - if (_subscriptions.TryGetValue(keyPath, out var sub) && sub.Callbacks.Count > 0) + if (_subscriptions.TryGetValue(kpcType, out var sub) && sub.Callbacks.Count > 0) { var managedResultsHandle = GCHandle.Alloc(_parent, GCHandleType.Weak); - _subscriptions[keyPath] = (_parent.Handle.Value.AddNotificationCallback(GCHandle.ToIntPtr(managedResultsHandle), shallow), sub.DeliveredInitialNotification, sub.Callbacks); + _subscriptions[kpcType] = (_parent.Handle.Value.AddNotificationCallback(GCHandle.ToIntPtr(managedResultsHandle), keyPathsCollection), sub.DeliveredInitialNotification, sub.Callbacks); } }); } } - public bool Remove(NotificationCallbackDelegate callback, bool shallow) + public bool Remove(NotificationCallbackDelegate callback, KeyPathsCollectionType kpcType) { - var keyPath = shallow ? KeyPath.Empty : KeyPath.Full; - if (_subscriptions.TryGetValue(keyPath, out var subscription)) + if (_subscriptions.TryGetValue(kpcType, out var subscription)) { subscription.Callbacks.Remove(callback); if (subscription.Callbacks.Count == 0) { subscription.Token?.Dispose(); - _subscriptions.Remove(keyPath); + _subscriptions.Remove(kpcType); } return true; @@ -669,14 +699,13 @@ public bool Remove(NotificationCallbackDelegate callback, bool shallow) return false; } - public void Notify(ChangeSet? changes, bool shallow) + public void Notify(ChangeSet? changes, KeyPathsCollectionType kpcType) { - var keyPath = shallow ? KeyPath.Empty : KeyPath.Full; - if (_subscriptions.TryGetValue(keyPath, out var subscription)) + if (_subscriptions.TryGetValue(kpcType, out var subscription)) { if (changes == null) { - _subscriptions[keyPath] = (subscription.Token, true, subscription.Callbacks); + _subscriptions[kpcType] = (subscription.Token, true, subscription.Callbacks); } foreach (var callback in subscription.Callbacks.ToArray()) @@ -695,13 +724,6 @@ public void RemoveAll() } } - // Eventually this should be a proper class holding the keypaths - internal enum KeyPath - { - Full, - Empty - } - /// /// Special invalid object that is used to avoid an exception in WPF /// when deleting an element from a collection bound to UI (#1903). diff --git a/Realm/Realm/DatabaseTypes/RealmDictionary.cs b/Realm/Realm/DatabaseTypes/RealmDictionary.cs index 230c7a91bc..6e7985ab63 100644 --- a/Realm/Realm/DatabaseTypes/RealmDictionary.cs +++ b/Realm/Realm/DatabaseTypes/RealmDictionary.cs @@ -243,15 +243,24 @@ private static void ValidateKey(string key) } } + protected override bool ContainsRealmObjects() + { + return typeof(IRealmObjectBase).IsAssignableFrom(typeof(TValue)); + } + internal override RealmCollectionBase> CreateCollection(Realm realm, CollectionHandleBase handle) => new RealmDictionary(realm, (DictionaryHandle)handle, Metadata); internal override CollectionHandleBase GetOrCreateHandle() => _dictionaryHandle; protected override KeyValuePair GetValueAtIndex(int index) => _dictionaryHandle.GetValueAtIndex(index, Realm); - void INotifiable.NotifyCallbacks(DictionaryHandle.DictionaryChangeSet? changes, bool shallow) + void INotifiable.NotifyCallbacks(DictionaryHandle.DictionaryChangeSet? changes, + KeyPathsCollectionType type, Delegate? callback) { - Debug.Assert(!shallow, "Shallow should always be false here as we don't expose a way to configure it."); + Debug.Assert(type == KeyPathsCollectionType.Full, + "Notifications should always be default here as we don't expose a way to configure it."); + + Debug.Assert(callback == null, "Dictionary notifications don't expose keypaths, so the callback should always be null"); DictionaryChangeSet? changeset = null; if (changes != null) @@ -267,9 +276,9 @@ private static void ValidateKey(string key) _deliveredInitialKeyNotification = true; } - foreach (var callback in _keyCallbacks.ToArray()) + foreach (var keyCallback in _keyCallbacks.ToArray()) { - callback(this, changeset); + keyCallback(this, changeset); } } } diff --git a/Realm/Realm/Extensions/CollectionExtensions.cs b/Realm/Realm/Extensions/CollectionExtensions.cs index 8532802f25..8cfb7f42d0 100644 --- a/Realm/Realm/Extensions/CollectionExtensions.cs +++ b/Realm/Realm/Extensions/CollectionExtensions.cs @@ -58,14 +58,16 @@ public static IRealmCollection AsRealmCollection(this IQueryable query) /// Type of the or in the results. /// /// The callback to be invoked with the updated . + /// An optional , that indicates which changes in properties should raise a notification. /// /// A subscription token. It must be kept alive for as long as you want to receive change notifications. /// To stop receiving notifications, call . /// - public static IDisposable SubscribeForNotifications(this IQueryable results, NotificationCallbackDelegate callback) + public static IDisposable SubscribeForNotifications(this IQueryable results, NotificationCallbackDelegate callback, + KeyPathsCollection? keyPathsCollection = null) where T : IRealmObjectBase? { - return results.AsRealmCollection().SubscribeForNotifications(callback); + return results.AsRealmCollection().SubscribeForNotifications(callback, keyPathsCollection); } /// @@ -90,11 +92,14 @@ public static IRealmCollection AsRealmCollection(this ISet set) /// Type of the elements in the set. /// /// The callback to be invoked with the updated . + /// An optional , that indicates which changes in properties should raise a notification. /// /// A subscription token. It must be kept alive for as long as you want to receive change notifications. /// To stop receiving notifications, call . /// - public static IDisposable SubscribeForNotifications(this ISet set, NotificationCallbackDelegate callback) => set.AsRealmCollection().SubscribeForNotifications(callback); + public static IDisposable SubscribeForNotifications(this ISet set, NotificationCallbackDelegate callback, + KeyPathsCollection? keyPathsCollection = null) + => set.AsRealmCollection().SubscribeForNotifications(callback, keyPathsCollection); /// /// A convenience method that casts to which implements @@ -190,11 +195,14 @@ public static IQueryable AsRealmQueryable(this ISet set) /// Type of the elements in the list. /// /// The callback to be invoked with the updated . + /// An optional , that indicates which changes in properties should raise a notification. /// /// A subscription token. It must be kept alive for as long as you want to receive change notifications. /// To stop receiving notifications, call . /// - public static IDisposable SubscribeForNotifications(this IList list, NotificationCallbackDelegate callback) => list.AsRealmCollection().SubscribeForNotifications(callback); + public static IDisposable SubscribeForNotifications(this IList list, NotificationCallbackDelegate callback, + KeyPathsCollection? keyPathsCollection = null) + => list.AsRealmCollection().SubscribeForNotifications(callback, keyPathsCollection); /// /// Move the specified item to a new position within the list. @@ -306,23 +314,25 @@ public static IQueryable AsRealmQueryable(this IDictionary dic } /// - /// A convenience method that casts to and subscribes for change notifications. + /// A convenience method that casts to and subscribes for change notifications. /// /// The to observe for changes. /// Type of the elements in the dictionary. /// /// The callback to be invoked with the updated . + /// An optional , that indicates which changes in properties should raise a notification. /// /// A subscription token. It must be kept alive for as long as you want to receive change notifications. /// To stop receiving notifications, call . /// - public static IDisposable SubscribeForNotifications(this IDictionary dictionary, NotificationCallbackDelegate> callback) + public static IDisposable SubscribeForNotifications(this IDictionary dictionary, + NotificationCallbackDelegate> callback, KeyPathsCollection? keyPathsCollection = null) { - return dictionary.AsRealmCollection().SubscribeForNotifications(callback); + return dictionary.AsRealmCollection().SubscribeForNotifications(callback, keyPathsCollection); } /// - /// A convenience method that casts to and subscribes for change notifications. + /// A convenience method that casts to and subscribes for key change notifications. /// /// The to observe for changes. /// Type of the elements in the dictionary. diff --git a/Realm/Realm/Handles/CollectionHandleBase.cs b/Realm/Realm/Handles/CollectionHandleBase.cs index aa30c427da..366260dd54 100644 --- a/Realm/Realm/Handles/CollectionHandleBase.cs +++ b/Realm/Realm/Handles/CollectionHandleBase.cs @@ -62,6 +62,7 @@ public ResultsHandle GetFilteredResults(string query, QueryArgument[] arguments) protected virtual IntPtr SnapshotCore(out NativeException ex) => throw new NotSupportedException("Snapshotting this collection is not supported."); - public abstract NotificationTokenHandle AddNotificationCallback(IntPtr managedObjectHandle, bool shallow); + public abstract NotificationTokenHandle AddNotificationCallback(IntPtr managedObjectHandle, KeyPathsCollection keyPathsCollection, + IntPtr callback = default); } } diff --git a/Realm/Realm/Handles/DictionaryHandle.cs b/Realm/Realm/Handles/DictionaryHandle.cs index d904eb14dc..03f6e01c58 100644 --- a/Realm/Realm/Handles/DictionaryHandle.cs +++ b/Realm/Realm/Handles/DictionaryHandle.cs @@ -18,6 +18,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Runtime.InteropServices; using Realms.Native; @@ -34,7 +35,7 @@ internal struct DictionaryChangeSet } [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - public unsafe delegate void KeyNotificationCallback(IntPtr managedHandle, DictionaryChangeSet* changes, [MarshalAs(UnmanagedType.U1)] bool shallow); + public unsafe delegate void KeyNotificationCallback(IntPtr managedHandle, DictionaryChangeSet* changes); private static class NativeMethods { @@ -48,7 +49,8 @@ private static class NativeMethods public static extern void destroy(IntPtr listInternalHandle); [DllImport(InteropConfig.DLL_NAME, EntryPoint = "realm_dictionary_add_notification_callback", CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr add_notification_callback(DictionaryHandle handle, IntPtr managedDictionaryHandle, [MarshalAs(UnmanagedType.U1)] bool shallow, out NativeException ex); + public static extern IntPtr add_notification_callback(DictionaryHandle handle, IntPtr managedDictionaryHandle, + KeyPathsCollectionType type, IntPtr callback, StringValue[] keypaths, IntPtr keypaths_len, out NativeException ex); [DllImport(InteropConfig.DLL_NAME, EntryPoint = "realm_dictionary_add_key_notification_callback", CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr add_key_notification_callback(DictionaryHandle handle, IntPtr managedDictionaryHandle, out NativeException ex); @@ -136,12 +138,18 @@ public override void Clear() nativeException.ThrowIfNecessary(); } - public override NotificationTokenHandle AddNotificationCallback(IntPtr managedObjectHandle, bool shallow) + public override NotificationTokenHandle AddNotificationCallback(IntPtr managedObjectHandle, + KeyPathsCollection keyPathsCollection, IntPtr callback) { EnsureIsOpen(); - var result = NativeMethods.add_notification_callback(this, managedObjectHandle, shallow, out var nativeException); + using Arena arena = new(); + var nativeKeyPathsArray = keyPathsCollection.GetStrings().Select(p => StringValue.AllocateFrom(p, arena)).ToArray(); + + var result = NativeMethods.add_notification_callback(this, managedObjectHandle, + keyPathsCollection.Type, callback, nativeKeyPathsArray, (IntPtr)nativeKeyPathsArray.Length, out var nativeException); nativeException.ThrowIfNecessary(); + return new NotificationTokenHandle(Root!, result); } @@ -338,11 +346,11 @@ public ResultsHandle GetKeys() } [MonoPInvokeCallback(typeof(KeyNotificationCallback))] - public static unsafe void NotifyDictionaryChanged(IntPtr managedHandle, DictionaryChangeSet* changes, bool shallow) + public static unsafe void NotifyDictionaryChanged(IntPtr managedHandle, DictionaryChangeSet* changes) { if (GCHandle.FromIntPtr(managedHandle).Target is INotifiable notifiable) { - notifiable.NotifyCallbacks(changes == null ? null : *changes, shallow); + notifiable.NotifyCallbacks(changes == null ? null : *changes, KeyPathsCollectionType.Full, callback: null); } } } diff --git a/Realm/Realm/Handles/ListHandle.cs b/Realm/Realm/Handles/ListHandle.cs index bfcdcf1d54..16016980f1 100644 --- a/Realm/Realm/Handles/ListHandle.cs +++ b/Realm/Realm/Handles/ListHandle.cs @@ -17,6 +17,7 @@ //////////////////////////////////////////////////////////////////////////// using System; +using System.Linq; using System.Runtime.InteropServices; using Realms.Native; @@ -63,7 +64,8 @@ private static class NativeMethods public static extern void destroy(IntPtr listInternalHandle); [DllImport(InteropConfig.DLL_NAME, EntryPoint = "list_add_notification_callback", CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr add_notification_callback(ListHandle listHandle, IntPtr managedListHandle, [MarshalAs(UnmanagedType.U1)] bool shallow, out NativeException ex); + public static extern IntPtr add_notification_callback(ListHandle listHandle, IntPtr managedListHandle, + KeyPathsCollectionType type, IntPtr callback, StringValue[] keypaths, IntPtr keypaths_len, out NativeException ex); [DllImport(InteropConfig.DLL_NAME, EntryPoint = "list_move", CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr move(ListHandle listHandle, IntPtr sourceIndex, IntPtr targetIndex, out NativeException ex); @@ -213,12 +215,18 @@ public void Move(IntPtr sourceIndex, IntPtr targetIndex) nativeException.ThrowIfNecessary(); } - public override NotificationTokenHandle AddNotificationCallback(IntPtr managedObjectHandle, bool shallow) + public override NotificationTokenHandle AddNotificationCallback(IntPtr managedObjectHandle, + KeyPathsCollection keyPathsCollection, IntPtr callback) { EnsureIsOpen(); - var result = NativeMethods.add_notification_callback(this, managedObjectHandle, shallow, out var nativeException); + using Arena arena = new(); + var nativeKeyPathsArray = keyPathsCollection.GetStrings().Select(p => StringValue.AllocateFrom(p, arena)).ToArray(); + + var result = NativeMethods.add_notification_callback(this, managedObjectHandle, + keyPathsCollection.Type, callback, nativeKeyPathsArray, (IntPtr)nativeKeyPathsArray.Length, out var nativeException); nativeException.ThrowIfNecessary(); + return new NotificationTokenHandle(Root!, result); } diff --git a/Realm/Realm/Handles/NotifiableObjectHandleBase.cs b/Realm/Realm/Handles/NotifiableObjectHandleBase.cs index 90cfff869a..e530114748 100644 --- a/Realm/Realm/Handles/NotifiableObjectHandleBase.cs +++ b/Realm/Realm/Handles/NotifiableObjectHandleBase.cs @@ -47,7 +47,7 @@ public struct Move } [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - public unsafe delegate void NotificationCallback(IntPtr managedHandle, CollectionChangeSet* changes, bool shallow); + public unsafe delegate void NotificationCallback(IntPtr managedHandle, CollectionChangeSet* changes, KeyPathsCollectionType type, IntPtr callback); protected NotifiableObjectHandleBase(SharedRealmHandle? root, IntPtr handle) : base(root, handle) { @@ -56,11 +56,12 @@ protected NotifiableObjectHandleBase(SharedRealmHandle? root, IntPtr handle) : b public abstract ThreadSafeReferenceHandle GetThreadSafeReference(); [MonoPInvokeCallback(typeof(NotificationCallback))] - public static unsafe void NotifyObjectChanged(IntPtr managedHandle, CollectionChangeSet* changes, bool shallow) + public static unsafe void NotifyObjectChanged(IntPtr managedHandle, CollectionChangeSet* changes, KeyPathsCollectionType type, IntPtr callback) { if (GCHandle.FromIntPtr(managedHandle).Target is INotifiable notifiable) { - notifiable.NotifyCallbacks(changes == null ? null : *changes, shallow); + var managedCallback = type == KeyPathsCollectionType.Explicit && GCHandle.FromIntPtr(callback).Target is Delegate c ? c : null; + notifiable.NotifyCallbacks(changes == null ? null : *changes, type, managedCallback); } } } diff --git a/Realm/Realm/Handles/ResultsHandle.cs b/Realm/Realm/Handles/ResultsHandle.cs index eddad7a50f..1edf1b369a 100644 --- a/Realm/Realm/Handles/ResultsHandle.cs +++ b/Realm/Realm/Handles/ResultsHandle.cs @@ -17,7 +17,11 @@ //////////////////////////////////////////////////////////////////////////// using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; using System.Runtime.InteropServices; +using Realms.Helpers; using Realms.Native; namespace Realms @@ -39,7 +43,8 @@ private static class NativeMethods public static extern void clear(ResultsHandle results, SharedRealmHandle realmHandle, out NativeException ex); [DllImport(InteropConfig.DLL_NAME, EntryPoint = "results_add_notification_callback", CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr add_notification_callback(ResultsHandle results, IntPtr managedResultsHandle, [MarshalAs(UnmanagedType.U1)] bool shallow, out NativeException ex); + public static extern IntPtr add_notification_callback(ResultsHandle results, IntPtr managedResultsHandle, + KeyPathsCollectionType type, IntPtr callback, StringValue[] keypaths, IntPtr keypaths_len, out NativeException ex); [DllImport(InteropConfig.DLL_NAME, EntryPoint = "results_get_query", CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr get_query(ResultsHandle results, out NativeException ex); @@ -155,12 +160,18 @@ public SortDescriptorHandle GetSortDescriptor() return new SortDescriptorHandle(Root!, result); } - public override NotificationTokenHandle AddNotificationCallback(IntPtr managedObjectHandle, bool shallow) + public override NotificationTokenHandle AddNotificationCallback(IntPtr managedObjectHandle, + KeyPathsCollection keyPathsCollection, IntPtr callback) { EnsureIsOpen(); - var result = NativeMethods.add_notification_callback(this, managedObjectHandle, shallow, out var nativeException); + using Arena arena = new(); + var nativeKeyPathsArray = keyPathsCollection.GetStrings().Select(p => StringValue.AllocateFrom(p, arena)).ToArray(); + + var result = NativeMethods.add_notification_callback(this, managedObjectHandle, + keyPathsCollection.Type, callback, nativeKeyPathsArray, (IntPtr)nativeKeyPathsArray.Length, out var nativeException); nativeException.ThrowIfNecessary(); + return new NotificationTokenHandle(Root!, result); } diff --git a/Realm/Realm/Handles/SetHandle.cs b/Realm/Realm/Handles/SetHandle.cs index b76542ea1f..0ab2c82855 100644 --- a/Realm/Realm/Handles/SetHandle.cs +++ b/Realm/Realm/Handles/SetHandle.cs @@ -17,6 +17,7 @@ //////////////////////////////////////////////////////////////////////////// using System; +using System.Linq; using System.Runtime.InteropServices; using Realms.Native; @@ -36,7 +37,8 @@ private static class NativeMethods public static extern void destroy(IntPtr handle); [DllImport(InteropConfig.DLL_NAME, EntryPoint = "realm_set_add_notification_callback", CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr add_notification_callback(SetHandle handle, IntPtr managedSetHandle, [MarshalAs(UnmanagedType.U1)] bool shallow, out NativeException ex); + public static extern IntPtr add_notification_callback(SetHandle handle, IntPtr managedSetHandle, + KeyPathsCollectionType type, IntPtr callback, StringValue[] keypaths, IntPtr keypaths_len, out NativeException ex); [DllImport(InteropConfig.DLL_NAME, EntryPoint = "realm_set_get_is_valid", CallingConvention = CallingConvention.Cdecl)] [return: MarshalAs(UnmanagedType.U1)] @@ -136,12 +138,18 @@ public override void Clear() nativeException.ThrowIfNecessary(); } - public override NotificationTokenHandle AddNotificationCallback(IntPtr managedObjectHandle, bool shallow) + public override NotificationTokenHandle AddNotificationCallback(IntPtr managedObjectHandle, + KeyPathsCollection keyPathsCollection, IntPtr callback) { EnsureIsOpen(); - var result = NativeMethods.add_notification_callback(this, managedObjectHandle, shallow, out var nativeException); + using Arena arena = new(); + var nativeKeyPathsArray = keyPathsCollection.GetStrings().Select(p => StringValue.AllocateFrom(p, arena)).ToArray(); + + var result = NativeMethods.add_notification_callback(this, managedObjectHandle, + keyPathsCollection.Type, callback, nativeKeyPathsArray, (IntPtr)nativeKeyPathsArray.Length, out var nativeException); nativeException.ThrowIfNecessary(); + return new(Root!, result); } diff --git a/Realm/Realm/Handles/SharedRealmHandle.cs b/Realm/Realm/Handles/SharedRealmHandle.cs index 394d255345..add76a9f13 100644 --- a/Realm/Realm/Handles/SharedRealmHandle.cs +++ b/Realm/Realm/Handles/SharedRealmHandle.cs @@ -264,7 +264,8 @@ public static unsafe void Initialize() GCHandle.Alloc(handleTaskCompletion); GCHandle.Alloc(onInitialization); - NativeMethods.install_callbacks(notifyRealm, getNativeSchema, openRealm, disposeGCHandle, logMessage, notifyObject, notifyDictionary, onMigration, shouldCompact, handleTaskCompletion, onInitialization); + NativeMethods.install_callbacks(notifyRealm, getNativeSchema, openRealm, disposeGCHandle, logMessage, + notifyObject, notifyDictionary, onMigration, shouldCompact, handleTaskCompletion, onInitialization); } public static void SetLogLevel(LogLevel level) => NativeMethods.set_log_level(level); diff --git a/Realm/Realm/Linq/IRealmCollection.cs b/Realm/Realm/Linq/IRealmCollection.cs index c498a755d3..3ea30ec5d8 100644 --- a/Realm/Realm/Linq/IRealmCollection.cs +++ b/Realm/Realm/Linq/IRealmCollection.cs @@ -132,6 +132,11 @@ public interface IRealmCollection : IReadOnlyList, INotifyCollectionCh /// it will contain information about which rows in the results were added, removed or modified. /// /// + /// When the collection contains realm objects, it is possible to pass an optional , that indicates which changes in properties should raise a notification. + /// If no is passed, will be used, so changes to all top-level properties and 4 nested levels will raise a notification. + /// See for more information about how to build it. + /// + /// /// If a write transaction did not modify any objects in this , the callback is not invoked at all. /// /// @@ -146,15 +151,17 @@ public interface IRealmCollection : IReadOnlyList, INotifyCollectionCh /// /// /// The callback to be invoked with the updated . + /// An optional collection of key paths that indicates which changes in properties should raise a notification. /// /// A subscription token. It must be kept alive for as long as you want to receive change notifications. /// To stop receiving notifications, call . /// - /// - /// - /// - /// + /// + /// + /// + /// + /// /// - IDisposable SubscribeForNotifications(NotificationCallbackDelegate callback); + IDisposable SubscribeForNotifications(NotificationCallbackDelegate callback, KeyPathsCollection? keyPathCollection = null); } } diff --git a/Tests/Realm.Tests/Database/NotificationTests.cs b/Tests/Realm.Tests/Database/NotificationTests.cs index 146849eda5..7a75955ebe 100644 --- a/Tests/Realm.Tests/Database/NotificationTests.cs +++ b/Tests/Realm.Tests/Database/NotificationTests.cs @@ -1298,6 +1298,8 @@ public void ResultsOfObjects_SubscribeForNotifications_DoesntReceiveModification { var changesets = new List(); + var kpCollection = shallow ? KeyPathsCollection.Shallow : KeyPathsCollection.Full; + // This is testing using the internal API because we're not exposing the shallow/keypath functionality publicly yet. var results = (RealmResults)_realm.All(); @@ -1307,7 +1309,7 @@ public void ResultsOfObjects_SubscribeForNotifications_DoesntReceiveModification { changesets.Add(changes); } - }, shallow); + }, kpCollection); _realm.Write(() => { @@ -1334,6 +1336,8 @@ public void ResultsOfObjects_SubscribeForNotifications_DoesntReceiveModification [Test] public void ListOfObjects_SubscribeForNotifications_DoesntReceiveModifications([Values(true, false)] bool shallow) { + var kpCollection = shallow ? KeyPathsCollection.Shallow : KeyPathsCollection.Full; + var testObject = _realm.Write(() => _realm.Add(new CollectionsObject())); var changesets = new List(); @@ -1345,7 +1349,7 @@ public void ListOfObjects_SubscribeForNotifications_DoesntReceiveModifications([ { changesets.Add(changes); } - }, shallow); + }, kpCollection); _realm.Write(() => { @@ -1395,6 +1399,8 @@ public void ListOfObjects_SubscribeForNotifications_DoesntReceiveModifications([ [Test] public void ListOfPrimitives_SubscribeForNotifications_ShallowHasNoEffect([Values(true, false)] bool shallow) { + var kpCollection = shallow ? KeyPathsCollection.Shallow : KeyPathsCollection.Full; + var testObject = _realm.Write(() => _realm.Add(new CollectionsObject())); var changesets = new List(); @@ -1406,7 +1412,7 @@ public void ListOfPrimitives_SubscribeForNotifications_ShallowHasNoEffect([Value { changesets.Add(changes); } - }, shallow); + }, kpCollection); _realm.Write(() => { @@ -1449,6 +1455,8 @@ public void ListOfPrimitives_SubscribeForNotifications_ShallowHasNoEffect([Value [Test] public void SetOfObjects_SubscribeForNotifications_DoesntReceiveModifications([Values(true, false)] bool shallow) { + var kpCollection = shallow ? KeyPathsCollection.Shallow : KeyPathsCollection.Full; + var testObject = _realm.Write(() => _realm.Add(new CollectionsObject())); var changesets = new List(); @@ -1460,7 +1468,7 @@ public void SetOfObjects_SubscribeForNotifications_DoesntReceiveModifications([V { changesets.Add(changes); } - }, shallow); + }, kpCollection); _realm.Write(() => { @@ -1495,6 +1503,8 @@ public void SetOfObjects_SubscribeForNotifications_DoesntReceiveModifications([V [Test] public void SetOfPrimitives_SubscribeForNotifications_ShallowHasNoEffect([Values(true, false)] bool shallow) { + var kpCollection = shallow ? KeyPathsCollection.Shallow : KeyPathsCollection.Full; + var testObject = _realm.Write(() => _realm.Add(new CollectionsObject())); var changesets = new List(); @@ -1506,7 +1516,7 @@ public void SetOfPrimitives_SubscribeForNotifications_ShallowHasNoEffect([Values { changesets.Add(changes); } - }, shallow); + }, kpCollection); _realm.Write(() => { @@ -1534,6 +1544,8 @@ public void SetOfPrimitives_SubscribeForNotifications_ShallowHasNoEffect([Values [Test] public void DictionaryOfObjects_SubscribeForNotifications_DoesntReceiveModifications([Values(true, false)] bool shallow) { + var kpCollection = shallow ? KeyPathsCollection.Shallow : KeyPathsCollection.Full; + var testObject = _realm.Write(() => _realm.Add(new CollectionsObject())); var changesets = new List(); @@ -1545,7 +1557,7 @@ public void DictionaryOfObjects_SubscribeForNotifications_DoesntReceiveModificat { changesets.Add(changes); } - }, shallow); + }, kpCollection); _realm.Write(() => { @@ -1587,6 +1599,8 @@ public void DictionaryOfObjects_SubscribeForNotifications_DoesntReceiveModificat [Test] public void DictionaryOfPrimitives_SubscribeForNotifications_ShallowHasNoEffect([Values(true, false)] bool shallow) { + var kpCollection = shallow ? KeyPathsCollection.Shallow : KeyPathsCollection.Full; + var testObject = _realm.Write(() => _realm.Add(new CollectionsObject())); var changesets = new List(); @@ -1598,7 +1612,7 @@ public void DictionaryOfPrimitives_SubscribeForNotifications_ShallowHasNoEffect( { changesets.Add(changes); } - }, shallow); + }, kpCollection); _realm.Write(() => { @@ -1630,45 +1644,1059 @@ public void DictionaryOfPrimitives_SubscribeForNotifications_ShallowHasNoEffect( VerifyNotifications(changesets, expectedDeleted: new[] { 0 }, expectedCleared: false); } - private void VerifyNotifications(List notifications, - int[]? expectedInserted = null, - int[]? expectedModified = null, - int[]? expectedDeleted = null, - Move[]? expectedMoves = null, - bool expectedCleared = false, - bool expectedNotifications = true) + #region Keypath filtering + + [Test] + public void KeyPath_ImplicitOperator_CorrectlyConvertsFromString() { - _realm.Refresh(); - Assert.That(notifications.Count, Is.EqualTo(expectedNotifications ? 1 : 0)); - if (expectedNotifications) + KeyPath keyPath = "test"; + + Assert.That(keyPath.Path, Is.EqualTo("test")); + } + + [Test] + public void KeyPathsCollection_CanBeBuiltInDifferentWays() + { + var kpString1 = "test1"; + var kpString2 = "test2"; + + KeyPath kp1 = "test1"; + KeyPath kp2 = "test2"; + + var expected = new List { kpString1, kpString2 }; + + KeyPathsCollection kpc; + + kpc = new List { kpString1, kpString2 }; + AssertKeyPathsCollectionCorrectness(kpc, expected); + + kpc = new List { kpString1, kp2 }; + AssertKeyPathsCollectionCorrectness(kpc, expected); + + kpc = new List { kp1, kp2 }; + AssertKeyPathsCollectionCorrectness(kpc, expected); + + kpc = new string[] { kpString1, kpString2 }; + AssertKeyPathsCollectionCorrectness(kpc, expected); + + kpc = new KeyPath[] { kpString1, kpString2 }; + AssertKeyPathsCollectionCorrectness(kpc, expected); + + kpc = new KeyPath[] { kp1, kp2 }; + AssertKeyPathsCollectionCorrectness(kpc, expected); + + kpc = KeyPathsCollection.Of(kpString1, kpString2); + AssertKeyPathsCollectionCorrectness(kpc, expected); + + kpc = KeyPathsCollection.Of(kp1, kp2); + AssertKeyPathsCollectionCorrectness(kpc, expected); + + kpc = KeyPathsCollection.Shallow; + Assert.That(kpc.Type, Is.EqualTo(KeyPathsCollectionType.Shallow)); + Assert.That(kpc.GetStrings(), Is.Empty); + + kpc = KeyPathsCollection.Of(); + Assert.That(kpc.Type, Is.EqualTo(KeyPathsCollectionType.Shallow)); + Assert.That(kpc.GetStrings(), Is.Empty); + + kpc = KeyPathsCollection.Full; + Assert.That(kpc.Type, Is.EqualTo(KeyPathsCollectionType.Full)); + Assert.That(kpc.GetStrings(), Is.Empty); + + void AssertKeyPathsCollectionCorrectness(KeyPathsCollection k, IEnumerable expected) { - Assert.That(notifications[0].InsertedIndices, expectedInserted == null ? Is.Empty : Is.EquivalentTo(expectedInserted)); - Assert.That(notifications[0].ModifiedIndices, expectedModified == null ? Is.Empty : Is.EquivalentTo(expectedModified)); - Assert.That(notifications[0].DeletedIndices, expectedDeleted == null ? Is.Empty : Is.EquivalentTo(expectedDeleted)); - Assert.That(notifications[0].Moves, expectedMoves == null ? Is.Empty : Is.EquivalentTo(expectedMoves)); - Assert.That(notifications[0].IsCleared, Is.EqualTo(expectedCleared)); + Assert.That(k.Type, Is.EqualTo(KeyPathsCollectionType.Explicit)); + Assert.That(k.GetStrings(), Is.EqualTo(expected)); } + } - notifications.Clear(); + [Test] + public void SubscribeWithKeypaths_AnyKeypath_RaisesNotificationsForResults() + { + var query = _realm.All(); + var changesets = new List(); + + void OnNotification(IRealmCollection s, ChangeSet? changes) + { + if (changes != null) + { + changesets.Add(changes); + } + } + + using (query.SubscribeForNotifications(OnNotification, KeyPathsCollection.Of("StringProperty"))) + { + var tno = new TestNotificationObject(); + + _realm.Write(() => _realm.Add(tno)); + VerifyNotifications(changesets, expectedInserted: new[] { 0 }); + + _realm.Write(() => tno.StringProperty = "NewString"); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + + _realm.Write(() => _realm.Remove(tno)); + VerifyNotifications(changesets, expectedDeleted: new[] { 0 }); + } } - } - public partial class OrderedContainer : TestRealmObject - { - public IList Items { get; } = null!; + [Test] + public void SubscribeWithKeypaths_ShallowKeypath_RaisesOnlyCollectionNotifications() + { + var query = _realm.All(); + var changesets = new List(); - public IDictionary ItemsDictionary { get; } = null!; - } + void OnNotification(IRealmCollection s, ChangeSet? changes) + { + if (changes != null) + { + changesets.Add(changes); + } + } - public partial class OrderedObject : TestRealmObject - { - public int Order { get; set; } + using (query.SubscribeForNotifications(OnNotification, KeyPathsCollection.Shallow)) + { + var tno = new TestNotificationObject(); - public bool IsPartOfResults { get; set; } + _realm.Write(() => _realm.Add(tno)); + VerifyNotifications(changesets, expectedInserted: new[] { 0 }); - public override string ToString() + _realm.Write(() => tno.StringProperty = "NewString"); + VerifyNotifications(changesets, expectedNotifications: false); + + _realm.Write(() => _realm.Remove(tno)); + VerifyNotifications(changesets, expectedDeleted: new[] { 0 }); + } + } + + [Test] + public void SubscribeWithKeypaths_FullKeyPath_SameAsFourLevelsDepth() { - return $"[OrderedObject: Order={Order}]"; + var query = _realm.All(); + var changesets = new List(); + + var dp5 = new DeepObject5(); + var dp4 = new DeepObject4() { RecursiveObject = dp5 }; + var dp3 = new DeepObject3() { RecursiveObject = dp4 }; + var dp2 = new DeepObject2() { RecursiveObject = dp3 }; + var dp1 = new DeepObject1() { RecursiveObject = dp2 }; + + void OnNotification(IRealmCollection s, ChangeSet? changes) + { + if (changes != null) + { + changesets.Add(changes); + } + } + + _realm.Write(() => _realm.Add(dp1)); + + using (query.SubscribeForNotifications(OnNotification, KeyPathsCollection.Of("*.*.*.*"))) + { + VerifyFourLevelsDepth(); + } + + using (query.SubscribeForNotifications(OnNotification, KeyPathsCollection.Full)) + { + VerifyFourLevelsDepth(); + } + + void VerifyFourLevelsDepth() + { + _realm.Write(() => dp2.StringValue = "NewString"); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + + _realm.Write(() => dp4.StringValue = "NewString"); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + + _realm.Write(() => dp5.StringValue = "New String"); + VerifyNotifications(changesets, expectedNotifications: false); + } + } + + [Test] + public void SubscribeWithKeypaths_TopLevelProperties_WorksWithScalar() + { + var query = _realm.All(); + var changesets = new List(); + + void OnNotification(IRealmCollection s, ChangeSet? changes) + { + if (changes != null) + { + changesets.Add(changes); + } + } + + var tno = new TestNotificationObject(); + _realm.Write(() => _realm.Add(tno)); + + using (query.SubscribeForNotifications(OnNotification, KeyPathsCollection.Of("StringProperty"))) + { + // Changing property in keypath + _realm.Write(() => tno.StringProperty = "NewString"); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + + // Changing properties not in keypath + _realm.Write(() => tno.IntProperty = 23); + _realm.Write(() => tno.ListDifferentType.Add(new Person())); + _realm.Write(() => tno.DictionaryDifferentType.Add("key", new Person())); + _realm.Write(() => tno.SetDifferentType.Add(new Person())); + VerifyNotifications(changesets, expectedNotifications: false); + + // Changing key path property again + _realm.Write(() => tno.StringProperty = "AgainNewString"); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + } } + + [Test] + public void SubscribeWithKeypaths_TopLevelProperties_WorksWithCollection() + { + var query = _realm.All(); + var changesets = new List(); + + void OnNotification(IRealmCollection s, ChangeSet? changes) + { + if (changes != null) + { + changesets.Add(changes); + } + } + + var tno = new TestNotificationObject(); + _realm.Write(() => _realm.Add(tno)); + + using (query.SubscribeForNotifications(OnNotification, KeyPathsCollection.Of("ListDifferentType"))) + { + // Changing collection in keypath + _realm.Write(() => tno.ListDifferentType.Add(new Person())); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + + // Changing properties not in keypath + _realm.Write(() => tno.StringProperty = "23"); + _realm.Write(() => tno.DictionaryDifferentType.Add("key", new Person())); + _realm.Write(() => tno.SetDifferentType.Add(new Person())); + VerifyNotifications(changesets, expectedNotifications: false); + + // Changing elements in the collection does not raise notification + _realm.Write(() => tno.ListDifferentType[0].FirstName = "FirstName"); + VerifyNotifications(changesets, expectedNotifications: false); + + // Changing collection + _realm.Write(() => tno.ListDifferentType.RemoveAt(0)); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + } + } + + [Test] + public void SubscribeWithKeypaths_NestedProperties_WorksWithScalar() + { + var query = _realm.All(); + var changesets = new List(); + + void OnNotification(IRealmCollection s, ChangeSet? changes) + { + if (changes != null) + { + changesets.Add(changes); + } + } + + var tno = new TestNotificationObject(); + _realm.Write(() => _realm.Add(tno)); + + using (query.SubscribeForNotifications(OnNotification, KeyPathsCollection.Of("LinkDifferentType.FirstName"))) + { + // Changing top level property on the keypath + _realm.Write(() => tno.LinkDifferentType = new Person()); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + + // Changing top level property not on the keypath + _realm.Write(() => tno.StringProperty = "23"); + _realm.Write(() => tno.DictionaryDifferentType.Add("key", new Person())); + _realm.Write(() => tno.SetDifferentType.Add(new Person())); + VerifyNotifications(changesets, expectedNotifications: false); + + // Changing keypath property + _realm.Write(() => tno.LinkDifferentType!.FirstName = "NewName"); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + + // Changing property not on keypath + _realm.Write(() => tno.LinkDifferentType!.LastName = "NewName"); + VerifyNotifications(changesets, expectedNotifications: false); + } + } + + [Test] + public void SubscribeWithKeypaths_NestedProperties_WorksWithCollections() + { + var query = _realm.All(); + var changesets = new List(); + + void OnNotification(IRealmCollection s, ChangeSet? changes) + { + if (changes != null) + { + changesets.Add(changes); + } + } + + var tno = new TestNotificationObject(); + _realm.Write(() => _realm.Add(tno)); + + using (query.SubscribeForNotifications(OnNotification, KeyPathsCollection.Of("ListDifferentType.FirstName"))) + { + // Changing top level property on the keypath + _realm.Write(() => tno.ListDifferentType.Add(new Person())); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + + // Changing keypath property + _realm.Write(() => tno.ListDifferentType[0].FirstName = "NewName"); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + + // Changing top level property not on the keypath + _realm.Write(() => tno.ListDifferentType[0].LastName = "NewName"); + _realm.Write(() => tno.StringProperty = "23"); + _realm.Write(() => tno.DictionaryDifferentType.Add("key", new Person())); + VerifyNotifications(changesets, expectedNotifications: false); + } + } + + [Test] + public void SubscribeWithKeypaths_WildCard_WorksWithTopLevel() + { + var query = _realm.All(); + var changesets = new List(); + + void OnNotification(IRealmCollection s, ChangeSet? changes) + { + if (changes != null) + { + changesets.Add(changes); + } + } + + var tno = new TestNotificationObject(); + _realm.Write(() => _realm.Add(tno)); + + using (query.SubscribeForNotifications(OnNotification, KeyPathsCollection.Of("*"))) + { + _realm.Write(() => tno.StringProperty = "NewString"); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + + _realm.Write(() => tno.IntProperty = 23); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + + // Modifying links / changing the elements of collections should raise a notification + _realm.Write(() => tno.LinkDifferentType = new Person()); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + + _realm.Write(() => tno.ListDifferentType.Add(new Person())); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + + _realm.Write(() => tno.DictionaryDifferentType.Add("key", new Person())); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + + // Modifying the collection elements/links should not raise a notification + _realm.Write(() => tno.LinkDifferentType!.FirstName = "NewName"); + _realm.Write(() => tno.ListDifferentType[0].LastName = "NewName"); + _realm.Write(() => tno.DictionaryDifferentType["key"]!.LastName = "NewName"); + VerifyNotifications(changesets, expectedNotifications: false); + } + } + + [Test] + public void SubscribeWithKeypaths_WildCard_WorksWithMultipleLevels() + { + var query = _realm.All(); + var changesets = new List(); + + void OnNotification(IRealmCollection s, ChangeSet? changes) + { + if (changes != null) + { + changesets.Add(changes); + } + } + + var tno = new TestNotificationObject() + { + LinkAnotherType = new Owner() + { + TopDog = new Dog(), + } + }; + _realm.Write(() => _realm.Add(tno)); + + using (query.SubscribeForNotifications(OnNotification, KeyPathsCollection.Of("*.*"))) + { + _realm.Write(() => tno.StringProperty = "NewString"); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + + _realm.Write(() => tno.IntProperty = 23); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + + // Modifying collection/links should raise a notification + _realm.Write(() => tno.LinkDifferentType = new Person()); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + + _realm.Write(() => tno.ListDifferentType.Add(new Person())); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + + _realm.Write(() => tno.DictionaryDifferentType.Add("key", new Person())); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + + _realm.Write(() => tno.LinkDifferentType!.FirstName = "NewName"); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + + _realm.Write(() => tno.ListDifferentType[0].LastName = "NewName"); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + + _realm.Write(() => tno.DictionaryDifferentType["key"]!.LastName = "NewName"); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + + _realm.Write(() => tno.LinkAnotherType!.ListOfDogs.Add(new Dog())); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + + // Modifying something 3 levels deep should not raise a notification + _realm.Write(() => tno.LinkAnotherType!.TopDog.Name = "Test"); + _realm.Write(() => tno.LinkAnotherType!.ListOfDogs[0].Name = "Test"); + VerifyNotifications(changesets, expectedNotifications: false); + } + } + + [Test] + public void SubscribeWithKeypaths_WildCard_WorksAfterLinkProperty() + { + var query = _realm.All(); + var changesets = new List(); + + void OnNotification(IRealmCollection s, ChangeSet? changes) + { + if (changes != null) + { + changesets.Add(changes); + } + } + + var tno = new TestNotificationObject(); + _realm.Write(() => _realm.Add(tno)); + + using (query.SubscribeForNotifications(OnNotification, KeyPathsCollection.Of("LinkDifferentType.*"))) + { + _realm.Write(() => tno.LinkDifferentType = new Person()); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + + _realm.Write(() => tno.LinkDifferentType!.FirstName = "NewName"); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + + _realm.Write(() => tno.LinkDifferentType!.Friends.Add(new Person())); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + + // Out of keypath + _realm.Write(() => tno.StringProperty = "NewString"); + _realm.Write(() => tno.ListDifferentType.Add(new Person())); + VerifyNotifications(changesets, expectedNotifications: false); + + // Too deep + _realm.Write(() => tno.LinkDifferentType!.Friends[0].FirstName = "Luis"); + VerifyNotifications(changesets, expectedNotifications: false); + } + } + + [Test] + public void SubscribeWithKeypaths_WildCard_WorksAfterCollectionProperty() + { + var query = _realm.All(); + var changesets = new List(); + + void OnNotification(IRealmCollection s, ChangeSet? changes) + { + if (changes != null) + { + changesets.Add(changes); + } + } + + var tno = new TestNotificationObject(); + _realm.Write(() => _realm.Add(tno)); + + using (query.SubscribeForNotifications(OnNotification, KeyPathsCollection.Of("ListDifferentType.*"))) + { + _realm.Write(() => tno.ListDifferentType.Add(new Person())); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + + _realm.Write(() => tno.ListDifferentType[0].FirstName = "Luis"); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + + _realm.Write(() => tno.ListDifferentType[0].Friends.Add(new Person())); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + + // Out of keypath + _realm.Write(() => tno.StringProperty = "NewString"); + _realm.Write(() => tno.LinkDifferentType = new Person()); + VerifyNotifications(changesets, expectedNotifications: false); + + // Too deep + _realm.Write(() => tno.ListDifferentType[0]!.Friends[0].FirstName = "Luis"); + VerifyNotifications(changesets, expectedNotifications: false); + } + } + + [Test] + public void SubscribeWithKeypaths_WildCard_WorksWithPropertyAfterward() + { + var query = _realm.All(); + var changesets = new List(); + + void OnNotification(IRealmCollection s, ChangeSet? changes) + { + if (changes != null) + { + changesets.Add(changes); + } + } + + var tno = new TestNotificationObject(); + _realm.Write(() => _realm.Add(tno)); + + using (query.SubscribeForNotifications(OnNotification, KeyPathsCollection.Of("*.FirstName"))) + { + _realm.Write(() => tno.LinkDifferentType = new Person()); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + + _realm.Write(() => tno.LinkDifferentType!.FirstName = "NewName"); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + + _realm.Write(() => tno.ListDifferentType.Add(new Person())); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + + _realm.Write(() => tno.ListDifferentType[0]!.FirstName = "NewName"); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + + // Out of keypath + _realm.Write(() => tno.LinkDifferentType!.LastName = "Test"); + _realm.Write(() => tno.ListDifferentType[0]!.LastName = "Test"); + VerifyNotifications(changesets, expectedNotifications: false); + } + } + + [Test] + public void SubscribeWithKeypaths_WildCard_CanGetDeeperThanFourLevels() + { + var query = _realm.All(); + var changesets = new List(); + + var dp5 = new DeepObject5(); + var dp4 = new DeepObject4() { RecursiveObject = dp5 }; + var dp3 = new DeepObject3() { RecursiveObject = dp4 }; + var dp2 = new DeepObject2() { RecursiveObject = dp3 }; + var dp1 = new DeepObject1() { RecursiveObject = dp2 }; + + void OnNotification(IRealmCollection s, ChangeSet? changes) + { + if (changes != null) + { + changesets.Add(changes); + } + } + + _realm.Write(() => _realm.Add(dp1)); + + using (query.SubscribeForNotifications(OnNotification, KeyPathsCollection.Of("*.*.*.*.*"))) + { + _realm.Write(() => dp2.StringValue = "NewString"); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + + _realm.Write(() => dp4.StringValue = "NewString"); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + + _realm.Write(() => dp5.StringValue = "New String"); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + } + } + + [Test] + public void SubscribeWithKeypaths_Backlinks() + { + var query = _realm.All(); + var changesets = new List(); + + void OnNotification(IRealmCollection s, ChangeSet? changes) + { + if (changes != null) + { + changesets.Add(changes); + } + } + + var dog = new Dog(); + _realm.Write(() => _realm.Add(dog)); + + using (query.SubscribeForNotifications(OnNotification, KeyPathsCollection.Of("Owners"))) + { + var owner = new Owner { Name = "Mario", ListOfDogs = { dog } }; + _realm.Write(() => _realm.Add(owner)); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + + _realm.Write(() => owner.Name = "Luigi"); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + + // Not in keypath + _realm.Write(() => dog.Name = "Test"); + VerifyNotifications(changesets, expectedNotifications: false); + } + } + + [Test] + public void SubscribeWithKeypaths_MultipleKeypaths() + { + var query = _realm.All(); + var changesets = new List(); + + void OnNotification(IRealmCollection s, ChangeSet? changes) + { + if (changes != null) + { + changesets.Add(changes); + } + } + + var tno = new TestNotificationObject(); + _realm.Write(() => _realm.Add(tno)); + + using (query.SubscribeForNotifications(OnNotification, KeyPathsCollection.Of("StringProperty", "LinkDifferentType"))) + { + _realm.Write(() => tno.StringProperty = "NewString"); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + + _realm.Write(() => tno.LinkDifferentType = new Person()); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + + // Not in keypath + _realm.Write(() => tno.IntProperty = 23); + _realm.Write(() => tno.LinkDifferentType!.FirstName = "Test"); + _realm.Write(() => tno.ListDifferentType.Add(new Person())); + VerifyNotifications(changesets, expectedNotifications: false); + } + } + + [Test] + public void SubscribeWithKeypaths_DisposingToken_CancelNotifications() + { + var query = _realm.All(); + var changesets = new List(); + + void OnNotification(IRealmCollection s, ChangeSet? changes) + { + if (changes != null) + { + changesets.Add(changes); + } + } + + var tno = new TestNotificationObject(); + _realm.Write(() => _realm.Add(tno)); + + var token = query.SubscribeForNotifications(OnNotification, KeyPathsCollection.Of("StringProperty")); + + _realm.Write(() => tno.StringProperty = "NewString"); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + + token.Dispose(); + + _realm.Write(() => tno.StringProperty = "NewValue"); + VerifyNotifications(changesets, expectedNotifications: false); + } + + [Test] + public void SubscribeWithKeypaths_MappedProperty_UsesOriginalName() + { + var query = _realm.All(); + var changesets = new List(); + + void OnNotification(IRealmCollection s, ChangeSet? changes) + { + if (changes != null) + { + changesets.Add(changes); + } + } + + var person = new Person(); + _realm.Write(() => _realm.Add(person)); + + using (query.SubscribeForNotifications(OnNotification, KeyPathsCollection.Of("Email_"))) + { + // Changing property in keypath + _realm.Write(() => person.Email = "email@test.com"); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + } + } + + [Test] + public void SubscribeWithKeypaths_MappedClass_WorksCorrectly() + { + var query = _realm.All(); + var changesets = new List(); + + void OnNotification(IRealmCollection s, ChangeSet? changes) + { + if (changes != null) + { + changesets.Add(changes); + } + } + + var rto = new RemappedTypeObject(); + _realm.Write(() => _realm.Add(rto)); + + using (query.SubscribeForNotifications(OnNotification, KeyPathsCollection.Of("StringValue"))) + { + // Changing property in keypath + _realm.Write(() => rto.StringValue = "email@test.com"); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + } + } + + [Test] + public void SubscribeWithKeypaths_WithRepeatedKeypath_IgnoresRepeated() + { + var query = _realm.All(); + var changesets = new List(); + + void OnNotification(IRealmCollection s, ChangeSet? changes) + { + if (changes != null) + { + changesets.Add(changes); + } + } + + var person = new Person(); + _realm.Write(() => _realm.Add(person)); + + using (query.SubscribeForNotifications(OnNotification, KeyPathsCollection.Of("FirstName", "FirstName"))) + { + // Changing property in keypath + _realm.Write(() => person.FirstName = "NewFirstName"); + VerifyNotifications(changesets, expectedModified: new[] { 0 }); + + // Changing property not in keypath + _realm.Write(() => person.LastName = "NewLastName"); + _realm.Write(() => person.Salary = 240); + VerifyNotifications(changesets, expectedNotifications: false); + } + } + + [Test, Ignore("Failing because of https://github.com/realm/realm-core/issues/7269")] + public void SubscribeWithKeypaths_WildcardOnScalarProperty_Throws() + { + var query = _realm.All(); + + void OnNotification(IRealmCollection s, ChangeSet? c) + { + } + + var exMessage = "Property 'FirstName' in KeyPath 'FirstName.*' " + + "is not a collection of objects or an object reference, so it cannot be used as an intermediate keypath element."; + + Assert.That(() => query.SubscribeForNotifications(OnNotification, KeyPathsCollection.Of("FirstName.*")), + Throws.Exception.TypeOf().With.Message.EqualTo(exMessage)); + } + + [Test] + public void SubscribeWithKeypaths_WithUnknownProperty_Throws() + { + var query = _realm.All(); + + void OnNotification(IRealmCollection s, ChangeSet? c) + { + } + + var exMessage = "not a valid property in Person"; + + // Top level property + Assert.That(() => query.SubscribeForNotifications(OnNotification, KeyPathsCollection.Of("unknownProp")), + Throws.Exception.TypeOf().With.Message.Contain(exMessage)); + + // Nested property + Assert.That(() => query.SubscribeForNotifications(OnNotification, KeyPathsCollection.Of("Friends.unknownProp")), + Throws.Exception.TypeOf().With.Message.Contain(exMessage)); + } + + [Test] + public void SubscribeWithKeypaths_WithEmptyOrWhiteSpaceKeypaths_Throws() + { + var query = _realm.All(); + + void OnNotification(IRealmCollection s, ChangeSet? c) + { + } + + var exMessage = "A key path cannot be null, empty, or consisting only of white spaces"; + + Assert.That(() => query.SubscribeForNotifications(OnNotification, KeyPathsCollection.Of(string.Empty)), + Throws.Exception.TypeOf().With.Message.EqualTo(exMessage)); + Assert.That(() => query.SubscribeForNotifications(OnNotification, KeyPathsCollection.Of(" ")), + Throws.Exception.TypeOf().With.Message.EqualTo(exMessage)); + Assert.That(() => query.SubscribeForNotifications(OnNotification, KeyPathsCollection.Of("test", null!)), + Throws.Exception.TypeOf().With.Message.EqualTo(exMessage)); + } + + [Test] + public void SubscribeWithKeypaths_WithNonRealmObjectType_Throws() + { + var collectionObject = _realm.Write(() => _realm.Add(new CollectionsObject + { + Int16List = { 1, 2 } + })); + + var list = collectionObject.Int16List; + + void OnNotification(IRealmCollection s, ChangeSet? c) + { + } + + var exMessage = "Key paths can be used only with collections of Realm objects"; + + Assert.That(() => list.SubscribeForNotifications(OnNotification, KeyPathsCollection.Of("test")), + Throws.Exception.TypeOf().With.Message.EqualTo(exMessage)); + } + + [Test] + public void SubscribeWithKeypaths_OnCollection_List() + { + var obj1 = _realm.Write(() => + { + return _realm.Add(new CollectionsObject()); + }); + + ChangeSet? changes = null!; + void OnNotification(IRealmCollection s, ChangeSet? c) => changes = c; + + using (obj1.ObjectList.SubscribeForNotifications(OnNotification, KeyPathsCollection.Of("Int"))) + { + var ipo = new IntPropertyObject(); + + _realm.Write(() => obj1.ObjectList.Add(ipo)); + _realm.Refresh(); + Assert.That(changes?.InsertedIndices, Is.EqualTo(new int[] { 0 })); + changes = null; + + _realm.Write(() => ipo.Int = 23); + _realm.Refresh(); + Assert.That(changes?.ModifiedIndices, Is.EqualTo(new int[] { 0 })); + changes = null; + + _realm.Write(() => ipo.GuidProperty = Guid.NewGuid()); + _realm.Refresh(); + Assert.That(changes, Is.Null); + } + } + + [Test] + public void SubscribeWithKeypaths_OnCollection_ListRemapped() + { + var obj1 = _realm.Write(() => + { + return _realm.Add(new TestNotificationObject()); + }); + + ChangeSet? changes = null!; + void OnNotification(IRealmCollection s, ChangeSet? c) => changes = c; + + using (obj1.ListRemappedType.SubscribeForNotifications(OnNotification, KeyPathsCollection.Of("StringValue"))) + { + var ipo = new RemappedTypeObject(); + + _realm.Write(() => obj1.ListRemappedType.Add(ipo)); + _realm.Refresh(); + Assert.That(changes?.InsertedIndices, Is.EqualTo(new int[] { 0 })); + changes = null; + } + } + + [Test] + public void SubscribeWithKeypaths_OnCollection_Set() + { + var obj1 = _realm.Write(() => + { + return _realm.Add(new CollectionsObject()); + }); + + ChangeSet? changes = null!; + void OnNotification(IRealmCollection s, ChangeSet? c) => changes = c; + + using (obj1.ObjectSet.SubscribeForNotifications(OnNotification, KeyPathsCollection.Of("Int"))) + { + var ipo = new IntPropertyObject(); + + _realm.Write(() => obj1.ObjectSet.Add(ipo)); + _realm.Refresh(); + Assert.That(changes?.InsertedIndices, Is.EqualTo(new int[] { 0 })); + changes = null; + + _realm.Write(() => ipo.Int = 23); + _realm.Refresh(); + Assert.That(changes?.ModifiedIndices, Is.EqualTo(new int[] { 0 })); + changes = null; + + _realm.Write(() => ipo.GuidProperty = Guid.NewGuid()); + _realm.Refresh(); + Assert.That(changes, Is.Null); + } + } + + [Test] + public void SubscribeWithKeypaths_OnCollection_SetRemapped() + { + var obj1 = _realm.Write(() => + { + return _realm.Add(new TestNotificationObject()); + }); + + ChangeSet? changes = null!; + void OnNotification(IRealmCollection s, ChangeSet? c) => changes = c; + + using (obj1.SetRemappedType.SubscribeForNotifications(OnNotification, KeyPathsCollection.Of("StringValue"))) + { + var ipo = new RemappedTypeObject(); + + _realm.Write(() => obj1.SetRemappedType.Add(ipo)); + _realm.Refresh(); + Assert.That(changes?.InsertedIndices, Is.EqualTo(new int[] { 0 })); + changes = null; + } + } + + [Test] + public void SubscribeWithKeypaths_OnCollection_Dictionary() + { + var obj1 = _realm.Write(() => + { + return _realm.Add(new CollectionsObject()); + }); + + ChangeSet? changes = null!; + void OnNotification(IRealmCollection> s, ChangeSet? c) => changes = c; + + using (obj1.ObjectDict.SubscribeForNotifications(OnNotification, KeyPathsCollection.Of("Int"))) + { + var ipo = new IntPropertyObject(); + + _realm.Write(() => obj1.ObjectDict.Add("main", ipo)); + _realm.Refresh(); + Assert.That(changes?.InsertedIndices, Is.EqualTo(new int[] { 0 })); + changes = null; + + _realm.Write(() => ipo.Int = 23); + _realm.Refresh(); + Assert.That(changes?.ModifiedIndices, Is.EqualTo(new int[] { 0 })); + changes = null; + + _realm.Write(() => ipo.GuidProperty = Guid.NewGuid()); + _realm.Refresh(); + Assert.That(changes, Is.Null); + } + } + + [Test] + public void SubscribeWithKeypaths_OnCollection_DictionaryRemapped() + { + var obj1 = _realm.Write(() => + { + return _realm.Add(new TestNotificationObject()); + }); + + ChangeSet? changes = null!; + void OnNotification(IRealmCollection> s, ChangeSet? c) => changes = c; + + using (obj1.DictionaryRemappedType.SubscribeForNotifications(OnNotification, KeyPathsCollection.Of("StringValue"))) + { + var ipo = new RemappedTypeObject(); + + _realm.Write(() => obj1.DictionaryRemappedType.Add("test", ipo)); + _realm.Refresh(); + Assert.That(changes?.InsertedIndices, Is.EqualTo(new int[] { 0 })); + changes = null; + } + } + + #endregion + + private void VerifyNotifications(List notifications, + int[]? expectedInserted = null, + int[]? expectedModified = null, + int[]? expectedDeleted = null, + Move[]? expectedMoves = null, + bool expectedCleared = false, + bool expectedNotifications = true) + { + _realm.Refresh(); + Assert.That(notifications.Count, Is.EqualTo(expectedNotifications ? 1 : 0)); + if (expectedNotifications) + { + Assert.That(notifications[0].InsertedIndices, expectedInserted == null ? Is.Empty : Is.EquivalentTo(expectedInserted)); + Assert.That(notifications[0].ModifiedIndices, expectedModified == null ? Is.Empty : Is.EquivalentTo(expectedModified)); + Assert.That(notifications[0].DeletedIndices, expectedDeleted == null ? Is.Empty : Is.EquivalentTo(expectedDeleted)); + Assert.That(notifications[0].Moves, expectedMoves == null ? Is.Empty : Is.EquivalentTo(expectedMoves)); + Assert.That(notifications[0].IsCleared, Is.EqualTo(expectedCleared)); + } + + notifications.Clear(); + } + } + + public partial class OrderedContainer : TestRealmObject + { + public IList Items { get; } = null!; + + public IDictionary ItemsDictionary { get; } = null!; + } + + public partial class OrderedObject : TestRealmObject + { + public int Order { get; set; } + + public bool IsPartOfResults { get; set; } + + public override string ToString() + { + return $"[OrderedObject: Order={Order}]"; + } + } + + public partial class DeepObject1 : TestRealmObject + { + public string? StringValue { get; set; } + + public DeepObject2? RecursiveObject { get; set; } + } + + public partial class DeepObject2 : TestRealmObject + { + public string? StringValue { get; set; } + + public DeepObject3? RecursiveObject { get; set; } + } + + public partial class DeepObject3 : TestRealmObject + { + public string? StringValue { get; set; } + + public DeepObject4? RecursiveObject { get; set; } + } + + public partial class DeepObject4 : TestRealmObject + { + public string? StringValue { get; set; } + + public DeepObject5? RecursiveObject { get; set; } + } + + public partial class DeepObject5 : TestRealmObject + { + public string? StringValue { get; set; } } } diff --git a/Tests/Realm.Tests/Database/TestNotificationObject.cs b/Tests/Realm.Tests/Database/TestNotificationObject.cs index 2de86c543c..27db9ab08b 100644 --- a/Tests/Realm.Tests/Database/TestNotificationObject.cs +++ b/Tests/Realm.Tests/Database/TestNotificationObject.cs @@ -30,6 +30,8 @@ public partial class TestNotificationObject : TestRealmObject { public string? StringProperty { get; set; } + public int IntProperty { get; set; } + public IList ListSameType { get; } = null!; public ISet SetSameType { get; } = null!; @@ -44,8 +46,16 @@ public partial class TestNotificationObject : TestRealmObject public IDictionary DictionaryDifferentType { get; } = null!; + public IList ListRemappedType { get; } = null!; + + public ISet SetRemappedType { get; } = null!; + + public IDictionary DictionaryRemappedType { get; } = null!; + public Person? LinkDifferentType { get; set; } + public Owner? LinkAnotherType { get; set; } + [Backlink(nameof(LinkSameType))] public IQueryable Backlink { get; } = null!; } diff --git a/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/DeepObject1_generated.cs b/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/DeepObject1_generated.cs new file mode 100644 index 0000000000..ce4427b07e --- /dev/null +++ b/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/DeepObject1_generated.cs @@ -0,0 +1,361 @@ +// +#nullable enable + +using NUnit.Framework; +using Realms; +using Realms.Logging; +using Realms.Schema; +using Realms.Tests.Database; +using Realms.Weaving; +using static Realms.ChangeSet; +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; +using System.Threading.Tasks; +using System.Xml.Serialization; +using TestRealmObject = Realms.IRealmObject; + +namespace Realms.Tests.Database +{ + [Generated] + [Woven(typeof(DeepObject1ObjectHelper)), Realms.Preserve(AllMembers = true)] + public partial class DeepObject1 : IRealmObject, INotifyPropertyChanged, IReflectableType + { + /// + /// Defines the schema for the class. + /// + public static Realms.Schema.ObjectSchema RealmSchema = new Realms.Schema.ObjectSchema.Builder("DeepObject1", ObjectSchema.ObjectType.RealmObject) + { + Realms.Schema.Property.Primitive("StringValue", Realms.RealmValueType.String, isPrimaryKey: false, indexType: IndexType.None, isNullable: true, managedName: "StringValue"), + Realms.Schema.Property.Object("RecursiveObject", "DeepObject2", managedName: "RecursiveObject"), + }.Build(); + + #region IRealmObject implementation + + private IDeepObject1Accessor? _accessor; + + Realms.IRealmAccessor Realms.IRealmObjectBase.Accessor => Accessor; + + private IDeepObject1Accessor Accessor => _accessor ??= new DeepObject1UnmanagedAccessor(typeof(DeepObject1)); + + /// + [IgnoreDataMember, XmlIgnore] + public bool IsManaged => Accessor.IsManaged; + + /// + [IgnoreDataMember, XmlIgnore] + public bool IsValid => Accessor.IsValid; + + /// + [IgnoreDataMember, XmlIgnore] + public bool IsFrozen => Accessor.IsFrozen; + + /// + [IgnoreDataMember, XmlIgnore] + public Realms.Realm? Realm => Accessor.Realm; + + /// + [IgnoreDataMember, XmlIgnore] + public Realms.Schema.ObjectSchema ObjectSchema => Accessor.ObjectSchema!; + + /// + [IgnoreDataMember, XmlIgnore] + public Realms.DynamicObjectApi DynamicApi => Accessor.DynamicApi; + + /// + [IgnoreDataMember, XmlIgnore] + public int BacklinksCount => Accessor.BacklinksCount; + + void ISettableManagedAccessor.SetManagedAccessor(Realms.IRealmAccessor managedAccessor, Realms.Weaving.IRealmObjectHelper? helper, bool update, bool skipDefaults) + { + var newAccessor = (IDeepObject1Accessor)managedAccessor; + var oldAccessor = _accessor; + _accessor = newAccessor; + + if (helper != null && oldAccessor != null) + { + if (!skipDefaults || oldAccessor.StringValue != default(string?)) + { + newAccessor.StringValue = oldAccessor.StringValue; + } + if (oldAccessor.RecursiveObject != null && newAccessor.Realm != null) + { + newAccessor.Realm.Add(oldAccessor.RecursiveObject, update); + } + newAccessor.RecursiveObject = oldAccessor.RecursiveObject; + } + + if (_propertyChanged != null) + { + SubscribeForNotifications(); + } + + OnManaged(); + } + + #endregion + + /// + /// Called when the object has been managed by a Realm. + /// + /// + /// This method will be called either when a managed object is materialized or when an unmanaged object has been + /// added to the Realm. It can be useful for providing some initialization logic as when the constructor is invoked, + /// it is not yet clear whether the object is managed or not. + /// + partial void OnManaged(); + + private event PropertyChangedEventHandler? _propertyChanged; + + /// + public event PropertyChangedEventHandler? PropertyChanged + { + add + { + if (_propertyChanged == null) + { + SubscribeForNotifications(); + } + + _propertyChanged += value; + } + + remove + { + _propertyChanged -= value; + + if (_propertyChanged == null) + { + UnsubscribeFromNotifications(); + } + } + } + + /// + /// Called when a property has changed on this class. + /// + /// The name of the property. + /// + /// For this method to be called, you need to have first subscribed to . + /// This can be used to react to changes to the current object, e.g. raising for computed properties. + /// + /// + /// + /// class MyClass : IRealmObject + /// { + /// public int StatusCodeRaw { get; set; } + /// public StatusCodeEnum StatusCode => (StatusCodeEnum)StatusCodeRaw; + /// partial void OnPropertyChanged(string propertyName) + /// { + /// if (propertyName == nameof(StatusCodeRaw)) + /// { + /// RaisePropertyChanged(nameof(StatusCode)); + /// } + /// } + /// } + /// + /// Here, we have a computed property that depends on a persisted one. In order to notify any + /// subscribers that StatusCode has changed, we implement and + /// raise manually by calling . + /// + partial void OnPropertyChanged(string? propertyName); + + private void RaisePropertyChanged([CallerMemberName] string propertyName = "") + { + _propertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + OnPropertyChanged(propertyName); + } + + private void SubscribeForNotifications() + { + Accessor.SubscribeForNotifications(RaisePropertyChanged); + } + + private void UnsubscribeFromNotifications() + { + Accessor.UnsubscribeFromNotifications(); + } + + /// + /// Converts a to . Equivalent to . + /// + /// The to convert. + /// The stored in the . + public static explicit operator DeepObject1?(Realms.RealmValue val) => val.Type == Realms.RealmValueType.Null ? null : val.AsRealmObject(); + + /// + /// Implicitly constructs a from . + /// + /// The value to store in the . + /// A containing the supplied . + public static implicit operator Realms.RealmValue(DeepObject1? val) => val == null ? Realms.RealmValue.Null : Realms.RealmValue.Object(val); + + /// + /// Implicitly constructs a from . + /// + /// The value to store in the . + /// A containing the supplied . + public static implicit operator Realms.QueryArgument(DeepObject1? val) => (Realms.RealmValue)val; + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public TypeInfo GetTypeInfo() => Accessor.GetTypeInfo(this); + + /// + public override bool Equals(object? obj) + { + if (obj is null) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is InvalidObject) + { + return !IsValid; + } + + if (!(obj is Realms.IRealmObjectBase iro)) + { + return false; + } + + return Accessor.Equals(iro.Accessor); + } + + /// + public override int GetHashCode() => IsManaged ? Accessor.GetHashCode() : base.GetHashCode(); + + /// + public override string? ToString() => Accessor.ToString(); + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + private class DeepObject1ObjectHelper : Realms.Weaving.IRealmObjectHelper + { + public void CopyToRealm(Realms.IRealmObjectBase instance, bool update, bool skipDefaults) + { + throw new InvalidOperationException("This method should not be called for source generated classes."); + } + + public Realms.ManagedAccessor CreateAccessor() => new DeepObject1ManagedAccessor(); + + public Realms.IRealmObjectBase CreateInstance() => new DeepObject1(); + + public bool TryGetPrimaryKeyValue(Realms.IRealmObjectBase instance, out RealmValue value) + { + value = RealmValue.Null; + return false; + } + } + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + internal interface IDeepObject1Accessor : Realms.IRealmAccessor + { + string? StringValue { get; set; } + + Realms.Tests.Database.DeepObject2? RecursiveObject { get; set; } + } + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + internal class DeepObject1ManagedAccessor : Realms.ManagedAccessor, IDeepObject1Accessor + { + public string? StringValue + { + get => (string?)GetValue("StringValue"); + set => SetValue("StringValue", value); + } + + public Realms.Tests.Database.DeepObject2? RecursiveObject + { + get => (Realms.Tests.Database.DeepObject2?)GetValue("RecursiveObject"); + set => SetValue("RecursiveObject", value); + } + } + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + internal class DeepObject1UnmanagedAccessor : Realms.UnmanagedAccessor, IDeepObject1Accessor + { + public override ObjectSchema ObjectSchema => DeepObject1.RealmSchema; + + private string? _stringValue; + public string? StringValue + { + get => _stringValue; + set + { + _stringValue = value; + RaisePropertyChanged("StringValue"); + } + } + + private Realms.Tests.Database.DeepObject2? _recursiveObject; + public Realms.Tests.Database.DeepObject2? RecursiveObject + { + get => _recursiveObject; + set + { + _recursiveObject = value; + RaisePropertyChanged("RecursiveObject"); + } + } + + public DeepObject1UnmanagedAccessor(Type objectType) : base(objectType) + { + } + + public override Realms.RealmValue GetValue(string propertyName) + { + return propertyName switch + { + "StringValue" => _stringValue, + "RecursiveObject" => _recursiveObject, + _ => throw new MissingMemberException($"The object does not have a gettable Realm property with name {propertyName}"), + }; + } + + public override void SetValue(string propertyName, Realms.RealmValue val) + { + switch (propertyName) + { + case "StringValue": + StringValue = (string?)val; + return; + case "RecursiveObject": + RecursiveObject = (Realms.Tests.Database.DeepObject2?)val; + return; + default: + throw new MissingMemberException($"The object does not have a settable Realm property with name {propertyName}"); + } + } + + public override void SetValueUnique(string propertyName, Realms.RealmValue val) + { + throw new InvalidOperationException("Cannot set the value of an non primary key property with SetValueUnique"); + } + + public override IList GetListValue(string propertyName) + { + throw new MissingMemberException($"The object does not have a Realm list property with name {propertyName}"); + } + + public override ISet GetSetValue(string propertyName) + { + throw new MissingMemberException($"The object does not have a Realm set property with name {propertyName}"); + } + + public override IDictionary GetDictionaryValue(string propertyName) + { + throw new MissingMemberException($"The object does not have a Realm dictionary property with name {propertyName}"); + } + } + } +} diff --git a/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/DeepObject2_generated.cs b/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/DeepObject2_generated.cs new file mode 100644 index 0000000000..babdbb7bd9 --- /dev/null +++ b/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/DeepObject2_generated.cs @@ -0,0 +1,361 @@ +// +#nullable enable + +using NUnit.Framework; +using Realms; +using Realms.Logging; +using Realms.Schema; +using Realms.Tests.Database; +using Realms.Weaving; +using static Realms.ChangeSet; +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; +using System.Threading.Tasks; +using System.Xml.Serialization; +using TestRealmObject = Realms.IRealmObject; + +namespace Realms.Tests.Database +{ + [Generated] + [Woven(typeof(DeepObject2ObjectHelper)), Realms.Preserve(AllMembers = true)] + public partial class DeepObject2 : IRealmObject, INotifyPropertyChanged, IReflectableType + { + /// + /// Defines the schema for the class. + /// + public static Realms.Schema.ObjectSchema RealmSchema = new Realms.Schema.ObjectSchema.Builder("DeepObject2", ObjectSchema.ObjectType.RealmObject) + { + Realms.Schema.Property.Primitive("StringValue", Realms.RealmValueType.String, isPrimaryKey: false, indexType: IndexType.None, isNullable: true, managedName: "StringValue"), + Realms.Schema.Property.Object("RecursiveObject", "DeepObject3", managedName: "RecursiveObject"), + }.Build(); + + #region IRealmObject implementation + + private IDeepObject2Accessor? _accessor; + + Realms.IRealmAccessor Realms.IRealmObjectBase.Accessor => Accessor; + + private IDeepObject2Accessor Accessor => _accessor ??= new DeepObject2UnmanagedAccessor(typeof(DeepObject2)); + + /// + [IgnoreDataMember, XmlIgnore] + public bool IsManaged => Accessor.IsManaged; + + /// + [IgnoreDataMember, XmlIgnore] + public bool IsValid => Accessor.IsValid; + + /// + [IgnoreDataMember, XmlIgnore] + public bool IsFrozen => Accessor.IsFrozen; + + /// + [IgnoreDataMember, XmlIgnore] + public Realms.Realm? Realm => Accessor.Realm; + + /// + [IgnoreDataMember, XmlIgnore] + public Realms.Schema.ObjectSchema ObjectSchema => Accessor.ObjectSchema!; + + /// + [IgnoreDataMember, XmlIgnore] + public Realms.DynamicObjectApi DynamicApi => Accessor.DynamicApi; + + /// + [IgnoreDataMember, XmlIgnore] + public int BacklinksCount => Accessor.BacklinksCount; + + void ISettableManagedAccessor.SetManagedAccessor(Realms.IRealmAccessor managedAccessor, Realms.Weaving.IRealmObjectHelper? helper, bool update, bool skipDefaults) + { + var newAccessor = (IDeepObject2Accessor)managedAccessor; + var oldAccessor = _accessor; + _accessor = newAccessor; + + if (helper != null && oldAccessor != null) + { + if (!skipDefaults || oldAccessor.StringValue != default(string?)) + { + newAccessor.StringValue = oldAccessor.StringValue; + } + if (oldAccessor.RecursiveObject != null && newAccessor.Realm != null) + { + newAccessor.Realm.Add(oldAccessor.RecursiveObject, update); + } + newAccessor.RecursiveObject = oldAccessor.RecursiveObject; + } + + if (_propertyChanged != null) + { + SubscribeForNotifications(); + } + + OnManaged(); + } + + #endregion + + /// + /// Called when the object has been managed by a Realm. + /// + /// + /// This method will be called either when a managed object is materialized or when an unmanaged object has been + /// added to the Realm. It can be useful for providing some initialization logic as when the constructor is invoked, + /// it is not yet clear whether the object is managed or not. + /// + partial void OnManaged(); + + private event PropertyChangedEventHandler? _propertyChanged; + + /// + public event PropertyChangedEventHandler? PropertyChanged + { + add + { + if (_propertyChanged == null) + { + SubscribeForNotifications(); + } + + _propertyChanged += value; + } + + remove + { + _propertyChanged -= value; + + if (_propertyChanged == null) + { + UnsubscribeFromNotifications(); + } + } + } + + /// + /// Called when a property has changed on this class. + /// + /// The name of the property. + /// + /// For this method to be called, you need to have first subscribed to . + /// This can be used to react to changes to the current object, e.g. raising for computed properties. + /// + /// + /// + /// class MyClass : IRealmObject + /// { + /// public int StatusCodeRaw { get; set; } + /// public StatusCodeEnum StatusCode => (StatusCodeEnum)StatusCodeRaw; + /// partial void OnPropertyChanged(string propertyName) + /// { + /// if (propertyName == nameof(StatusCodeRaw)) + /// { + /// RaisePropertyChanged(nameof(StatusCode)); + /// } + /// } + /// } + /// + /// Here, we have a computed property that depends on a persisted one. In order to notify any + /// subscribers that StatusCode has changed, we implement and + /// raise manually by calling . + /// + partial void OnPropertyChanged(string? propertyName); + + private void RaisePropertyChanged([CallerMemberName] string propertyName = "") + { + _propertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + OnPropertyChanged(propertyName); + } + + private void SubscribeForNotifications() + { + Accessor.SubscribeForNotifications(RaisePropertyChanged); + } + + private void UnsubscribeFromNotifications() + { + Accessor.UnsubscribeFromNotifications(); + } + + /// + /// Converts a to . Equivalent to . + /// + /// The to convert. + /// The stored in the . + public static explicit operator DeepObject2?(Realms.RealmValue val) => val.Type == Realms.RealmValueType.Null ? null : val.AsRealmObject(); + + /// + /// Implicitly constructs a from . + /// + /// The value to store in the . + /// A containing the supplied . + public static implicit operator Realms.RealmValue(DeepObject2? val) => val == null ? Realms.RealmValue.Null : Realms.RealmValue.Object(val); + + /// + /// Implicitly constructs a from . + /// + /// The value to store in the . + /// A containing the supplied . + public static implicit operator Realms.QueryArgument(DeepObject2? val) => (Realms.RealmValue)val; + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public TypeInfo GetTypeInfo() => Accessor.GetTypeInfo(this); + + /// + public override bool Equals(object? obj) + { + if (obj is null) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is InvalidObject) + { + return !IsValid; + } + + if (!(obj is Realms.IRealmObjectBase iro)) + { + return false; + } + + return Accessor.Equals(iro.Accessor); + } + + /// + public override int GetHashCode() => IsManaged ? Accessor.GetHashCode() : base.GetHashCode(); + + /// + public override string? ToString() => Accessor.ToString(); + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + private class DeepObject2ObjectHelper : Realms.Weaving.IRealmObjectHelper + { + public void CopyToRealm(Realms.IRealmObjectBase instance, bool update, bool skipDefaults) + { + throw new InvalidOperationException("This method should not be called for source generated classes."); + } + + public Realms.ManagedAccessor CreateAccessor() => new DeepObject2ManagedAccessor(); + + public Realms.IRealmObjectBase CreateInstance() => new DeepObject2(); + + public bool TryGetPrimaryKeyValue(Realms.IRealmObjectBase instance, out RealmValue value) + { + value = RealmValue.Null; + return false; + } + } + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + internal interface IDeepObject2Accessor : Realms.IRealmAccessor + { + string? StringValue { get; set; } + + Realms.Tests.Database.DeepObject3? RecursiveObject { get; set; } + } + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + internal class DeepObject2ManagedAccessor : Realms.ManagedAccessor, IDeepObject2Accessor + { + public string? StringValue + { + get => (string?)GetValue("StringValue"); + set => SetValue("StringValue", value); + } + + public Realms.Tests.Database.DeepObject3? RecursiveObject + { + get => (Realms.Tests.Database.DeepObject3?)GetValue("RecursiveObject"); + set => SetValue("RecursiveObject", value); + } + } + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + internal class DeepObject2UnmanagedAccessor : Realms.UnmanagedAccessor, IDeepObject2Accessor + { + public override ObjectSchema ObjectSchema => DeepObject2.RealmSchema; + + private string? _stringValue; + public string? StringValue + { + get => _stringValue; + set + { + _stringValue = value; + RaisePropertyChanged("StringValue"); + } + } + + private Realms.Tests.Database.DeepObject3? _recursiveObject; + public Realms.Tests.Database.DeepObject3? RecursiveObject + { + get => _recursiveObject; + set + { + _recursiveObject = value; + RaisePropertyChanged("RecursiveObject"); + } + } + + public DeepObject2UnmanagedAccessor(Type objectType) : base(objectType) + { + } + + public override Realms.RealmValue GetValue(string propertyName) + { + return propertyName switch + { + "StringValue" => _stringValue, + "RecursiveObject" => _recursiveObject, + _ => throw new MissingMemberException($"The object does not have a gettable Realm property with name {propertyName}"), + }; + } + + public override void SetValue(string propertyName, Realms.RealmValue val) + { + switch (propertyName) + { + case "StringValue": + StringValue = (string?)val; + return; + case "RecursiveObject": + RecursiveObject = (Realms.Tests.Database.DeepObject3?)val; + return; + default: + throw new MissingMemberException($"The object does not have a settable Realm property with name {propertyName}"); + } + } + + public override void SetValueUnique(string propertyName, Realms.RealmValue val) + { + throw new InvalidOperationException("Cannot set the value of an non primary key property with SetValueUnique"); + } + + public override IList GetListValue(string propertyName) + { + throw new MissingMemberException($"The object does not have a Realm list property with name {propertyName}"); + } + + public override ISet GetSetValue(string propertyName) + { + throw new MissingMemberException($"The object does not have a Realm set property with name {propertyName}"); + } + + public override IDictionary GetDictionaryValue(string propertyName) + { + throw new MissingMemberException($"The object does not have a Realm dictionary property with name {propertyName}"); + } + } + } +} diff --git a/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/DeepObject3_generated.cs b/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/DeepObject3_generated.cs new file mode 100644 index 0000000000..8207c59534 --- /dev/null +++ b/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/DeepObject3_generated.cs @@ -0,0 +1,361 @@ +// +#nullable enable + +using NUnit.Framework; +using Realms; +using Realms.Logging; +using Realms.Schema; +using Realms.Tests.Database; +using Realms.Weaving; +using static Realms.ChangeSet; +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; +using System.Threading.Tasks; +using System.Xml.Serialization; +using TestRealmObject = Realms.IRealmObject; + +namespace Realms.Tests.Database +{ + [Generated] + [Woven(typeof(DeepObject3ObjectHelper)), Realms.Preserve(AllMembers = true)] + public partial class DeepObject3 : IRealmObject, INotifyPropertyChanged, IReflectableType + { + /// + /// Defines the schema for the class. + /// + public static Realms.Schema.ObjectSchema RealmSchema = new Realms.Schema.ObjectSchema.Builder("DeepObject3", ObjectSchema.ObjectType.RealmObject) + { + Realms.Schema.Property.Primitive("StringValue", Realms.RealmValueType.String, isPrimaryKey: false, indexType: IndexType.None, isNullable: true, managedName: "StringValue"), + Realms.Schema.Property.Object("RecursiveObject", "DeepObject4", managedName: "RecursiveObject"), + }.Build(); + + #region IRealmObject implementation + + private IDeepObject3Accessor? _accessor; + + Realms.IRealmAccessor Realms.IRealmObjectBase.Accessor => Accessor; + + private IDeepObject3Accessor Accessor => _accessor ??= new DeepObject3UnmanagedAccessor(typeof(DeepObject3)); + + /// + [IgnoreDataMember, XmlIgnore] + public bool IsManaged => Accessor.IsManaged; + + /// + [IgnoreDataMember, XmlIgnore] + public bool IsValid => Accessor.IsValid; + + /// + [IgnoreDataMember, XmlIgnore] + public bool IsFrozen => Accessor.IsFrozen; + + /// + [IgnoreDataMember, XmlIgnore] + public Realms.Realm? Realm => Accessor.Realm; + + /// + [IgnoreDataMember, XmlIgnore] + public Realms.Schema.ObjectSchema ObjectSchema => Accessor.ObjectSchema!; + + /// + [IgnoreDataMember, XmlIgnore] + public Realms.DynamicObjectApi DynamicApi => Accessor.DynamicApi; + + /// + [IgnoreDataMember, XmlIgnore] + public int BacklinksCount => Accessor.BacklinksCount; + + void ISettableManagedAccessor.SetManagedAccessor(Realms.IRealmAccessor managedAccessor, Realms.Weaving.IRealmObjectHelper? helper, bool update, bool skipDefaults) + { + var newAccessor = (IDeepObject3Accessor)managedAccessor; + var oldAccessor = _accessor; + _accessor = newAccessor; + + if (helper != null && oldAccessor != null) + { + if (!skipDefaults || oldAccessor.StringValue != default(string?)) + { + newAccessor.StringValue = oldAccessor.StringValue; + } + if (oldAccessor.RecursiveObject != null && newAccessor.Realm != null) + { + newAccessor.Realm.Add(oldAccessor.RecursiveObject, update); + } + newAccessor.RecursiveObject = oldAccessor.RecursiveObject; + } + + if (_propertyChanged != null) + { + SubscribeForNotifications(); + } + + OnManaged(); + } + + #endregion + + /// + /// Called when the object has been managed by a Realm. + /// + /// + /// This method will be called either when a managed object is materialized or when an unmanaged object has been + /// added to the Realm. It can be useful for providing some initialization logic as when the constructor is invoked, + /// it is not yet clear whether the object is managed or not. + /// + partial void OnManaged(); + + private event PropertyChangedEventHandler? _propertyChanged; + + /// + public event PropertyChangedEventHandler? PropertyChanged + { + add + { + if (_propertyChanged == null) + { + SubscribeForNotifications(); + } + + _propertyChanged += value; + } + + remove + { + _propertyChanged -= value; + + if (_propertyChanged == null) + { + UnsubscribeFromNotifications(); + } + } + } + + /// + /// Called when a property has changed on this class. + /// + /// The name of the property. + /// + /// For this method to be called, you need to have first subscribed to . + /// This can be used to react to changes to the current object, e.g. raising for computed properties. + /// + /// + /// + /// class MyClass : IRealmObject + /// { + /// public int StatusCodeRaw { get; set; } + /// public StatusCodeEnum StatusCode => (StatusCodeEnum)StatusCodeRaw; + /// partial void OnPropertyChanged(string propertyName) + /// { + /// if (propertyName == nameof(StatusCodeRaw)) + /// { + /// RaisePropertyChanged(nameof(StatusCode)); + /// } + /// } + /// } + /// + /// Here, we have a computed property that depends on a persisted one. In order to notify any + /// subscribers that StatusCode has changed, we implement and + /// raise manually by calling . + /// + partial void OnPropertyChanged(string? propertyName); + + private void RaisePropertyChanged([CallerMemberName] string propertyName = "") + { + _propertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + OnPropertyChanged(propertyName); + } + + private void SubscribeForNotifications() + { + Accessor.SubscribeForNotifications(RaisePropertyChanged); + } + + private void UnsubscribeFromNotifications() + { + Accessor.UnsubscribeFromNotifications(); + } + + /// + /// Converts a to . Equivalent to . + /// + /// The to convert. + /// The stored in the . + public static explicit operator DeepObject3?(Realms.RealmValue val) => val.Type == Realms.RealmValueType.Null ? null : val.AsRealmObject(); + + /// + /// Implicitly constructs a from . + /// + /// The value to store in the . + /// A containing the supplied . + public static implicit operator Realms.RealmValue(DeepObject3? val) => val == null ? Realms.RealmValue.Null : Realms.RealmValue.Object(val); + + /// + /// Implicitly constructs a from . + /// + /// The value to store in the . + /// A containing the supplied . + public static implicit operator Realms.QueryArgument(DeepObject3? val) => (Realms.RealmValue)val; + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public TypeInfo GetTypeInfo() => Accessor.GetTypeInfo(this); + + /// + public override bool Equals(object? obj) + { + if (obj is null) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is InvalidObject) + { + return !IsValid; + } + + if (!(obj is Realms.IRealmObjectBase iro)) + { + return false; + } + + return Accessor.Equals(iro.Accessor); + } + + /// + public override int GetHashCode() => IsManaged ? Accessor.GetHashCode() : base.GetHashCode(); + + /// + public override string? ToString() => Accessor.ToString(); + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + private class DeepObject3ObjectHelper : Realms.Weaving.IRealmObjectHelper + { + public void CopyToRealm(Realms.IRealmObjectBase instance, bool update, bool skipDefaults) + { + throw new InvalidOperationException("This method should not be called for source generated classes."); + } + + public Realms.ManagedAccessor CreateAccessor() => new DeepObject3ManagedAccessor(); + + public Realms.IRealmObjectBase CreateInstance() => new DeepObject3(); + + public bool TryGetPrimaryKeyValue(Realms.IRealmObjectBase instance, out RealmValue value) + { + value = RealmValue.Null; + return false; + } + } + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + internal interface IDeepObject3Accessor : Realms.IRealmAccessor + { + string? StringValue { get; set; } + + Realms.Tests.Database.DeepObject4? RecursiveObject { get; set; } + } + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + internal class DeepObject3ManagedAccessor : Realms.ManagedAccessor, IDeepObject3Accessor + { + public string? StringValue + { + get => (string?)GetValue("StringValue"); + set => SetValue("StringValue", value); + } + + public Realms.Tests.Database.DeepObject4? RecursiveObject + { + get => (Realms.Tests.Database.DeepObject4?)GetValue("RecursiveObject"); + set => SetValue("RecursiveObject", value); + } + } + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + internal class DeepObject3UnmanagedAccessor : Realms.UnmanagedAccessor, IDeepObject3Accessor + { + public override ObjectSchema ObjectSchema => DeepObject3.RealmSchema; + + private string? _stringValue; + public string? StringValue + { + get => _stringValue; + set + { + _stringValue = value; + RaisePropertyChanged("StringValue"); + } + } + + private Realms.Tests.Database.DeepObject4? _recursiveObject; + public Realms.Tests.Database.DeepObject4? RecursiveObject + { + get => _recursiveObject; + set + { + _recursiveObject = value; + RaisePropertyChanged("RecursiveObject"); + } + } + + public DeepObject3UnmanagedAccessor(Type objectType) : base(objectType) + { + } + + public override Realms.RealmValue GetValue(string propertyName) + { + return propertyName switch + { + "StringValue" => _stringValue, + "RecursiveObject" => _recursiveObject, + _ => throw new MissingMemberException($"The object does not have a gettable Realm property with name {propertyName}"), + }; + } + + public override void SetValue(string propertyName, Realms.RealmValue val) + { + switch (propertyName) + { + case "StringValue": + StringValue = (string?)val; + return; + case "RecursiveObject": + RecursiveObject = (Realms.Tests.Database.DeepObject4?)val; + return; + default: + throw new MissingMemberException($"The object does not have a settable Realm property with name {propertyName}"); + } + } + + public override void SetValueUnique(string propertyName, Realms.RealmValue val) + { + throw new InvalidOperationException("Cannot set the value of an non primary key property with SetValueUnique"); + } + + public override IList GetListValue(string propertyName) + { + throw new MissingMemberException($"The object does not have a Realm list property with name {propertyName}"); + } + + public override ISet GetSetValue(string propertyName) + { + throw new MissingMemberException($"The object does not have a Realm set property with name {propertyName}"); + } + + public override IDictionary GetDictionaryValue(string propertyName) + { + throw new MissingMemberException($"The object does not have a Realm dictionary property with name {propertyName}"); + } + } + } +} diff --git a/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/DeepObject4_generated.cs b/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/DeepObject4_generated.cs new file mode 100644 index 0000000000..64be4c5bab --- /dev/null +++ b/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/DeepObject4_generated.cs @@ -0,0 +1,361 @@ +// +#nullable enable + +using NUnit.Framework; +using Realms; +using Realms.Logging; +using Realms.Schema; +using Realms.Tests.Database; +using Realms.Weaving; +using static Realms.ChangeSet; +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; +using System.Threading.Tasks; +using System.Xml.Serialization; +using TestRealmObject = Realms.IRealmObject; + +namespace Realms.Tests.Database +{ + [Generated] + [Woven(typeof(DeepObject4ObjectHelper)), Realms.Preserve(AllMembers = true)] + public partial class DeepObject4 : IRealmObject, INotifyPropertyChanged, IReflectableType + { + /// + /// Defines the schema for the class. + /// + public static Realms.Schema.ObjectSchema RealmSchema = new Realms.Schema.ObjectSchema.Builder("DeepObject4", ObjectSchema.ObjectType.RealmObject) + { + Realms.Schema.Property.Primitive("StringValue", Realms.RealmValueType.String, isPrimaryKey: false, indexType: IndexType.None, isNullable: true, managedName: "StringValue"), + Realms.Schema.Property.Object("RecursiveObject", "DeepObject5", managedName: "RecursiveObject"), + }.Build(); + + #region IRealmObject implementation + + private IDeepObject4Accessor? _accessor; + + Realms.IRealmAccessor Realms.IRealmObjectBase.Accessor => Accessor; + + private IDeepObject4Accessor Accessor => _accessor ??= new DeepObject4UnmanagedAccessor(typeof(DeepObject4)); + + /// + [IgnoreDataMember, XmlIgnore] + public bool IsManaged => Accessor.IsManaged; + + /// + [IgnoreDataMember, XmlIgnore] + public bool IsValid => Accessor.IsValid; + + /// + [IgnoreDataMember, XmlIgnore] + public bool IsFrozen => Accessor.IsFrozen; + + /// + [IgnoreDataMember, XmlIgnore] + public Realms.Realm? Realm => Accessor.Realm; + + /// + [IgnoreDataMember, XmlIgnore] + public Realms.Schema.ObjectSchema ObjectSchema => Accessor.ObjectSchema!; + + /// + [IgnoreDataMember, XmlIgnore] + public Realms.DynamicObjectApi DynamicApi => Accessor.DynamicApi; + + /// + [IgnoreDataMember, XmlIgnore] + public int BacklinksCount => Accessor.BacklinksCount; + + void ISettableManagedAccessor.SetManagedAccessor(Realms.IRealmAccessor managedAccessor, Realms.Weaving.IRealmObjectHelper? helper, bool update, bool skipDefaults) + { + var newAccessor = (IDeepObject4Accessor)managedAccessor; + var oldAccessor = _accessor; + _accessor = newAccessor; + + if (helper != null && oldAccessor != null) + { + if (!skipDefaults || oldAccessor.StringValue != default(string?)) + { + newAccessor.StringValue = oldAccessor.StringValue; + } + if (oldAccessor.RecursiveObject != null && newAccessor.Realm != null) + { + newAccessor.Realm.Add(oldAccessor.RecursiveObject, update); + } + newAccessor.RecursiveObject = oldAccessor.RecursiveObject; + } + + if (_propertyChanged != null) + { + SubscribeForNotifications(); + } + + OnManaged(); + } + + #endregion + + /// + /// Called when the object has been managed by a Realm. + /// + /// + /// This method will be called either when a managed object is materialized or when an unmanaged object has been + /// added to the Realm. It can be useful for providing some initialization logic as when the constructor is invoked, + /// it is not yet clear whether the object is managed or not. + /// + partial void OnManaged(); + + private event PropertyChangedEventHandler? _propertyChanged; + + /// + public event PropertyChangedEventHandler? PropertyChanged + { + add + { + if (_propertyChanged == null) + { + SubscribeForNotifications(); + } + + _propertyChanged += value; + } + + remove + { + _propertyChanged -= value; + + if (_propertyChanged == null) + { + UnsubscribeFromNotifications(); + } + } + } + + /// + /// Called when a property has changed on this class. + /// + /// The name of the property. + /// + /// For this method to be called, you need to have first subscribed to . + /// This can be used to react to changes to the current object, e.g. raising for computed properties. + /// + /// + /// + /// class MyClass : IRealmObject + /// { + /// public int StatusCodeRaw { get; set; } + /// public StatusCodeEnum StatusCode => (StatusCodeEnum)StatusCodeRaw; + /// partial void OnPropertyChanged(string propertyName) + /// { + /// if (propertyName == nameof(StatusCodeRaw)) + /// { + /// RaisePropertyChanged(nameof(StatusCode)); + /// } + /// } + /// } + /// + /// Here, we have a computed property that depends on a persisted one. In order to notify any + /// subscribers that StatusCode has changed, we implement and + /// raise manually by calling . + /// + partial void OnPropertyChanged(string? propertyName); + + private void RaisePropertyChanged([CallerMemberName] string propertyName = "") + { + _propertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + OnPropertyChanged(propertyName); + } + + private void SubscribeForNotifications() + { + Accessor.SubscribeForNotifications(RaisePropertyChanged); + } + + private void UnsubscribeFromNotifications() + { + Accessor.UnsubscribeFromNotifications(); + } + + /// + /// Converts a to . Equivalent to . + /// + /// The to convert. + /// The stored in the . + public static explicit operator DeepObject4?(Realms.RealmValue val) => val.Type == Realms.RealmValueType.Null ? null : val.AsRealmObject(); + + /// + /// Implicitly constructs a from . + /// + /// The value to store in the . + /// A containing the supplied . + public static implicit operator Realms.RealmValue(DeepObject4? val) => val == null ? Realms.RealmValue.Null : Realms.RealmValue.Object(val); + + /// + /// Implicitly constructs a from . + /// + /// The value to store in the . + /// A containing the supplied . + public static implicit operator Realms.QueryArgument(DeepObject4? val) => (Realms.RealmValue)val; + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public TypeInfo GetTypeInfo() => Accessor.GetTypeInfo(this); + + /// + public override bool Equals(object? obj) + { + if (obj is null) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is InvalidObject) + { + return !IsValid; + } + + if (!(obj is Realms.IRealmObjectBase iro)) + { + return false; + } + + return Accessor.Equals(iro.Accessor); + } + + /// + public override int GetHashCode() => IsManaged ? Accessor.GetHashCode() : base.GetHashCode(); + + /// + public override string? ToString() => Accessor.ToString(); + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + private class DeepObject4ObjectHelper : Realms.Weaving.IRealmObjectHelper + { + public void CopyToRealm(Realms.IRealmObjectBase instance, bool update, bool skipDefaults) + { + throw new InvalidOperationException("This method should not be called for source generated classes."); + } + + public Realms.ManagedAccessor CreateAccessor() => new DeepObject4ManagedAccessor(); + + public Realms.IRealmObjectBase CreateInstance() => new DeepObject4(); + + public bool TryGetPrimaryKeyValue(Realms.IRealmObjectBase instance, out RealmValue value) + { + value = RealmValue.Null; + return false; + } + } + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + internal interface IDeepObject4Accessor : Realms.IRealmAccessor + { + string? StringValue { get; set; } + + Realms.Tests.Database.DeepObject5? RecursiveObject { get; set; } + } + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + internal class DeepObject4ManagedAccessor : Realms.ManagedAccessor, IDeepObject4Accessor + { + public string? StringValue + { + get => (string?)GetValue("StringValue"); + set => SetValue("StringValue", value); + } + + public Realms.Tests.Database.DeepObject5? RecursiveObject + { + get => (Realms.Tests.Database.DeepObject5?)GetValue("RecursiveObject"); + set => SetValue("RecursiveObject", value); + } + } + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + internal class DeepObject4UnmanagedAccessor : Realms.UnmanagedAccessor, IDeepObject4Accessor + { + public override ObjectSchema ObjectSchema => DeepObject4.RealmSchema; + + private string? _stringValue; + public string? StringValue + { + get => _stringValue; + set + { + _stringValue = value; + RaisePropertyChanged("StringValue"); + } + } + + private Realms.Tests.Database.DeepObject5? _recursiveObject; + public Realms.Tests.Database.DeepObject5? RecursiveObject + { + get => _recursiveObject; + set + { + _recursiveObject = value; + RaisePropertyChanged("RecursiveObject"); + } + } + + public DeepObject4UnmanagedAccessor(Type objectType) : base(objectType) + { + } + + public override Realms.RealmValue GetValue(string propertyName) + { + return propertyName switch + { + "StringValue" => _stringValue, + "RecursiveObject" => _recursiveObject, + _ => throw new MissingMemberException($"The object does not have a gettable Realm property with name {propertyName}"), + }; + } + + public override void SetValue(string propertyName, Realms.RealmValue val) + { + switch (propertyName) + { + case "StringValue": + StringValue = (string?)val; + return; + case "RecursiveObject": + RecursiveObject = (Realms.Tests.Database.DeepObject5?)val; + return; + default: + throw new MissingMemberException($"The object does not have a settable Realm property with name {propertyName}"); + } + } + + public override void SetValueUnique(string propertyName, Realms.RealmValue val) + { + throw new InvalidOperationException("Cannot set the value of an non primary key property with SetValueUnique"); + } + + public override IList GetListValue(string propertyName) + { + throw new MissingMemberException($"The object does not have a Realm list property with name {propertyName}"); + } + + public override ISet GetSetValue(string propertyName) + { + throw new MissingMemberException($"The object does not have a Realm set property with name {propertyName}"); + } + + public override IDictionary GetDictionaryValue(string propertyName) + { + throw new MissingMemberException($"The object does not have a Realm dictionary property with name {propertyName}"); + } + } + } +} diff --git a/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/DeepObject5_generated.cs b/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/DeepObject5_generated.cs new file mode 100644 index 0000000000..87ec57eb67 --- /dev/null +++ b/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/DeepObject5_generated.cs @@ -0,0 +1,332 @@ +// +#nullable enable + +using NUnit.Framework; +using Realms; +using Realms.Logging; +using Realms.Schema; +using Realms.Tests.Database; +using Realms.Weaving; +using static Realms.ChangeSet; +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; +using System.Threading.Tasks; +using System.Xml.Serialization; +using TestRealmObject = Realms.IRealmObject; + +namespace Realms.Tests.Database +{ + [Generated] + [Woven(typeof(DeepObject5ObjectHelper)), Realms.Preserve(AllMembers = true)] + public partial class DeepObject5 : IRealmObject, INotifyPropertyChanged, IReflectableType + { + /// + /// Defines the schema for the class. + /// + public static Realms.Schema.ObjectSchema RealmSchema = new Realms.Schema.ObjectSchema.Builder("DeepObject5", ObjectSchema.ObjectType.RealmObject) + { + Realms.Schema.Property.Primitive("StringValue", Realms.RealmValueType.String, isPrimaryKey: false, indexType: IndexType.None, isNullable: true, managedName: "StringValue"), + }.Build(); + + #region IRealmObject implementation + + private IDeepObject5Accessor? _accessor; + + Realms.IRealmAccessor Realms.IRealmObjectBase.Accessor => Accessor; + + private IDeepObject5Accessor Accessor => _accessor ??= new DeepObject5UnmanagedAccessor(typeof(DeepObject5)); + + /// + [IgnoreDataMember, XmlIgnore] + public bool IsManaged => Accessor.IsManaged; + + /// + [IgnoreDataMember, XmlIgnore] + public bool IsValid => Accessor.IsValid; + + /// + [IgnoreDataMember, XmlIgnore] + public bool IsFrozen => Accessor.IsFrozen; + + /// + [IgnoreDataMember, XmlIgnore] + public Realms.Realm? Realm => Accessor.Realm; + + /// + [IgnoreDataMember, XmlIgnore] + public Realms.Schema.ObjectSchema ObjectSchema => Accessor.ObjectSchema!; + + /// + [IgnoreDataMember, XmlIgnore] + public Realms.DynamicObjectApi DynamicApi => Accessor.DynamicApi; + + /// + [IgnoreDataMember, XmlIgnore] + public int BacklinksCount => Accessor.BacklinksCount; + + void ISettableManagedAccessor.SetManagedAccessor(Realms.IRealmAccessor managedAccessor, Realms.Weaving.IRealmObjectHelper? helper, bool update, bool skipDefaults) + { + var newAccessor = (IDeepObject5Accessor)managedAccessor; + var oldAccessor = _accessor; + _accessor = newAccessor; + + if (helper != null && oldAccessor != null) + { + if (!skipDefaults || oldAccessor.StringValue != default(string?)) + { + newAccessor.StringValue = oldAccessor.StringValue; + } + } + + if (_propertyChanged != null) + { + SubscribeForNotifications(); + } + + OnManaged(); + } + + #endregion + + /// + /// Called when the object has been managed by a Realm. + /// + /// + /// This method will be called either when a managed object is materialized or when an unmanaged object has been + /// added to the Realm. It can be useful for providing some initialization logic as when the constructor is invoked, + /// it is not yet clear whether the object is managed or not. + /// + partial void OnManaged(); + + private event PropertyChangedEventHandler? _propertyChanged; + + /// + public event PropertyChangedEventHandler? PropertyChanged + { + add + { + if (_propertyChanged == null) + { + SubscribeForNotifications(); + } + + _propertyChanged += value; + } + + remove + { + _propertyChanged -= value; + + if (_propertyChanged == null) + { + UnsubscribeFromNotifications(); + } + } + } + + /// + /// Called when a property has changed on this class. + /// + /// The name of the property. + /// + /// For this method to be called, you need to have first subscribed to . + /// This can be used to react to changes to the current object, e.g. raising for computed properties. + /// + /// + /// + /// class MyClass : IRealmObject + /// { + /// public int StatusCodeRaw { get; set; } + /// public StatusCodeEnum StatusCode => (StatusCodeEnum)StatusCodeRaw; + /// partial void OnPropertyChanged(string propertyName) + /// { + /// if (propertyName == nameof(StatusCodeRaw)) + /// { + /// RaisePropertyChanged(nameof(StatusCode)); + /// } + /// } + /// } + /// + /// Here, we have a computed property that depends on a persisted one. In order to notify any + /// subscribers that StatusCode has changed, we implement and + /// raise manually by calling . + /// + partial void OnPropertyChanged(string? propertyName); + + private void RaisePropertyChanged([CallerMemberName] string propertyName = "") + { + _propertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + OnPropertyChanged(propertyName); + } + + private void SubscribeForNotifications() + { + Accessor.SubscribeForNotifications(RaisePropertyChanged); + } + + private void UnsubscribeFromNotifications() + { + Accessor.UnsubscribeFromNotifications(); + } + + /// + /// Converts a to . Equivalent to . + /// + /// The to convert. + /// The stored in the . + public static explicit operator DeepObject5?(Realms.RealmValue val) => val.Type == Realms.RealmValueType.Null ? null : val.AsRealmObject(); + + /// + /// Implicitly constructs a from . + /// + /// The value to store in the . + /// A containing the supplied . + public static implicit operator Realms.RealmValue(DeepObject5? val) => val == null ? Realms.RealmValue.Null : Realms.RealmValue.Object(val); + + /// + /// Implicitly constructs a from . + /// + /// The value to store in the . + /// A containing the supplied . + public static implicit operator Realms.QueryArgument(DeepObject5? val) => (Realms.RealmValue)val; + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public TypeInfo GetTypeInfo() => Accessor.GetTypeInfo(this); + + /// + public override bool Equals(object? obj) + { + if (obj is null) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is InvalidObject) + { + return !IsValid; + } + + if (!(obj is Realms.IRealmObjectBase iro)) + { + return false; + } + + return Accessor.Equals(iro.Accessor); + } + + /// + public override int GetHashCode() => IsManaged ? Accessor.GetHashCode() : base.GetHashCode(); + + /// + public override string? ToString() => Accessor.ToString(); + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + private class DeepObject5ObjectHelper : Realms.Weaving.IRealmObjectHelper + { + public void CopyToRealm(Realms.IRealmObjectBase instance, bool update, bool skipDefaults) + { + throw new InvalidOperationException("This method should not be called for source generated classes."); + } + + public Realms.ManagedAccessor CreateAccessor() => new DeepObject5ManagedAccessor(); + + public Realms.IRealmObjectBase CreateInstance() => new DeepObject5(); + + public bool TryGetPrimaryKeyValue(Realms.IRealmObjectBase instance, out RealmValue value) + { + value = RealmValue.Null; + return false; + } + } + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + internal interface IDeepObject5Accessor : Realms.IRealmAccessor + { + string? StringValue { get; set; } + } + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + internal class DeepObject5ManagedAccessor : Realms.ManagedAccessor, IDeepObject5Accessor + { + public string? StringValue + { + get => (string?)GetValue("StringValue"); + set => SetValue("StringValue", value); + } + } + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + internal class DeepObject5UnmanagedAccessor : Realms.UnmanagedAccessor, IDeepObject5Accessor + { + public override ObjectSchema ObjectSchema => DeepObject5.RealmSchema; + + private string? _stringValue; + public string? StringValue + { + get => _stringValue; + set + { + _stringValue = value; + RaisePropertyChanged("StringValue"); + } + } + + public DeepObject5UnmanagedAccessor(Type objectType) : base(objectType) + { + } + + public override Realms.RealmValue GetValue(string propertyName) + { + return propertyName switch + { + "StringValue" => _stringValue, + _ => throw new MissingMemberException($"The object does not have a gettable Realm property with name {propertyName}"), + }; + } + + public override void SetValue(string propertyName, Realms.RealmValue val) + { + switch (propertyName) + { + case "StringValue": + StringValue = (string?)val; + return; + default: + throw new MissingMemberException($"The object does not have a settable Realm property with name {propertyName}"); + } + } + + public override void SetValueUnique(string propertyName, Realms.RealmValue val) + { + throw new InvalidOperationException("Cannot set the value of an non primary key property with SetValueUnique"); + } + + public override IList GetListValue(string propertyName) + { + throw new MissingMemberException($"The object does not have a Realm list property with name {propertyName}"); + } + + public override ISet GetSetValue(string propertyName) + { + throw new MissingMemberException($"The object does not have a Realm set property with name {propertyName}"); + } + + public override IDictionary GetDictionaryValue(string propertyName) + { + throw new MissingMemberException($"The object does not have a Realm dictionary property with name {propertyName}"); + } + } + } +} diff --git a/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/TestNotificationObject_generated.cs b/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/TestNotificationObject_generated.cs index 022eafe9fb..19e1eec1cd 100644 --- a/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/TestNotificationObject_generated.cs +++ b/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/TestNotificationObject_generated.cs @@ -35,6 +35,7 @@ static TestNotificationObject() public static Realms.Schema.ObjectSchema RealmSchema = new Realms.Schema.ObjectSchema.Builder("TestNotificationObject", ObjectSchema.ObjectType.RealmObject) { Realms.Schema.Property.Primitive("StringProperty", Realms.RealmValueType.String, isPrimaryKey: false, indexType: IndexType.None, isNullable: true, managedName: "StringProperty"), + Realms.Schema.Property.Primitive("IntProperty", Realms.RealmValueType.Int, isPrimaryKey: false, indexType: IndexType.None, isNullable: false, managedName: "IntProperty"), Realms.Schema.Property.ObjectList("ListSameType", "TestNotificationObject", managedName: "ListSameType"), Realms.Schema.Property.ObjectSet("SetSameType", "TestNotificationObject", managedName: "SetSameType"), Realms.Schema.Property.ObjectDictionary("DictionarySameType", "TestNotificationObject", managedName: "DictionarySameType"), @@ -42,7 +43,11 @@ static TestNotificationObject() Realms.Schema.Property.ObjectList("ListDifferentType", "Person", managedName: "ListDifferentType"), Realms.Schema.Property.ObjectSet("SetDifferentType", "Person", managedName: "SetDifferentType"), Realms.Schema.Property.ObjectDictionary("DictionaryDifferentType", "Person", managedName: "DictionaryDifferentType"), + Realms.Schema.Property.ObjectList("ListRemappedType", "__RemappedTypeObject", managedName: "ListRemappedType"), + Realms.Schema.Property.ObjectSet("SetRemappedType", "__RemappedTypeObject", managedName: "SetRemappedType"), + Realms.Schema.Property.ObjectDictionary("DictionaryRemappedType", "__RemappedTypeObject", managedName: "DictionaryRemappedType"), Realms.Schema.Property.Object("LinkDifferentType", "Person", managedName: "LinkDifferentType"), + Realms.Schema.Property.Object("LinkAnotherType", "Owner", managedName: "LinkAnotherType"), Realms.Schema.Property.Backlinks("Backlink", "TestNotificationObject", "LinkSameType", managedName: "Backlink"), }.Build(); @@ -98,12 +103,19 @@ void ISettableManagedAccessor.SetManagedAccessor(Realms.IRealmAccessor managedAc newAccessor.ListDifferentType.Clear(); newAccessor.SetDifferentType.Clear(); newAccessor.DictionaryDifferentType.Clear(); + newAccessor.ListRemappedType.Clear(); + newAccessor.SetRemappedType.Clear(); + newAccessor.DictionaryRemappedType.Clear(); } if (!skipDefaults || oldAccessor.StringProperty != default(string?)) { newAccessor.StringProperty = oldAccessor.StringProperty; } + if (!skipDefaults || oldAccessor.IntProperty != default(int)) + { + newAccessor.IntProperty = oldAccessor.IntProperty; + } Realms.CollectionExtensions.PopulateCollection(oldAccessor.ListSameType, newAccessor.ListSameType, update, skipDefaults); Realms.CollectionExtensions.PopulateCollection(oldAccessor.SetSameType, newAccessor.SetSameType, update, skipDefaults); Realms.CollectionExtensions.PopulateCollection(oldAccessor.DictionarySameType, newAccessor.DictionarySameType, update, skipDefaults); @@ -115,11 +127,19 @@ void ISettableManagedAccessor.SetManagedAccessor(Realms.IRealmAccessor managedAc Realms.CollectionExtensions.PopulateCollection(oldAccessor.ListDifferentType, newAccessor.ListDifferentType, update, skipDefaults); Realms.CollectionExtensions.PopulateCollection(oldAccessor.SetDifferentType, newAccessor.SetDifferentType, update, skipDefaults); Realms.CollectionExtensions.PopulateCollection(oldAccessor.DictionaryDifferentType, newAccessor.DictionaryDifferentType, update, skipDefaults); + Realms.CollectionExtensions.PopulateCollection(oldAccessor.ListRemappedType, newAccessor.ListRemappedType, update, skipDefaults); + Realms.CollectionExtensions.PopulateCollection(oldAccessor.SetRemappedType, newAccessor.SetRemappedType, update, skipDefaults); + Realms.CollectionExtensions.PopulateCollection(oldAccessor.DictionaryRemappedType, newAccessor.DictionaryRemappedType, update, skipDefaults); if (oldAccessor.LinkDifferentType != null && newAccessor.Realm != null) { newAccessor.Realm.Add(oldAccessor.LinkDifferentType, update); } newAccessor.LinkDifferentType = oldAccessor.LinkDifferentType; + if (oldAccessor.LinkAnotherType != null && newAccessor.Realm != null) + { + newAccessor.Realm.Add(oldAccessor.LinkAnotherType, update); + } + newAccessor.LinkAnotherType = oldAccessor.LinkAnotherType; } if (_propertyChanged != null) @@ -294,6 +314,8 @@ internal interface ITestNotificationObjectAccessor : Realms.IRealmAccessor { string? StringProperty { get; set; } + int IntProperty { get; set; } + System.Collections.Generic.IList ListSameType { get; } System.Collections.Generic.ISet SetSameType { get; } @@ -308,8 +330,16 @@ internal interface ITestNotificationObjectAccessor : Realms.IRealmAccessor System.Collections.Generic.IDictionary DictionaryDifferentType { get; } + System.Collections.Generic.IList ListRemappedType { get; } + + System.Collections.Generic.ISet SetRemappedType { get; } + + System.Collections.Generic.IDictionary DictionaryRemappedType { get; } + Realms.Tests.Database.Person? LinkDifferentType { get; set; } + Realms.Tests.Owner? LinkAnotherType { get; set; } + System.Linq.IQueryable Backlink { get; } } @@ -322,6 +352,12 @@ public string? StringProperty set => SetValue("StringProperty", value); } + public int IntProperty + { + get => (int)GetValue("IntProperty"); + set => SetValue("IntProperty", value); + } + private System.Collections.Generic.IList _listSameType = null!; public System.Collections.Generic.IList ListSameType { @@ -412,12 +448,60 @@ public System.Collections.Generic.ISet SetDifferen } } + private System.Collections.Generic.IList _listRemappedType = null!; + public System.Collections.Generic.IList ListRemappedType + { + get + { + if (_listRemappedType == null) + { + _listRemappedType = GetListValue("ListRemappedType"); + } + + return _listRemappedType; + } + } + + private System.Collections.Generic.ISet _setRemappedType = null!; + public System.Collections.Generic.ISet SetRemappedType + { + get + { + if (_setRemappedType == null) + { + _setRemappedType = GetSetValue("SetRemappedType"); + } + + return _setRemappedType; + } + } + + private System.Collections.Generic.IDictionary _dictionaryRemappedType = null!; + public System.Collections.Generic.IDictionary DictionaryRemappedType + { + get + { + if (_dictionaryRemappedType == null) + { + _dictionaryRemappedType = GetDictionaryValue("DictionaryRemappedType"); + } + + return _dictionaryRemappedType; + } + } + public Realms.Tests.Database.Person? LinkDifferentType { get => (Realms.Tests.Database.Person?)GetValue("LinkDifferentType"); set => SetValue("LinkDifferentType", value); } + public Realms.Tests.Owner? LinkAnotherType + { + get => (Realms.Tests.Owner?)GetValue("LinkAnotherType"); + set => SetValue("LinkAnotherType", value); + } + private System.Linq.IQueryable _backlink = null!; public System.Linq.IQueryable Backlink { @@ -449,6 +533,17 @@ public string? StringProperty } } + private int _intProperty; + public int IntProperty + { + get => _intProperty; + set + { + _intProperty = value; + RaisePropertyChanged("IntProperty"); + } + } + public System.Collections.Generic.IList ListSameType { get; } = new List(); public System.Collections.Generic.ISet SetSameType { get; } = new HashSet(RealmSet.Comparer); @@ -472,6 +567,12 @@ public Realms.Tests.Database.TestNotificationObject? LinkSameType public System.Collections.Generic.IDictionary DictionaryDifferentType { get; } = new Dictionary(); + public System.Collections.Generic.IList ListRemappedType { get; } = new List(); + + public System.Collections.Generic.ISet SetRemappedType { get; } = new HashSet(RealmSet.Comparer); + + public System.Collections.Generic.IDictionary DictionaryRemappedType { get; } = new Dictionary(); + private Realms.Tests.Database.Person? _linkDifferentType; public Realms.Tests.Database.Person? LinkDifferentType { @@ -483,6 +584,17 @@ public Realms.Tests.Database.Person? LinkDifferentType } } + private Realms.Tests.Owner? _linkAnotherType; + public Realms.Tests.Owner? LinkAnotherType + { + get => _linkAnotherType; + set + { + _linkAnotherType = value; + RaisePropertyChanged("LinkAnotherType"); + } + } + public System.Linq.IQueryable Backlink => throw new NotSupportedException("Using backlinks is only possible for managed(persisted) objects."); public TestNotificationObjectUnmanagedAccessor(Type objectType) : base(objectType) @@ -494,8 +606,10 @@ public override Realms.RealmValue GetValue(string propertyName) return propertyName switch { "StringProperty" => _stringProperty, + "IntProperty" => _intProperty, "LinkSameType" => _linkSameType, "LinkDifferentType" => _linkDifferentType, + "LinkAnotherType" => _linkAnotherType, "Backlink" => throw new NotSupportedException("Using backlinks is only possible for managed(persisted) objects."), _ => throw new MissingMemberException($"The object does not have a gettable Realm property with name {propertyName}"), }; @@ -508,12 +622,18 @@ public override void SetValue(string propertyName, Realms.RealmValue val) case "StringProperty": StringProperty = (string?)val; return; + case "IntProperty": + IntProperty = (int)val; + return; case "LinkSameType": LinkSameType = (Realms.Tests.Database.TestNotificationObject?)val; return; case "LinkDifferentType": LinkDifferentType = (Realms.Tests.Database.Person?)val; return; + case "LinkAnotherType": + LinkAnotherType = (Realms.Tests.Owner?)val; + return; default: throw new MissingMemberException($"The object does not have a settable Realm property with name {propertyName}"); } @@ -530,6 +650,7 @@ public override IList GetListValue(string propertyName) { "ListSameType" => (IList)ListSameType, "ListDifferentType" => (IList)ListDifferentType, + "ListRemappedType" => (IList)ListRemappedType, _ => throw new MissingMemberException($"The object does not have a Realm list property with name {propertyName}"), }; } @@ -540,6 +661,7 @@ public override ISet GetSetValue(string propertyName) { "SetSameType" => (ISet)SetSameType, "SetDifferentType" => (ISet)SetDifferentType, + "SetRemappedType" => (ISet)SetRemappedType, _ => throw new MissingMemberException($"The object does not have a Realm set property with name {propertyName}"), }; } @@ -550,6 +672,7 @@ public override IDictionary GetDictionaryValue(string pr { "DictionarySameType" => (IDictionary)DictionarySameType, "DictionaryDifferentType" => (IDictionary)DictionaryDifferentType, + "DictionaryRemappedType" => (IDictionary)DictionaryRemappedType, _ => throw new MissingMemberException($"The object does not have a Realm dictionary property with name {propertyName}"), }; } diff --git a/wrappers/src/dictionary_cs.cpp b/wrappers/src/dictionary_cs.cpp index ad74e036fd..1909198e71 100644 --- a/wrappers/src/dictionary_cs.cpp +++ b/wrappers/src/dictionary_cs.cpp @@ -170,13 +170,14 @@ extern "C" { delete dictionary; } - REALM_EXPORT ManagedNotificationTokenContext* realm_dictionary_add_notification_callback(object_store::Dictionary* dictionary, void* managed_dict, bool shallow, NativeException::Marshallable& ex) - + REALM_EXPORT ManagedNotificationTokenContext* realm_dictionary_add_notification_callback(object_store::Dictionary* dictionary, void* managed_dict, + key_path_collection_type type, void* managedCallback, realm_string_t* keypaths, size_t keypaths_len, NativeException::Marshallable& ex) { return handle_errors(ex, [=]() { - return subscribe_for_notifications(managed_dict, [dictionary, shallow](CollectionChangeCallback callback) { - return dictionary->add_notification_callback(callback, shallow ? std::make_optional(KeyPathArray()) : std::nullopt); - }, shallow); + auto keypath_array = build_keypath_array(dictionary, type, keypaths, keypaths_len); + return subscribe_for_notifications(managed_dict, [dictionary, keypath_array](CollectionChangeCallback callback) { + return dictionary->add_notification_callback(callback, keypath_array); + }, type, managedCallback); }); } @@ -187,7 +188,7 @@ extern "C" { context->managed_object = managed_dict; context->token = dictionary->add_key_based_notification_callback([context](DictionaryChangeSet changes) { if (changes.deletions.empty() && changes.insertions.empty() && changes.modifications.empty()) { - s_dictionary_notification_callback(context->managed_object, nullptr, false); + s_dictionary_notification_callback(context->managed_object, nullptr); } else { auto deletions = get_keys_vector(changes.deletions); @@ -200,7 +201,7 @@ extern "C" { modifications, }; - s_dictionary_notification_callback(context->managed_object, &marshallable_changes, false); + s_dictionary_notification_callback(context->managed_object, &marshallable_changes); } }, KeyPathArray()); diff --git a/wrappers/src/list_cs.cpp b/wrappers/src/list_cs.cpp index 1fef7762c3..b5ec43ba2f 100644 --- a/wrappers/src/list_cs.cpp +++ b/wrappers/src/list_cs.cpp @@ -212,12 +212,14 @@ REALM_EXPORT void list_destroy(List* list) delete list; } -REALM_EXPORT ManagedNotificationTokenContext* list_add_notification_callback(List* list, void* managed_list, bool shallow, NativeException::Marshallable& ex) +REALM_EXPORT ManagedNotificationTokenContext* list_add_notification_callback(List* list, void* managed_list, + key_path_collection_type type, void* managedCallback, realm_string_t* keypaths, size_t keypaths_len, NativeException::Marshallable& ex) { return handle_errors(ex, [=]() { - return subscribe_for_notifications(managed_list, [list, shallow](CollectionChangeCallback callback) { - return list->add_notification_callback(callback, shallow ? std::make_optional(KeyPathArray()) : std::nullopt); - }, shallow); + auto keypath_array = build_keypath_array(list, type, keypaths, keypaths_len); + return subscribe_for_notifications(managed_list, [list, keypath_array](CollectionChangeCallback callback) { + return list->add_notification_callback(callback, keypath_array); + }, type, managedCallback); }); } diff --git a/wrappers/src/marshalling.hpp b/wrappers/src/marshalling.hpp index 4470f76331..c8c6fa69e0 100644 --- a/wrappers/src/marshalling.hpp +++ b/wrappers/src/marshalling.hpp @@ -103,6 +103,12 @@ enum class realm_value_type : uint8_t { RLM_TYPE_UUID, }; +enum class key_path_collection_type : uint8_t { + DEFAULT, + SHALLOW, + FULL +}; + enum class query_argument_type : uint8_t { PRIMITIVE, BOX, diff --git a/wrappers/src/notifications_cs.hpp b/wrappers/src/notifications_cs.hpp index fa47beb69f..158ea4c010 100644 --- a/wrappers/src/notifications_cs.hpp +++ b/wrappers/src/notifications_cs.hpp @@ -52,8 +52,8 @@ struct ManagedNotificationTokenContext { ObjectSchema* schema; }; -using ObjectNotificationCallbackT = void(void* managed_results, MarshallableCollectionChangeSet*, bool shallow); -using DictionaryNotificationCallbackT = void(void* managed_results, MarshallableDictionaryChangeSet*, bool shallow); +using ObjectNotificationCallbackT = void(void* managed_results, MarshallableCollectionChangeSet*, key_path_collection_type type, void* callback); +using DictionaryNotificationCallbackT = void(void* managed_results, MarshallableDictionaryChangeSet*); extern std::function s_object_notification_callback; extern std::function s_dictionary_notification_callback; @@ -93,9 +93,10 @@ static inline std::vector get_keys_vector(const std::vectormanaged_object, nullptr, shallow); + s_object_notification_callback(context->managed_object, nullptr, type, callback); } else { auto deletions = get_indexes_vector(changes.deletions); @@ -106,9 +107,9 @@ static inline void handle_changes(ManagedNotificationTokenContext* context, Coll std::vector properties; for (auto& pair : changes.columns) { - if (!pair.second.empty()) { - properties.emplace_back(get_property_index(context->schema, ColKey(pair.first))); - } + if (!pair.second.empty()) { + properties.emplace_back(get_property_index(context->schema, ColKey(pair.first))); + } } MarshallableCollectionChangeSet marshallable_changes{ @@ -121,23 +122,62 @@ static inline void handle_changes(ManagedNotificationTokenContext* context, Coll properties }; - s_object_notification_callback(context->managed_object, &marshallable_changes, shallow); + s_object_notification_callback(context->managed_object, &marshallable_changes, type, callback); } } - template -inline ManagedNotificationTokenContext* subscribe_for_notifications(void* managed_object, Subscriber subscriber, bool shallow, ObjectSchema* schema = nullptr) +inline ManagedNotificationTokenContext* subscribe_for_notifications(void* managed_object, Subscriber subscriber, + key_path_collection_type type, void* callback = nullptr, ObjectSchema* schema = nullptr) { auto context = new ManagedNotificationTokenContext(); context->managed_object = managed_object; context->schema = schema; - context->token = subscriber([context, shallow](CollectionChangeSet changes) { - handle_changes(context, changes, shallow); + context->token = subscriber([context, type, callback](CollectionChangeSet changes) { + handle_changes(context, changes, type, callback); }); return context; } + +static inline std::optional build_keypath_array_impl(const SharedRealm& realm, StringData class_name, key_path_collection_type type, realm_string_t* keypaths, size_t len) { + std::optional keypath_array; + + switch (type) { + case key_path_collection_type::FULL: { + std::vector keypaths_vector; + for (size_t i = 0; i < len; i++) { + keypaths_vector.push_back(capi_to_std(keypaths[i])); + } + keypath_array = realm->create_key_path_array(class_name, keypaths_vector); + break; + } + case key_path_collection_type::SHALLOW: + keypath_array = std::make_optional(KeyPathArray()); + break; + case key_path_collection_type::DEFAULT: + keypath_array = std::nullopt; + break; + default: + REALM_UNREACHABLE(); + break; + } + + return keypath_array; +} + +static inline std::optional build_keypath_array(Results* results, key_path_collection_type type, + realm_string_t* keypaths, size_t keypaths_len) { + const auto& class_name = type == key_path_collection_type::FULL ? results->get_table()->get_class_name() : ""; + return build_keypath_array_impl(results->get_realm(), class_name, type, keypaths, keypaths_len); +} + +static inline std::optional build_keypath_array(object_store::Collection* collection, key_path_collection_type type, + realm_string_t* keypaths, size_t keypaths_len) { + const auto& class_name = type == key_path_collection_type::FULL ? collection->get_object_schema().name : ""; + return build_keypath_array_impl(collection->get_realm(), class_name, type, keypaths, keypaths_len); +} + } #endif // NOTIFICATIONS_CS_HPP diff --git a/wrappers/src/object_cs.cpp b/wrappers/src/object_cs.cpp index d9259f5334..d7f7fed587 100644 --- a/wrappers/src/object_cs.cpp +++ b/wrappers/src/object_cs.cpp @@ -286,7 +286,7 @@ extern "C" { return subscribe_for_notifications(managed_object, [&](CollectionChangeCallback callback) { auto keyPaths = construct_key_path_array(object->get_object_schema()); return object->add_notification_callback(callback, keyPaths); - }, true, new ObjectSchema(object->get_object_schema())); + }, key_path_collection_type::SHALLOW, nullptr, new ObjectSchema(object->get_object_schema())); }); } diff --git a/wrappers/src/results_cs.cpp b/wrappers/src/results_cs.cpp index f1027d9693..4d72b6c669 100644 --- a/wrappers/src/results_cs.cpp +++ b/wrappers/src/results_cs.cpp @@ -91,12 +91,14 @@ REALM_EXPORT size_t results_count(Results& results, NativeException::Marshallabl }); } -REALM_EXPORT ManagedNotificationTokenContext* results_add_notification_callback(Results* results, void* managed_results, bool shallow, NativeException::Marshallable& ex) +REALM_EXPORT ManagedNotificationTokenContext* results_add_notification_callback(Results* results, void* managed_results, + key_path_collection_type type, void* managedCallback, realm_string_t* keypaths, size_t keypaths_len, NativeException::Marshallable& ex) { return handle_errors(ex, [=]() { - return subscribe_for_notifications(managed_results, [results, shallow](CollectionChangeCallback callback) { - return results->add_notification_callback(callback, shallow ? std::make_optional(KeyPathArray()) : std::nullopt); - }, shallow); + auto keypath_array = build_keypath_array(results, type, keypaths, keypaths_len); + return subscribe_for_notifications(managed_results, [results, keypath_array](CollectionChangeCallback callback) { + return results->add_notification_callback(callback, keypath_array); + }, type, managedCallback); }); } diff --git a/wrappers/src/set_cs.cpp b/wrappers/src/set_cs.cpp index a67fc23b9f..3977fcd82d 100644 --- a/wrappers/src/set_cs.cpp +++ b/wrappers/src/set_cs.cpp @@ -156,12 +156,14 @@ REALM_EXPORT void realm_set_destroy(object_store::Set* set) delete set; } -REALM_EXPORT ManagedNotificationTokenContext* realm_set_add_notification_callback(object_store::Set* set, void* managed_set, bool shallow, NativeException::Marshallable& ex) +REALM_EXPORT ManagedNotificationTokenContext* realm_set_add_notification_callback(object_store::Set* set, void* managed_set, + key_path_collection_type type, void* managedCallback, realm_string_t* keypaths, size_t keypaths_len, NativeException::Marshallable& ex) { return handle_errors(ex, [=]() { - return subscribe_for_notifications(managed_set, [set, shallow](CollectionChangeCallback callback) { - return set->add_notification_callback(callback, shallow ? std::make_optional(KeyPathArray()) : std::nullopt); - }, shallow); + auto keypath_array = build_keypath_array(set, type, keypaths, keypaths_len); + return subscribe_for_notifications(managed_set, [set, keypath_array](CollectionChangeCallback callback) { + return set->add_notification_callback(callback, keypath_array); + }, type, managedCallback); }); }