diff --git a/CHANGELOG.md b/CHANGELOG.md index dcbc9613d8..8f6c064ffb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,10 @@ ## vNext (TBD) ### Enhancements -* None +* Added support for `Migration.FindInNewRealm` which is a helper that allows you to lookup the object in the post-migration Realm that corresponds to an object from the pre-migration Realm. (Issue [#3600](https://github.com/realm/realm-dotnet/issues/3600)) ### Fixed -* None +* Fixed an issue that would cause `RealmObject.DynamicApi.GetList/Set/Dictionary` to fail when the collection contains primitive values. (Issue [#3597](https://github.com/realm/realm-dotnet/issues/3597)) ### Compatibility * Realm Studio: 15.0.0 or later. diff --git a/Realm/Realm/Handles/SharedRealmHandle.cs b/Realm/Realm/Handles/SharedRealmHandle.cs index add76a9f13..ff223c3e46 100644 --- a/Realm/Realm/Handles/SharedRealmHandle.cs +++ b/Realm/Realm/Handles/SharedRealmHandle.cs @@ -229,6 +229,9 @@ public static extern void rename_property(SharedRealmHandle sharedRealm, [DllImport(InteropConfig.DLL_NAME, EntryPoint = "shared_realm_get_operating_system", CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr get_operating_system(IntPtr buffer, IntPtr buffer_length); + [DllImport(InteropConfig.DLL_NAME, EntryPoint = "shared_realm_get_object_for_object", CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr get_object_for_object(SharedRealmHandle realmHandle, ObjectHandle handle, out NativeException ex); + #pragma warning restore SA1121 // Use built-in type alias #pragma warning restore IDE0049 // Use built-in type alias } @@ -677,6 +680,21 @@ public bool TryFindObject(TableKey tableKey, in RealmValue id, [MaybeNullWhen(fa return true; } + public bool TryFindObject(ObjectHandle handle, [MaybeNullWhen(false)] out ObjectHandle objectHandle) + { + var result = NativeMethods.get_object_for_object(this, handle, out var ex); + ex.ThrowIfNecessary(); + + if (result == IntPtr.Zero) + { + objectHandle = null; + return false; + } + + objectHandle = new ObjectHandle(this, result); + return true; + } + public void RenameProperty(string typeName, string oldName, string newName, IntPtr migrationSchema) { NativeMethods.rename_property(this, typeName, (IntPtr)typeName.Length, diff --git a/Realm/Realm/Migration.cs b/Realm/Realm/Migration.cs index 02dea33a7c..998e8bc738 100644 --- a/Realm/Realm/Migration.cs +++ b/Realm/Realm/Migration.cs @@ -115,5 +115,27 @@ public void RenameProperty(string typeName, string oldPropertyName, string newPr NewRealm.SharedRealmHandle.RenameProperty(typeName, oldPropertyName, newPropertyName, _migrationSchema); } + + /// + /// Finds an object obtained from in . + /// + /// The type of the object in the new realm. + /// The object obtained from the old realm. + /// The corresponding object post-migration or null if the object no longer exists in the new realm. + /// + /// + /// foreach (var oldPerson in migration.OldRealm.DynamicApi.All("Person")) + /// { + /// var newPerson = migration.FindInNewRealm<Person>(oldPerson) + /// newPerson.Name = $"{oldPerson.DynamicApi.Get<string>("FirstName")} {oldPerson.DynamicApi.Get<string>("LastName")}"; + /// } + /// + /// + public T? FindInNewRealm(IRealmObject obj) + where T : IRealmObject + { + Argument.Ensure(obj.IsManaged, "Only managed RealmObject instances can be looked up in the new Realm", nameof(obj)); + return NewRealm.FindExisting(obj); + } } } diff --git a/Realm/Realm/Native/PrimitiveValue.cs b/Realm/Realm/Native/PrimitiveValue.cs index 0f333fcfe9..b31ec8921b 100644 --- a/Realm/Realm/Native/PrimitiveValue.cs +++ b/Realm/Realm/Native/PrimitiveValue.cs @@ -353,7 +353,12 @@ public static StringValue AllocateFrom(string? value, Arena arena) public static implicit operator bool(in StringValue value) => value.data != null; - public static implicit operator string?(in StringValue value) => !value ? null : Encoding.UTF8.GetString(value.data, (int)value.size); + public static implicit operator string?(in StringValue value) => value.ToDotnetString(); + + public readonly string? ToDotnetString(bool treatEmptyAsNull = false) + => data == null || (size == 0 && treatEmptyAsNull) + ? null + : Encoding.UTF8.GetString(data, (int)size); } [StructLayout(LayoutKind.Sequential)] diff --git a/Realm/Realm/Realm.cs b/Realm/Realm/Realm.cs index 45e2138ed8..9d741994d7 100644 --- a/Realm/Realm/Realm.cs +++ b/Realm/Realm/Realm.cs @@ -1111,6 +1111,21 @@ internal IQueryable AllEmbedded() return default; } + // This is only used during migrations. obj here is the object in the old realm, and we're trying to find + // its counterpart post-migration. + internal T? FindExisting(IRealmObject obj) + { + ThrowIfDisposed(); + Argument.Ensure(obj.IsManaged, "Only managed objects can be used in FindExisting", nameof(obj)); + if (SharedRealmHandle.TryFindObject(obj.GetObjectHandle()!, out var objectHandle)) + { + var metadata = Metadata[typeof(T).GetMappedOrOriginalName()]; + return (T)MakeObject(metadata, objectHandle); + } + + return default; + } + #endregion Quick Find using primary key #region Thread Handover @@ -1589,7 +1604,7 @@ internal Dynamic(Realm realm) public IRealmObjectBase CreateObject(string className) => CreateObjectCore(className, primaryKey: null); /// - /// Factory for a managed object without a primary key in a realm. Only valid within a write . + /// Factory for a managed object with a primary key in a realm. Only valid within a write . /// /// A dynamically-accessed Realm object. /// The type of object to create as defined in the schema. @@ -1609,7 +1624,7 @@ internal Dynamic(Realm realm) public IRealmObjectBase CreateObject(string className, long? primaryKey) => CreateObjectCore(className, primaryKey); /// - /// Factory for a managed object without a primary key in a realm. Only valid within a write . + /// Factory for a managed object with a primary key in a realm. Only valid within a write . /// /// A dynamically-accessed Realm object. /// The type of object to create as defined in the schema. @@ -1629,7 +1644,7 @@ internal Dynamic(Realm realm) public IRealmObjectBase CreateObject(string className, string? primaryKey) => CreateObjectCore(className, primaryKey); /// - /// Factory for a managed object without a primary key in a realm. Only valid within a write . + /// Factory for a managed object with a primary key in a realm. Only valid within a write . /// /// A dynamically-accessed Realm object. /// The type of object to create as defined in the schema. @@ -1649,7 +1664,7 @@ internal Dynamic(Realm realm) public IRealmObjectBase CreateObject(string className, ObjectId? primaryKey) => CreateObjectCore(className, primaryKey); /// - /// Factory for a managed object without a primary key in a realm. Only valid within a write . + /// Factory for a managed object with a primary key in a realm. Only valid within a write . /// /// A dynamically-accessed Realm object. /// The type of object to create as defined in the schema. diff --git a/Realm/Realm/Schema/Property.cs b/Realm/Realm/Schema/Property.cs index b2324b4ca4..2378756816 100644 --- a/Realm/Realm/Schema/Property.cs +++ b/Realm/Realm/Schema/Property.cs @@ -170,11 +170,10 @@ public Property(string name, PropertyType type, string? objectType = null, strin internal Property(in SchemaProperty nativeProperty) { Name = nativeProperty.name!; - string? managedName = nativeProperty.managed_name; - ManagedName = !string.IsNullOrEmpty(managedName) ? managedName! : Name; + ManagedName = nativeProperty.managed_name.ToDotnetString(treatEmptyAsNull: true) ?? Name; Type = nativeProperty.type; - ObjectType = nativeProperty.object_type; - LinkOriginPropertyName = nativeProperty.link_origin_property_name; + ObjectType = nativeProperty.object_type.ToDotnetString(treatEmptyAsNull: true); + LinkOriginPropertyName = nativeProperty.link_origin_property_name.ToDotnetString(treatEmptyAsNull: true); IsPrimaryKey = nativeProperty.is_primary; IndexType = nativeProperty.index; } diff --git a/Tests/Realm.Tests/Database/MigrationTests.cs b/Tests/Realm.Tests/Database/MigrationTests.cs index 20ab122c9d..d507100660 100644 --- a/Tests/Realm.Tests/Database/MigrationTests.cs +++ b/Tests/Realm.Tests/Database/MigrationTests.cs @@ -834,6 +834,113 @@ public void Migration_ToEmbedded_DeletesOrphans() Assert.That(newRealm.AllEmbedded().Count(), Is.EqualTo(1)); Assert.That(newRealm.AllEmbedded().Any(e => e.Value == "bar"), Is.False); } + + [Test] + public void Migration_FindInNewRealm_WhenObjectIsDeleted_ReturnsNull() + { + var oldConfig = new RealmConfiguration(Guid.NewGuid().ToString()) + { + Schema = new[] + { + typeof(Dotnet_3597_Old) + } + }; + + using (var oldRealm = GetRealm(oldConfig)) + { + oldRealm.Write(() => + { + oldRealm.Add(new Dotnet_3597_Old { FloatProp = 1, IntProp = 1 }); + oldRealm.Add(new Dotnet_3597_Old { FloatProp = 2, IntProp = 2 }); + }); + } + + var newConfig = new RealmConfiguration(oldConfig.DatabasePath) + { + Schema = new[] + { + typeof(Dotnet_3597) + }, + SchemaVersion = 2, + MigrationCallback = (migration, version) => + { + var oldObjects = migration.OldRealm.DynamicApi.All(nameof(Dotnet_3597)).ToArray(); + var old1 = oldObjects.First(o => o.DynamicApi.Get("IntProp") == 1); + var old2 = oldObjects.First(o => o.DynamicApi.Get("IntProp") == 2); + + var newObjects = migration.NewRealm.All().ToArray(); + var new1 = newObjects.First(o => o.IntProp == 1); + var new2 = newObjects.First(o => o.IntProp == 2); + + Assert.That(migration.FindInNewRealm(old1), Is.EqualTo(new1)); + Assert.That(migration.FindInNewRealm(old2), Is.EqualTo(new2)); + + migration.NewRealm.Remove(new1); + + Assert.That(migration.FindInNewRealm(old1), Is.Null); + } + }; + + var newRealm = GetRealm(newConfig); + Assert.That(newRealm.All().Count(), Is.EqualTo(1)); + } + + // Test for https://github.com/realm/realm-dotnet/issues/3597 + [Test] + public void Migration_MigratesListOfFloats() + { + var oldConfig = new RealmConfiguration(Guid.NewGuid().ToString()) + { + Schema = new[] + { + typeof(Dotnet_3597_Old) + } + }; + + using (var oldRealm = GetRealm(oldConfig)) + { + oldRealm.Write(() => + { + oldRealm.Add(new Dotnet_3597_Old + { + FloatProp = 3.14f, + FloatList = + { + 1, + 2, + -1.23f + } + }); + }); + } + + var newConfig = new RealmConfiguration(oldConfig.DatabasePath) + { + Schema = new[] + { + typeof(Dotnet_3597) + }, + SchemaVersion = 2, + MigrationCallback = (migration, version) => + { + foreach (var item in migration.OldRealm.DynamicApi.All(nameof(Dotnet_3597))) + { + var newItem = migration.FindInNewRealm(item)!; + newItem.FloatProp = item.DynamicApi.Get("FloatProp").ToString()!; + foreach (var floatValue in item.DynamicApi.GetList("FloatList")) + { + newItem.FloatList.Add(floatValue.ToString()!); + } + } + } + }; + + var newRealm = GetRealm(newConfig); + + var obj = newRealm.All().Single(); + Assert.That(obj.FloatProp, Is.EqualTo("3.14")); + CollectionAssert.AreEqual(obj.FloatList, new[] { "1", "2", "-1.23" }); + } } [Explicit] @@ -884,4 +991,26 @@ public partial class ObjectEmbedded : TestEmbeddedObject { public string? Value { get; set; } } + + [Explicit] + [MapTo("Dotnet_3597")] + public partial class Dotnet_3597_Old : TestRealmObject + { + public int IntProp { get; set; } + + public float? FloatProp { get; set; } + + public IList FloatList { get; } + } + + [Explicit] + [MapTo("Dotnet_3597")] + public partial class Dotnet_3597 : TestRealmObject + { + public int IntProp { get; set; } + + public string FloatProp { get; set; } + + public IList FloatList { get; } + } } diff --git a/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/Dotnet_3597_Old_generated.cs b/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/Dotnet_3597_Old_generated.cs new file mode 100644 index 0000000000..be32bf8ac4 --- /dev/null +++ b/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/Dotnet_3597_Old_generated.cs @@ -0,0 +1,449 @@ +// +#nullable enable + +using MongoDB.Bson.Serialization; +using NUnit.Framework; +using Realms; +using Realms.Exceptions; +using Realms.Extensions; +using Realms.Schema; +using Realms.Tests.Database; +using Realms.Weaving; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; +using System.Xml.Serialization; +using TestEmbeddedObject = Realms.IEmbeddedObject; +using TestRealmObject = Realms.IRealmObject; + +namespace Realms.Tests.Database +{ + [Generated] + [Woven(typeof(Dotnet_3597_OldObjectHelper)), Realms.Preserve(AllMembers = true)] + public partial class Dotnet_3597_Old : IRealmObject, INotifyPropertyChanged, IReflectableType + { + + [Realms.Preserve] + static Dotnet_3597_Old() + { + Realms.Serialization.RealmObjectSerializer.Register(new Dotnet_3597_OldSerializer()); + } + + /// + /// Defines the schema for the class. + /// + public static Realms.Schema.ObjectSchema RealmSchema = new Realms.Schema.ObjectSchema.Builder("Dotnet_3597", ObjectSchema.ObjectType.RealmObject) + { + Realms.Schema.Property.Primitive("IntProp", Realms.RealmValueType.Int, isPrimaryKey: false, indexType: IndexType.None, isNullable: false, managedName: "IntProp"), + Realms.Schema.Property.Primitive("FloatProp", Realms.RealmValueType.Float, isPrimaryKey: false, indexType: IndexType.None, isNullable: true, managedName: "FloatProp"), + Realms.Schema.Property.PrimitiveList("FloatList", Realms.RealmValueType.Float, areElementsNullable: true, managedName: "FloatList"), + }.Build(); + + #region IRealmObject implementation + + private IDotnet_3597_OldAccessor? _accessor; + + Realms.IRealmAccessor Realms.IRealmObjectBase.Accessor => Accessor; + + private IDotnet_3597_OldAccessor Accessor => _accessor ??= new Dotnet_3597_OldUnmanagedAccessor(typeof(Dotnet_3597_Old)); + + /// + [IgnoreDataMember, XmlIgnore] + public bool IsManaged => Accessor.IsManaged; + + /// + [IgnoreDataMember, XmlIgnore] + public bool IsValid => Accessor.IsValid; + + /// + [IgnoreDataMember, XmlIgnore] + public bool IsFrozen => Accessor.IsFrozen; + + /// + [IgnoreDataMember, XmlIgnore] + public Realms.Realm? Realm => Accessor.Realm; + + /// + [IgnoreDataMember, XmlIgnore] + public Realms.Schema.ObjectSchema ObjectSchema => Accessor.ObjectSchema!; + + /// + [IgnoreDataMember, XmlIgnore] + public Realms.DynamicObjectApi DynamicApi => Accessor.DynamicApi; + + /// + [IgnoreDataMember, XmlIgnore] + public int BacklinksCount => Accessor.BacklinksCount; + + void ISettableManagedAccessor.SetManagedAccessor(Realms.IRealmAccessor managedAccessor, Realms.Weaving.IRealmObjectHelper? helper, bool update, bool skipDefaults) + { + var newAccessor = (IDotnet_3597_OldAccessor)managedAccessor; + var oldAccessor = _accessor; + _accessor = newAccessor; + + if (helper != null && oldAccessor != null) + { + if (!skipDefaults) + { + newAccessor.FloatList.Clear(); + } + + if (!skipDefaults || oldAccessor.IntProp != default(int)) + { + newAccessor.IntProp = oldAccessor.IntProp; + } + if (!skipDefaults || oldAccessor.FloatProp != default(float?)) + { + newAccessor.FloatProp = oldAccessor.FloatProp; + } + Realms.CollectionExtensions.PopulateCollection(oldAccessor.FloatList, newAccessor.FloatList, update, skipDefaults); + } + + if (_propertyChanged != null) + { + SubscribeForNotifications(); + } + + OnManaged(); + } + + #endregion + + /// + /// Called when the object has been managed by a Realm. + /// + /// + /// This method will be called either when a managed object is materialized or when an unmanaged object has been + /// added to the Realm. It can be useful for providing some initialization logic as when the constructor is invoked, + /// it is not yet clear whether the object is managed or not. + /// + partial void OnManaged(); + + private event PropertyChangedEventHandler? _propertyChanged; + + /// + public event PropertyChangedEventHandler? PropertyChanged + { + add + { + if (_propertyChanged == null) + { + SubscribeForNotifications(); + } + + _propertyChanged += value; + } + + remove + { + _propertyChanged -= value; + + if (_propertyChanged == null) + { + UnsubscribeFromNotifications(); + } + } + } + + /// + /// Called when a property has changed on this class. + /// + /// The name of the property. + /// + /// For this method to be called, you need to have first subscribed to . + /// This can be used to react to changes to the current object, e.g. raising for computed properties. + /// + /// + /// + /// class MyClass : IRealmObject + /// { + /// public int StatusCodeRaw { get; set; } + /// public StatusCodeEnum StatusCode => (StatusCodeEnum)StatusCodeRaw; + /// partial void OnPropertyChanged(string propertyName) + /// { + /// if (propertyName == nameof(StatusCodeRaw)) + /// { + /// RaisePropertyChanged(nameof(StatusCode)); + /// } + /// } + /// } + /// + /// Here, we have a computed property that depends on a persisted one. In order to notify any + /// subscribers that StatusCode has changed, we implement and + /// raise manually by calling . + /// + partial void OnPropertyChanged(string? propertyName); + + private void RaisePropertyChanged([CallerMemberName] string propertyName = "") + { + _propertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + OnPropertyChanged(propertyName); + } + + private void SubscribeForNotifications() + { + Accessor.SubscribeForNotifications(RaisePropertyChanged); + } + + private void UnsubscribeFromNotifications() + { + Accessor.UnsubscribeFromNotifications(); + } + + /// + /// Converts a to . Equivalent to . + /// + /// The to convert. + /// The stored in the . + public static explicit operator Dotnet_3597_Old?(Realms.RealmValue val) => val.Type == Realms.RealmValueType.Null ? null : val.AsRealmObject(); + + /// + /// Implicitly constructs a from . + /// + /// The value to store in the . + /// A containing the supplied . + public static implicit operator Realms.RealmValue(Dotnet_3597_Old? val) => val == null ? Realms.RealmValue.Null : Realms.RealmValue.Object(val); + + /// + /// Implicitly constructs a from . + /// + /// The value to store in the . + /// A containing the supplied . + public static implicit operator Realms.QueryArgument(Dotnet_3597_Old? val) => (Realms.RealmValue)val; + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public TypeInfo GetTypeInfo() => Accessor.GetTypeInfo(this); + + /// + public override bool Equals(object? obj) + { + if (obj is null) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is InvalidObject) + { + return !IsValid; + } + + if (!(obj is Realms.IRealmObjectBase iro)) + { + return false; + } + + return Accessor.Equals(iro.Accessor); + } + + /// + public override int GetHashCode() => IsManaged ? Accessor.GetHashCode() : base.GetHashCode(); + + /// + public override string? ToString() => Accessor.ToString(); + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + private class Dotnet_3597_OldObjectHelper : Realms.Weaving.IRealmObjectHelper + { + public void CopyToRealm(Realms.IRealmObjectBase instance, bool update, bool skipDefaults) + { + throw new InvalidOperationException("This method should not be called for source generated classes."); + } + + public Realms.ManagedAccessor CreateAccessor() => new Dotnet_3597_OldManagedAccessor(); + + public Realms.IRealmObjectBase CreateInstance() => new Dotnet_3597_Old(); + + public bool TryGetPrimaryKeyValue(Realms.IRealmObjectBase instance, out RealmValue value) + { + value = RealmValue.Null; + return false; + } + } + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + internal interface IDotnet_3597_OldAccessor : Realms.IRealmAccessor + { + int IntProp { get; set; } + + float? FloatProp { get; set; } + + System.Collections.Generic.IList FloatList { get; } + } + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + private class Dotnet_3597_OldManagedAccessor : Realms.ManagedAccessor, IDotnet_3597_OldAccessor + { + public int IntProp + { + get => (int)GetValue("IntProp"); + set => SetValue("IntProp", value); + } + + public float? FloatProp + { + get => (float?)GetValue("FloatProp"); + set => SetValue("FloatProp", value); + } + + private System.Collections.Generic.IList _floatList = null!; + public System.Collections.Generic.IList FloatList + { + get + { + if (_floatList == null) + { + _floatList = GetListValue("FloatList"); + } + + return _floatList; + } + } + } + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + private class Dotnet_3597_OldUnmanagedAccessor : Realms.UnmanagedAccessor, IDotnet_3597_OldAccessor + { + public override ObjectSchema ObjectSchema => Dotnet_3597_Old.RealmSchema; + + private int _intProp; + public int IntProp + { + get => _intProp; + set + { + _intProp = value; + RaisePropertyChanged("IntProp"); + } + } + + private float? _floatProp; + public float? FloatProp + { + get => _floatProp; + set + { + _floatProp = value; + RaisePropertyChanged("FloatProp"); + } + } + + public System.Collections.Generic.IList FloatList { get; } = new List(); + + public Dotnet_3597_OldUnmanagedAccessor(Type objectType) : base(objectType) + { + } + + public override Realms.RealmValue GetValue(string propertyName) + { + return propertyName switch + { + "IntProp" => _intProp, + "FloatProp" => _floatProp, + _ => throw new MissingMemberException($"The object does not have a gettable Realm property with name {propertyName}"), + }; + } + + public override void SetValue(string propertyName, Realms.RealmValue val) + { + switch (propertyName) + { + case "IntProp": + IntProp = (int)val; + return; + case "FloatProp": + FloatProp = (float?)val; + return; + default: + throw new MissingMemberException($"The object does not have a settable Realm property with name {propertyName}"); + } + } + + public override void SetValueUnique(string propertyName, Realms.RealmValue val) + { + throw new InvalidOperationException("Cannot set the value of an non primary key property with SetValueUnique"); + } + + public override IList GetListValue(string propertyName) + { + return propertyName switch + { + "FloatList" => (IList)FloatList, + _ => throw new MissingMemberException($"The object does not have a Realm list property with name {propertyName}"), + }; + } + + public override ISet GetSetValue(string propertyName) + { + throw new MissingMemberException($"The object does not have a Realm set property with name {propertyName}"); + } + + public override IDictionary GetDictionaryValue(string propertyName) + { + throw new MissingMemberException($"The object does not have a Realm dictionary property with name {propertyName}"); + } + } + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + private class Dotnet_3597_OldSerializer : Realms.Serialization.RealmObjectSerializerBase + { + public override string SchemaName => "Dotnet_3597"; + + protected override void SerializeValue(MongoDB.Bson.Serialization.BsonSerializationContext context, BsonSerializationArgs args, Dotnet_3597_Old value) + { + context.Writer.WriteStartDocument(); + + WriteValue(context, args, "IntProp", value.IntProp); + WriteValue(context, args, "FloatProp", value.FloatProp); + WriteList(context, args, "FloatList", value.FloatList); + + context.Writer.WriteEndDocument(); + } + + protected override Dotnet_3597_Old CreateInstance() => new Dotnet_3597_Old(); + + protected override void ReadValue(Dotnet_3597_Old instance, string name, BsonDeserializationContext context) + { + switch (name) + { + case "IntProp": + instance.IntProp = BsonSerializer.LookupSerializer().Deserialize(context); + break; + case "FloatProp": + instance.FloatProp = BsonSerializer.LookupSerializer().Deserialize(context); + break; + case "FloatList": + ReadArray(instance, name, context); + break; + default: + context.Reader.SkipValue(); + break; + } + } + + protected override void ReadArrayElement(Dotnet_3597_Old instance, string name, BsonDeserializationContext context) + { + switch (name) + { + case "FloatList": + instance.FloatList.Add(BsonSerializer.LookupSerializer().Deserialize(context)); + break; + } + } + + protected override void ReadDocumentField(Dotnet_3597_Old instance, string name, string fieldName, BsonDeserializationContext context) + { + // No persisted dictionary properties to deserialize + } + } + } +} diff --git a/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/Dotnet_3597_generated.cs b/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/Dotnet_3597_generated.cs new file mode 100644 index 0000000000..eca00f0a47 --- /dev/null +++ b/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/Dotnet_3597_generated.cs @@ -0,0 +1,446 @@ +// +#nullable enable + +using MongoDB.Bson.Serialization; +using NUnit.Framework; +using Realms; +using Realms.Exceptions; +using Realms.Extensions; +using Realms.Schema; +using Realms.Tests.Database; +using Realms.Weaving; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; +using System.Xml.Serialization; +using TestEmbeddedObject = Realms.IEmbeddedObject; +using TestRealmObject = Realms.IRealmObject; + +namespace Realms.Tests.Database +{ + [Generated] + [Woven(typeof(Dotnet_3597ObjectHelper)), Realms.Preserve(AllMembers = true)] + public partial class Dotnet_3597 : IRealmObject, INotifyPropertyChanged, IReflectableType + { + + [Realms.Preserve] + static Dotnet_3597() + { + Realms.Serialization.RealmObjectSerializer.Register(new Dotnet_3597Serializer()); + } + + /// + /// Defines the schema for the class. + /// + public static Realms.Schema.ObjectSchema RealmSchema = new Realms.Schema.ObjectSchema.Builder("Dotnet_3597", ObjectSchema.ObjectType.RealmObject) + { + Realms.Schema.Property.Primitive("IntProp", Realms.RealmValueType.Int, isPrimaryKey: false, indexType: IndexType.None, isNullable: false, managedName: "IntProp"), + Realms.Schema.Property.Primitive("FloatProp", Realms.RealmValueType.String, isPrimaryKey: false, indexType: IndexType.None, isNullable: false, managedName: "FloatProp"), + Realms.Schema.Property.PrimitiveList("FloatList", Realms.RealmValueType.String, areElementsNullable: false, managedName: "FloatList"), + }.Build(); + + #region IRealmObject implementation + + private IDotnet_3597Accessor? _accessor; + + Realms.IRealmAccessor Realms.IRealmObjectBase.Accessor => Accessor; + + private IDotnet_3597Accessor Accessor => _accessor ??= new Dotnet_3597UnmanagedAccessor(typeof(Dotnet_3597)); + + /// + [IgnoreDataMember, XmlIgnore] + public bool IsManaged => Accessor.IsManaged; + + /// + [IgnoreDataMember, XmlIgnore] + public bool IsValid => Accessor.IsValid; + + /// + [IgnoreDataMember, XmlIgnore] + public bool IsFrozen => Accessor.IsFrozen; + + /// + [IgnoreDataMember, XmlIgnore] + public Realms.Realm? Realm => Accessor.Realm; + + /// + [IgnoreDataMember, XmlIgnore] + public Realms.Schema.ObjectSchema ObjectSchema => Accessor.ObjectSchema!; + + /// + [IgnoreDataMember, XmlIgnore] + public Realms.DynamicObjectApi DynamicApi => Accessor.DynamicApi; + + /// + [IgnoreDataMember, XmlIgnore] + public int BacklinksCount => Accessor.BacklinksCount; + + void ISettableManagedAccessor.SetManagedAccessor(Realms.IRealmAccessor managedAccessor, Realms.Weaving.IRealmObjectHelper? helper, bool update, bool skipDefaults) + { + var newAccessor = (IDotnet_3597Accessor)managedAccessor; + var oldAccessor = _accessor; + _accessor = newAccessor; + + if (helper != null && oldAccessor != null) + { + if (!skipDefaults) + { + newAccessor.FloatList.Clear(); + } + + if (!skipDefaults || oldAccessor.IntProp != default(int)) + { + newAccessor.IntProp = oldAccessor.IntProp; + } + newAccessor.FloatProp = oldAccessor.FloatProp; + Realms.CollectionExtensions.PopulateCollection(oldAccessor.FloatList, newAccessor.FloatList, update, skipDefaults); + } + + if (_propertyChanged != null) + { + SubscribeForNotifications(); + } + + OnManaged(); + } + + #endregion + + /// + /// Called when the object has been managed by a Realm. + /// + /// + /// This method will be called either when a managed object is materialized or when an unmanaged object has been + /// added to the Realm. It can be useful for providing some initialization logic as when the constructor is invoked, + /// it is not yet clear whether the object is managed or not. + /// + partial void OnManaged(); + + private event PropertyChangedEventHandler? _propertyChanged; + + /// + public event PropertyChangedEventHandler? PropertyChanged + { + add + { + if (_propertyChanged == null) + { + SubscribeForNotifications(); + } + + _propertyChanged += value; + } + + remove + { + _propertyChanged -= value; + + if (_propertyChanged == null) + { + UnsubscribeFromNotifications(); + } + } + } + + /// + /// Called when a property has changed on this class. + /// + /// The name of the property. + /// + /// For this method to be called, you need to have first subscribed to . + /// This can be used to react to changes to the current object, e.g. raising for computed properties. + /// + /// + /// + /// class MyClass : IRealmObject + /// { + /// public int StatusCodeRaw { get; set; } + /// public StatusCodeEnum StatusCode => (StatusCodeEnum)StatusCodeRaw; + /// partial void OnPropertyChanged(string propertyName) + /// { + /// if (propertyName == nameof(StatusCodeRaw)) + /// { + /// RaisePropertyChanged(nameof(StatusCode)); + /// } + /// } + /// } + /// + /// Here, we have a computed property that depends on a persisted one. In order to notify any + /// subscribers that StatusCode has changed, we implement and + /// raise manually by calling . + /// + partial void OnPropertyChanged(string? propertyName); + + private void RaisePropertyChanged([CallerMemberName] string propertyName = "") + { + _propertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + OnPropertyChanged(propertyName); + } + + private void SubscribeForNotifications() + { + Accessor.SubscribeForNotifications(RaisePropertyChanged); + } + + private void UnsubscribeFromNotifications() + { + Accessor.UnsubscribeFromNotifications(); + } + + /// + /// Converts a to . Equivalent to . + /// + /// The to convert. + /// The stored in the . + public static explicit operator Dotnet_3597?(Realms.RealmValue val) => val.Type == Realms.RealmValueType.Null ? null : val.AsRealmObject(); + + /// + /// Implicitly constructs a from . + /// + /// The value to store in the . + /// A containing the supplied . + public static implicit operator Realms.RealmValue(Dotnet_3597? val) => val == null ? Realms.RealmValue.Null : Realms.RealmValue.Object(val); + + /// + /// Implicitly constructs a from . + /// + /// The value to store in the . + /// A containing the supplied . + public static implicit operator Realms.QueryArgument(Dotnet_3597? val) => (Realms.RealmValue)val; + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public TypeInfo GetTypeInfo() => Accessor.GetTypeInfo(this); + + /// + public override bool Equals(object? obj) + { + if (obj is null) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is InvalidObject) + { + return !IsValid; + } + + if (!(obj is Realms.IRealmObjectBase iro)) + { + return false; + } + + return Accessor.Equals(iro.Accessor); + } + + /// + public override int GetHashCode() => IsManaged ? Accessor.GetHashCode() : base.GetHashCode(); + + /// + public override string? ToString() => Accessor.ToString(); + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + private class Dotnet_3597ObjectHelper : Realms.Weaving.IRealmObjectHelper + { + public void CopyToRealm(Realms.IRealmObjectBase instance, bool update, bool skipDefaults) + { + throw new InvalidOperationException("This method should not be called for source generated classes."); + } + + public Realms.ManagedAccessor CreateAccessor() => new Dotnet_3597ManagedAccessor(); + + public Realms.IRealmObjectBase CreateInstance() => new Dotnet_3597(); + + public bool TryGetPrimaryKeyValue(Realms.IRealmObjectBase instance, out RealmValue value) + { + value = RealmValue.Null; + return false; + } + } + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + internal interface IDotnet_3597Accessor : Realms.IRealmAccessor + { + int IntProp { get; set; } + + string FloatProp { get; set; } + + System.Collections.Generic.IList FloatList { get; } + } + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + private class Dotnet_3597ManagedAccessor : Realms.ManagedAccessor, IDotnet_3597Accessor + { + public int IntProp + { + get => (int)GetValue("IntProp"); + set => SetValue("IntProp", value); + } + + public string FloatProp + { + get => (string)GetValue("FloatProp")!; + set => SetValue("FloatProp", value); + } + + private System.Collections.Generic.IList _floatList = null!; + public System.Collections.Generic.IList FloatList + { + get + { + if (_floatList == null) + { + _floatList = GetListValue("FloatList"); + } + + return _floatList; + } + } + } + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + private class Dotnet_3597UnmanagedAccessor : Realms.UnmanagedAccessor, IDotnet_3597Accessor + { + public override ObjectSchema ObjectSchema => Dotnet_3597.RealmSchema; + + private int _intProp; + public int IntProp + { + get => _intProp; + set + { + _intProp = value; + RaisePropertyChanged("IntProp"); + } + } + + private string _floatProp = null!; + public string FloatProp + { + get => _floatProp; + set + { + _floatProp = value; + RaisePropertyChanged("FloatProp"); + } + } + + public System.Collections.Generic.IList FloatList { get; } = new List(); + + public Dotnet_3597UnmanagedAccessor(Type objectType) : base(objectType) + { + } + + public override Realms.RealmValue GetValue(string propertyName) + { + return propertyName switch + { + "IntProp" => _intProp, + "FloatProp" => _floatProp, + _ => throw new MissingMemberException($"The object does not have a gettable Realm property with name {propertyName}"), + }; + } + + public override void SetValue(string propertyName, Realms.RealmValue val) + { + switch (propertyName) + { + case "IntProp": + IntProp = (int)val; + return; + case "FloatProp": + FloatProp = (string)val!; + return; + default: + throw new MissingMemberException($"The object does not have a settable Realm property with name {propertyName}"); + } + } + + public override void SetValueUnique(string propertyName, Realms.RealmValue val) + { + throw new InvalidOperationException("Cannot set the value of an non primary key property with SetValueUnique"); + } + + public override IList GetListValue(string propertyName) + { + return propertyName switch + { + "FloatList" => (IList)FloatList, + _ => throw new MissingMemberException($"The object does not have a Realm list property with name {propertyName}"), + }; + } + + public override ISet GetSetValue(string propertyName) + { + throw new MissingMemberException($"The object does not have a Realm set property with name {propertyName}"); + } + + public override IDictionary GetDictionaryValue(string propertyName) + { + throw new MissingMemberException($"The object does not have a Realm dictionary property with name {propertyName}"); + } + } + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + private class Dotnet_3597Serializer : Realms.Serialization.RealmObjectSerializerBase + { + public override string SchemaName => "Dotnet_3597"; + + protected override void SerializeValue(MongoDB.Bson.Serialization.BsonSerializationContext context, BsonSerializationArgs args, Dotnet_3597 value) + { + context.Writer.WriteStartDocument(); + + WriteValue(context, args, "IntProp", value.IntProp); + WriteValue(context, args, "FloatProp", value.FloatProp); + WriteList(context, args, "FloatList", value.FloatList); + + context.Writer.WriteEndDocument(); + } + + protected override Dotnet_3597 CreateInstance() => new Dotnet_3597(); + + protected override void ReadValue(Dotnet_3597 instance, string name, BsonDeserializationContext context) + { + switch (name) + { + case "IntProp": + instance.IntProp = BsonSerializer.LookupSerializer().Deserialize(context); + break; + case "FloatProp": + instance.FloatProp = BsonSerializer.LookupSerializer().Deserialize(context); + break; + case "FloatList": + ReadArray(instance, name, context); + break; + default: + context.Reader.SkipValue(); + break; + } + } + + protected override void ReadArrayElement(Dotnet_3597 instance, string name, BsonDeserializationContext context) + { + switch (name) + { + case "FloatList": + instance.FloatList.Add(BsonSerializer.LookupSerializer().Deserialize(context)); + break; + } + } + + protected override void ReadDocumentField(Dotnet_3597 instance, string name, string fieldName, BsonDeserializationContext context) + { + // No persisted dictionary properties to deserialize + } + } + } +} diff --git a/wrappers/src/shared_realm_cs.cpp b/wrappers/src/shared_realm_cs.cpp index 25661bfc71..8b1d6b9061 100644 --- a/wrappers/src/shared_realm_cs.cpp +++ b/wrappers/src/shared_realm_cs.cpp @@ -693,6 +693,21 @@ REALM_EXPORT Object* shared_realm_get_object_for_primary_key(SharedRealm& realm, }); } +REALM_EXPORT Object* shared_realm_get_object_for_object(SharedRealm& realm, Object& object, NativeException::Marshallable& ex) +{ + return handle_errors(ex, [&]() -> Object* { + realm->verify_thread(); + + auto table = realm->read_group().get_table(object.get_object_schema().table_key); + auto obj = table->try_get_object(object.get_obj().get_key()); + if (!obj) { + return nullptr; + } + + return new Object(realm, std::move(obj)); + }); +} + REALM_EXPORT Results* shared_realm_create_results(SharedRealm& realm, TableKey table_key, NativeException::Marshallable& ex) { return handle_errors(ex, [&]() {