osu/osu.Game/Database/RealmObjectExtensions.cs

261 lines
13 KiB
C#
Raw Normal View History

// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
2021-09-30 14:46:16 +00:00
using System.Linq;
using AutoMapper;
using osu.Framework.Development;
2022-01-07 05:17:22 +00:00
using osu.Game.Beatmaps;
using osu.Game.Input.Bindings;
2022-01-07 05:17:22 +00:00
using osu.Game.Models;
using osu.Game.Rulesets;
using osu.Game.Scoring;
using Realms;
#nullable enable
namespace osu.Game.Database
{
public static class RealmObjectExtensions
{
2022-01-11 09:23:04 +00:00
private static readonly IMapper write_mapper = new MapperConfiguration(c =>
{
c.ShouldMapField = fi => false;
// If we want to limit this further, we can avoid mapping properties with no setter that are not IList<>.
// Takes a bit of effort to determine whether this is the case though, see https://stackoverflow.com/questions/951536/how-do-i-tell-whether-a-type-implements-ilist
c.ShouldMapProperty = pi => pi.SetMethod?.IsPublic == true;
c.CreateMap<RealmKeyBinding, RealmKeyBinding>();
c.CreateMap<BeatmapMetadata, BeatmapMetadata>();
c.CreateMap<BeatmapDifficulty, BeatmapDifficulty>();
c.CreateMap<RulesetInfo, RulesetInfo>();
c.CreateMap<ScoreInfo, ScoreInfo>();
c.CreateMap<RealmUser, RealmUser>();
c.CreateMap<RealmFile, RealmFile>();
c.CreateMap<RealmNamedFileUsage, RealmNamedFileUsage>();
Handle automapper realm cyclic references via `AfterMap`s This may not be the cleanest solution, but there don't seem to be any way towards this either. - `UseDestinationValue` has been inherited by default as noted in https://docs.automapper.org/en/stable/10.0-Upgrade-Guide.html#usedestinationvalue-is-now-inherited-by-default, and its behaviour in this case would be using the nested **managed** realm object for the destination member rather than creating an unmanaged version. - `MaxDepth` already sets `PreserveReferences` so there's no point of using it. - `MaxDepth` should probably not be set for all maps, only for those with cyclic references, to avoid the expensive overhead of `PreserveReferences`, as mentioned in https://docs.automapper.org/en/stable/5.0-Upgrade-Guide.html#circular-references. That aside, `MaxDepth` should actually only be set to `1` for `BeatmapSetInfo` mapping, because we don't want AutoMapper to create a nested instance of `BeatmapSetInfo` in each mapped/detached beatmap, but for some reason, doing that will cause automapper to not map any beatmap inside the set and leave it with 0 beatmaps. While on the other hand, using `MaxDepth(2)` for `BeatmapSetInfo` works, but creates an unused instance of a `BeatmapSetInfo` inside each mapped beatmap, which may not be ideal. For `BeatmapInfo`, it has to be `MaxDepth(2)`, in which the first `BeatmapInfo` depth would be itself (when detaching a beatmap), and the second would be nested beatmaps inside the mapped/detached `BeatmapSetInfo` within the beatmap. (note that when detaching a beatmap set, the unused instance of `BeatmapSetInfo` within each beatmap of that beatmap set doesn't also have a list of unused beatmaps as one might expect from the depth specification, it surprisingly has 0 beatmaps) This causes it to create an unused instance of `BeatmapInfo` in the beatmap set resembling the root mapped/detached beatmap, but that one might be inevitable.
2022-01-11 21:32:32 +00:00
c.CreateMap<BeatmapInfo, BeatmapInfo>().MaxDepth(2).AfterMap((s, d) =>
2022-01-11 09:23:04 +00:00
{
Handle automapper realm cyclic references via `AfterMap`s This may not be the cleanest solution, but there don't seem to be any way towards this either. - `UseDestinationValue` has been inherited by default as noted in https://docs.automapper.org/en/stable/10.0-Upgrade-Guide.html#usedestinationvalue-is-now-inherited-by-default, and its behaviour in this case would be using the nested **managed** realm object for the destination member rather than creating an unmanaged version. - `MaxDepth` already sets `PreserveReferences` so there's no point of using it. - `MaxDepth` should probably not be set for all maps, only for those with cyclic references, to avoid the expensive overhead of `PreserveReferences`, as mentioned in https://docs.automapper.org/en/stable/5.0-Upgrade-Guide.html#circular-references. That aside, `MaxDepth` should actually only be set to `1` for `BeatmapSetInfo` mapping, because we don't want AutoMapper to create a nested instance of `BeatmapSetInfo` in each mapped/detached beatmap, but for some reason, doing that will cause automapper to not map any beatmap inside the set and leave it with 0 beatmaps. While on the other hand, using `MaxDepth(2)` for `BeatmapSetInfo` works, but creates an unused instance of a `BeatmapSetInfo` inside each mapped beatmap, which may not be ideal. For `BeatmapInfo`, it has to be `MaxDepth(2)`, in which the first `BeatmapInfo` depth would be itself (when detaching a beatmap), and the second would be nested beatmaps inside the mapped/detached `BeatmapSetInfo` within the beatmap. (note that when detaching a beatmap set, the unused instance of `BeatmapSetInfo` within each beatmap of that beatmap set doesn't also have a list of unused beatmaps as one might expect from the depth specification, it surprisingly has 0 beatmaps) This causes it to create an unused instance of `BeatmapInfo` in the beatmap set resembling the root mapped/detached beatmap, but that one might be inevitable.
2022-01-11 21:32:32 +00:00
for (int i = 0; i < d.BeatmapSet?.Beatmaps.Count; i++)
{
if (d.BeatmapSet.Beatmaps[i].Equals(d))
{
d.BeatmapSet.Beatmaps[i] = d;
break;
}
}
2022-01-11 09:23:04 +00:00
});
Handle automapper realm cyclic references via `AfterMap`s This may not be the cleanest solution, but there don't seem to be any way towards this either. - `UseDestinationValue` has been inherited by default as noted in https://docs.automapper.org/en/stable/10.0-Upgrade-Guide.html#usedestinationvalue-is-now-inherited-by-default, and its behaviour in this case would be using the nested **managed** realm object for the destination member rather than creating an unmanaged version. - `MaxDepth` already sets `PreserveReferences` so there's no point of using it. - `MaxDepth` should probably not be set for all maps, only for those with cyclic references, to avoid the expensive overhead of `PreserveReferences`, as mentioned in https://docs.automapper.org/en/stable/5.0-Upgrade-Guide.html#circular-references. That aside, `MaxDepth` should actually only be set to `1` for `BeatmapSetInfo` mapping, because we don't want AutoMapper to create a nested instance of `BeatmapSetInfo` in each mapped/detached beatmap, but for some reason, doing that will cause automapper to not map any beatmap inside the set and leave it with 0 beatmaps. While on the other hand, using `MaxDepth(2)` for `BeatmapSetInfo` works, but creates an unused instance of a `BeatmapSetInfo` inside each mapped beatmap, which may not be ideal. For `BeatmapInfo`, it has to be `MaxDepth(2)`, in which the first `BeatmapInfo` depth would be itself (when detaching a beatmap), and the second would be nested beatmaps inside the mapped/detached `BeatmapSetInfo` within the beatmap. (note that when detaching a beatmap set, the unused instance of `BeatmapSetInfo` within each beatmap of that beatmap set doesn't also have a list of unused beatmaps as one might expect from the depth specification, it surprisingly has 0 beatmaps) This causes it to create an unused instance of `BeatmapInfo` in the beatmap set resembling the root mapped/detached beatmap, but that one might be inevitable.
2022-01-11 21:32:32 +00:00
c.CreateMap<BeatmapSetInfo, BeatmapSetInfo>().MaxDepth(2).AfterMap((s, d) =>
{
foreach (var beatmap in d.Beatmaps)
beatmap.BeatmapSet = d;
});
c.AddGlobalIgnore(nameof(RealmObjectBase.ObjectSchema));
2022-01-11 09:23:04 +00:00
}).CreateMapper();
private static readonly IMapper read_mapper = new MapperConfiguration(c =>
{
c.ShouldMapField = fi => false;
// If we want to limit this further, we can avoid mapping properties with no setter that are not IList<>.
// Takes a bit of effort to determine whether this is the case though, see https://stackoverflow.com/questions/951536/how-do-i-tell-whether-a-type-implements-ilist
c.ShouldMapProperty = pi => pi.GetMethod?.IsPublic == true;
c.CreateMap<RealmKeyBinding, RealmKeyBinding>();
2022-01-07 05:17:22 +00:00
c.CreateMap<BeatmapMetadata, BeatmapMetadata>();
c.CreateMap<BeatmapDifficulty, BeatmapDifficulty>();
c.CreateMap<RulesetInfo, RulesetInfo>();
c.CreateMap<ScoreInfo, ScoreInfo>();
2022-01-07 05:17:22 +00:00
c.CreateMap<RealmUser, RealmUser>();
c.CreateMap<RealmFile, RealmFile>();
c.CreateMap<RealmNamedFileUsage, RealmNamedFileUsage>();
Handle automapper realm cyclic references via `AfterMap`s This may not be the cleanest solution, but there don't seem to be any way towards this either. - `UseDestinationValue` has been inherited by default as noted in https://docs.automapper.org/en/stable/10.0-Upgrade-Guide.html#usedestinationvalue-is-now-inherited-by-default, and its behaviour in this case would be using the nested **managed** realm object for the destination member rather than creating an unmanaged version. - `MaxDepth` already sets `PreserveReferences` so there's no point of using it. - `MaxDepth` should probably not be set for all maps, only for those with cyclic references, to avoid the expensive overhead of `PreserveReferences`, as mentioned in https://docs.automapper.org/en/stable/5.0-Upgrade-Guide.html#circular-references. That aside, `MaxDepth` should actually only be set to `1` for `BeatmapSetInfo` mapping, because we don't want AutoMapper to create a nested instance of `BeatmapSetInfo` in each mapped/detached beatmap, but for some reason, doing that will cause automapper to not map any beatmap inside the set and leave it with 0 beatmaps. While on the other hand, using `MaxDepth(2)` for `BeatmapSetInfo` works, but creates an unused instance of a `BeatmapSetInfo` inside each mapped beatmap, which may not be ideal. For `BeatmapInfo`, it has to be `MaxDepth(2)`, in which the first `BeatmapInfo` depth would be itself (when detaching a beatmap), and the second would be nested beatmaps inside the mapped/detached `BeatmapSetInfo` within the beatmap. (note that when detaching a beatmap set, the unused instance of `BeatmapSetInfo` within each beatmap of that beatmap set doesn't also have a list of unused beatmaps as one might expect from the depth specification, it surprisingly has 0 beatmaps) This causes it to create an unused instance of `BeatmapInfo` in the beatmap set resembling the root mapped/detached beatmap, but that one might be inevitable.
2022-01-11 21:32:32 +00:00
c.CreateMap<BeatmapInfo, BeatmapInfo>().MaxDepth(2).AfterMap((s, d) =>
2022-01-07 05:17:22 +00:00
{
Handle automapper realm cyclic references via `AfterMap`s This may not be the cleanest solution, but there don't seem to be any way towards this either. - `UseDestinationValue` has been inherited by default as noted in https://docs.automapper.org/en/stable/10.0-Upgrade-Guide.html#usedestinationvalue-is-now-inherited-by-default, and its behaviour in this case would be using the nested **managed** realm object for the destination member rather than creating an unmanaged version. - `MaxDepth` already sets `PreserveReferences` so there's no point of using it. - `MaxDepth` should probably not be set for all maps, only for those with cyclic references, to avoid the expensive overhead of `PreserveReferences`, as mentioned in https://docs.automapper.org/en/stable/5.0-Upgrade-Guide.html#circular-references. That aside, `MaxDepth` should actually only be set to `1` for `BeatmapSetInfo` mapping, because we don't want AutoMapper to create a nested instance of `BeatmapSetInfo` in each mapped/detached beatmap, but for some reason, doing that will cause automapper to not map any beatmap inside the set and leave it with 0 beatmaps. While on the other hand, using `MaxDepth(2)` for `BeatmapSetInfo` works, but creates an unused instance of a `BeatmapSetInfo` inside each mapped beatmap, which may not be ideal. For `BeatmapInfo`, it has to be `MaxDepth(2)`, in which the first `BeatmapInfo` depth would be itself (when detaching a beatmap), and the second would be nested beatmaps inside the mapped/detached `BeatmapSetInfo` within the beatmap. (note that when detaching a beatmap set, the unused instance of `BeatmapSetInfo` within each beatmap of that beatmap set doesn't also have a list of unused beatmaps as one might expect from the depth specification, it surprisingly has 0 beatmaps) This causes it to create an unused instance of `BeatmapInfo` in the beatmap set resembling the root mapped/detached beatmap, but that one might be inevitable.
2022-01-11 21:32:32 +00:00
for (int i = 0; i < d.BeatmapSet?.Beatmaps.Count; i++)
{
if (d.BeatmapSet.Beatmaps[i].Equals(d))
{
d.BeatmapSet.Beatmaps[i] = d;
break;
}
}
2022-01-07 05:17:22 +00:00
});
Handle automapper realm cyclic references via `AfterMap`s This may not be the cleanest solution, but there don't seem to be any way towards this either. - `UseDestinationValue` has been inherited by default as noted in https://docs.automapper.org/en/stable/10.0-Upgrade-Guide.html#usedestinationvalue-is-now-inherited-by-default, and its behaviour in this case would be using the nested **managed** realm object for the destination member rather than creating an unmanaged version. - `MaxDepth` already sets `PreserveReferences` so there's no point of using it. - `MaxDepth` should probably not be set for all maps, only for those with cyclic references, to avoid the expensive overhead of `PreserveReferences`, as mentioned in https://docs.automapper.org/en/stable/5.0-Upgrade-Guide.html#circular-references. That aside, `MaxDepth` should actually only be set to `1` for `BeatmapSetInfo` mapping, because we don't want AutoMapper to create a nested instance of `BeatmapSetInfo` in each mapped/detached beatmap, but for some reason, doing that will cause automapper to not map any beatmap inside the set and leave it with 0 beatmaps. While on the other hand, using `MaxDepth(2)` for `BeatmapSetInfo` works, but creates an unused instance of a `BeatmapSetInfo` inside each mapped beatmap, which may not be ideal. For `BeatmapInfo`, it has to be `MaxDepth(2)`, in which the first `BeatmapInfo` depth would be itself (when detaching a beatmap), and the second would be nested beatmaps inside the mapped/detached `BeatmapSetInfo` within the beatmap. (note that when detaching a beatmap set, the unused instance of `BeatmapSetInfo` within each beatmap of that beatmap set doesn't also have a list of unused beatmaps as one might expect from the depth specification, it surprisingly has 0 beatmaps) This causes it to create an unused instance of `BeatmapInfo` in the beatmap set resembling the root mapped/detached beatmap, but that one might be inevitable.
2022-01-11 21:32:32 +00:00
c.CreateMap<BeatmapSetInfo, BeatmapSetInfo>().MaxDepth(2).AfterMap((s, d) =>
{
foreach (var beatmap in d.Beatmaps)
beatmap.BeatmapSet = d;
});
c.AddGlobalIgnore(nameof(RealmObjectBase.ObjectSchema));
}).CreateMapper();
/// <summary>
/// Create a detached copy of the each item in the collection.
/// </summary>
/// <remarks>
/// Items which are already detached (ie. not managed by realm) will not be modified.
/// </remarks>
/// <param name="items">A list of managed <see cref="RealmObject"/>s to detach.</param>
/// <typeparam name="T">The type of object.</typeparam>
/// <returns>A list containing non-managed copies of provided items.</returns>
2022-01-07 05:17:22 +00:00
public static List<T> Detach<T>(this IEnumerable<T> items) where T : RealmObjectBase
{
var list = new List<T>();
foreach (var obj in items)
list.Add(obj.Detach());
return list;
}
/// <summary>
/// Create a detached copy of the item.
/// </summary>
/// <remarks>
/// If the item if already detached (ie. not managed by realm) it will not be detached again and the original instance will be returned. This allows this method to be potentially called at multiple levels while only incurring the clone overhead once.
/// </remarks>
/// <param name="item">The managed <see cref="RealmObject"/> to detach.</param>
/// <typeparam name="T">The type of object.</typeparam>
/// <returns>A non-managed copy of provided item. Will return the provided item if already detached.</returns>
2022-01-07 05:17:22 +00:00
public static T Detach<T>(this T item) where T : RealmObjectBase
{
if (!item.IsManaged)
return item;
2022-01-11 09:23:04 +00:00
return read_mapper.Map<T>(item);
}
public static void CopyChangesToRealm<T>(this T source, T destination) where T : RealmObjectBase
{
write_mapper.Map(source, destination);
}
2021-09-30 14:46:16 +00:00
public static List<ILive<T>> ToLiveUnmanaged<T>(this IEnumerable<T> realmList)
2021-09-30 14:46:16 +00:00
where T : RealmObject, IHasGuidPrimaryKey
{
return realmList.Select(l => new RealmLiveUnmanaged<T>(l)).Cast<ILive<T>>().ToList();
2021-09-30 14:46:16 +00:00
}
2022-01-07 05:17:22 +00:00
public static ILive<T> ToLiveUnmanaged<T>(this T realmObject
)
2021-09-30 14:46:16 +00:00
where T : RealmObject, IHasGuidPrimaryKey
{
return new RealmLiveUnmanaged<T>(realmObject);
}
public static List<ILive<T>> ToLive<T>(this IEnumerable<T> realmList, RealmContextFactory realmContextFactory)
where T : RealmObject, IHasGuidPrimaryKey
{
return realmList.Select(l => new RealmLive<T>(l, realmContextFactory)).Cast<ILive<T>>().ToList();
}
public static ILive<T> ToLive<T>(this T realmObject, RealmContextFactory realmContextFactory)
where T : RealmObject, IHasGuidPrimaryKey
{
return new RealmLive<T>(realmObject, realmContextFactory);
2021-09-30 14:46:16 +00:00
}
/// <summary>
/// Register a callback to be invoked each time this <see cref="T:Realms.IRealmCollection`1" /> changes.
/// </summary>
/// <remarks>
/// <para>
/// This adds osu! specific thread and managed state safety checks on top of <see cref="IRealmCollection{T}.SubscribeForNotifications"/>.
/// </para>
/// <para>
/// The first callback will be invoked with the initial <see cref="T:Realms.IRealmCollection`1" /> after the asynchronous query completes,
/// and then called again after each write transaction which changes either any of the objects in the collection, or
/// which objects are in the collection. The <c>changes</c> parameter will
/// be <c>null</c> the first time the callback is invoked with the initial results. For each call after that,
/// it will contain information about which rows in the results were added, removed or modified.
/// </para>
/// <para>
/// If a write transaction did not modify any objects in this <see cref="T:Realms.IRealmCollection`1" />, the callback is not invoked at all.
/// If an error occurs the callback will be invoked with <c>null</c> for the <c>sender</c> parameter and a non-<c>null</c> <c>error</c>.
/// Currently the only errors that can occur are when opening the <see cref="T:Realms.Realm" /> on the background worker thread.
/// </para>
/// <para>
/// At the time when the block is called, the <see cref="T:Realms.IRealmCollection`1" /> object will be fully evaluated
/// and up-to-date, and as long as you do not perform a write transaction on the same thread
/// or explicitly call <see cref="M:Realms.Realm.Refresh" />, accessing it will never perform blocking work.
/// </para>
/// <para>
/// Notifications are delivered via the standard event loop, and so can't be delivered while the event loop is blocked by other activity.
/// When notifications can't be delivered instantly, multiple notifications may be coalesced into a single notification.
/// This can include the notification with the initial collection.
/// </para>
/// </remarks>
/// <param name="collection">The <see cref="IRealmCollection{T}"/> to observe for changes.</param>
/// <param name="callback">The callback to be invoked with the updated <see cref="T:Realms.IRealmCollection`1" />.</param>
/// <returns>
/// A subscription token. It must be kept alive for as long as you want to receive change notifications.
/// To stop receiving notifications, call <see cref="M:System.IDisposable.Dispose" />.
///
/// May be null in the case the provided collection is not managed.
/// </returns>
/// <seealso cref="M:Realms.CollectionExtensions.SubscribeForNotifications``1(System.Collections.Generic.IList{``0},Realms.NotificationCallbackDelegate{``0})" />
/// <seealso cref="M:Realms.CollectionExtensions.SubscribeForNotifications``1(System.Linq.IQueryable{``0},Realms.NotificationCallbackDelegate{``0})" />
public static IDisposable? QueryAsyncWithNotifications<T>(this IRealmCollection<T> collection, NotificationCallbackDelegate<T> callback)
where T : RealmObjectBase
{
// Subscriptions can only work on the main thread.
if (!ThreadSafety.IsUpdateThread)
throw new InvalidOperationException("Cannot subscribe for realm notifications from a non-update thread.");
return collection.SubscribeForNotifications(callback);
}
/// <summary>
/// A convenience method that casts <see cref="IQueryable{T}"/> to <see cref="IRealmCollection{T}"/> and subscribes for change notifications.
/// </summary>
/// <remarks>
/// This adds osu! specific thread and managed state safety checks on top of <see cref="IRealmCollection{T}.SubscribeForNotifications"/>.
/// </remarks>
/// <param name="list">The <see cref="IQueryable{T}"/> to observe for changes.</param>
/// <typeparam name="T">Type of the elements in the list.</typeparam>
/// <seealso cref="IRealmCollection{T}.SubscribeForNotifications"/>
/// <param name="callback">The callback to be invoked with the updated <see cref="IRealmCollection{T}"/>.</param>
/// <returns>
/// A subscription token. It must be kept alive for as long as you want to receive change notifications.
/// To stop receiving notifications, call <see cref="IDisposable.Dispose"/>.
///
/// May be null in the case the provided collection is not managed.
/// </returns>
public static IDisposable? QueryAsyncWithNotifications<T>(this IQueryable<T> list, NotificationCallbackDelegate<T> callback)
where T : RealmObjectBase
{
// Subscribing to non-managed instances doesn't work.
// In this usage, the instance may be non-managed in tests.
if (!(list is IRealmCollection<T> realmCollection))
return null;
return QueryAsyncWithNotifications(realmCollection, callback);
}
/// <summary>
/// A convenience method that casts <see cref="IList{T}"/> to <see cref="IRealmCollection{T}"/> and subscribes for change notifications.
/// </summary>
/// <remarks>
/// This adds osu! specific thread and managed state safety checks on top of <see cref="IRealmCollection{T}.SubscribeForNotifications"/>.
/// </remarks>
/// <param name="list">The <see cref="IList{T}"/> to observe for changes.</param>
/// <typeparam name="T">Type of the elements in the list.</typeparam>
/// <seealso cref="IRealmCollection{T}.SubscribeForNotifications"/>
/// <param name="callback">The callback to be invoked with the updated <see cref="IRealmCollection{T}"/>.</param>
/// <returns>
/// A subscription token. It must be kept alive for as long as you want to receive change notifications.
/// To stop receiving notifications, call <see cref="IDisposable.Dispose"/>.
///
/// May be null in the case the provided collection is not managed.
/// </returns>
public static IDisposable? QueryAsyncWithNotifications<T>(this IList<T> list, NotificationCallbackDelegate<T> callback)
where T : RealmObjectBase
{
// Subscribing to non-managed instances doesn't work.
// In this usage, the instance may be non-managed in tests.
if (!(list is IRealmCollection<T> realmCollection))
return null;
return QueryAsyncWithNotifications(realmCollection, callback);
}
}
}