Skip to content

Commit

Permalink
Add keypath filtering to notifications (#3501)
Browse files Browse the repository at this point in the history
* A comment before pausing this for now

* Stub

* Added basic tests and keypath to extension mehtods

* Returned to initial implementation

* Stubs

* Updated core to v13.25.1

* Updated changelog

* Stub

* Stub

* Adding tests

* Fixed unsubuscription

* Added new tests

* Small cleaning up

* Added tests

* Added lots of tests

* Added more tests

* Moved tests down

* Slight improvement to handlers

* Corrected order of operations

* Improved docs

* Added tests

* Fixes

* Improved API

* Improved API and corrected tests

* Fixing API

* Removed test

* Small corrections

* Stub

* First step internal API unification

* almost finished unification

* Improved API unification

* Simplified API and added tests for collection construction [skip-ci]

* Small corrections

* Simplified with the use of marshaled vector

* Moved keypaths related to another file

* Small corrections [skip-ci]

* Small corrections

* Divided implementation

* Fixed API for collections

* Corrected tests

* Added missing tests

* Small fix

* Removed comment

* Fixes following PR

* Apply suggestions from code review

Co-authored-by: Nikola Irinchev <irinchev@me.com>

* Various PR comments fixes

* Various corrections according to PR

* Updated changelog and docs

* Fixes

* Changed marshaling to go around .net framework limitation

* Proposed delegate change (#3512)

* Small fix

* Update CHANGELOG.md

* Update Realm/Realm/DatabaseTypes/RealmCollectionBase.cs

---------

Co-authored-by: Nikola Irinchev <irinchev@me.com>
  • Loading branch information
papafe and nirinchev authored Feb 9, 2024
1 parent fedcd8c commit 9495446
Show file tree
Hide file tree
Showing 30 changed files with 3,408 additions and 142 deletions.
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Person>();

KeyPathsCollection kpc;

//Equivalent declarations
kpc = KeyPathsCollection.Of("Email", "Name");
kpc = new List<KeyPath> {"Email", "Name"};

query.SubscribeForNotifications(NotificationCallback, kpc);
```
(PR [#3501 ](https://github.com/realm/realm-dotnet/pull/3501))
* Added the `MongoClient.GetCollection<T>` 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
Expand Down
4 changes: 3 additions & 1 deletion Realm/Realm/DatabaseTypes/Accessors/ManagedAccessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,10 @@ public void UnsubscribeFromNotifications()
}

/// <inheritdoc/>
void INotifiable<NotifiableObjectHandleBase.CollectionChangeSet>.NotifyCallbacks(NotifiableObjectHandleBase.CollectionChangeSet? changes, bool shallow)
void INotifiable<NotifiableObjectHandleBase.CollectionChangeSet>.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)
Expand Down
5 changes: 3 additions & 2 deletions Realm/Realm/DatabaseTypes/INotifiable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ internal interface INotifiable<TChangeset>
/// Method called when there are changes to report for that object.
/// </summary>
/// <param name="changes">The changes that occurred.</param>
/// <param name="shallow">Whether the changes are coming from a shallow notifier or not.</param>
void NotifyCallbacks(TChangeset? changes, bool shallow);
/// <param name="type">The type of the key paths collection related to the notification.</param>
/// <param name="callback">The eventual callback to call for the notification (if type == Explicit).</param>
void NotifyCallbacks(TChangeset? changes, KeyPathsCollectionType type, Delegate? callback);
}

internal class NotificationToken<TCallback> : IDisposable
Expand Down
166 changes: 166 additions & 0 deletions Realm/Realm/DatabaseTypes/KeyPathCollection.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Represents a collection of <see cref="KeyPath"/> that can be used when subscribing to notifications with <see cref="IRealmCollection{T}.SubscribeForNotifications(Realms.NotificationCallbackDelegate{T}, Realms.KeyPathsCollection?)"/>.
/// <remarks>
/// <para>
/// A <see cref="KeyPathsCollection"/> can be obtained by:
/// <list type="bullet">
/// <item>
/// <description>building it explicitly by using the method <see cref="KeyPathsCollection.Of(Realms.KeyPath[])"/>;</description>
/// </item>
/// <item>
/// <description>building it implicitly with the conversion from a <see cref="List{T}"/> or array of <see cref="KeyPath"/> or strings;</description>
/// </item>
/// <item>
/// <description>getting one of the static values <see cref="Full"/> and <see cref="Shallow"/> for full and shallow notifications respectively.</description>
/// </item>
/// </list>
/// </para>
/// </remarks>
/// </summary>
public class KeyPathsCollection : IEnumerable<KeyPath>
{
private IEnumerable<KeyPath> _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<KeyPath>? 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<KeyPath>();

VerifyKeyPaths();
}

/// <summary>
/// Builds a <see cref="KeyPathsCollection"/> from an array of <see cref="KeyPath"/>.
/// </summary>
/// <param name="paths">The array of <see cref="KeyPath"/> to use for the <see cref="KeyPathsCollection"/>.</param>
/// <returns>The <see cref="KeyPathsCollection"/> built from the input array of <see cref="KeyPath"/>.</returns>
public static KeyPathsCollection Of(params KeyPath[] paths)
{
if (paths.Length == 0)
{
return new KeyPathsCollection(KeyPathsCollectionType.Shallow);
}

return new KeyPathsCollection(KeyPathsCollectionType.Explicit, paths);
}

/// <summary>
/// Gets a <see cref="KeyPathsCollection"/> 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.
/// </summary>
public static KeyPathsCollection Shallow => _shallow;

/// <summary>
/// Gets a <see cref="KeyPathsCollection"/> value for full notifications, for which changes to all top-level properties and 4 nested levels will raise a notification. This is the default <see cref="KeyPathsCollection"/> value.
/// </summary>
public static KeyPathsCollection Full => _full;

public static implicit operator KeyPathsCollection(List<string> paths) =>
new(KeyPathsCollectionType.Explicit, paths.Select(path => (KeyPath)path).ToArray());

public static implicit operator KeyPathsCollection(List<KeyPath> 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<string> 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");
}
}
}

/// <inheritdoc/>
public IEnumerator<KeyPath> GetEnumerator()
{
return _collection.GetEnumerator();
}

/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}

/// <summary>
/// Represents a key path that can be used as a part of a <see cref="KeyPathsCollection"/> when subscribing for notifications.
/// A <see cref="KeyPath"/> 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").
/// </summary>
public readonly struct KeyPath
{
internal string Path { get; }

private KeyPath(string path)
{
Path = path;
}

public static implicit operator KeyPath(string s) => new(s);

/// <inheritdoc/>
public override bool Equals(object? obj) => obj is KeyPath path && Path == path.Path;

/// <inheritdoc/>
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
}
Loading

0 comments on commit 9495446

Please sign in to comment.