From 187f501a4dffcb3ee81933af44e2ef92cbb45fdd Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Mon, 12 Aug 2024 13:32:35 +0200 Subject: [PATCH] Added stub for unmanaged object dynamic api --- .../Accessors/ManagedAccessor.cs | 4 +- .../Accessors/UnmanagedAccessor.cs | 32 +++ .../Realm/Dynamic/DynamicManagedObjectApi.cs | 235 ++++++++++++++++++ Realm/Realm/Dynamic/DynamicObjectApi.cs | 194 +-------------- .../Dynamic/DynamicUnmanagedObjectApi.cs | 97 ++++++++ .../Database/RelaxedSchemaTests.cs | 2 + 6 files changed, 380 insertions(+), 184 deletions(-) create mode 100644 Realm/Realm/Dynamic/DynamicManagedObjectApi.cs create mode 100644 Realm/Realm/Dynamic/DynamicUnmanagedObjectApi.cs diff --git a/Realm/Realm/DatabaseTypes/Accessors/ManagedAccessor.cs b/Realm/Realm/DatabaseTypes/Accessors/ManagedAccessor.cs index c621787407..972734a4bc 100644 --- a/Realm/Realm/DatabaseTypes/Accessors/ManagedAccessor.cs +++ b/Realm/Realm/DatabaseTypes/Accessors/ManagedAccessor.cs @@ -87,7 +87,7 @@ protected ManagedAccessor() { _hashCode = new(() => ObjectHandle!.GetObjHash()); _objectSchema = new(() => Realm!.Config.RelaxedSchema ? Metadata!.Schema.MakeCopyWithHandle(ObjectHandle!) : Metadata!.Schema); - _dynamicObjectApi = new(() => new(this)); + _dynamicObjectApi = new(() => new DynamicManagedObjectApi(this)); } [MemberNotNull(nameof(Realm), nameof(ObjectHandle), nameof(Metadata))] @@ -114,7 +114,7 @@ public RealmValue GetValue(string propertyName) return ObjectHandle.GetValue(propertyName, Metadata, Realm); } - /// + /// AddDocs public bool TryGetValue(string propertyName, out RealmValue value) { return ObjectHandle.TryGetValue(propertyName, Metadata, Realm, out value); diff --git a/Realm/Realm/DatabaseTypes/Accessors/UnmanagedAccessor.cs b/Realm/Realm/DatabaseTypes/Accessors/UnmanagedAccessor.cs index 4022e94cd4..378bbb7951 100644 --- a/Realm/Realm/DatabaseTypes/Accessors/UnmanagedAccessor.cs +++ b/Realm/Realm/DatabaseTypes/Accessors/UnmanagedAccessor.cs @@ -37,6 +37,9 @@ public abstract class UnmanagedAccessor : IRealmAccessor private Action? _onNotifyPropertyChanged; + //TODO we could initialize this lazily + protected Dictionary _extraProperties = new(); + /// public bool IsManaged => false; @@ -93,6 +96,10 @@ public IQueryable GetBacklinks(string propertyName) /// public abstract void SetValueUnique(string propertyName, RealmValue val); + public abstract bool TryGet(string propertyName, out RealmValue value); + + public abstract bool Unset(string propertyName); + /// public virtual void SubscribeForNotifications(Action notifyPropertyChangedDelegate) { @@ -168,5 +175,30 @@ public override void SetValueUnique(string propertyName, RealmValue val) { throw new NotSupportedException("This should not be used for now"); } + + public override bool TryGet(string propertyName, out RealmValue value) + { + return _extraProperties.TryGetValue(propertyName, out value); + } + + public override bool Unset(string propertyName) + { + return _extraProperties.Remove(propertyName); + } + + public bool TryGetExtraProperty(string propertyName, out RealmValue value) + { + return _extraProperties.TryGetValue(propertyName, out value); + } + + public RealmValue GetExtraProperty(string propertyName) + { + return _extraProperties[propertyName]; + } + + public void SetExtraProperty(string propertyName, RealmValue val) + { + _extraProperties[propertyName] = val; + } } } diff --git a/Realm/Realm/Dynamic/DynamicManagedObjectApi.cs b/Realm/Realm/Dynamic/DynamicManagedObjectApi.cs new file mode 100644 index 0000000000..e4ec997625 --- /dev/null +++ b/Realm/Realm/Dynamic/DynamicManagedObjectApi.cs @@ -0,0 +1,235 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2024 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using Realms.Helpers; +using Realms.Schema; + +namespace Realms +{ + /// + public class DynamicManagedObjectApi : DynamicObjectApi + { + private readonly ManagedAccessor _managedAccessor; + + private readonly bool _isRelaxedSchema; + + internal DynamicManagedObjectApi(ManagedAccessor managedAccessor) + { + _managedAccessor = managedAccessor; + _isRelaxedSchema = managedAccessor.Realm.Config.RelaxedSchema; + } + + /// + public override RealmValue Get(string propertyName) + { + CheckGetPropertySuitability(propertyName); + + return _managedAccessor.GetValue(propertyName); + } + + /// + public override T Get(string propertyName) + { + return Get(propertyName).As(); + } + + /// + public override bool TryGet(string propertyName, out RealmValue propertyValue) + { + CheckGetPropertySuitability(propertyName); + + return _managedAccessor.TryGetValue(propertyName, out propertyValue); + } + + /// + public override bool TryGet(string propertyName, out T? propertyValue) + where T : default + { + var foundValue = TryGet(propertyName, out var val); + if (foundValue) + { + propertyValue = val.As(); + return true; + } + + propertyValue = default; + return false; + } + + /// + public override void Set(string propertyName, RealmValue value) + { + if (GetModelProperty(propertyName, throwOnMissing: !_isRelaxedSchema) is Property property) + { + if (property.Type.IsComputed()) + { + throw new NotSupportedException( + $"{_managedAccessor.ObjectSchema.Name}.{propertyName} is {property.GetDotnetTypeName()} (backlinks collection) and can't be set directly"); + } + + if (property.Type.IsCollection(out _)) + { + throw new NotSupportedException( + $"{_managedAccessor.ObjectSchema.Name}.{propertyName} is {property.GetDotnetTypeName()} (collection) and can't be set directly."); + } + + if (!property.Type.IsNullable() && value.Type == RealmValueType.Null) + { + throw new ArgumentException($"{_managedAccessor.ObjectSchema.Name}.{propertyName} is {property.GetDotnetTypeName()} which is not nullable, but the supplied value is ."); + } + + if (!property.Type.IsRealmValue() && value.Type != RealmValueType.Null && property.Type.ToRealmValueType() != value.Type) + { + throw new ArgumentException($"{_managedAccessor.ObjectSchema.Name}.{propertyName} is {property.GetDotnetTypeName()} but the supplied value is {value.AsAny()?.GetType().Name} ({value})."); + } + + if (property.IsPrimaryKey) + { + _managedAccessor.SetValueUnique(propertyName, value); + return; + } + } + + _managedAccessor.SetValue(propertyName, value); + } + + /// + public override bool Unset(string propertyName) + { + return _managedAccessor.UnsetProperty(propertyName); + } + + /// + public override IQueryable GetBacklinks(string propertyName) + { + var property = GetModelProperty(propertyName, PropertyTypeEx.IsComputed); + + var resultsHandle = _managedAccessor.ObjectHandle.GetBacklinks(propertyName, _managedAccessor.Metadata); + + var relatedMeta = _managedAccessor.Realm.Metadata[property.ObjectType!]; + if (relatedMeta.Schema.BaseType == ObjectSchema.ObjectType.EmbeddedObject) + { + return new RealmResults(_managedAccessor.Realm, resultsHandle, relatedMeta); + } + + return new RealmResults(_managedAccessor.Realm, resultsHandle, relatedMeta); + } + + /// + public override IQueryable GetBacklinksFromType(string fromObjectType, string fromPropertyName) + { + Argument.Ensure(_managedAccessor.Realm.Metadata.TryGetValue(fromObjectType, out var relatedMeta), $"Could not find schema for type {fromObjectType}", nameof(fromObjectType)); + + var resultsHandle = _managedAccessor.ObjectHandle.GetBacklinksForType(relatedMeta.TableKey, fromPropertyName, relatedMeta); + if (relatedMeta.Schema.BaseType == ObjectSchema.ObjectType.EmbeddedObject) + { + return new RealmResults(_managedAccessor.Realm, resultsHandle, relatedMeta); + } + + return new RealmResults(_managedAccessor.Realm, resultsHandle, relatedMeta); + } + + /// + public override IList GetList(string propertyName) + { + var property = GetModelProperty(propertyName, PropertyTypeEx.IsList); + + var result = _managedAccessor.ObjectHandle.GetList(_managedAccessor.Realm, propertyName, _managedAccessor.Metadata, property.ObjectType); + result.IsDynamic = true; + return result; + } + + /// + public override ISet GetSet(string propertyName) + { + var property = GetModelProperty(propertyName, PropertyTypeEx.IsSet); + + var result = _managedAccessor.ObjectHandle.GetSet(_managedAccessor.Realm, propertyName, _managedAccessor.Metadata, property.ObjectType); + result.IsDynamic = true; + return result; + } + + /// + public override IDictionary GetDictionary(string propertyName) + { + var property = GetModelProperty(propertyName, PropertyTypeEx.IsDictionary); + + var result = _managedAccessor.ObjectHandle.GetDictionary(_managedAccessor.Realm, propertyName, _managedAccessor.Metadata, property.ObjectType); + result.IsDynamic = true; + return result; + } + + private void CheckGetPropertySuitability(string propertyName) + { + if (GetModelProperty(propertyName, throwOnMissing: !_isRelaxedSchema) is Property property) + { + if (property.Type.IsComputed()) + { + throw new NotSupportedException( + $"{_managedAccessor.ObjectSchema.Name}.{propertyName} is {property.GetDotnetTypeName()} (backlinks collection) and can't be accessed using {nameof(Dynamic)}.{nameof(Get)}. Use {nameof(GetBacklinks)} instead."); + } + + if (property.Type.IsCollection(out var collectionType) && collectionType == PropertyType.Set) + { + throw new NotSupportedException( + $"{_managedAccessor.ObjectSchema.Name}.{propertyName} is {property.GetDotnetTypeName()} and can't be accessed using {nameof(Dynamic)}.{nameof(Get)}. Use GetSet instead."); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Property? GetModelProperty(string propertyName, bool throwOnMissing) + { + Argument.NotNull(propertyName, nameof(propertyName)); + + if (!_managedAccessor.ObjectSchema.TryFindModelProperty(propertyName, out var property)) + { + if (throwOnMissing) + { + throw new MissingMemberException(_managedAccessor.ObjectSchema.Name, propertyName); + } + + return null; + } + + return property; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Property GetModelProperty(string propertyName, Func typeCheck, [CallerMemberName] string methodName = "") + { + Argument.NotNull(propertyName, nameof(propertyName)); + + if (!_managedAccessor.ObjectSchema.TryFindModelProperty(propertyName, out var property)) + { + throw new MissingMemberException(_managedAccessor.ObjectSchema.Name, propertyName); + } + + if (!typeCheck(property.Type)) + { + throw new ArgumentException($"{_managedAccessor.ObjectSchema.Name}.{propertyName} is {property.GetDotnetTypeName()} which can't be accessed using {methodName}."); + } + + return property; + } + } +} diff --git a/Realm/Realm/Dynamic/DynamicObjectApi.cs b/Realm/Realm/Dynamic/DynamicObjectApi.cs index 6115222d8b..43b87cff60 100644 --- a/Realm/Realm/Dynamic/DynamicObjectApi.cs +++ b/Realm/Realm/Dynamic/DynamicObjectApi.cs @@ -29,25 +29,10 @@ namespace Realms /// A class that exposes a set of API to access the data in a managed RealmObject dynamically. /// /// - public class DynamicObjectApi + public abstract class DynamicObjectApi { - private readonly ManagedAccessor _managedAccessor; - - private readonly bool _isRelaxedSchema; - - internal DynamicObjectApi(ManagedAccessor managedAccessor) - { - _managedAccessor = managedAccessor; - _isRelaxedSchema = managedAccessor.Realm.Config.RelaxedSchema; - } - //TODO Add docs - public RealmValue Get(string propertyName) - { - CheckGetPropertySuitability(propertyName); - - return _managedAccessor.GetValue(propertyName); - } + public abstract RealmValue Get(string propertyName); /// /// Gets the value of the property and casts it to @@ -63,32 +48,13 @@ public RealmValue Get(string propertyName) /// Casting to is always valid. When the property is of type /// object, casting to is always valid. /// - public T Get(string propertyName) - { - return Get(propertyName).As(); - } + public abstract T Get(string propertyName); //TODO Add docs - public bool TryGet(string propertyName, out RealmValue propertyValue) - { - CheckGetPropertySuitability(propertyName); - - return _managedAccessor.TryGetValue(propertyName, out propertyValue); - } + public abstract bool TryGet(string propertyName, out RealmValue propertyValue); //TODO Add docs - public bool TryGet(string propertyName, out T? propertyValue) - { - var foundValue = TryGet(propertyName, out var val); - if (foundValue) - { - propertyValue = val.As(); - return true; - } - - propertyValue = default; - return false; - } + public abstract bool TryGet(string propertyName, out T? propertyValue); /// /// Sets the value of the property at to @@ -96,47 +62,10 @@ public bool TryGet(string propertyName, out T? propertyValue) /// /// The name of the property to set. /// The new value of the property. - public void Set(string propertyName, RealmValue value) - { - if (GetModelProperty(propertyName, throwOnMissing: !_isRelaxedSchema) is Property property) - { - if (property.Type.IsComputed()) - { - throw new NotSupportedException( - $"{_managedAccessor.ObjectSchema.Name}.{propertyName} is {property.GetDotnetTypeName()} (backlinks collection) and can't be set directly"); - } - - if (property.Type.IsCollection(out _)) - { - throw new NotSupportedException( - $"{_managedAccessor.ObjectSchema.Name}.{propertyName} is {property.GetDotnetTypeName()} (collection) and can't be set directly."); - } - - if (!property.Type.IsNullable() && value.Type == RealmValueType.Null) - { - throw new ArgumentException($"{_managedAccessor.ObjectSchema.Name}.{propertyName} is {property.GetDotnetTypeName()} which is not nullable, but the supplied value is ."); - } - - if (!property.Type.IsRealmValue() && value.Type != RealmValueType.Null && property.Type.ToRealmValueType() != value.Type) - { - throw new ArgumentException($"{_managedAccessor.ObjectSchema.Name}.{propertyName} is {property.GetDotnetTypeName()} but the supplied value is {value.AsAny()?.GetType().Name} ({value})."); - } - - if (property.IsPrimaryKey) - { - _managedAccessor.SetValueUnique(propertyName, value); - return; - } - } - - _managedAccessor.SetValue(propertyName, value); - } + public abstract void Set(string propertyName, RealmValue value); //TODO Add docs - public bool Unset(string propertyName) - { - return _managedAccessor.UnsetProperty(propertyName); - } + public abstract bool Unset(string propertyName); /// /// Gets the value of a backlink property. This property must have been declared @@ -147,20 +76,7 @@ public bool Unset(string propertyName) /// A queryable collection containing all objects pointing to this one via the /// property specified in . /// - public IQueryable GetBacklinks(string propertyName) - { - var property = GetModelProperty(propertyName, PropertyTypeEx.IsComputed); - - var resultsHandle = _managedAccessor.ObjectHandle.GetBacklinks(propertyName, _managedAccessor.Metadata); - - var relatedMeta = _managedAccessor.Realm.Metadata[property.ObjectType!]; - if (relatedMeta.Schema.BaseType == ObjectSchema.ObjectType.EmbeddedObject) - { - return new RealmResults(_managedAccessor.Realm, resultsHandle, relatedMeta); - } - - return new RealmResults(_managedAccessor.Realm, resultsHandle, relatedMeta); - } + public abstract IQueryable GetBacklinks(string propertyName); /// /// Gets a collection of all the objects that link to this object in the specified relationship. @@ -171,18 +87,7 @@ public IQueryable GetBacklinks(string propertyName) /// A queryable collection containing all objects of that link /// to the current object via . /// - public IQueryable GetBacklinksFromType(string fromObjectType, string fromPropertyName) - { - Argument.Ensure(_managedAccessor.Realm.Metadata.TryGetValue(fromObjectType, out var relatedMeta), $"Could not find schema for type {fromObjectType}", nameof(fromObjectType)); - - var resultsHandle = _managedAccessor.ObjectHandle.GetBacklinksForType(relatedMeta.TableKey, fromPropertyName, relatedMeta); - if (relatedMeta.Schema.BaseType == ObjectSchema.ObjectType.EmbeddedObject) - { - return new RealmResults(_managedAccessor.Realm, resultsHandle, relatedMeta); - } - - return new RealmResults(_managedAccessor.Realm, resultsHandle, relatedMeta); - } + public abstract IQueryable GetBacklinksFromType(string fromObjectType, string fromPropertyName); /// /// Gets a property. @@ -197,14 +102,7 @@ public IQueryable GetBacklinksFromType(string fromObjectType, /// Casting the elements to is always valid. When the collection /// contains objects, casting to is always valid. /// - public IList GetList(string propertyName) - { - var property = GetModelProperty(propertyName, PropertyTypeEx.IsList); - - var result = _managedAccessor.ObjectHandle.GetList(_managedAccessor.Realm, propertyName, _managedAccessor.Metadata, property.ObjectType); - result.IsDynamic = true; - return result; - } + public abstract IList GetList(string propertyName); /// /// Gets a property. @@ -219,14 +117,7 @@ public IList GetList(string propertyName) /// Casting the elements to is always valid. When the collection /// contains objects, casting to is always valid. /// - public ISet GetSet(string propertyName) - { - var property = GetModelProperty(propertyName, PropertyTypeEx.IsSet); - - var result = _managedAccessor.ObjectHandle.GetSet(_managedAccessor.Realm, propertyName, _managedAccessor.Metadata, property.ObjectType); - result.IsDynamic = true; - return result; - } + public abstract ISet GetSet(string propertyName); /// /// Gets a property. @@ -241,67 +132,6 @@ public ISet GetSet(string propertyName) /// Casting the values to is always valid. When the collection /// contains objects, casting to is always valid. /// - public IDictionary GetDictionary(string propertyName) - { - var property = GetModelProperty(propertyName, PropertyTypeEx.IsDictionary); - - var result = _managedAccessor.ObjectHandle.GetDictionary(_managedAccessor.Realm, propertyName, _managedAccessor.Metadata, property.ObjectType); - result.IsDynamic = true; - return result; - } - - private void CheckGetPropertySuitability(string propertyName) - { - if (GetModelProperty(propertyName, throwOnMissing: !_isRelaxedSchema) is Property property) - { - if (property.Type.IsComputed()) - { - throw new NotSupportedException( - $"{_managedAccessor.ObjectSchema.Name}.{propertyName} is {property.GetDotnetTypeName()} (backlinks collection) and can't be accessed using {nameof(Dynamic)}.{nameof(Get)}. Use {nameof(GetBacklinks)} instead."); - } - - if (property.Type.IsCollection(out var collectionType) && collectionType == PropertyType.Set) - { - throw new NotSupportedException( - $"{_managedAccessor.ObjectSchema.Name}.{propertyName} is {property.GetDotnetTypeName()} and can't be accessed using {nameof(Dynamic)}.{nameof(Get)}. Use GetSet instead."); - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private Property? GetModelProperty(string propertyName, bool throwOnMissing) - { - Argument.NotNull(propertyName, nameof(propertyName)); - - if (!_managedAccessor.ObjectSchema.TryFindModelProperty(propertyName, out var property)) - { - if (throwOnMissing) - { - throw new MissingMemberException(_managedAccessor.ObjectSchema.Name, propertyName); - } - - return null; - } - - return property; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private Property GetModelProperty(string propertyName, Func typeCheck, [CallerMemberName] string methodName = "") - { - Argument.NotNull(propertyName, nameof(propertyName)); - - if (!_managedAccessor.ObjectSchema.TryFindModelProperty(propertyName, out var property)) - { - throw new MissingMemberException(_managedAccessor.ObjectSchema.Name, propertyName); - } - - if (!typeCheck(property.Type)) - { - throw new ArgumentException($"{_managedAccessor.ObjectSchema.Name}.{propertyName} is {property.GetDotnetTypeName()} which can't be accessed using {methodName}."); - } - - return property; - } + public abstract IDictionary GetDictionary(string propertyName); } } diff --git a/Realm/Realm/Dynamic/DynamicUnmanagedObjectApi.cs b/Realm/Realm/Dynamic/DynamicUnmanagedObjectApi.cs new file mode 100644 index 0000000000..951469f78e --- /dev/null +++ b/Realm/Realm/Dynamic/DynamicUnmanagedObjectApi.cs @@ -0,0 +1,97 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2024 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Realms.Dynamic +{ + public class DynamicUnmanagedObjectApi : DynamicObjectApi + { + private readonly UnmanagedAccessor _unmanagedAccessor; + + public DynamicUnmanagedObjectApi(UnmanagedAccessor unmanagedAccessor) + { + _unmanagedAccessor = unmanagedAccessor; + } + + /// + public override RealmValue Get(string propertyName) + { + return _unmanagedAccessor.GetValue(propertyName); + } + + /// + public override T Get(string propertyName) + { + return _unmanagedAccessor.GetValue(propertyName).As(); + } + + /// + public override bool TryGet(string propertyName, out RealmValue propertyValue) + { + return _unmanagedAccessor.TryGet(propertyName, out propertyValue); + } + + /// + public override bool TryGet(string propertyName, out T? propertyValue) where T : default + { + throw new NotImplementedException(); + } + + /// + public override IList GetList(string propertyName) + { + return _unmanagedAccessor.GetListValue(propertyName); + } + + /// + public override IDictionary GetDictionary(string propertyName) + { + return _unmanagedAccessor.GetDictionaryValue(propertyName); + } + + /// + public override ISet GetSet(string propertyName) + { + return _unmanagedAccessor.GetSetValue(propertyName); + } + + /// + public override void Set(string propertyName, RealmValue value) + { + _unmanagedAccessor.SetValue(propertyName, value); + } + + /// + public override bool Unset(string propertyName) + { + return _unmanagedAccessor.Unset(propertyName); + } + + /// + public override IQueryable GetBacklinks(string propertyName) => + throw new NotSupportedException("Using the GetBacklinks is only possible for managed (persisted) objects."); + + /// + public override IQueryable GetBacklinksFromType(string fromObjectType, string fromPropertyName) => + throw new NotSupportedException("Using the GetBacklinks is only possible for managed (persisted) objects."); + } +} diff --git a/Tests/Realm.Tests/Database/RelaxedSchemaTests.cs b/Tests/Realm.Tests/Database/RelaxedSchemaTests.cs index 43f64159b9..bebe78a4e8 100644 --- a/Tests/Realm.Tests/Database/RelaxedSchemaTests.cs +++ b/Tests/Realm.Tests/Database/RelaxedSchemaTests.cs @@ -340,6 +340,8 @@ public void ObjectSchema_TryFindProperty_ReturnsExtraProperties() * - move unmanaged object to managed with extra properties and relaxed schema on (should copy properties) * - move unmanaged object to managed with extra properties and relaxed schema off (should throw) * - tests for unmanaged object dynamic api + * + * - serialization/deserialization */