diff --git a/Realm/Realm.SourceGenerator/ClassBuilders/ClassCodeBuilderBase.cs b/Realm/Realm.SourceGenerator/ClassBuilders/ClassCodeBuilderBase.cs new file mode 100644 index 0000000000..b8671e273d --- /dev/null +++ b/Realm/Realm.SourceGenerator/ClassBuilders/ClassCodeBuilderBase.cs @@ -0,0 +1,118 @@ +//////////////////////////////////////////////////////////////////////////// +// +// 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 Microsoft.CodeAnalysis.CSharp; + +namespace Realms.SourceGenerator; + +internal abstract class ClassCodeBuilderBase +{ + private readonly string[] _defaultNamespaces = + { + "MongoDB.Bson.Serialization", + "System", + "System.Collections.Generic", + "System.Linq", + "System.Runtime.CompilerServices", + "System.Runtime.Serialization", + "System.Xml.Serialization", + "System.Reflection", + "System.ComponentModel", + "Realms", + "Realms.Weaving", + "Realms.Schema", + }; + + protected readonly ClassInfo _classInfo; + protected readonly Lazy _ignoreFieldAttribute; + + protected string _baseInterface => $"I{_classInfo.ObjectType}"; + + protected ClassCodeBuilderBase(ClassInfo classInfo, GeneratorConfig generatorConfig) + { + _classInfo = classInfo; + + _ignoreFieldAttribute = new(() => + { + var result = "[IgnoreDataMember, XmlIgnore]"; + var customAttribute = generatorConfig.CustomIgnoreAttribute; + if (!string.IsNullOrEmpty(customAttribute)) + { + result += customAttribute; + } + + return result; + }); + } + + public static ClassCodeBuilderBase CreateBuilder(ClassInfo classInfo, GeneratorConfig generatorConfig) => classInfo.ObjectType switch + { + ObjectType.MappedObject => new MappedObjectCodeBuilder(classInfo, generatorConfig), + ObjectType.AsymmetricObject or ObjectType.EmbeddedObject or ObjectType.RealmObject => new RealmObjectCodeBuilder(classInfo, generatorConfig), + _ => throw new NotSupportedException($"Unexpected ObjectType: {classInfo.ObjectType}") + }; + + public string GenerateSource() + { + var usings = GetUsings(); + + var classString = GeneratePartialClass(); + + foreach (var enclosingClass in _classInfo.EnclosingClasses) + { + classString = $@"{SyntaxFacts.GetText(enclosingClass.Accessibility)} partial class {enclosingClass.Name} +{{ +{classString.Indent()} +}}"; + } + + if (!_classInfo.NamespaceInfo.IsGlobal) + { + classString = $@"namespace {_classInfo.NamespaceInfo.OriginalName} +{{ +{classString.Indent()} +}}"; + } + + return $@"// +#nullable enable + +{usings} + +{classString} +"; + } + + private string GetUsings() + { + var namespaces = new HashSet(_defaultNamespaces); + namespaces.UnionWith(_classInfo.Usings); + + if (!_classInfo.NamespaceInfo.IsGlobal) + { + namespaces.Add(_classInfo.NamespaceInfo.OriginalName); + } + + return string.Join(Environment.NewLine, namespaces.Where(n => !string.IsNullOrWhiteSpace(n)).OrderBy(s => s).Select(s => $"using {s};")); + } + + protected abstract string GeneratePartialClass(); +} diff --git a/Realm/Realm.SourceGenerator/ClassBuilders/MappedObjectCodeBuilder.cs b/Realm/Realm.SourceGenerator/ClassBuilders/MappedObjectCodeBuilder.cs new file mode 100644 index 0000000000..00d3328656 --- /dev/null +++ b/Realm/Realm.SourceGenerator/ClassBuilders/MappedObjectCodeBuilder.cs @@ -0,0 +1,114 @@ +//////////////////////////////////////////////////////////////////////////// +// +// 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 Microsoft.CodeAnalysis.CSharp; + +namespace Realms.SourceGenerator; + +internal class MappedObjectCodeBuilder : ClassCodeBuilderBase +{ + public MappedObjectCodeBuilder(ClassInfo classInfo, GeneratorConfig generatorConfig) : base(classInfo, generatorConfig) + { + } + + protected override string GeneratePartialClass() + { + var contents = @"private IDictionary _backingStorage = null!; + +public void SetBackingStorage(IDictionary dictionary) +{ + _backingStorage = dictionary; +}"; + + return $@"[Generated] +[Realms.Preserve(AllMembers = true)] +{SyntaxFacts.GetText(_classInfo.Accessibility)} partial class {_classInfo.Name} : {_baseInterface}, INotifyPropertyChanged +{{ +{contents.Indent()} + +{GetPropertyChanged().Indent()} +}}"; + } + + private string GetPropertyChanged() + { + return _classInfo.HasPropertyChangedEvent ? string.Empty : + @"#region INotifyPropertyChanged + +private IDisposable? _notificationToken; + +private event PropertyChangedEventHandler? _propertyChanged; + +/// +public event PropertyChangedEventHandler? PropertyChanged +{ + add + { + if (_propertyChanged == null) + { + SubscribeForNotifications(); + } + + _propertyChanged += value; + } + + remove + { + _propertyChanged -= value; + + if (_propertyChanged == null) + { + UnsubscribeFromNotifications(); + } + } +} + +partial void OnPropertyChanged(string? propertyName); + +private void RaisePropertyChanged([CallerMemberName] string propertyName = """") +{ + _propertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + OnPropertyChanged(propertyName); +} + +private void SubscribeForNotifications() +{ + _notificationToken = _backingStorage.SubscribeForKeyNotifications((sender, changes) => + { + if (changes == null) + { + return; + } + + foreach (var key in changes.ModifiedKeys) + { + RaisePropertyChanged(key); + } + + // TODO: what do we do with deleted/inserted keys + }); +} + +private void UnsubscribeFromNotifications() +{ + _notificationToken?.Dispose(); +} + +#endregion"; + } +} diff --git a/Realm/Realm.SourceGenerator/ClassBuilders/RealmObjectCodeBuilder.cs b/Realm/Realm.SourceGenerator/ClassBuilders/RealmObjectCodeBuilder.cs new file mode 100644 index 0000000000..356e8d4222 --- /dev/null +++ b/Realm/Realm.SourceGenerator/ClassBuilders/RealmObjectCodeBuilder.cs @@ -0,0 +1,959 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2022 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.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace Realms.SourceGenerator; + +internal class RealmObjectCodeBuilder : ClassCodeBuilderBase +{ + private readonly string _helperClassName; + private readonly string _accessorInterfaceName; + private readonly string _managedAccessorClassName; + private readonly string _unmanagedAccessorClassName; + private readonly string _serializerClassName; + + public RealmObjectCodeBuilder(ClassInfo classInfo, GeneratorConfig generatorConfig) + : base(classInfo, generatorConfig) + { + var className = _classInfo.Name; + + _helperClassName = $"{className}ObjectHelper"; + _accessorInterfaceName = $"I{className}Accessor"; + _managedAccessorClassName = $"{className}ManagedAccessor"; + _unmanagedAccessorClassName = $"{className}UnmanagedAccessor"; + _serializerClassName = $"{className}Serializer"; + } + + private string GenerateInterface() + { + var propertiesBuilder = new StringBuilder(); + + foreach (var property in _classInfo.Properties) + { + var type = property.TypeInfo.GetCorrectlyAnnotatedTypeName(property.IsRequired).CompleteType; + var name = property.Name; + var hasSetter = !property.TypeInfo.IsCollection; + var setterString = hasSetter ? " set;" : string.Empty; + + propertiesBuilder.AppendLine($@"{type} {name} {{ get;{setterString} }}"); + propertiesBuilder.AppendLine(); + } + + return $@"[EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] +internal interface {_accessorInterfaceName} : Realms.IRealmAccessor +{{ +{propertiesBuilder.Indent(trimNewLines: true)} +}}"; + } + + protected override string GeneratePartialClass() + { + var schemaProperties = new StringBuilder(); + var copyToRealm = new StringBuilder(); + var skipDefaultsContent = new StringBuilder(); + + foreach (var property in _classInfo.Properties) + { + if (property.TypeInfo.IsCollection) + { + if (property.TypeInfo.IsBacklink) + { + var backlinkProperty = property.GetMappedOrOriginalBacklink(); + var backlinkType = property.TypeInfo.InternalType.MapTo ?? property.TypeInfo.InternalType.TypeString; + + schemaProperties.AppendLine(@$"Realms.Schema.Property.Backlinks(""{property.GetMappedOrOriginalName()}"", ""{backlinkType}"", ""{backlinkProperty}"", managedName: ""{property.Name}""),"); + + // Nothing to do for the copy to realm part + } + else + { + var internalType = property.TypeInfo.InternalType; + + var internalTypeIsObject = internalType.ScalarType == ScalarType.Object; + var internalTypeIsRealmValue = internalType.ScalarType == ScalarType.RealmValue; + + if (internalTypeIsObject) + { + var builderMethodName = $"Object{property.TypeInfo.CollectionType}"; + + var internalTypeString = internalType.MapTo ?? internalType.TypeString; + schemaProperties.AppendLine(@$"Realms.Schema.Property.{builderMethodName}(""{property.GetMappedOrOriginalName()}"", ""{internalTypeString}"", managedName: ""{property.Name}""),"); + } + else if (internalTypeIsRealmValue) + { + var builderMethodName = $"RealmValue{property.TypeInfo.CollectionType}"; + + schemaProperties.AppendLine(@$"Realms.Schema.Property.{builderMethodName}(""{property.GetMappedOrOriginalName()}"", managedName: ""{property.Name}""),"); + } + else + { + var builderMethodName = $"Primitive{property.TypeInfo.CollectionType}"; + + var internalTypeString = GetRealmValueType(internalType); + var internalTypeNullable = property.IsRequired ? "false" : internalType.IsNullable.ToCodeString(); + + schemaProperties.AppendLine(@$"Realms.Schema.Property.{builderMethodName}(""{property.GetMappedOrOriginalName()}"", {internalTypeString}, areElementsNullable: {internalTypeNullable}, managedName: ""{property.Name}""),"); + } + + skipDefaultsContent.AppendLine($"newAccessor.{property.Name}.Clear();"); + + // The namespace is necessary, otherwise there is a conflict if the class is in the global namespace + copyToRealm.AppendLine($@"Realms.CollectionExtensions.PopulateCollection(oldAccessor.{property.Name}, newAccessor.{property.Name}, update, skipDefaults);"); + } + } + else if (property.TypeInfo.ScalarType == ScalarType.Object) + { + var objectName = property.TypeInfo.MapTo ?? property.TypeInfo.TypeString; + schemaProperties.AppendLine(@$"Realms.Schema.Property.Object(""{property.GetMappedOrOriginalName()}"", ""{objectName}"", managedName: ""{property.Name}""),"); + + if (property.TypeInfo.ObjectType == ObjectType.RealmObject) + { + copyToRealm.AppendLine(@$"if (oldAccessor.{property.Name} != null && newAccessor.Realm != null) +{{ + newAccessor.Realm.Add(oldAccessor.{property.Name}, update); +}}"); + } + + copyToRealm.AppendLine(@$"newAccessor.{property.Name} = oldAccessor.{property.Name};"); + } + else if (property.TypeInfo.ScalarType == ScalarType.RealmValue) + { + schemaProperties.AppendLine(@$"Realms.Schema.Property.RealmValue(""{property.GetMappedOrOriginalName()}"", managedName: ""{property.Name}""),"); + + copyToRealm.AppendLine(@$"newAccessor.{property.Name} = oldAccessor.{property.Name};"); + } + else + { + var realmValueType = GetRealmValueType(property.TypeInfo); + var isPrimaryKey = property.IsPrimaryKey.ToCodeString(); + var indexType = property.Index.ToCodeString(); + var isNullable = property.IsRequired ? "false" : property.TypeInfo.IsNullable.ToCodeString(); + schemaProperties.AppendLine(@$"Realms.Schema.Property.Primitive(""{property.GetMappedOrOriginalName()}"", {realmValueType}, isPrimaryKey: {isPrimaryKey}, indexType: {indexType}, isNullable: {isNullable}, managedName: ""{property.Name}""),"); + + // The rules for determining whether to always set the property value are: + // 1. If the property has [Required], always set it - this is only the case for string and byte[] properties. + // 2. If the property is a string or byte[], and it's not nullable, always set it. This is because Core's default + // for these properties is "" and byte[0], which is different from the C# default (null). + // 3. If the property is a DateTimeOffset, always set it. This is because Core's default for this property is + // 1970-01-01T00:00:00Z, which is different from the C# default (0000-00-00T00:00:00Z). + var shouldSetAlways = property.IsRequired || + (property.TypeInfo.ScalarType == ScalarType.String && property.TypeInfo.NullableAnnotation != NullableAnnotation.Annotated) || + (property.TypeInfo.ScalarType == ScalarType.Data && property.TypeInfo.NullableAnnotation != NullableAnnotation.Annotated) || + property.TypeInfo.ScalarType == ScalarType.Date; + + if (shouldSetAlways) + { + copyToRealm.AppendLine(@$"newAccessor.{property.Name} = oldAccessor.{property.Name};"); + } + else + { + copyToRealm.AppendLine(@$"if (!skipDefaults || oldAccessor.{property.Name} != default({property.TypeInfo.CompleteFullyQualifiedString})) +{{ + newAccessor.{property.Name} = oldAccessor.{property.Name}; +}}"); + } + } + } + + var skipDefaults = string.Empty; + + if (skipDefaultsContent.Length != 0) + { + skipDefaults = $@"if (!skipDefaults) +{{ +{skipDefaultsContent.Indent(trimNewLines: true)} +}} +"; + } + + var objectTypeString = $"ObjectSchema.ObjectType.{_classInfo.ObjectType}"; + + var schema = @$"/// +/// Defines the schema for the class. +/// +[System.Reflection.Obfuscation] +public static Realms.Schema.ObjectSchema RealmSchema = new Realms.Schema.ObjectSchema.Builder(""{_classInfo.MapTo ?? _classInfo.Name}"", {objectTypeString}) +{{ +{schemaProperties.Indent(trimNewLines: true)} +}}.Build();"; + + var parameterlessConstructorString = _classInfo.HasParameterlessConstructor + ? string.Empty + : @$"#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. +private {_classInfo.Name}() {{}} +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable."; + + var helperString = string.Empty; + + if (!string.IsNullOrEmpty(skipDefaults) || copyToRealm.Length > 0) + { + var helperContent = new StringBuilder(); + + if (!string.IsNullOrEmpty(skipDefaults)) + { + helperContent.AppendLine(skipDefaults); + } + + if (copyToRealm.Length > 0) + { + helperContent.Append(copyToRealm); + } + + helperString = @$"if (helper != null && oldAccessor != null) +{{ +{helperContent.Indent()}}}"; + } + + var contents = $@"{schema} + +{parameterlessConstructorString} + +#region {_baseInterface} implementation + +private {_accessorInterfaceName}? _accessor; + +Realms.IRealmAccessor Realms.IRealmObjectBase.Accessor => Accessor; + +private {_accessorInterfaceName} Accessor => _accessor ??= new {_unmanagedAccessorClassName}(typeof({_classInfo.Name})); + +/// +{_ignoreFieldAttribute.Value} +public bool IsManaged => Accessor.IsManaged; + +/// +{_ignoreFieldAttribute.Value} +public bool IsValid => Accessor.IsValid; + +/// +{_ignoreFieldAttribute.Value} +public bool IsFrozen => Accessor.IsFrozen; + +/// +{_ignoreFieldAttribute.Value} +public Realms.Realm? Realm => Accessor.Realm; + +/// +{_ignoreFieldAttribute.Value} +public Realms.Schema.ObjectSchema ObjectSchema => Accessor.ObjectSchema!; + +/// +{_ignoreFieldAttribute.Value} +public Realms.DynamicObjectApi DynamicApi => Accessor.DynamicApi; + +/// +{_ignoreFieldAttribute.Value} +public int BacklinksCount => Accessor.BacklinksCount; + +{(_classInfo.ObjectType != ObjectType.EmbeddedObject ? string.Empty : + $@"/// +{_ignoreFieldAttribute.Value} +public Realms.IRealmObjectBase? Parent => Accessor.GetParent();")} + +void ISettableManagedAccessor.SetManagedAccessor(Realms.IRealmAccessor managedAccessor, Realms.Weaving.IRealmObjectHelper? helper, bool update, bool skipDefaults) +{{ + var newAccessor = ({_accessorInterfaceName})managedAccessor; + var oldAccessor = _accessor; + _accessor = newAccessor; + +{helperString.Indent()} + + 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(); + +{(_classInfo.HasPropertyChangedEvent ? string.Empty : + @"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 {_classInfo.Name}?(Realms.RealmValue val) => val.Type == Realms.RealmValueType.Null ? null : val.AsRealmObject<{_classInfo.Name}>(); + +/// +/// Implicitly constructs a from . +/// +/// The value to store in the . +/// A containing the supplied . +public static implicit operator Realms.RealmValue({_classInfo.Name}? 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({_classInfo.Name}? val) => (Realms.RealmValue)val; + +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public TypeInfo GetTypeInfo() => Accessor.GetTypeInfo(this); + +{(_classInfo.OverridesEquals ? string.Empty : + @"/// +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); +}")} + +{(_classInfo.OverridesGetHashCode ? string.Empty : + @"/// +public override int GetHashCode() => IsManaged ? Accessor.GetHashCode() : base.GetHashCode();")} + +{(_classInfo.OverridesToString ? string.Empty : + @"/// +public override string? ToString() => Accessor.ToString();")}"; + + var classString = $@"[Generated] +[Woven(typeof({_helperClassName})), Realms.Preserve(AllMembers = true)] +{SyntaxFacts.GetText(_classInfo.Accessibility)} partial class {_classInfo.Name} : {_baseInterface}, INotifyPropertyChanged, IReflectableType +{{ + + [Realms.Preserve] + static {_classInfo.Name}() + {{ + Realms.Serialization.RealmObjectSerializer.Register(new {_serializerClassName}()); + }} + +{contents.Indent()} + +{GenerateClassObjectHelper().Indent()} + +{GenerateInterface().Indent()} + +{GenerateManagedAccessor().Indent()} + +{GenerateUnmanagedAccessor().Indent()} + +{GenerateSerializer().Indent()} +}}"; + + return classString; + } + + private string GenerateClassObjectHelper() + { + var primaryKeyProperty = _classInfo.PrimaryKey; + var valueAccessor = primaryKeyProperty == null ? "RealmValue.Null" : $"(({_accessorInterfaceName})instance.Accessor).{primaryKeyProperty.Name}"; + + return $@"[EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] +private class {_helperClassName} : 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 {_managedAccessorClassName}(); + + public Realms.IRealmObjectBase CreateInstance() => new {_classInfo.Name}(); + + public bool TryGetPrimaryKeyValue(Realms.IRealmObjectBase instance, out RealmValue value) + {{ + value = {valueAccessor}; + return {BoolToString(primaryKeyProperty != null)}; + }} +}}"; + } + + private string GenerateUnmanagedAccessor() + { + var propertiesString = new StringBuilder(); + var getValueLines = new StringBuilder(); + var setValueLines = new StringBuilder(); + var setValueUniqueLines = new StringBuilder(); + var getListValueLines = new StringBuilder(); + var getSetValueLines = new StringBuilder(); + var getDictionaryValueLines = new StringBuilder(); + + foreach (var property in _classInfo.Properties) + { + var name = property.Name; + var backingFieldName = GetBackingFieldName(name); + var (type, internalType, needsNullForgiving) = property.TypeInfo.GetCorrectlyAnnotatedTypeName(property.IsRequired); + var stringName = property.MapTo ?? name; + + if (property.TypeInfo.IsCollection) + { + if (property.TypeInfo.IsBacklink) + { + // Properties + var propertyString = @$"public {type} {name} => throw new NotSupportedException(""Using backlinks is only possible for managed(persisted) objects."");"; + + propertiesString.AppendLine(propertyString); + propertiesString.AppendLine(); + + // GetValue + getValueLines.AppendLine(@$"""{stringName}"" => throw new NotSupportedException(""Using backlinks is only possible for managed(persisted) objects.""),"); + } + else + { + string constructorString; + + switch (property.TypeInfo.CollectionType) + { + case CollectionType.List: + constructorString = $"new List<{internalType}>()"; + getListValueLines.AppendLine($@"""{stringName}"" => (IList){property.Name},"); + break; + case CollectionType.Set: + constructorString = $"new HashSet<{internalType}>(RealmSet<{internalType}>.Comparer)"; + getSetValueLines.AppendLine($@"""{stringName}"" => (ISet){property.Name},"); + break; + case CollectionType.Dictionary: + constructorString = $"new Dictionary()"; + getDictionaryValueLines.AppendLine($@"""{stringName}"" => (IDictionary){property.Name},"); + break; + default: + throw new NotImplementedException($"Collection {property.TypeInfo.CollectionType} is not supported yet"); + } + + var propertyString = $@"public {property.TypeInfo.GetCorrectlyAnnotatedTypeName(property.IsRequired).CompleteType} {property.Name} {{ get; }} = {constructorString};"; + + propertiesString.AppendLine(propertyString); + propertiesString.AppendLine(); + } + } + else + { + // Properties + var initializerString = string.Empty; + + if (!string.IsNullOrEmpty(property.Initializer)) + { + initializerString = $" {property.Initializer}"; + } + else if (needsNullForgiving) + { + initializerString = " = null!"; + } + + var backingFieldString = $"private {type} {backingFieldName}{initializerString};"; + + var propertyString = @$"public {type} {name} +{{ + get => {backingFieldName}; + set + {{ + {backingFieldName} = value; + RaisePropertyChanged(""{name}""); + }} +}}"; + + propertiesString.AppendLine(backingFieldString); + propertiesString.AppendLine(propertyString); + propertiesString.AppendLine(); + + // GetValue + getValueLines.AppendLine(@$"""{stringName}"" => {backingFieldName},"); + + // SetValue/SetValueUnique + setValueLines.AppendLine($@"case ""{stringName}"":"); + + var forceNotNullable = type == "string" || type == "byte[]" ? "!" : string.Empty; + + if (property.IsPrimaryKey) + { + setValueLines.AppendLine(@"throw new InvalidOperationException(""Cannot set the value of a primary key property with SetValue. You need to use SetValueUnique"");".Indent()); + + setValueUniqueLines.Append($@"if (propertyName != ""{stringName}"") +{{ + throw new InvalidOperationException($""Cannot set the value of non primary key property ({{propertyName}}) with SetValueUnique""); +}} + +{name} = ({type})val{forceNotNullable};"); + } + else + { + setValueLines.AppendLine(@$"{name} = ({type})val{forceNotNullable}; +return;".Indent()); + } + } + } + + // GetValue + string getValueBody; + + if (getValueLines.Length == 0) + { + getValueBody = @"throw new MissingMemberException($""The object does not have a gettable Realm property with name {propertyName}"");"; + } + else + { + getValueBody = $@"return propertyName switch +{{ +{getValueLines.Indent(trimNewLines: true)} + _ => throw new MissingMemberException($""The object does not have a gettable Realm property with name {{propertyName}}""), +}};"; + } + + // SetValue + string setValueBody; + + if (setValueLines.Length == 0) + { + setValueBody = @"throw new MissingMemberException($""The object does not have a settable Realm property with name {propertyName}"");"; + } + else + { + setValueBody = $@"switch (propertyName) +{{ +{setValueLines.Indent(trimNewLines: true)} + default: + throw new MissingMemberException($""The object does not have a settable Realm property with name {{propertyName}}""); +}}"; + } + + // SetValueUnique + if (setValueUniqueLines.Length == 0) + { + setValueUniqueLines.Append(@"throw new InvalidOperationException(""Cannot set the value of an non primary key property with SetValueUnique"");"); + } + + // GetListValue + string getListValueBody; + + if (getListValueLines.Length == 0) + { + getListValueBody = @"throw new MissingMemberException($""The object does not have a Realm list property with name {propertyName}"");"; + } + else + { + getListValueBody = $@"return propertyName switch +{{ +{getListValueLines.Indent(trimNewLines: true)} + _ => throw new MissingMemberException($""The object does not have a Realm list property with name {{propertyName}}""), +}};"; + } + + // GetSetValue + string getSetValueBody; + + if (getSetValueLines.Length == 0) + { + getSetValueBody = @"throw new MissingMemberException($""The object does not have a Realm set property with name {propertyName}"");"; + } + else + { + getSetValueBody = $@"return propertyName switch +{{ +{getSetValueLines.Indent(trimNewLines: true)} + _ => throw new MissingMemberException($""The object does not have a Realm set property with name {{propertyName}}""), +}};"; + } + + // GetDictionaryValue + string getDictionaryValueBody; + + if (getDictionaryValueLines.Length == 0) + { + getDictionaryValueBody = @"throw new MissingMemberException($""The object does not have a Realm dictionary property with name {propertyName}"");"; + } + else + { + getDictionaryValueBody = $@"return propertyName switch +{{ +{getDictionaryValueLines.Indent(trimNewLines: true)} + _ => throw new MissingMemberException($""The object does not have a Realm dictionary property with name {{propertyName}}""), +}};"; + } + + return $@"[EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] +private class {_unmanagedAccessorClassName} : Realms.UnmanagedAccessor, {_accessorInterfaceName} +{{ + public override ObjectSchema ObjectSchema => {_classInfo.Name}.RealmSchema; + +{propertiesString.Indent(trimNewLines: true)} + + public {_unmanagedAccessorClassName}(Type objectType) : base(objectType) + {{ + }} + + public override Realms.RealmValue GetValue(string propertyName) + {{ +{getValueBody.Indent(2, trimNewLines: true)} + }} + + public override void SetValue(string propertyName, Realms.RealmValue val) + {{ +{setValueBody.Indent(2, trimNewLines: true)} + }} + + public override void SetValueUnique(string propertyName, Realms.RealmValue val) + {{ +{setValueUniqueLines.Indent(2, trimNewLines: true)} + }} + + public override IList GetListValue(string propertyName) + {{ +{getListValueBody.Indent(2, trimNewLines: true)} + }} + + public override ISet GetSetValue(string propertyName) + {{ +{getSetValueBody.Indent(2, trimNewLines: true)} + }} + + public override IDictionary GetDictionaryValue(string propertyName) + {{ +{getDictionaryValueBody.Indent(2, trimNewLines: true)} + }} +}}"; + } + + private string GenerateManagedAccessor() + { + var propertiesBuilder = new StringBuilder(); + + foreach (var property in _classInfo.Properties) + { + var (type, internalType, needsNullForgiving) = property.TypeInfo.GetCorrectlyAnnotatedTypeName(property.IsRequired); + var name = property.Name; + var stringName = property.MapTo ?? name; + + if (property.TypeInfo.IsCollection) + { + var backingFieldName = GetBackingFieldName(property.Name); + var nullableForgivingString = needsNullForgiving ? " = null!" : string.Empty; + var backingFieldString = $@"private {type} {backingFieldName}{nullableForgivingString};"; + + string getFieldString; + + if (property.TypeInfo.IsBacklink) + { + getFieldString = "GetBacklinks"; + } + else + { + getFieldString = property.TypeInfo.CollectionType switch + { + CollectionType.List => "GetListValue", + CollectionType.Set => "GetSetValue", + CollectionType.Dictionary => "GetDictionaryValue", + _ => throw new NotImplementedException(), + }; + } + + propertiesBuilder.AppendLine(@$"{backingFieldString} +public {type} {name} +{{ + get + {{ + if ({backingFieldName} == null) + {{ + {backingFieldName} = {getFieldString}<{internalType}>(""{property.GetMappedOrOriginalName()}""); + }} + + return {backingFieldName}; + }} +}}"); + } + else + { + var forceNotNullable = type is "string" or "byte[]" ? "!" : string.Empty; + + var getterString = $@"get => ({type})GetValue(""{stringName}""){forceNotNullable};"; + + var setterMethod = property.IsPrimaryKey ? "SetValueUnique" : "SetValue"; + var setterString = $@"set => {setterMethod}(""{stringName}"", value);"; + + propertiesBuilder.AppendLine(@$"public {type} {name} +{{ + {getterString} + {setterString} +}}"); + } + + propertiesBuilder.AppendLine(); + } + + return $@"[EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] +private class {_managedAccessorClassName} : Realms.ManagedAccessor, {_accessorInterfaceName} +{{ +{propertiesBuilder.Indent(trimNewLines: true)} +}}"; + } + + private string GenerateSerializer() + { + var serializeValueLines = new StringBuilder(); + var readValueLines = new StringBuilder(); + var readArrayElementLines = new StringBuilder(); + var readDocumentFieldLines = new StringBuilder(); + var readArrayLines = new StringBuilder(); + var readDictionaryLines = new StringBuilder(); + + foreach (var property in _classInfo.Properties) + { + var name = property.Name; + var stringName = property.GetMappedOrOriginalName(); + + if (property.TypeInfo.IsBacklink) + { + continue; // Backlinks are not de/serialized + } + else if (property.TypeInfo.IsCollection) + { + serializeValueLines.AppendLine($"Write{property.TypeInfo.CollectionType}(context, args, \"{stringName}\", value.{name});"); + if (property.TypeInfo.IsDictionary) + { + var type = property.TypeInfo.GetCorrectlyAnnotatedTypeName(property.IsRequired).InternalType; + + var deserialize = property.TypeInfo.InternalType!.ObjectType is ObjectType.None or ObjectType.EmbeddedObject + ? $"BsonSerializer.LookupSerializer<{type}>().Deserialize(context)" + : $"Realms.Serialization.RealmObjectSerializer.LookupSerializer<{type}>()!.DeserializeById(context)!"; + + readDocumentFieldLines.AppendLine($@"case ""{stringName}"": + instance.{name}[fieldName] = {deserialize}; + break;"); + + readDictionaryLines.AppendLine($@"case ""{stringName}"":"); + } + else + { + var type = property.TypeInfo.GetCorrectlyAnnotatedTypeName(property.IsRequired).InternalType; + + var deserialize = property.TypeInfo.InternalType!.ObjectType is ObjectType.None or ObjectType.EmbeddedObject + ? $"BsonSerializer.LookupSerializer<{type}>().Deserialize(context)" + : $"Realms.Serialization.RealmObjectSerializer.LookupSerializer<{type}>()!.DeserializeById(context)!"; + + readArrayElementLines.AppendLine($@"case ""{stringName}"": + instance.{name}.Add({deserialize}); + break;"); + readArrayLines.AppendLine($@"case ""{stringName}"":"); + } + } + else + { + var type = property.TypeInfo.GetCorrectlyAnnotatedTypeName(property.IsRequired).CompleteType; + + serializeValueLines.AppendLine($"WriteValue(context, args, \"{stringName}\", value.{name});"); + var deserialize = property.TypeInfo.ObjectType is ObjectType.None or ObjectType.EmbeddedObject + ? $"BsonSerializer.LookupSerializer<{type}>().Deserialize(context)" + : $"Realms.Serialization.RealmObjectSerializer.LookupSerializer<{type}>()!.DeserializeById(context)"; + readValueLines.AppendLine($@"case ""{stringName}"": + instance.{name} = {deserialize}; + break;"); + } + } + + if (readArrayLines.Length > 0) + { + readValueLines.Append(readArrayLines); + readValueLines.AppendLine(@" ReadArray(instance, name, context); + break;"); + } + + if (readDictionaryLines.Length > 0) + { + readValueLines.Append(readDictionaryLines); + readValueLines.AppendLine(@" ReadDictionary(instance, name, context); + break;"); + } + + return $@"[EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] +private class {_serializerClassName} : Realms.Serialization.RealmObjectSerializerBase<{_classInfo.Name}> +{{ + public override string SchemaName => ""{_classInfo.MapTo ?? _classInfo.Name}""; + + protected override void SerializeValue(MongoDB.Bson.Serialization.BsonSerializationContext context, BsonSerializationArgs args, {_classInfo.Name} value) + {{ + context.Writer.WriteStartDocument(); + +{serializeValueLines.Indent(2, trimNewLines: true)} + + context.Writer.WriteEndDocument(); + }} + + protected override {_classInfo.Name} CreateInstance() => new {_classInfo.Name}(); + + protected override void ReadValue({_classInfo.Name} instance, string name, BsonDeserializationContext context) + {{ +{(readValueLines.Length == 0 + ? "// No Realm properties to deserialize" + : $@"switch (name) +{{ +{readValueLines.Indent(trimNewLines: true)} + default: + context.Reader.SkipValue(); + break; +}}").Indent(2)} + }} + + protected override void ReadArrayElement({_classInfo.Name} instance, string name, BsonDeserializationContext context) + {{ +{(readArrayElementLines.Length == 0 + ? "// No persisted list/set properties to deserialize" + : $@"switch (name) +{{ +{readArrayElementLines.Indent(trimNewLines: true)} +}}").Indent(2)} + }} + + protected override void ReadDocumentField({_classInfo.Name} instance, string name, string fieldName, BsonDeserializationContext context) + {{ +{(readDocumentFieldLines.Length == 0 + ? "// No persisted dictionary properties to deserialize" + : $@"switch (name) +{{ +{readDocumentFieldLines.Indent(trimNewLines: true)} +}}").Indent(2)} + }} +}}"; + } + + private static string GetBackingFieldName(string propertyName) + { + return "_" + char.ToLowerInvariant(propertyName[0]) + propertyName[1..]; + } + + private static string GetRealmValueType(PropertyTypeInfo propertyTypeInfo) + { + var scalarType = propertyTypeInfo.IsRealmInteger ? propertyTypeInfo.InternalType.ScalarType : propertyTypeInfo.ScalarType; + + var endString = scalarType switch + { + ScalarType.Int => "Int", + ScalarType.Bool => "Bool", + ScalarType.String => "String", + ScalarType.Data => "Data", + ScalarType.Date => "Date", + ScalarType.Float => "Float", + ScalarType.Double => "Double", + ScalarType.Object => "Object", + ScalarType.RealmValue => "RealmValue", + ScalarType.ObjectId => "ObjectId", + ScalarType.Decimal => "Decimal128", + ScalarType.Guid => "Guid", + _ => throw new NotImplementedException(), + }; + + return "Realms.RealmValueType." + endString; + } + + private static string BoolToString(bool value) => value ? "true" : "false"; +} diff --git a/Realm/Realm.SourceGenerator/ClassCodeBuilder.cs b/Realm/Realm.SourceGenerator/ClassCodeBuilder.cs deleted file mode 100644 index 24b30ae249..0000000000 --- a/Realm/Realm.SourceGenerator/ClassCodeBuilder.cs +++ /dev/null @@ -1,1040 +0,0 @@ -//////////////////////////////////////////////////////////////////////////// -// -// Copyright 2022 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.InteropServices; -using System.Text; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; - -namespace Realms.SourceGenerator -{ - internal class ClassCodeBuilder - { - private readonly string[] _defaultNamespaces = - { - "MongoDB.Bson.Serialization", - "System", - "System.Collections.Generic", - "System.Linq", - "System.Runtime.CompilerServices", - "System.Runtime.Serialization", - "System.Xml.Serialization", - "System.Reflection", - "System.ComponentModel", - "Realms", - "Realms.Weaving", - "Realms.Schema", - }; - - private readonly ClassInfo _classInfo; - private readonly Lazy _ignoreFieldAttribute; - - private readonly string _helperClassName; - private readonly string _accessorInterfaceName; - private readonly string _managedAccessorClassName; - private readonly string _unmanagedAccessorClassName; - private readonly string _serializerClassName; - - public ClassCodeBuilder(ClassInfo classInfo, GeneratorConfig generatorConfig) - { - _classInfo = classInfo; - - _ignoreFieldAttribute = new(() => - { - var result = "[IgnoreDataMember, XmlIgnore]"; - var customAttribute = generatorConfig.CustomIgnoreAttribute; - if (!string.IsNullOrEmpty(customAttribute)) - { - result += customAttribute; - } - - return result; - }); - - var className = _classInfo.Name; - - _helperClassName = $"{className}ObjectHelper"; - _accessorInterfaceName = $"I{className}Accessor"; - _managedAccessorClassName = $"{className}ManagedAccessor"; - _unmanagedAccessorClassName = $"{className}UnmanagedAccessor"; - _serializerClassName = $"{className}Serializer"; - } - - public string GenerateSource() - { - var usings = GetUsings(); - - var partialClassString = GeneratePartialClass(); - - return $@"// -#nullable enable - -{usings} - -{partialClassString} -"; - } - - private string GetUsings() - { - var namespaces = new HashSet(_defaultNamespaces); - namespaces.UnionWith(_classInfo.Usings); - - if (!_classInfo.NamespaceInfo.IsGlobal) - { - namespaces.Add(_classInfo.NamespaceInfo.OriginalName); - } - - return string.Join(Environment.NewLine, namespaces.Where(n => !string.IsNullOrWhiteSpace(n)).OrderBy(s => s).Select(s => $"using {s};")); - } - - private string GenerateInterface() - { - var propertiesBuilder = new StringBuilder(); - - foreach (var property in _classInfo.Properties) - { - var type = property.TypeInfo.GetCorrectlyAnnotatedTypeName(property.IsRequired).CompleteType; - var name = property.Name; - var hasSetter = !property.TypeInfo.IsCollection; - var setterString = hasSetter ? " set;" : string.Empty; - - propertiesBuilder.AppendLine($@"{type} {name} {{ get;{setterString} }}"); - propertiesBuilder.AppendLine(); - } - - return $@"[EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] -internal interface {_accessorInterfaceName} : Realms.IRealmAccessor -{{ -{propertiesBuilder.Indent(trimNewLines: true)} -}}"; - } - - private string GeneratePartialClass() - { - var schemaProperties = new StringBuilder(); - var copyToRealm = new StringBuilder(); - var skipDefaultsContent = new StringBuilder(); - - foreach (var property in _classInfo.Properties) - { - if (property.TypeInfo.IsCollection) - { - if (property.TypeInfo.IsBacklink) - { - var backlinkProperty = property.GetMappedOrOriginalBacklink(); - var backlinkType = property.TypeInfo.InternalType.MapTo ?? property.TypeInfo.InternalType.TypeString; - - schemaProperties.AppendLine(@$"Realms.Schema.Property.Backlinks(""{property.GetMappedOrOriginalName()}"", ""{backlinkType}"", ""{backlinkProperty}"", managedName: ""{property.Name}""),"); - - // Nothing to do for the copy to realm part - } - else - { - var internalType = property.TypeInfo.InternalType; - - var internalTypeIsObject = internalType.ScalarType == ScalarType.Object; - var internalTypeIsRealmValue = internalType.ScalarType == ScalarType.RealmValue; - - if (internalTypeIsObject) - { - var builderMethodName = $"Object{property.TypeInfo.CollectionType}"; - - var internalTypeString = internalType.MapTo ?? internalType.TypeString; - schemaProperties.AppendLine(@$"Realms.Schema.Property.{builderMethodName}(""{property.GetMappedOrOriginalName()}"", ""{internalTypeString}"", managedName: ""{property.Name}""),"); - } - else if (internalTypeIsRealmValue) - { - var builderMethodName = $"RealmValue{property.TypeInfo.CollectionType}"; - - schemaProperties.AppendLine(@$"Realms.Schema.Property.{builderMethodName}(""{property.GetMappedOrOriginalName()}"", managedName: ""{property.Name}""),"); - } - else - { - var builderMethodName = $"Primitive{property.TypeInfo.CollectionType}"; - - var internalTypeString = GetRealmValueType(internalType); - var internalTypeNullable = property.IsRequired ? "false" : internalType.IsNullable.ToCodeString(); - - schemaProperties.AppendLine(@$"Realms.Schema.Property.{builderMethodName}(""{property.GetMappedOrOriginalName()}"", {internalTypeString}, areElementsNullable: {internalTypeNullable}, managedName: ""{property.Name}""),"); - } - - skipDefaultsContent.AppendLine($"newAccessor.{property.Name}.Clear();"); - - // The namespace is necessary, otherwise there is a conflict if the class is in the global namespace - copyToRealm.AppendLine($@"Realms.CollectionExtensions.PopulateCollection(oldAccessor.{property.Name}, newAccessor.{property.Name}, update, skipDefaults);"); - } - } - else if (property.TypeInfo.ScalarType == ScalarType.Object) - { - var objectName = property.TypeInfo.MapTo ?? property.TypeInfo.TypeString; - schemaProperties.AppendLine(@$"Realms.Schema.Property.Object(""{property.GetMappedOrOriginalName()}"", ""{objectName}"", managedName: ""{property.Name}""),"); - - if (property.TypeInfo.ObjectType == ObjectType.RealmObject) - { - copyToRealm.AppendLine(@$"if (oldAccessor.{property.Name} != null && newAccessor.Realm != null) -{{ - newAccessor.Realm.Add(oldAccessor.{property.Name}, update); -}}"); - } - - copyToRealm.AppendLine(@$"newAccessor.{property.Name} = oldAccessor.{property.Name};"); - } - else if (property.TypeInfo.ScalarType == ScalarType.RealmValue) - { - schemaProperties.AppendLine(@$"Realms.Schema.Property.RealmValue(""{property.GetMappedOrOriginalName()}"", managedName: ""{property.Name}""),"); - - copyToRealm.AppendLine(@$"newAccessor.{property.Name} = oldAccessor.{property.Name};"); - } - else - { - var realmValueType = GetRealmValueType(property.TypeInfo); - var isPrimaryKey = property.IsPrimaryKey.ToCodeString(); - var indexType = property.Index.ToCodeString(); - var isNullable = property.IsRequired ? "false" : property.TypeInfo.IsNullable.ToCodeString(); - schemaProperties.AppendLine(@$"Realms.Schema.Property.Primitive(""{property.GetMappedOrOriginalName()}"", {realmValueType}, isPrimaryKey: {isPrimaryKey}, indexType: {indexType}, isNullable: {isNullable}, managedName: ""{property.Name}""),"); - - // The rules for determining whether to always set the property value are: - // 1. If the property has [Required], always set it - this is only the case for string and byte[] properties. - // 2. If the property is a string or byte[], and it's not nullable, always set it. This is because Core's default - // for these properties is "" and byte[0], which is different from the C# default (null). - // 3. If the property is a DateTimeOffset, always set it. This is because Core's default for this property is - // 1970-01-01T00:00:00Z, which is different from the C# default (0000-00-00T00:00:00Z). - var shouldSetAlways = property.IsRequired || - (property.TypeInfo.ScalarType == ScalarType.String && property.TypeInfo.NullableAnnotation != NullableAnnotation.Annotated) || - (property.TypeInfo.ScalarType == ScalarType.Data && property.TypeInfo.NullableAnnotation != NullableAnnotation.Annotated) || - property.TypeInfo.ScalarType == ScalarType.Date; - - if (shouldSetAlways) - { - copyToRealm.AppendLine(@$"newAccessor.{property.Name} = oldAccessor.{property.Name};"); - } - else - { - copyToRealm.AppendLine(@$"if (!skipDefaults || oldAccessor.{property.Name} != default({property.TypeInfo.CompleteFullyQualifiedString})) -{{ - newAccessor.{property.Name} = oldAccessor.{property.Name}; -}}"); - } - } - } - - var skipDefaults = string.Empty; - - if (skipDefaultsContent.Length != 0) - { - skipDefaults = $@"if (!skipDefaults) -{{ -{skipDefaultsContent.Indent(trimNewLines: true)} -}} -"; - } - - var objectTypeString = $"ObjectSchema.ObjectType.{_classInfo.ObjectType}"; - - var schema = @$"/// -/// Defines the schema for the class. -/// -[System.Reflection.Obfuscation] -public static Realms.Schema.ObjectSchema RealmSchema = new Realms.Schema.ObjectSchema.Builder(""{_classInfo.MapTo ?? _classInfo.Name}"", {objectTypeString}) -{{ -{schemaProperties.Indent(trimNewLines: true)} -}}.Build();"; - - var baseInterface = $"I{_classInfo.ObjectType}"; - var parameterlessConstructorString = _classInfo.HasParameterlessConstructor - ? string.Empty - : @$"#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. -private {_classInfo.Name}() {{}} -#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable."; - - var helperString = string.Empty; - - if (!string.IsNullOrEmpty(skipDefaults) || copyToRealm.Length > 0) - { - var helperContent = new StringBuilder(); - - if (!string.IsNullOrEmpty(skipDefaults)) - { - helperContent.AppendLine(skipDefaults); - } - - if (copyToRealm.Length > 0) - { - helperContent.Append(copyToRealm); - } - - helperString = @$"if (helper != null && oldAccessor != null) -{{ -{helperContent.Indent()}}}"; - } - - var contents = $@"{schema} - -{parameterlessConstructorString} - -#region {baseInterface} implementation - -private {_accessorInterfaceName}? _accessor; - -Realms.IRealmAccessor Realms.IRealmObjectBase.Accessor => Accessor; - -private {_accessorInterfaceName} Accessor => _accessor ??= new {_unmanagedAccessorClassName}(typeof({_classInfo.Name})); - -/// -{_ignoreFieldAttribute.Value} -public bool IsManaged => Accessor.IsManaged; - -/// -{_ignoreFieldAttribute.Value} -public bool IsValid => Accessor.IsValid; - -/// -{_ignoreFieldAttribute.Value} -public bool IsFrozen => Accessor.IsFrozen; - -/// -{_ignoreFieldAttribute.Value} -public Realms.Realm? Realm => Accessor.Realm; - -/// -{_ignoreFieldAttribute.Value} -public Realms.Schema.ObjectSchema ObjectSchema => Accessor.ObjectSchema!; - -/// -{_ignoreFieldAttribute.Value} -public Realms.DynamicObjectApi DynamicApi => Accessor.DynamicApi; - -/// -{_ignoreFieldAttribute.Value} -public int BacklinksCount => Accessor.BacklinksCount; - -{(_classInfo.ObjectType != ObjectType.EmbeddedObject ? string.Empty : -$@"/// -{_ignoreFieldAttribute.Value} -public Realms.IRealmObjectBase? Parent => Accessor.GetParent();")} - -void ISettableManagedAccessor.SetManagedAccessor(Realms.IRealmAccessor managedAccessor, Realms.Weaving.IRealmObjectHelper? helper, bool update, bool skipDefaults) -{{ - var newAccessor = ({_accessorInterfaceName})managedAccessor; - var oldAccessor = _accessor; - _accessor = newAccessor; - -{helperString.Indent()} - - 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(); - -{(_classInfo.HasPropertyChangedEvent ? string.Empty : -@"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 {_classInfo.Name}?(Realms.RealmValue val) => val.Type == Realms.RealmValueType.Null ? null : val.AsRealmObject<{_classInfo.Name}>(); - -/// -/// Implicitly constructs a from . -/// -/// The value to store in the . -/// A containing the supplied . -public static implicit operator Realms.RealmValue({_classInfo.Name}? 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({_classInfo.Name}? val) => (Realms.RealmValue)val; - -/// -[EditorBrowsable(EditorBrowsableState.Never)] -public TypeInfo GetTypeInfo() => Accessor.GetTypeInfo(this); - -{(_classInfo.OverridesEquals ? string.Empty : -@"/// -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); -}")} - -{(_classInfo.OverridesGetHashCode ? string.Empty : -@"/// -public override int GetHashCode() => IsManaged ? Accessor.GetHashCode() : base.GetHashCode();")} - -{(_classInfo.OverridesToString ? string.Empty : -@"/// -public override string? ToString() => Accessor.ToString();")}"; - - var classString = $@"[Generated] -[Woven(typeof({_helperClassName})), Realms.Preserve(AllMembers = true)] -{SyntaxFacts.GetText(_classInfo.Accessibility)} partial class {_classInfo.Name} : {baseInterface}, INotifyPropertyChanged, IReflectableType -{{ - - [Realms.Preserve] - static {_classInfo.Name}() - {{ - Realms.Serialization.RealmObjectSerializer.Register(new {_serializerClassName}()); - }} - -{contents.Indent()} - -{GenerateClassObjectHelper().Indent()} - -{GenerateInterface().Indent()} - -{GenerateManagedAccessor().Indent()} - -{GenerateUnmanagedAccessor().Indent()} - -{GenerateSerializer().Indent()} -}}"; - - foreach (var enclosingClass in _classInfo.EnclosingClasses) - { - classString = $@"{SyntaxFacts.GetText(enclosingClass.Accessibility)} partial class {enclosingClass.Name} -{{ -{classString.Indent()} -}}"; - } - - if (!_classInfo.NamespaceInfo.IsGlobal) - { - classString = $@"namespace {_classInfo.NamespaceInfo.OriginalName} -{{ -{classString.Indent()} -}}"; - } - - return classString; - } - - private string GenerateClassObjectHelper() - { - var primaryKeyProperty = _classInfo.PrimaryKey; - var valueAccessor = primaryKeyProperty == null ? "RealmValue.Null" : $"(({_accessorInterfaceName})instance.Accessor).{primaryKeyProperty.Name}"; - - return $@"[EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] -private class {_helperClassName} : 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 {_managedAccessorClassName}(); - - public Realms.IRealmObjectBase CreateInstance() => new {_classInfo.Name}(); - - public bool TryGetPrimaryKeyValue(Realms.IRealmObjectBase instance, out RealmValue value) - {{ - value = {valueAccessor}; - return {BoolToString(primaryKeyProperty != null)}; - }} -}}"; - } - - private string GenerateUnmanagedAccessor() - { - var propertiesString = new StringBuilder(); - var getValueLines = new StringBuilder(); - var setValueLines = new StringBuilder(); - var setValueUniqueLines = new StringBuilder(); - var getListValueLines = new StringBuilder(); - var getSetValueLines = new StringBuilder(); - var getDictionaryValueLines = new StringBuilder(); - - foreach (var property in _classInfo.Properties) - { - var name = property.Name; - var backingFieldName = GetBackingFieldName(name); - var (type, internalType, needsNullForgiving) = property.TypeInfo.GetCorrectlyAnnotatedTypeName(property.IsRequired); - var stringName = property.MapTo ?? name; - - if (property.TypeInfo.IsCollection) - { - if (property.TypeInfo.IsBacklink) - { - // Properties - var propertyString = @$"public {type} {name} => throw new NotSupportedException(""Using backlinks is only possible for managed(persisted) objects."");"; - - propertiesString.AppendLine(propertyString); - propertiesString.AppendLine(); - - // GetValue - getValueLines.AppendLine(@$"""{stringName}"" => throw new NotSupportedException(""Using backlinks is only possible for managed(persisted) objects.""),"); - } - else - { - string constructorString; - - switch (property.TypeInfo.CollectionType) - { - case CollectionType.List: - constructorString = $"new List<{internalType}>()"; - getListValueLines.AppendLine($@"""{stringName}"" => (IList){property.Name},"); - break; - case CollectionType.Set: - constructorString = $"new HashSet<{internalType}>(RealmSet<{internalType}>.Comparer)"; - getSetValueLines.AppendLine($@"""{stringName}"" => (ISet){property.Name},"); - break; - case CollectionType.Dictionary: - constructorString = $"new Dictionary()"; - getDictionaryValueLines.AppendLine($@"""{stringName}"" => (IDictionary){property.Name},"); - break; - default: - throw new NotImplementedException($"Collection {property.TypeInfo.CollectionType} is not supported yet"); - } - - var propertyString = $@"public {property.TypeInfo.GetCorrectlyAnnotatedTypeName(property.IsRequired).CompleteType} {property.Name} {{ get; }} = {constructorString};"; - - propertiesString.AppendLine(propertyString); - propertiesString.AppendLine(); - } - } - else - { - // Properties - var initializerString = string.Empty; - - if (!string.IsNullOrEmpty(property.Initializer)) - { - initializerString = $" {property.Initializer}"; - } - else if (needsNullForgiving) - { - initializerString = " = null!"; - } - - var backingFieldString = $"private {type} {backingFieldName}{initializerString};"; - - var propertyString = @$"public {type} {name} -{{ - get => {backingFieldName}; - set - {{ - {backingFieldName} = value; - RaisePropertyChanged(""{name}""); - }} -}}"; - - propertiesString.AppendLine(backingFieldString); - propertiesString.AppendLine(propertyString); - propertiesString.AppendLine(); - - // GetValue - getValueLines.AppendLine(@$"""{stringName}"" => {backingFieldName},"); - - // SetValue/SetValueUnique - setValueLines.AppendLine($@"case ""{stringName}"":"); - - var forceNotNullable = type == "string" || type == "byte[]" ? "!" : string.Empty; - - if (property.IsPrimaryKey) - { - setValueLines.AppendLine(@"throw new InvalidOperationException(""Cannot set the value of a primary key property with SetValue. You need to use SetValueUnique"");".Indent()); - - setValueUniqueLines.Append($@"if (propertyName != ""{stringName}"") -{{ - throw new InvalidOperationException($""Cannot set the value of non primary key property ({{propertyName}}) with SetValueUnique""); -}} - -{name} = ({type})val{forceNotNullable};"); - } - else - { - setValueLines.AppendLine(@$"{name} = ({type})val{forceNotNullable}; -return;".Indent()); - } - } - } - - // GetValue - string getValueBody; - - if (getValueLines.Length == 0) - { - getValueBody = @"throw new MissingMemberException($""The object does not have a gettable Realm property with name {propertyName}"");"; - } - else - { - getValueBody = $@"return propertyName switch -{{ -{getValueLines.Indent(trimNewLines: true)} - _ => throw new MissingMemberException($""The object does not have a gettable Realm property with name {{propertyName}}""), -}};"; - } - - // SetValue - string setValueBody; - - if (setValueLines.Length == 0) - { - setValueBody = @"throw new MissingMemberException($""The object does not have a settable Realm property with name {propertyName}"");"; - } - else - { - setValueBody = $@"switch (propertyName) -{{ -{setValueLines.Indent(trimNewLines: true)} - default: - throw new MissingMemberException($""The object does not have a settable Realm property with name {{propertyName}}""); -}}"; - } - - // SetValueUnique - if (setValueUniqueLines.Length == 0) - { - setValueUniqueLines.Append(@"throw new InvalidOperationException(""Cannot set the value of an non primary key property with SetValueUnique"");"); - } - - // GetListValue - string getListValueBody; - - if (getListValueLines.Length == 0) - { - getListValueBody = @"throw new MissingMemberException($""The object does not have a Realm list property with name {propertyName}"");"; - } - else - { - getListValueBody = $@"return propertyName switch -{{ -{getListValueLines.Indent(trimNewLines: true)} - _ => throw new MissingMemberException($""The object does not have a Realm list property with name {{propertyName}}""), -}};"; - } - - // GetSetValue - string getSetValueBody; - - if (getSetValueLines.Length == 0) - { - getSetValueBody = @"throw new MissingMemberException($""The object does not have a Realm set property with name {propertyName}"");"; - } - else - { - getSetValueBody = $@"return propertyName switch -{{ -{getSetValueLines.Indent(trimNewLines: true)} - _ => throw new MissingMemberException($""The object does not have a Realm set property with name {{propertyName}}""), -}};"; - } - - // GetDictionaryValue - string getDictionaryValueBody; - - if (getDictionaryValueLines.Length == 0) - { - getDictionaryValueBody = @"throw new MissingMemberException($""The object does not have a Realm dictionary property with name {propertyName}"");"; - } - else - { - getDictionaryValueBody = $@"return propertyName switch -{{ -{getDictionaryValueLines.Indent(trimNewLines: true)} - _ => throw new MissingMemberException($""The object does not have a Realm dictionary property with name {{propertyName}}""), -}};"; - } - - return $@"[EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] -private class {_unmanagedAccessorClassName} : Realms.UnmanagedAccessor, {_accessorInterfaceName} -{{ - public override ObjectSchema ObjectSchema => {_classInfo.Name}.RealmSchema; - -{propertiesString.Indent(trimNewLines: true)} - - public {_unmanagedAccessorClassName}(Type objectType) : base(objectType) - {{ - }} - - public override Realms.RealmValue GetValue(string propertyName) - {{ -{getValueBody.Indent(2, trimNewLines: true)} - }} - - public override void SetValue(string propertyName, Realms.RealmValue val) - {{ -{setValueBody.Indent(2, trimNewLines: true)} - }} - - public override void SetValueUnique(string propertyName, Realms.RealmValue val) - {{ -{setValueUniqueLines.Indent(2, trimNewLines: true)} - }} - - public override IList GetListValue(string propertyName) - {{ -{getListValueBody.Indent(2, trimNewLines: true)} - }} - - public override ISet GetSetValue(string propertyName) - {{ -{getSetValueBody.Indent(2, trimNewLines: true)} - }} - - public override IDictionary GetDictionaryValue(string propertyName) - {{ -{getDictionaryValueBody.Indent(2, trimNewLines: true)} - }} -}}"; - } - - private string GenerateManagedAccessor() - { - var propertiesBuilder = new StringBuilder(); - - foreach (var property in _classInfo.Properties) - { - var (type, internalType, needsNullForgiving) = property.TypeInfo.GetCorrectlyAnnotatedTypeName(property.IsRequired); - var name = property.Name; - var stringName = property.MapTo ?? name; - - if (property.TypeInfo.IsCollection) - { - var backingFieldName = GetBackingFieldName(property.Name); - var nullableForgivingString = needsNullForgiving ? " = null!" : string.Empty; - var backingFieldString = $@"private {type} {backingFieldName}{nullableForgivingString};"; - - string getFieldString; - - if (property.TypeInfo.IsBacklink) - { - getFieldString = "GetBacklinks"; - } - else - { - getFieldString = property.TypeInfo.CollectionType switch - { - CollectionType.List => "GetListValue", - CollectionType.Set => "GetSetValue", - CollectionType.Dictionary => "GetDictionaryValue", - _ => throw new NotImplementedException(), - }; - } - - propertiesBuilder.AppendLine(@$"{backingFieldString} -public {type} {name} -{{ - get - {{ - if ({backingFieldName} == null) - {{ - {backingFieldName} = {getFieldString}<{internalType}>(""{property.GetMappedOrOriginalName()}""); - }} - - return {backingFieldName}; - }} -}}"); - } - else - { - var forceNotNullable = type is "string" or "byte[]" ? "!" : string.Empty; - - var getterString = $@"get => ({type})GetValue(""{stringName}""){forceNotNullable};"; - - var setterMethod = property.IsPrimaryKey ? "SetValueUnique" : "SetValue"; - var setterString = $@"set => {setterMethod}(""{stringName}"", value);"; - - propertiesBuilder.AppendLine(@$"public {type} {name} -{{ - {getterString} - {setterString} -}}"); - } - - propertiesBuilder.AppendLine(); - } - - return $@"[EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] -private class {_managedAccessorClassName} : Realms.ManagedAccessor, {_accessorInterfaceName} -{{ -{propertiesBuilder.Indent(trimNewLines: true)} -}}"; - } - - private string GenerateSerializer() - { - var serializeValueLines = new StringBuilder(); - var readValueLines = new StringBuilder(); - var readArrayElementLines = new StringBuilder(); - var readDocumentFieldLines = new StringBuilder(); - var readArrayLines = new StringBuilder(); - var readDictionaryLines = new StringBuilder(); - - foreach (var property in _classInfo.Properties) - { - var name = property.Name; - var stringName = property.GetMappedOrOriginalName(); - - if (property.TypeInfo.IsBacklink) - { - continue; // Backlinks are not de/serialized - } - else if (property.TypeInfo.IsCollection) - { - serializeValueLines.AppendLine($"Write{property.TypeInfo.CollectionType}(context, args, \"{stringName}\", value.{name});"); - if (property.TypeInfo.IsDictionary) - { - var type = property.TypeInfo.GetCorrectlyAnnotatedTypeName(property.IsRequired).InternalType; - - var deserialize = property.TypeInfo.InternalType!.ObjectType is ObjectType.None or ObjectType.EmbeddedObject - ? $"BsonSerializer.LookupSerializer<{type}>().Deserialize(context)" - : $"Realms.Serialization.RealmObjectSerializer.LookupSerializer<{type}>()!.DeserializeById(context)!"; - - readDocumentFieldLines.AppendLine($@"case ""{stringName}"": - instance.{name}[fieldName] = {deserialize}; - break;"); - - readDictionaryLines.AppendLine($@"case ""{stringName}"":"); - } - else - { - var type = property.TypeInfo.GetCorrectlyAnnotatedTypeName(property.IsRequired).InternalType; - - var deserialize = property.TypeInfo.InternalType!.ObjectType is ObjectType.None or ObjectType.EmbeddedObject - ? $"BsonSerializer.LookupSerializer<{type}>().Deserialize(context)" - : $"Realms.Serialization.RealmObjectSerializer.LookupSerializer<{type}>()!.DeserializeById(context)!"; - - readArrayElementLines.AppendLine($@"case ""{stringName}"": - instance.{name}.Add({deserialize}); - break;"); - readArrayLines.AppendLine($@"case ""{stringName}"":"); - } - } - else - { - var type = property.TypeInfo.GetCorrectlyAnnotatedTypeName(property.IsRequired).CompleteType; - - serializeValueLines.AppendLine($"WriteValue(context, args, \"{stringName}\", value.{name});"); - var deserialize = property.TypeInfo.ObjectType is ObjectType.None or ObjectType.EmbeddedObject - ? $"BsonSerializer.LookupSerializer<{type}>().Deserialize(context)" - : $"Realms.Serialization.RealmObjectSerializer.LookupSerializer<{type}>()!.DeserializeById(context)"; - readValueLines.AppendLine($@"case ""{stringName}"": - instance.{name} = {deserialize}; - break;"); - } - } - - if (readArrayLines.Length > 0) - { - readValueLines.Append(readArrayLines); - readValueLines.AppendLine(@" ReadArray(instance, name, context); - break;"); - } - - if (readDictionaryLines.Length > 0) - { - readValueLines.Append(readDictionaryLines); - readValueLines.AppendLine(@" ReadDictionary(instance, name, context); - break;"); - } - - return $@"[EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] -private class {_serializerClassName} : Realms.Serialization.RealmObjectSerializerBase<{_classInfo.Name}> -{{ - public override string SchemaName => ""{_classInfo.MapTo ?? _classInfo.Name}""; - - protected override void SerializeValue(MongoDB.Bson.Serialization.BsonSerializationContext context, BsonSerializationArgs args, {_classInfo.Name} value) - {{ - context.Writer.WriteStartDocument(); - -{serializeValueLines.Indent(2, trimNewLines: true)} - - context.Writer.WriteEndDocument(); - }} - - protected override {_classInfo.Name} CreateInstance() => new {_classInfo.Name}(); - - protected override void ReadValue({_classInfo.Name} instance, string name, BsonDeserializationContext context) - {{ -{(readValueLines.Length == 0 - ? "// No Realm properties to deserialize" - : $@"switch (name) -{{ -{readValueLines.Indent(trimNewLines: true)} - default: - context.Reader.SkipValue(); - break; -}}").Indent(2)} - }} - - protected override void ReadArrayElement({_classInfo.Name} instance, string name, BsonDeserializationContext context) - {{ -{(readArrayElementLines.Length == 0 - ? "// No persisted list/set properties to deserialize" - : $@"switch (name) -{{ -{readArrayElementLines.Indent(trimNewLines: true)} -}}").Indent(2)} - }} - - protected override void ReadDocumentField({_classInfo.Name} instance, string name, string fieldName, BsonDeserializationContext context) - {{ -{(readDocumentFieldLines.Length == 0 - ? "// No persisted dictionary properties to deserialize" - : $@"switch (name) -{{ -{readDocumentFieldLines.Indent(trimNewLines: true)} -}}").Indent(2)} - }} -}}"; - } - - private static string GetBackingFieldName(string propertyName) - { - return "_" + char.ToLowerInvariant(propertyName[0]) + propertyName[1..]; - } - - private static string GetRealmValueType(PropertyTypeInfo propertyTypeInfo) - { - var scalarType = propertyTypeInfo.IsRealmInteger ? propertyTypeInfo.InternalType.ScalarType : propertyTypeInfo.ScalarType; - - var endString = scalarType switch - { - ScalarType.Int => "Int", - ScalarType.Bool => "Bool", - ScalarType.String => "String", - ScalarType.Data => "Data", - ScalarType.Date => "Date", - ScalarType.Float => "Float", - ScalarType.Double => "Double", - ScalarType.Object => "Object", - ScalarType.RealmValue => "RealmValue", - ScalarType.ObjectId => "ObjectId", - ScalarType.Decimal => "Decimal128", - ScalarType.Guid => "Guid", - _ => throw new NotImplementedException(), - }; - - return "Realms.RealmValueType." + endString; - } - - private static string BoolToString(bool value) => value ? "true" : "false"; - } -} diff --git a/Realm/Realm.SourceGenerator/CodeEmitter.cs b/Realm/Realm.SourceGenerator/CodeEmitter.cs index 218a15a9a4..3a418c0bc8 100644 --- a/Realm/Realm.SourceGenerator/CodeEmitter.cs +++ b/Realm/Realm.SourceGenerator/CodeEmitter.cs @@ -38,16 +38,11 @@ public CodeEmitter(GeneratorExecutionContext context, GeneratorConfig generatorC public void Emit(ParsingResults parsingResults) { - foreach (var classInfo in parsingResults.ClassInfo) + foreach (var classInfo in parsingResults.ClassInfo.Where(ShouldEmit)) { - if (!ShouldEmit(classInfo)) - { - continue; - } - try { - var generatedSource = new ClassCodeBuilder(classInfo, _generatorConfig).GenerateSource(); + var generatedSource = ClassCodeBuilderBase.CreateBuilder(classInfo, _generatorConfig).GenerateSource(); // Replace all occurrences of at least 3 newlines with only 2 var formattedSource = Regex.Replace(generatedSource, @$"[{Environment.NewLine}]{{3,}}", $"{Environment.NewLine}{Environment.NewLine}"); diff --git a/Realm/Realm.SourceGenerator/InfoClasses.cs b/Realm/Realm.SourceGenerator/InfoClasses.cs index c852de97d2..51cfd3d428 100644 --- a/Realm/Realm.SourceGenerator/InfoClasses.cs +++ b/Realm/Realm.SourceGenerator/InfoClasses.cs @@ -381,7 +381,8 @@ internal enum ObjectType None, RealmObject, EmbeddedObject, - AsymmetricObject + AsymmetricObject, + MappedObject, } internal enum CollectionType diff --git a/Realm/Realm.SourceGenerator/Parser.cs b/Realm/Realm.SourceGenerator/Parser.cs index e612ee109e..ea8056e739 100644 --- a/Realm/Realm.SourceGenerator/Parser.cs +++ b/Realm/Realm.SourceGenerator/Parser.cs @@ -42,6 +42,8 @@ public Parser(GeneratorExecutionContext context, GeneratorConfig generatorConfig public ParsingResults Parse(IEnumerable realmClasses) { + // TODO (flx-poc): Add diagnostics for MappedObject (e.g. no [PrimaryKey], no [Indexed], etc.) + var result = new ParsingResults(); var classNames = new HashSet(); var duplicateClassNames = new HashSet(); diff --git a/Realm/Realm.SourceGenerator/Utils.cs b/Realm/Realm.SourceGenerator/Utils.cs index 7a0b6e4881..294c8ddd27 100644 --- a/Realm/Realm.SourceGenerator/Utils.cs +++ b/Realm/Realm.SourceGenerator/Utils.cs @@ -111,6 +111,10 @@ public static IEnumerable ImplementingObjectTypes(this ITypeSymbol s { yield return ObjectType.AsymmetricObject; } + else if (IsIMappedObjectInterface(i)) + { + yield return ObjectType.MappedObject; + } } } @@ -128,6 +132,8 @@ public static IEnumerable ImplementingObjectTypes(this ITypeSymbol s private static bool IsIAsymmetricObjectInterface(this INamedTypeSymbol interfaceSymbol) => interfaceSymbol.Name == "IAsymmetricObject"; + private static bool IsIMappedObjectInterface(this INamedTypeSymbol interfaceSymbol) => interfaceSymbol.Name == "IMappedObject"; + public static INamedTypeSymbol AsNamed(this ITypeSymbol symbol) { if (symbol is INamedTypeSymbol namedSymbol) diff --git a/Realm/Realm.Weaver/Extensions/TypeDefinitionExtensions.cs b/Realm/Realm.Weaver/Extensions/TypeDefinitionExtensions.cs index 4f9e44be47..c26821a468 100644 --- a/Realm/Realm.Weaver/Extensions/TypeDefinitionExtensions.cs +++ b/Realm/Realm.Weaver/Extensions/TypeDefinitionExtensions.cs @@ -43,6 +43,9 @@ public static bool IsIEmbeddedObjectImplementor(this TypeDefinition type, Import public static bool IsIAsymmetricObjectImplementor(this TypeDefinition type, ImportedReferences references) => IsImplementorOf(type, references.IAsymmetricObject); + public static bool IsIMappedObjectImplementor(this TypeDefinition type, ImportedReferences references) => + IsImplementorOf(type, references.IMappedObject); + public static bool IsImplementorOf(TypeDefinition @this, params TypeReference[] targetInterfaces) { foreach (var @interface in @this.Interfaces) diff --git a/Realm/Realm.Weaver/ImportedReferences.cs b/Realm/Realm.Weaver/ImportedReferences.cs index 1aa113a0f2..3c611a031c 100644 --- a/Realm/Realm.Weaver/ImportedReferences.cs +++ b/Realm/Realm.Weaver/ImportedReferences.cs @@ -80,6 +80,8 @@ internal abstract class ImportedReferences public TypeReference IAsymmetricObject { get; private set; } + public TypeReference IMappedObject { get; private set; } + public TypeReference ManagedAccessor { get; private set; } public TypeReference EmbeddedObject { get; private set; } @@ -230,6 +232,7 @@ private void InitializeRealm(IMetadataScope realmAssembly) RealmValue = new TypeReference("Realms", "RealmValue", Module, realmAssembly, valueType: true); IEmbeddedObject = new TypeReference("Realms", "IEmbeddedObject", Module, realmAssembly, valueType: false); IAsymmetricObject = new TypeReference("Realms", "IAsymmetricObject", Module, realmAssembly, valueType: false); + IMappedObject = new TypeReference("Realms", "IMappedObject", Module, realmAssembly, valueType: false); RealmValue_GetNull = new MethodReference("get_Null", RealmValue, RealmValue) { HasThis = false }; { diff --git a/Realm/Realm.Weaver/RealmWeaver.Schema.cs b/Realm/Realm.Weaver/RealmWeaver.Schema.cs index 42c64dd3de..bb9cd94d7d 100644 --- a/Realm/Realm.Weaver/RealmWeaver.Schema.cs +++ b/Realm/Realm.Weaver/RealmWeaver.Schema.cs @@ -44,7 +44,7 @@ private void WeaveSchema(TypeDefinition[] types) referencedTypes = referencedTypes.Concat(types); } - var realmTypes = referencedTypes.Where(ShouldInclude) + var realmTypes = referencedTypes.Where(t => !t.IsIMappedObjectImplementor(_references) && ShouldInclude(t)) .Select(_moduleDefinition.ImportReference) .Distinct() .ToArray(); diff --git a/Realm/Realm.Weaver/RealmWeaver.cs b/Realm/Realm.Weaver/RealmWeaver.cs index f301457e41..431633ebd5 100644 --- a/Realm/Realm.Weaver/RealmWeaver.cs +++ b/Realm/Realm.Weaver/RealmWeaver.cs @@ -290,6 +290,12 @@ private WeaveTypeResult WeaveGeneratedType(TypeDefinition type) { _logger.Debug("Weaving generated " + type.Name); + if (type.IsIMappedObjectImplementor(_references)) + { + // TODO (flx-poc): this should actually do something useful + return WeaveTypeResult.Success(type.Name, Enumerable.Empty(), isGenerated: true); + } + // The forward slash is used to indicate a nested class var interfaceType = _moduleDefinition.GetType($"{type.FullName}/I{type.Name}Accessor"); diff --git a/Realm/Realm/DatabaseTypes/IRealmObjectBase.cs b/Realm/Realm/DatabaseTypes/IRealmObjectBase.cs index d8f5d56183..859a567dd9 100644 --- a/Realm/Realm/DatabaseTypes/IRealmObjectBase.cs +++ b/Realm/Realm/DatabaseTypes/IRealmObjectBase.cs @@ -16,6 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// +using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using Realms.Schema; @@ -159,6 +160,15 @@ public interface IEmbeddedObject : IRealmObjectBase public IRealmObjectBase? Parent { get; } } + /// + /// An object that can be duck-type mapped from a dictionary/RealmValue field. + /// + /// TODO: figure out a proper name for this + public interface IMappedObject + { + void SetBackingStorage(IDictionary dictionary); + } + /// /// Represents an object whose managed accessor can be set. /// This interface is used only internally for now. diff --git a/Realm/Realm/DatabaseTypes/RealmValue.cs b/Realm/Realm/DatabaseTypes/RealmValue.cs index f94deaefee..13ddaf39f0 100644 --- a/Realm/Realm/DatabaseTypes/RealmValue.cs +++ b/Realm/Realm/DatabaseTypes/RealmValue.cs @@ -789,6 +789,21 @@ public T AsRealmObject() where T : class, IRealmObjectBase => Type == RealmValueType.Null ? null : AsRealmObject(); + // TODO (ni): add docs + public T AsMappedObject() + where T : class, IMappedObject, new() + { + EnsureType("dictionary", RealmValueType.Dictionary); + var result = new T(); + result.SetBackingStorage(_dictionaryValue!); + return result; + } + + // TODO (ni): add docs + public T? AsNullableMappedObject() + where T : class, IMappedObject, new() + => Type == RealmValueType.Null ? null : AsMappedObject(); + /// /// Returns the stored value converted to . /// @@ -802,6 +817,21 @@ public T As() return Operator.Convert(this); } + if (typeof(IMappedObject).IsAssignableFrom(typeof(T))) + { + switch (Type) + { + case RealmValueType.Null: + return Operator.Convert(null)!; + case RealmValueType.Dictionary: + var result = Activator.CreateInstance()!; + ((IMappedObject)result).SetBackingStorage(_dictionaryValue!); + return result; + default: + throw new InvalidCastException($"Can't convert from {Type} to dictionary, which is the backing storage type for {typeof(T)}"); + } + } + // This largely copies AsAny to avoid boxing the underlying value in an object return Type switch { diff --git a/Realm/Realm/Extensions/CollectionExtensions.cs b/Realm/Realm/Extensions/CollectionExtensions.cs index 54a7f81eee..66f909c790 100644 --- a/Realm/Realm/Extensions/CollectionExtensions.cs +++ b/Realm/Realm/Extensions/CollectionExtensions.cs @@ -557,6 +557,15 @@ public static async Task> SubscribeAsync(this IQueryable que return query; } + // TODO (ni): add docs + public static T As(this IDictionary dict) + where T : IMappedObject, new() + { + var result = new T(); + result.SetBackingStorage(dict); + return result; + } + [EditorBrowsable(EditorBrowsableState.Never)] [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "This is only used by the weaver/source generated classes and should not be exposed to users.")] diff --git a/Tests/Realm.Tests/Database/FlexibleSchemaPocTests.cs b/Tests/Realm.Tests/Database/FlexibleSchemaPocTests.cs new file mode 100644 index 0000000000..da5629308d --- /dev/null +++ b/Tests/Realm.Tests/Database/FlexibleSchemaPocTests.cs @@ -0,0 +1,191 @@ +//////////////////////////////////////////////////////////////////////////// +// +// 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.ComponentModel; +using System.Linq; +using NUnit.Framework; + +namespace Realms.Tests.Database; + +[TestFixture, Preserve(AllMembers = true)] +public partial class FlexibleSchemaPocTests : RealmInstanceTest +{ + [Test] + public void RealmValue_AsMappedType_ReturnsCorrectObject() + { + AddData(); + + var dogContainer = _realm.All().First(c => c.ContainedObjectType == nameof(Dog)); + + var dog = dogContainer.MixedProperty.As(); + + // TODO: add assertions for the values + Assert.That(dog, Is.TypeOf()); + + var dog2 = dogContainer.MixedProperty.AsMappedObject(); + + // TODO: add assertions for the values + Assert.That(dog2, Is.TypeOf()); + + var nullContainer = _realm.Write(() => _realm.Add(new FlexibleSchemaPocContainer("null") + { + MixedProperty = RealmValue.Null + })); + + var nullDog = nullContainer.MixedProperty.As(); + Assert.That(nullDog, Is.Null); + + var nullDog2 = nullContainer.MixedProperty.AsNullableMappedObject(); + Assert.That(nullDog2, Is.Null); + } + + [Test] + public void RealmValue_AsMappedType_WhenTypeIsIncorrect_Throws() + { + var intContainer = _realm.Write(() => _realm.Add(new FlexibleSchemaPocContainer("int") + { + MixedProperty = 5 + })); + + Assert.Throws(() => intContainer.MixedProperty.As()); + Assert.Throws(() => intContainer.MixedProperty.AsMappedObject()); + Assert.Throws(() => intContainer.MixedProperty.AsNullableMappedObject()); + } + + [Test] + public void AccessMappedTypeProperties_ReadsValuesFromBackingStorage() + { + // TODO: LJ to get this to work - this is an example test that should start passing once we wire up the properties. + AddData(); + var dogContainer = _realm.All().First(c => c.ContainedObjectType == nameof(Dog)); + + var dog = new Dog(); + dog.SetBackingStorage(dogContainer.MixedDict); + + Assert.That(dog.Name, Is.EqualTo("Fido")); + Assert.That(dog.BarkCount, Is.EqualTo(5)); + + var birdContainer = _realm.All().First(c => c.ContainedObjectType == nameof(Bird)); + + var bird = new Bird(); + bird.SetBackingStorage(birdContainer.MixedDict); + + Assert.That(bird.Name, Is.EqualTo("Tweety")); + Assert.That(bird.CanFly, Is.True); + } + + [Test] + public void NotifyPropertyChanged_NotifiesForModifications() + { + AddData(); + + var dogContainer = _realm.All().First(c => c.ContainedObjectType == nameof(Dog)); + + var dog = dogContainer.MixedProperty.As(); + var changes = new List(); + dog.PropertyChanged += (s, e) => + { + Assert.That(s, Is.EqualTo(dog)); + changes.Add(e); + }; + + _realm.Write(() => + { + dogContainer.MixedProperty.AsDictionary()[nameof(Dog.BarkCount)] = 10; + }); + + _realm.Refresh(); + + Assert.That(changes.Count, Is.EqualTo(1)); + Assert.That(changes[0].PropertyName, Is.EqualTo(nameof(Dog.BarkCount))); + + _realm.Write(() => + { + dogContainer.MixedProperty.AsDictionary()[nameof(Dog.BarkCount)] = 15; + dogContainer.MixedProperty.AsDictionary()[nameof(Dog.Name)] = "Fido III"; + }); + _realm.Refresh(); + + Assert.That(changes.Count, Is.EqualTo(3)); + Assert.That(changes[1].PropertyName, Is.EqualTo(nameof(Dog.BarkCount))); + Assert.That(changes[2].PropertyName, Is.EqualTo(nameof(Dog.Name))); + } + + private void AddData() + { + _realm.Write(() => + { + var dogContainer = new FlexibleSchemaPocContainer(nameof(Dog)) + { + MixedDict = + { + [nameof(Dog.Name)] = "Fido", [nameof(Dog.BarkCount)] = 5 + }, + MixedProperty = new Dictionary + { + [nameof(Dog.Name)] = "Fido", [nameof(Dog.BarkCount)] = 5 + } + }; + + var birdContainer = new FlexibleSchemaPocContainer(nameof(Bird)) + { + MixedDict = + { + [nameof(Bird.Name)] = "Tweety", [nameof(Bird.CanFly)] = true + }, + MixedProperty = new Dictionary + { + [nameof(Bird.Name)] = "Tweety", [nameof(Bird.CanFly)] = true + } + }; + + _realm.Add(dogContainer); + _realm.Add(birdContainer); + }); + } + + public partial class FlexibleSchemaPocContainer : IRealmObject + { + public string ContainedObjectType { get; set; } + + public RealmValue MixedProperty { get; set; } + + public IDictionary MixedDict { get; } = null!; + + public FlexibleSchemaPocContainer(string containedObjectType) + { + ContainedObjectType = containedObjectType; + } + } + + public partial class Dog : IMappedObject + { + public string Name { get; set; } + + public int BarkCount { get; set; } + } + + public partial class Bird : IMappedObject + { + public string Name { get; set; } + + public bool CanFly { get; set; } + } +} diff --git a/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/Bird_generated.cs b/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/Bird_generated.cs new file mode 100644 index 0000000000..e0dc683710 --- /dev/null +++ b/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/Bird_generated.cs @@ -0,0 +1,98 @@ +// +#nullable enable + +using MongoDB.Bson.Serialization; +using NUnit.Framework; +using Realms; +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; + +namespace Realms.Tests.Database +{ + public partial class FlexibleSchemaPocTests + { + [Generated] + [Realms.Preserve(AllMembers = true)] + public partial class Bird : IMappedObject, INotifyPropertyChanged + { + private IDictionary _backingStorage = null!; + + public void SetBackingStorage(IDictionary dictionary) + { + _backingStorage = dictionary; + } + + #region INotifyPropertyChanged + + private IDisposable? _notificationToken; + + private event PropertyChangedEventHandler? _propertyChanged; + + /// + public event PropertyChangedEventHandler? PropertyChanged + { + add + { + if (_propertyChanged == null) + { + SubscribeForNotifications(); + } + + _propertyChanged += value; + } + + remove + { + _propertyChanged -= value; + + if (_propertyChanged == null) + { + UnsubscribeFromNotifications(); + } + } + } + + partial void OnPropertyChanged(string? propertyName); + + private void RaisePropertyChanged([CallerMemberName] string propertyName = "") + { + _propertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + OnPropertyChanged(propertyName); + } + + private void SubscribeForNotifications() + { + _notificationToken = _backingStorage.SubscribeForKeyNotifications((sender, changes) => + { + if (changes == null) + { + return; + } + + foreach (var key in changes.ModifiedKeys) + { + RaisePropertyChanged(key); + } + + // TODO: what do we do with deleted/inserted keys + }); + } + + private void UnsubscribeFromNotifications() + { + _notificationToken?.Dispose(); + } + + #endregion + } + } +} diff --git a/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/FlexibleSchemaPocContainer_generated.cs b/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/FlexibleSchemaPocContainer_generated.cs new file mode 100644 index 0000000000..f43e0a4892 --- /dev/null +++ b/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/FlexibleSchemaPocContainer_generated.cs @@ -0,0 +1,447 @@ +// +#nullable enable + +using MongoDB.Bson.Serialization; +using NUnit.Framework; +using Realms; +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; + +namespace Realms.Tests.Database +{ + public partial class FlexibleSchemaPocTests + { + [Generated] + [Woven(typeof(FlexibleSchemaPocContainerObjectHelper)), Realms.Preserve(AllMembers = true)] + public partial class FlexibleSchemaPocContainer : IRealmObject, INotifyPropertyChanged, IReflectableType + { + + [Realms.Preserve] + static FlexibleSchemaPocContainer() + { + Realms.Serialization.RealmObjectSerializer.Register(new FlexibleSchemaPocContainerSerializer()); + } + + /// + /// Defines the schema for the class. + /// + [System.Reflection.Obfuscation] + public static Realms.Schema.ObjectSchema RealmSchema = new Realms.Schema.ObjectSchema.Builder("FlexibleSchemaPocContainer", ObjectSchema.ObjectType.RealmObject) + { + Realms.Schema.Property.Primitive("ContainedObjectType", Realms.RealmValueType.String, isPrimaryKey: false, indexType: IndexType.None, isNullable: false, managedName: "ContainedObjectType"), + Realms.Schema.Property.RealmValue("MixedProperty", managedName: "MixedProperty"), + Realms.Schema.Property.RealmValueDictionary("MixedDict", managedName: "MixedDict"), + }.Build(); + + #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + private FlexibleSchemaPocContainer() {} + #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + + #region IRealmObject implementation + + private IFlexibleSchemaPocContainerAccessor? _accessor; + + Realms.IRealmAccessor Realms.IRealmObjectBase.Accessor => Accessor; + + private IFlexibleSchemaPocContainerAccessor Accessor => _accessor ??= new FlexibleSchemaPocContainerUnmanagedAccessor(typeof(FlexibleSchemaPocContainer)); + + /// + [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 = (IFlexibleSchemaPocContainerAccessor)managedAccessor; + var oldAccessor = _accessor; + _accessor = newAccessor; + + if (helper != null && oldAccessor != null) + { + if (!skipDefaults) + { + newAccessor.MixedDict.Clear(); + } + + newAccessor.ContainedObjectType = oldAccessor.ContainedObjectType; + newAccessor.MixedProperty = oldAccessor.MixedProperty; + Realms.CollectionExtensions.PopulateCollection(oldAccessor.MixedDict, newAccessor.MixedDict, 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 FlexibleSchemaPocContainer?(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(FlexibleSchemaPocContainer? 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(FlexibleSchemaPocContainer? 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 FlexibleSchemaPocContainerObjectHelper : 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 FlexibleSchemaPocContainerManagedAccessor(); + + public Realms.IRealmObjectBase CreateInstance() => new FlexibleSchemaPocContainer(); + + public bool TryGetPrimaryKeyValue(Realms.IRealmObjectBase instance, out RealmValue value) + { + value = RealmValue.Null; + return false; + } + } + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + internal interface IFlexibleSchemaPocContainerAccessor : Realms.IRealmAccessor + { + string ContainedObjectType { get; set; } + + Realms.RealmValue MixedProperty { get; set; } + + System.Collections.Generic.IDictionary MixedDict { get; } + } + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + private class FlexibleSchemaPocContainerManagedAccessor : Realms.ManagedAccessor, IFlexibleSchemaPocContainerAccessor + { + public string ContainedObjectType + { + get => (string)GetValue("ContainedObjectType")!; + set => SetValue("ContainedObjectType", value); + } + + public Realms.RealmValue MixedProperty + { + get => (Realms.RealmValue)GetValue("MixedProperty"); + set => SetValue("MixedProperty", value); + } + + private System.Collections.Generic.IDictionary _mixedDict = null!; + public System.Collections.Generic.IDictionary MixedDict + { + get + { + if (_mixedDict == null) + { + _mixedDict = GetDictionaryValue("MixedDict"); + } + + return _mixedDict; + } + } + } + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + private class FlexibleSchemaPocContainerUnmanagedAccessor : Realms.UnmanagedAccessor, IFlexibleSchemaPocContainerAccessor + { + public override ObjectSchema ObjectSchema => FlexibleSchemaPocContainer.RealmSchema; + + private string _containedObjectType = null!; + public string ContainedObjectType + { + get => _containedObjectType; + set + { + _containedObjectType = value; + RaisePropertyChanged("ContainedObjectType"); + } + } + + private Realms.RealmValue _mixedProperty; + public Realms.RealmValue MixedProperty + { + get => _mixedProperty; + set + { + _mixedProperty = value; + RaisePropertyChanged("MixedProperty"); + } + } + + public System.Collections.Generic.IDictionary MixedDict { get; } = new Dictionary(); + + public FlexibleSchemaPocContainerUnmanagedAccessor(Type objectType) : base(objectType) + { + } + + public override Realms.RealmValue GetValue(string propertyName) + { + return propertyName switch + { + "ContainedObjectType" => _containedObjectType, + "MixedProperty" => _mixedProperty, + _ => 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 "ContainedObjectType": + ContainedObjectType = (string)val!; + return; + case "MixedProperty": + MixedProperty = (Realms.RealmValue)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) + { + 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) + { + return propertyName switch + { + "MixedDict" => (IDictionary)MixedDict, + _ => throw new MissingMemberException($"The object does not have a Realm dictionary property with name {propertyName}"), + }; + } + } + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + private class FlexibleSchemaPocContainerSerializer : Realms.Serialization.RealmObjectSerializerBase + { + public override string SchemaName => "FlexibleSchemaPocContainer"; + + protected override void SerializeValue(MongoDB.Bson.Serialization.BsonSerializationContext context, BsonSerializationArgs args, FlexibleSchemaPocContainer value) + { + context.Writer.WriteStartDocument(); + + WriteValue(context, args, "ContainedObjectType", value.ContainedObjectType); + WriteValue(context, args, "MixedProperty", value.MixedProperty); + WriteDictionary(context, args, "MixedDict", value.MixedDict); + + context.Writer.WriteEndDocument(); + } + + protected override FlexibleSchemaPocContainer CreateInstance() => new FlexibleSchemaPocContainer(); + + protected override void ReadValue(FlexibleSchemaPocContainer instance, string name, BsonDeserializationContext context) + { + switch (name) + { + case "ContainedObjectType": + instance.ContainedObjectType = BsonSerializer.LookupSerializer().Deserialize(context); + break; + case "MixedProperty": + instance.MixedProperty = BsonSerializer.LookupSerializer().Deserialize(context); + break; + case "MixedDict": + ReadDictionary(instance, name, context); + break; + default: + context.Reader.SkipValue(); + break; + } + } + + protected override void ReadArrayElement(FlexibleSchemaPocContainer instance, string name, BsonDeserializationContext context) + { + // No persisted list/set properties to deserialize + } + + protected override void ReadDocumentField(FlexibleSchemaPocContainer instance, string name, string fieldName, BsonDeserializationContext context) + { + switch (name) + { + case "MixedDict": + instance.MixedDict[fieldName] = BsonSerializer.LookupSerializer().Deserialize(context); + break; + } + } + } + } + } +} diff --git a/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/Realms.Tests.Database_Dog_generated.cs b/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/Realms.Tests.Database_Dog_generated.cs new file mode 100644 index 0000000000..85f509c035 --- /dev/null +++ b/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/Realms.Tests.Database_Dog_generated.cs @@ -0,0 +1,98 @@ +// +#nullable enable + +using MongoDB.Bson.Serialization; +using NUnit.Framework; +using Realms; +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; + +namespace Realms.Tests.Database +{ + public partial class FlexibleSchemaPocTests + { + [Generated] + [Realms.Preserve(AllMembers = true)] + public partial class Dog : IMappedObject, INotifyPropertyChanged + { + private IDictionary _backingStorage = null!; + + public void SetBackingStorage(IDictionary dictionary) + { + _backingStorage = dictionary; + } + + #region INotifyPropertyChanged + + private IDisposable? _notificationToken; + + private event PropertyChangedEventHandler? _propertyChanged; + + /// + public event PropertyChangedEventHandler? PropertyChanged + { + add + { + if (_propertyChanged == null) + { + SubscribeForNotifications(); + } + + _propertyChanged += value; + } + + remove + { + _propertyChanged -= value; + + if (_propertyChanged == null) + { + UnsubscribeFromNotifications(); + } + } + } + + partial void OnPropertyChanged(string? propertyName); + + private void RaisePropertyChanged([CallerMemberName] string propertyName = "") + { + _propertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + OnPropertyChanged(propertyName); + } + + private void SubscribeForNotifications() + { + _notificationToken = _backingStorage.SubscribeForKeyNotifications((sender, changes) => + { + if (changes == null) + { + return; + } + + foreach (var key in changes.ModifiedKeys) + { + RaisePropertyChanged(key); + } + + // TODO: what do we do with deleted/inserted keys + }); + } + + private void UnsubscribeFromNotifications() + { + _notificationToken?.Dispose(); + } + + #endregion + } + } +} diff --git a/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/Realms.Tests_Dog_generated.cs b/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/Realms.Tests_Dog_generated.cs new file mode 100644 index 0000000000..b4003b2c98 --- /dev/null +++ b/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/Realms.Tests_Dog_generated.cs @@ -0,0 +1,496 @@ +// +#nullable enable + +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using Realms; +using Realms.Schema; +using Realms.Tests; +using Realms.Tests.Database; +using Realms.Weaving; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +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 +{ + [Generated] + [Woven(typeof(DogObjectHelper)), Realms.Preserve(AllMembers = true)] + public partial class Dog : IRealmObject, INotifyPropertyChanged, IReflectableType + { + + [Realms.Preserve] + static Dog() + { + Realms.Serialization.RealmObjectSerializer.Register(new DogSerializer()); + } + + /// + /// Defines the schema for the class. + /// + [System.Reflection.Obfuscation] + public static Realms.Schema.ObjectSchema RealmSchema = new Realms.Schema.ObjectSchema.Builder("Dog", ObjectSchema.ObjectType.RealmObject) + { + Realms.Schema.Property.Primitive("Name", Realms.RealmValueType.String, isPrimaryKey: false, indexType: IndexType.None, isNullable: true, managedName: "Name"), + Realms.Schema.Property.Primitive("Color", Realms.RealmValueType.String, isPrimaryKey: false, indexType: IndexType.None, isNullable: true, managedName: "Color"), + Realms.Schema.Property.Primitive("Vaccinated", Realms.RealmValueType.Bool, isPrimaryKey: false, indexType: IndexType.None, isNullable: false, managedName: "Vaccinated"), + Realms.Schema.Property.Primitive("Age", Realms.RealmValueType.Int, isPrimaryKey: false, indexType: IndexType.None, isNullable: false, managedName: "Age"), + Realms.Schema.Property.Backlinks("Owners", "Owner", "ListOfDogs", managedName: "Owners"), + }.Build(); + + #region IRealmObject implementation + + private IDogAccessor? _accessor; + + Realms.IRealmAccessor Realms.IRealmObjectBase.Accessor => Accessor; + + private IDogAccessor Accessor => _accessor ??= new DogUnmanagedAccessor(typeof(Dog)); + + /// + [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 = (IDogAccessor)managedAccessor; + var oldAccessor = _accessor; + _accessor = newAccessor; + + if (helper != null && oldAccessor != null) + { + if (!skipDefaults || oldAccessor.Name != default(string?)) + { + newAccessor.Name = oldAccessor.Name; + } + if (!skipDefaults || oldAccessor.Color != default(string?)) + { + newAccessor.Color = oldAccessor.Color; + } + if (!skipDefaults || oldAccessor.Vaccinated != default(bool)) + { + newAccessor.Vaccinated = oldAccessor.Vaccinated; + } + if (!skipDefaults || oldAccessor.Age != default(int)) + { + newAccessor.Age = oldAccessor.Age; + } + } + + 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 Dog?(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(Dog? 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(Dog? 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 DogObjectHelper : 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 DogManagedAccessor(); + + public Realms.IRealmObjectBase CreateInstance() => new Dog(); + + public bool TryGetPrimaryKeyValue(Realms.IRealmObjectBase instance, out RealmValue value) + { + value = RealmValue.Null; + return false; + } + } + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + internal interface IDogAccessor : Realms.IRealmAccessor + { + string? Name { get; set; } + + string? Color { get; set; } + + bool Vaccinated { get; set; } + + int Age { get; set; } + + System.Linq.IQueryable Owners { get; } + } + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + private class DogManagedAccessor : Realms.ManagedAccessor, IDogAccessor + { + public string? Name + { + get => (string?)GetValue("Name"); + set => SetValue("Name", value); + } + + public string? Color + { + get => (string?)GetValue("Color"); + set => SetValue("Color", value); + } + + public bool Vaccinated + { + get => (bool)GetValue("Vaccinated"); + set => SetValue("Vaccinated", value); + } + + public int Age + { + get => (int)GetValue("Age"); + set => SetValue("Age", value); + } + + private System.Linq.IQueryable _owners = null!; + public System.Linq.IQueryable Owners + { + get + { + if (_owners == null) + { + _owners = GetBacklinks("Owners"); + } + + return _owners; + } + } + } + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + private class DogUnmanagedAccessor : Realms.UnmanagedAccessor, IDogAccessor + { + public override ObjectSchema ObjectSchema => Dog.RealmSchema; + + private string? _name; + public string? Name + { + get => _name; + set + { + _name = value; + RaisePropertyChanged("Name"); + } + } + + private string? _color; + public string? Color + { + get => _color; + set + { + _color = value; + RaisePropertyChanged("Color"); + } + } + + private bool _vaccinated; + public bool Vaccinated + { + get => _vaccinated; + set + { + _vaccinated = value; + RaisePropertyChanged("Vaccinated"); + } + } + + private int _age; + public int Age + { + get => _age; + set + { + _age = value; + RaisePropertyChanged("Age"); + } + } + + public System.Linq.IQueryable Owners => throw new NotSupportedException("Using backlinks is only possible for managed(persisted) objects."); + + public DogUnmanagedAccessor(Type objectType) : base(objectType) + { + } + + public override Realms.RealmValue GetValue(string propertyName) + { + return propertyName switch + { + "Name" => _name, + "Color" => _color, + "Vaccinated" => _vaccinated, + "Age" => _age, + "Owners" => throw new NotSupportedException("Using backlinks is only possible for managed(persisted) objects."), + _ => 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 "Name": + Name = (string?)val; + return; + case "Color": + Color = (string?)val; + return; + case "Vaccinated": + Vaccinated = (bool)val; + return; + case "Age": + Age = (int)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) + { + 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 DogSerializer : Realms.Serialization.RealmObjectSerializerBase + { + public override string SchemaName => "Dog"; + + protected override void SerializeValue(MongoDB.Bson.Serialization.BsonSerializationContext context, BsonSerializationArgs args, Dog value) + { + context.Writer.WriteStartDocument(); + + WriteValue(context, args, "Name", value.Name); + WriteValue(context, args, "Color", value.Color); + WriteValue(context, args, "Vaccinated", value.Vaccinated); + WriteValue(context, args, "Age", value.Age); + + context.Writer.WriteEndDocument(); + } + + protected override Dog CreateInstance() => new Dog(); + + protected override void ReadValue(Dog instance, string name, BsonDeserializationContext context) + { + switch (name) + { + case "Name": + instance.Name = BsonSerializer.LookupSerializer().Deserialize(context); + break; + case "Color": + instance.Color = BsonSerializer.LookupSerializer().Deserialize(context); + break; + case "Vaccinated": + instance.Vaccinated = BsonSerializer.LookupSerializer().Deserialize(context); + break; + case "Age": + instance.Age = BsonSerializer.LookupSerializer().Deserialize(context); + break; + default: + context.Reader.SkipValue(); + break; + } + } + + protected override void ReadArrayElement(Dog instance, string name, BsonDeserializationContext context) + { + // No persisted list/set properties to deserialize + } + + protected override void ReadDocumentField(Dog instance, string name, string fieldName, BsonDeserializationContext context) + { + // No persisted dictionary properties to deserialize + } + } + } +} diff --git a/Tests/Realm.Tests/Realm.Tests.csproj b/Tests/Realm.Tests/Realm.Tests.csproj index dc7fa2dacc..dbde1e14b9 100644 --- a/Tests/Realm.Tests/Realm.Tests.csproj +++ b/Tests/Realm.Tests/Realm.Tests.csproj @@ -9,7 +9,7 @@ $(TargetFrameworks);net6.0;net8.0 $(TargetFrameworks);net461 $(TargetFrameworks);netstandard2.0 - 9.0 + 10 Realms.Tests true false