diff --git a/src/MongoDB.Bson/Serialization/Attributes/BsonDiscriminatorMemberAttribute.cs b/src/MongoDB.Bson/Serialization/Attributes/BsonDiscriminatorMemberAttribute.cs new file mode 100644 index 00000000000..04244e0e942 --- /dev/null +++ b/src/MongoDB.Bson/Serialization/Attributes/BsonDiscriminatorMemberAttribute.cs @@ -0,0 +1,55 @@ +/* Copyright 2010-present MongoDB 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; + +namespace MongoDB.Bson.Serialization.Attributes; + +/// +/// Identifies a class member to be used as the discriminator. +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +public class BsonDiscriminatorMemberAttribute : Attribute, IBsonMemberMapAttribute +{ + private readonly string _elementName; + + /// + /// Initializes a new instance of BsonDiscriminatorMemberAttribute. + /// + public BsonDiscriminatorMemberAttribute() + { + } + + /// + /// Initializes a new instance of BsonDiscriminatorMemberAttribute. + /// + /// The element name. + public BsonDiscriminatorMemberAttribute(string elementName) + { + _elementName = elementName; + } + + /// + public void Apply(BsonMemberMap memberMap) + { + var classMap = memberMap.ClassMap; + classMap.SetDiscriminatorMember(memberMap); + + if (_elementName != null) + { + memberMap.SetElementName(_elementName); + } + } +} diff --git a/src/MongoDB.Bson/Serialization/BsonClassMap.cs b/src/MongoDB.Bson/Serialization/BsonClassMap.cs index e81624f0026..a3a2ef8b92b 100644 --- a/src/MongoDB.Bson/Serialization/BsonClassMap.cs +++ b/src/MongoDB.Bson/Serialization/BsonClassMap.cs @@ -52,6 +52,7 @@ public class BsonClassMap private Func _creator; private string _discriminator; private bool _discriminatorIsRequired; + private BsonMemberMap _discriminatorMemberMap; private bool _hasRootClass; private bool _isRootClass; private BsonMemberMap _idMemberMap; @@ -156,6 +157,14 @@ public bool DiscriminatorIsRequired get { return _discriminatorIsRequired; } } + /// + /// Gets the discriminator member map (null if none). + /// + public BsonMemberMap DiscriminatorMemberMap + { + get { return _discriminatorMemberMap; } + } + /// /// Gets the member map of the member used to hold extra elements. /// @@ -637,6 +646,15 @@ public BsonClassMap Freeze() } } + if (_discriminatorMemberMap == null) + { + // see if we can inherit the discriminatorMemberMap from our base class + if (_baseClassMap != null) + { + _discriminatorMemberMap = _baseClassMap.DiscriminatorMemberMap; + } + } + if (_extraElementsMemberMap == null) { // see if we can inherit the extraElementsMemberMap from our base class @@ -1118,6 +1136,29 @@ public void SetDiscriminatorIsRequired(bool discriminatorIsRequired) _discriminatorIsRequired = discriminatorIsRequired; } + /// + /// Sets the discriminator member. + /// + /// The discriminator member (null if none). + public void SetDiscriminatorMember(BsonMemberMap memberMap) + { + if (memberMap != null) + { + EnsureMemberMapIsForThisClass(memberMap); + + if (memberMap.MemberInfo is not PropertyInfo propertyInfo || + propertyInfo.CanWrite || + !propertyInfo.GetMethod.IsVirtual) + { + throw new ArgumentException("The discriminator member must be a virtual read-only property.", nameof(memberMap)); + } + } + + if (_frozen) { ThrowFrozenException(); } + + _discriminatorMemberMap = memberMap; + } + /// /// Sets the member map of the member used to hold extra elements. /// @@ -1329,7 +1370,17 @@ internal IDiscriminatorConvention GetDiscriminatorConvention() if (discriminatorConvention != null) { - EnsureNoMemberMapConflicts(discriminatorConvention.ElementName); + if (_discriminatorMemberMap == null) + { + EnsureNoMemberMapConflicts(discriminatorConvention.ElementName); + } + else + { + if (_discriminatorMemberMap.ElementName != discriminatorConvention.ElementName) + { + throw new BsonSerializationException("Discriminator convention and disciminator map element names do not match."); + } + } } } diff --git a/src/MongoDB.Bson/Serialization/Serializers/BsonClassMapSerializer.cs b/src/MongoDB.Bson/Serialization/Serializers/BsonClassMapSerializer.cs index bec64ba12af..867b8171ad5 100644 --- a/src/MongoDB.Bson/Serialization/Serializers/BsonClassMapSerializer.cs +++ b/src/MongoDB.Bson/Serialization/Serializers/BsonClassMapSerializer.cs @@ -581,7 +581,7 @@ private void SerializeClass(BsonSerializationContext context, BsonSerializationA if (ShouldSerializeDiscriminator(args.NominalType)) { - SerializeDiscriminator(context, args.NominalType, document); + SerializeDiscriminator(context, args.NominalType, document.GetType()); } foreach (var memberMap in _classMap.AllMemberMaps) @@ -625,12 +625,11 @@ private void SerializeExtraElements(BsonSerializationContext context, object obj } } - private void SerializeDiscriminator(BsonSerializationContext context, Type nominalType, object obj) + private void SerializeDiscriminator(BsonSerializationContext context, Type nominalType, Type actualType) { var discriminatorConvention = _classMap.GetDiscriminatorConvention(); if (discriminatorConvention != null) { - var actualType = obj.GetType(); var discriminator = discriminatorConvention.GetDiscriminator(nominalType, actualType); if (discriminator != null) { @@ -642,6 +641,14 @@ private void SerializeDiscriminator(BsonSerializationContext context, Type nomin private void SerializeMember(BsonSerializationContext context, object obj, BsonMemberMap memberMap) { + if (memberMap == _classMap.DiscriminatorMemberMap) + { + // the discriminator has already been serialized + // verify that the value of the read-only property matches the discriminator value returned by the discriminator convention + VerifyDiscriminatorMemberValue(obj, memberMap); + return; + } + try { if (memberMap != _classMap.ExtraElementsMemberMap) @@ -682,6 +689,26 @@ private bool ShouldSerializeDiscriminator(Type nominalType) return (nominalType != _classMap.ClassType || _classMap.DiscriminatorIsRequired || _classMap.HasRootClass) && !_classMap.IsAnonymous; } + private void VerifyDiscriminatorMemberValue(object obj, BsonMemberMap discriminatorMemberMap) + { + var discriminatorConvention = _classMap.GetDiscriminatorConvention(); + if (discriminatorConvention != null) + { + var discriminatorMemberSerializer = discriminatorMemberMap.GetSerializer(); + var discriminatorMemberValue = discriminatorMemberMap.Getter(obj); + var serializedDiscriminatorMemberValue = discriminatorMemberSerializer.ToBsonValue(discriminatorMemberValue); + + var nominalType = _classMap.ClassType; + var actualType = obj.GetType(); + var conventionDiscriminatorValue = discriminatorConvention.GetDiscriminator(nominalType, actualType); + + if (!object.Equals(serializedDiscriminatorMemberValue, conventionDiscriminatorValue)) + { + throw new BsonSerializationException("Discriminator member value does not match discriminator convention GetDiscriminator return value."); + } + } + } + // nested classes // helper class that implements member map bit array helper functions internal static class FastMemberMapHelper diff --git a/tests/MongoDB.Bson.Tests/Jira/PublicDiscriminatorTests.cs b/tests/MongoDB.Bson.Tests/Jira/PublicDiscriminatorTests.cs new file mode 100644 index 00000000000..fe6feb38f20 --- /dev/null +++ b/tests/MongoDB.Bson.Tests/Jira/PublicDiscriminatorTests.cs @@ -0,0 +1,62 @@ +/* Copyright 2010-present MongoDB 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 FluentAssertions; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Attributes; +using Xunit; + +namespace MongoDB.Bson.Tests.Jira; + +public class PublicDiscriminatorTests +{ + [Fact] + public void Deserialize_with_public_discriminator_should_work() + { + var json = "{ _id : 1, _t : ['C', 'D'] }"; + var serializer = BsonSerializer.LookupSerializer(); + + var document = BsonSerializer.Deserialize(json); + + document.Should().BeOfType(); + document.Id.Should().Be(1); + document.Discriminator.Should().Equal("C", "D"); + } + + [Fact] + public void Serialize_with_public_discriminator_should_work() + { + var document = new D { Id = 1 }; + + var serializedDocument = document.ToBsonDocument(args: new BsonSerializationArgs { SerializeIdFirst = true }); + + serializedDocument.Should().Be("{ _id : 1, _t : ['C', 'D'] }"); + } + + [BsonDiscriminator(RootClass = true)] + [BsonKnownTypes(typeof(D))] + public abstract class C + { + public int Id { get; set; } + [BsonDiscriminatorMember("_t")] public virtual IReadOnlyList Discriminator => ["C"]; + } + + public sealed class D : C + { + public override IReadOnlyList Discriminator => ["C", "D"]; + } +}