diff --git a/CHANGELOG.md b/CHANGELOG.md index 55cea6f908..d87f368aed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,19 +14,26 @@ * 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 explicitly by using the methods `KeyPathsCollection.Of` or `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. + A `KeyPath` can be obtained by implicit conversion from a string or built from an expression using the `KeyPath.ForExpression` method. + For example: ```csharp var query = realm.All(); + KeyPath kp1 = "Email"; + KeyPath kp2 = KeyPath.ForExpression(p => p.Name); + KeyPathsCollection kpc; //Equivalent declarations kpc = KeyPathsCollection.Of("Email", "Name"); + kpc = KeyPathsCollection.Of(p => p.Email, p => p.Name); kpc = new List {"Email", "Name"}; + kpc = new List {kp1, kp2}; query.SubscribeForNotifications(NotificationCallback, kpc); ``` diff --git a/Realm/Realm/DatabaseTypes/KeyPathCollection.cs b/Realm/Realm/DatabaseTypes/KeyPathCollection.cs index d33aef4f93..57e63ffe80 100644 --- a/Realm/Realm/DatabaseTypes/KeyPathCollection.cs +++ b/Realm/Realm/DatabaseTypes/KeyPathCollection.cs @@ -21,6 +21,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Linq.Expressions; +using System.Reflection; namespace Realms; @@ -78,6 +80,19 @@ public static KeyPathsCollection Of(params KeyPath[] paths) return new KeyPathsCollection(KeyPathsCollectionType.Explicit, paths); } + /// + /// Builds a from an array of . + /// Each of the expressions must represent the path to a realm object property, eventually chained. + /// + /// The realm object. type. + /// The array of to use for the . + /// The built from the input array of . + public static KeyPathsCollection Of(params Expression>[] expressions) + where T : IRealmObject + { + return Of(expressions.Select(KeyPath.ForExpression).ToArray()); + } + /// /// 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. @@ -129,6 +144,8 @@ IEnumerator IEnumerable.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"). +/// A can also be built using the method, that creates the corresponding +/// to the property path represented by the input expression. /// public readonly struct KeyPath { @@ -139,8 +156,44 @@ private KeyPath(string path) Path = path; } + /// + /// Creates a from an that specifies a property path for a given realm object type. + /// + /// The type of the realm object.. + /// The expression specifying the path to the property. + /// A representing the full path to the specified property. + /// + /// + /// var keyPath = KeyPath.For<Person>(p => p.Dog.Name); + /// + /// + public static KeyPath ForExpression(Expression> expression) + where T : IRealmObject + { + if (expression is null) + { + throw new ArgumentException("The input expression cannot be null", nameof(expression)); + } + + return new(GetFullPath(expression.Body)); + } + public static implicit operator KeyPath(string s) => new(s); + private static string GetFullPath(Expression expression) + { + return expression switch + { + // MemberExpression: field or property expression; + // Expression == null for static members; + // Member: PropertyInfo to filter out field access + MemberExpression { Expression: { } innerExpression, Member: PropertyInfo pi } => + innerExpression is ParameterExpression ? pi.Name : $"{GetFullPath(innerExpression)}.{pi.Name}", + ParameterExpression => string.Empty, + _ => throw new ArgumentException("The input expression is not a path to a property"), + }; + } + /// public override bool Equals(object? obj) => obj is KeyPath path && Path == path.Path; diff --git a/Tests/Realm.Tests/Database/NotificationTests.cs b/Tests/Realm.Tests/Database/NotificationTests.cs index 33a21b73c2..96e99f754e 100644 --- a/Tests/Realm.Tests/Database/NotificationTests.cs +++ b/Tests/Realm.Tests/Database/NotificationTests.cs @@ -1654,6 +1654,28 @@ public void KeyPath_ImplicitOperator_CorrectlyConvertsFromString() Assert.That(keyPath.Path, Is.EqualTo("test")); } + [Test] + public void KeyPath_CanBeBuiltFromExpressions() + { + KeyPath keyPath; + + keyPath = KeyPath.ForExpression(t => t.ListSameType); + Assert.That(keyPath.Path, Is.EqualTo("ListSameType")); + + keyPath = KeyPath.ForExpression(t => t.LinkAnotherType!.DictOfDogs); + Assert.That(keyPath.Path, Is.EqualTo("LinkAnotherType.DictOfDogs")); + } + + [Test] + public void KeyPath_WithInvalidExpressions_ThrowsException() + { + Assert.That(() => KeyPath.ForExpression(t => t.Equals(this)), + Throws.Exception.TypeOf().With.Message.Contains("The input expression is not a path to a property")); + + Assert.That(() => KeyPath.ForExpression(null!), + Throws.Exception.TypeOf().With.Message.Contains("The input expression cannot be null")); + } + [Test] public void KeyPathsCollection_CanBeBuiltInDifferentWays() { @@ -1710,6 +1732,35 @@ void AssertKeyPathsCollectionCorrectness(KeyPathsCollection k, IEnumerable { "ListSameType", "LinkAnotherType.DictOfDogs" }; + + var kpc = KeyPathsCollection.Of(t => t.ListSameType, t => t.LinkAnotherType!.DictOfDogs); + AssertKeyPathsCollectionCorrectness(kpc, expected); + + kpc = KeyPathsCollection.Of(KeyPath.ForExpression(t => t.ListSameType), + KeyPath.ForExpression(t => t.LinkAnotherType!.DictOfDogs)); + AssertKeyPathsCollectionCorrectness(kpc, expected); + + void AssertKeyPathsCollectionCorrectness(KeyPathsCollection k, IEnumerable expectedStrings) + { + Assert.That(k.Type, Is.EqualTo(KeyPathsCollectionType.Explicit)); + Assert.That(k.GetStrings(), Is.EqualTo(expectedStrings)); + } + } + + [Test] + public void KeyPathsCollection_WithInvalidExpressions_ThrowsExceptions() + { + Assert.That(() => KeyPathsCollection.Of(t => t.ListSameType, null!), + Throws.Exception.TypeOf().With.Message.Contains("The input expression cannot be null")); + + Assert.That(() => KeyPathsCollection.Of(t => t.Equals(this)), + Throws.Exception.TypeOf().With.Message.Contains("The input expression is not a path to a property")); + } + [Test] public void SubscribeWithKeypaths_AnyKeypath_RaisesNotificationsForResults() {