Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for expressions in keypath building #3518

Merged
merged 13 commits into from
Mar 6, 2024
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@
* 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<T>`;
- 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>();
Expand All @@ -20,6 +19,7 @@

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

query.SubscribeForNotifications(NotificationCallback, kpc);
Expand Down
59 changes: 59 additions & 0 deletions Realm/Realm/DatabaseTypes/KeyPathCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace Realms;

Expand Down Expand Up @@ -78,6 +80,19 @@
return new KeyPathsCollection(KeyPathsCollectionType.Explicit, paths);
}

/// <summary>
/// Builds a <see cref="KeyPathsCollection"/> from an array of <see cref="Expression"/>.
/// Each of the expressions must represent the path to a <see cref="IRealmObject">realm object</see> property, eventually chained.
/// </summary>
/// <typeparam name="T">The <see cref="IRealmObject">realm object.</see> type.</typeparam>
/// <param name="expressions">The array of <see cref="Expression"/> to use for the <see cref="KeyPathsCollection"/>.</param>
/// <returns>The <see cref="KeyPathsCollection"/> built from the input array of <see cref="Expression"/>.</returns>
public static KeyPathsCollection Of<T>(params Expression<Func<T, object>>[] expressions)
papafe marked this conversation as resolved.
Show resolved Hide resolved
where T : IRealmObject
{
return Of(expressions.Select(KeyPath.For).ToArray());
}

/// <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.
Expand Down Expand Up @@ -125,10 +140,14 @@
}
}

//TODO Fix changelog

Check warning on line 143 in Realm/Realm/DatabaseTypes/KeyPathCollection.cs

View workflow job for this annotation

GitHub Actions / Verify TODOs

Realm/Realm/DatabaseTypes/KeyPathCollection.cs#L143

TODO entry doesn't have a link to Github issue or Jira ticket Fix changelog

Check warning on line 143 in Realm/Realm/DatabaseTypes/KeyPathCollection.cs

View workflow job for this annotation

GitHub Actions / Test / Weaver (ubuntu-latest, linux-x64)

Check warning on line 143 in Realm/Realm/DatabaseTypes/KeyPathCollection.cs

View workflow job for this annotation

GitHub Actions / Test / Weaver (ubuntu-latest, linux-x64)

Check warning on line 143 in Realm/Realm/DatabaseTypes/KeyPathCollection.cs

View workflow job for this annotation

GitHub Actions / Test / Weaver (ubuntu-latest, linux-x64)

Check warning on line 143 in Realm/Realm/DatabaseTypes/KeyPathCollection.cs

View workflow job for this annotation

GitHub Actions / Test / Source Generation

Check warning on line 143 in Realm/Realm/DatabaseTypes/KeyPathCollection.cs

View workflow job for this annotation

GitHub Actions / Test / Weaver (windows-latest, win-x64)

Check warning on line 143 in Realm/Realm/DatabaseTypes/KeyPathCollection.cs

View workflow job for this annotation

GitHub Actions / Test / Weaver (windows-latest, win-x64)

Check warning on line 143 in Realm/Realm/DatabaseTypes/KeyPathCollection.cs

View workflow job for this annotation

GitHub Actions / Test / Weaver (windows-latest, win-x64)

Check warning on line 143 in Realm/Realm/DatabaseTypes/KeyPathCollection.cs

View workflow job for this annotation

GitHub Actions / Test / Weaver (macos-latest, osx-x64)

Check warning on line 143 in Realm/Realm/DatabaseTypes/KeyPathCollection.cs

View workflow job for this annotation

GitHub Actions / Test / Weaver (macos-latest, osx-x64)

Check warning on line 143 in Realm/Realm/DatabaseTypes/KeyPathCollection.cs

View workflow job for this annotation

GitHub Actions / Test / Weaver (macos-latest, osx-x64)

Check warning on line 143 in Realm/Realm/DatabaseTypes/KeyPathCollection.cs

View workflow job for this annotation

GitHub Actions / Package / NuGet

Check warning on line 143 in Realm/Realm/DatabaseTypes/KeyPathCollection.cs

View workflow job for this annotation

GitHub Actions / Package / NuGet

Check warning on line 143 in Realm/Realm/DatabaseTypes/KeyPathCollection.cs

View workflow job for this annotation

GitHub Actions / Analyze C#

Check warning on line 143 in Realm/Realm/DatabaseTypes/KeyPathCollection.cs

View workflow job for this annotation

GitHub Actions / Analyze C#

Check warning on line 143 in Realm/Realm/DatabaseTypes/KeyPathCollection.cs

View workflow job for this annotation

GitHub Actions / Test / Code Coverage

Single line comment should begin with a space. (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1005.md) [/home/runner/work/realm-dotnet/realm-dotnet/Realm/Realm/Realm.csproj::TargetFramework=net6.0]

/// <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").
/// A <see cref="KeyPath"/> can also be built using the <see cref="For{T}(Expression{Func{T, object}})"/> method, that creates the <see cref="KeyPath"/> corresponding
/// to the property path represented by the input expression.
/// </summary>
public readonly struct KeyPath
{
Expand All @@ -139,8 +158,48 @@
Path = path;
}

/// <summary>
/// Creates a <see cref="KeyPath"/> from an <see cref="Expression"/> that specifies a property path for a given <see cref="IRealmObject">realm object</see> type.
/// </summary>
/// <typeparam name="T">The type of the <see cref="IRealmObject">realm object.</see>.</typeparam>
/// <param name="expression">The expression specifying the path to the property.</param>
/// <returns>A <see cref="KeyPath"/> representing the full path to the specified property.</returns>
/// <example>
/// <code>
/// var keyPath = KeyPath.For&lt;Person&gt;(p => p.Dog.Name);
/// </code>
/// </example>
public static KeyPath For<T>(Expression<Func<T, object>> expression)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the For name something from Swift/ObjC? I guess I understand where it's coming from, but it's also kind of unusual for a .NET API name.

Copy link
Contributor Author

@papafe papafe Mar 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it's something I thought made sense for this case 😁
I also thought it was following a similar "style" of KeyPathCollection.Of. Do you have any other name suggestion? I would much more prefer to have a static method than using the constructor to be honest.

Copy link
Contributor Author

@papafe papafe Mar 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nirinchev, do you have any suggestion here about an alternative name?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it'd be impossible to use the ctor here, because we need the generic arg. Perhaps it's a bit verbose, but how about FromExpression?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it sounds reasonable :)

papafe marked this conversation as resolved.
Show resolved Hide resolved
where T : IRealmObject
{
if (expression is null)
{
throw new ArgumentException("The input expression cannot be null");
papafe marked this conversation as resolved.
Show resolved Hide resolved
}

return new KeyPath(GetFullPath(expression.Body));
papafe marked this conversation as resolved.
Show resolved Hide resolved
}

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

private static string GetFullPath(Expression expression)
{
if (expression is MemberExpression memberExpression //// Either field or property expression
&& memberExpression.Expression is not null //// Filtering out static members
&& memberExpression.Member is PropertyInfo) //// Filtering for property expressions only
{
var subPath = GetFullPath(memberExpression.Expression);
return string.IsNullOrEmpty(subPath) ? memberExpression.Member.Name : $"{subPath}.{memberExpression.Member.Name}";
}
else if (expression is ParameterExpression)
{
// This is the parameter of the expression, nothing to add
return string.Empty;
}

throw new ArgumentException("The input expression is not a path to a property");
papafe marked this conversation as resolved.
Show resolved Hide resolved
}

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

Expand Down
53 changes: 53 additions & 0 deletions Tests/Realm.Tests/Database/NotificationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.For<TestNotificationObject>(t => t.ListSameType);
Assert.That(keyPath.Path, Is.EqualTo("ListSameType"));

keyPath = KeyPath.For<TestNotificationObject>(t => t.LinkAnotherType!.DictOfDogs);
Assert.That(keyPath.Path, Is.EqualTo("LinkAnotherType.DictOfDogs"));
}

[Test]
public void KeyPath_WithInvalidExpressions_ThrowsException()
{
Assert.That(() => KeyPath.For<TestNotificationObject>(t => t.Equals(this)),
Throws.Exception.TypeOf<ArgumentException>().With.Message.EqualTo("The input expression is not a path to a property"));

Assert.That(() => KeyPath.For<TestNotificationObject>(null!),
Throws.Exception.TypeOf<ArgumentException>().With.Message.EqualTo("The input expression cannot be null"));
}

[Test]
public void KeyPathsCollection_CanBeBuiltInDifferentWays()
{
Expand Down Expand Up @@ -1710,6 +1732,37 @@ void AssertKeyPathsCollectionCorrectness(KeyPathsCollection k, IEnumerable<strin
}
}

[Test]
public void KeyPathsCollection_CanBeBuiltFromExpressions()
{
KeyPathsCollection kpc;

var expected = new List<string> { "ListSameType", "LinkAnotherType.DictOfDogs" };

kpc = KeyPathsCollection.Of<TestNotificationObject>(t => t.ListSameType, t => t.LinkAnotherType!.DictOfDogs);
papafe marked this conversation as resolved.
Show resolved Hide resolved
AssertKeyPathsCollectionCorrectness(kpc, expected);

kpc = KeyPathsCollection.Of(KeyPath.For<TestNotificationObject>(t => t.ListSameType),
KeyPath.For<TestNotificationObject>(t => t.LinkAnotherType!.DictOfDogs));
AssertKeyPathsCollectionCorrectness(kpc, expected);

void AssertKeyPathsCollectionCorrectness(KeyPathsCollection k, IEnumerable<string> expected)
papafe marked this conversation as resolved.
Show resolved Hide resolved
{
Assert.That(k.Type, Is.EqualTo(KeyPathsCollectionType.Explicit));
Assert.That(k.GetStrings(), Is.EqualTo(expected));
}
}

[Test]
public void KeyPathsCollection_WithInvalidExpressions_ThrowsExceptions()
{
Assert.That(() => KeyPathsCollection.Of<TestNotificationObject>(t => t.ListSameType, null!),
Throws.Exception.TypeOf<ArgumentException>().With.Message.EqualTo("The input expression cannot be null"));

Assert.That(() => KeyPathsCollection.Of<TestNotificationObject>(t => t.Equals(this)),
Throws.Exception.TypeOf<ArgumentException>().With.Message.EqualTo("The input expression is not a path to a property"));
}

[Test]
public void SubscribeWithKeypaths_AnyKeypath_RaisesNotificationsForResults()
{
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading