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"];
+ }
+}