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

Feature: MergeManyChangeSets with Parent Item Comparison #750

Conversation

dwcullop
Copy link
Member

@dwcullop dwcullop commented Nov 5, 2023

Description

This PR adds overloads to the Cache verion of MergeManyChangeSets that allow an additional IComparer parameter that operates on the source type so that priority of the resulting changeset can be based on factors in the source item, and not just child items.

Two IComparer instance can be provided so that child-based comparisons can be done as a fallback when the source comparison evaluates as the same.

Examples

Say there's a mobile app that aggregates movie review web sites and has these basic interfaces:

interface IMovieReview
{
    Guid MovieId { get; }
    Guid CriticId { get; }
    double Rating { get; }
    DateTimeOffset DatePublished { get; }
    string Title { get; }
    Uri Link { get; }
}

interface IMovieCritic : INotifyPropertyChanged
{
    Guid Id { get; }
    string Name { get; }
    double Popularity { get; }
    IObservableCache<IMovieReview, Guid> Reviews { get; }
}

interface IMovieWebService
{
    IObservableCache<IMovieCritic, Guid> AllCritics { get; }
}

public static class MovieWebApi
{
    public IMovieWebService Service => GetTheWebServiceSomehow();
}

And the goal is to create an observable changeset that has a review for each movie, but when the movie has been reviewed by multiple critics, it should emit the review from the most popular critic. This is problematic because the Internet is fickle and popularity can change. How can you do such a thing? DynamicData and Rx will come to the rescue.

With this PR, all such a changeset can be created with just a few lines of code:

// Need a comparer for Critics
class CriticComparer : IComparer<IMovieCritic>
{
    // Not a production-quality implementation, but you get the idea
    public int Compare(IMovieCritic x, IMovieCritic y) => y.Popularity.CompareTo(x.Popularity);
}

// Create the observable cache of movie reviews for every movie using the review for the most popular critic to have rated that movie
using var movieReviewCache = MovieWebApi.Service.AllCritics.Connect()
                                .AutoRefresh(critic => critic.Popularity)
                                .MergeManyChangeSets((critic, id) => critic.Reviews.Connect(), new CriticComparer())
                                .AsObservableCache();

That's all there is to it! And now movieReviewCache will always contain the review of each movie from the most popular critic to have reviewed that movie. For example, it will exhibit some of the following behaviors:

  1. If a new critic is added, any movies they've reviewed will be added (unless they've already been reviewed by a more popular reviewer)
  2. If a critic is removed, any movies only reviewed by them will be removed. Other movies will be Updated to the review from next most-popular reviewer.
  3. If any critic is the first to review a movie, it will be added. When another critic reviews the same movie, their review may or may not replace the first one, depending on which reviewer is the most popular.
  4. If the most popular critic to review a given movie removes their review, it will be updated to the next most popular critic to have reviewed that movie. If no one else has a review for that movie, it will be removed from the cache instead.
  5. If the Popularity of the Reviewer changes, reviews will be added/updated depending on how many more popular reviewers have reviewed the same movies such that the result cache always contains the review from the most popular reviewer.

Another Example

At this point, you might be wondering "What if two Critics have the same popularity and rate the same movie? I need it to always include the Best/First/Most Recent/etc review in those cases."

Well, there's an overload for that as well. You just need a comparer for the child objects that will be used as a fallback when two parents compare to be the same.

// Need a comparer for Reviews (sorts earliest publishing dates first)
class FirstPublishedReviewComparer : IComparer<IMovieReview>
{
    // Not a production-quality implementation, but you get the idea
    public int Compare(IMovieReview x, IMovieReview y) => x.DatePublished.CompareTo(y.DatePublished);
}

// Create the observable cache of movie reviews for every movie using the review for the most popular critic to have rated that movie (going by publish date when multiple reviewers are tied)
using var movieReviewCache = MovieWebApi.Service.AllCritics.Connect()
                                .AutoRefresh(critic => critic.Popularity)
                                .MergeManyChangeSets((critic, id) => critic.Reviews.Connect(), new CriticComparer(), new FirstPublishedReviewComparer())
                                .AsObservableCache();

Testing Improvements

  • Moves the data classes Market and MarketPrice to be part of Domain namespace promote re-use
  • Adds extension methods for creating tests

@dwcullop dwcullop marked this pull request as ready for review November 6, 2023 20:11
Copy link

codecov bot commented Nov 6, 2023

Codecov Report

Merging #750 (cf5f071) into main (0434568) will increase coverage by 0.13%.
The diff coverage is 77.39%.

@@            Coverage Diff             @@
##             main     #750      +/-   ##
==========================================
+ Coverage   64.61%   64.74%   +0.13%     
==========================================
  Files         225      226       +1     
  Lines       11354    11464     +110     
  Branches     2318     2335      +17     
==========================================
+ Hits         7336     7422      +86     
- Misses       3067     3085      +18     
- Partials      951      957       +6     
Files Coverage Δ
...ynamicData/Cache/Internal/ChangeSetMergeTracker.cs 75.82% <61.11%> (-2.10%) ⬇️
src/DynamicData/Cache/ObservableCacheEx.cs 34.84% <53.33%> (+0.16%) ⬆️
.../Internal/MergeManyCacheChangeSetsSourceCompare.cs 85.36% <85.36%> (ø)

@dwcullop
Copy link
Member Author

dwcullop commented Nov 6, 2023

@RolandPheasant I think this one is now ready to go. Please take a look at your leisure and let me know what you think.

@SuperJMN
Copy link

SuperJMN commented Nov 6, 2023

ExplodindoBoomExplosãoCabeçaexplodindoGIF

Copy link
Collaborator

@RolandPheasant RolandPheasant left a comment

Choose a reason for hiding this comment

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

It's optional whether to action the zero allocation thing or not.

Maybe that would be best done as a distinct task / pr/

@RolandPheasant RolandPheasant merged commit 269828b into reactivemarbles:main Nov 7, 2023
3 checks passed
Copy link

This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Nov 22, 2023
@dwcullop dwcullop deleted the feature/mergemanychangesets-parent-sort branch November 22, 2023 16:20
@dwcullop dwcullop self-assigned this Dec 20, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants