Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Identifies a class member to be used as the discriminator.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class BsonDiscriminatorMemberAttribute : Attribute, IBsonMemberMapAttribute
{
private readonly string _elementName;

/// <summary>
/// Initializes a new instance of BsonDiscriminatorMemberAttribute.
/// </summary>
public BsonDiscriminatorMemberAttribute()
{
}

/// <summary>
/// Initializes a new instance of BsonDiscriminatorMemberAttribute.
/// </summary>
/// <param name="elementName">The element name.</param>
public BsonDiscriminatorMemberAttribute(string elementName)
{
_elementName = elementName;
}

/// <inheritdoc/>
public void Apply(BsonMemberMap memberMap)
{
var classMap = memberMap.ClassMap;
classMap.SetDiscriminatorMember(memberMap);

if (_elementName != null)
{
memberMap.SetElementName(_elementName);
}
}
}
53 changes: 52 additions & 1 deletion src/MongoDB.Bson/Serialization/BsonClassMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public class BsonClassMap
private Func<object> _creator;
private string _discriminator;
private bool _discriminatorIsRequired;
private BsonMemberMap _discriminatorMemberMap;
private bool _hasRootClass;
private bool _isRootClass;
private BsonMemberMap _idMemberMap;
Expand Down Expand Up @@ -156,6 +157,14 @@ public bool DiscriminatorIsRequired
get { return _discriminatorIsRequired; }
}

/// <summary>
/// Gets the discriminator member map (null if none).
/// </summary>
public BsonMemberMap DiscriminatorMemberMap
{
get { return _discriminatorMemberMap; }
}

/// <summary>
/// Gets the member map of the member used to hold extra elements.
/// </summary>
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1118,6 +1136,29 @@ public void SetDiscriminatorIsRequired(bool discriminatorIsRequired)
_discriminatorIsRequired = discriminatorIsRequired;
}

/// <summary>
/// Sets the discriminator member.
/// </summary>
/// <param name="memberMap">The discriminator member (null if none).</param>
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;
}

/// <summary>
/// Sets the member map of the member used to hold extra elements.
/// </summary>
Expand Down Expand Up @@ -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.");
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
{
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
62 changes: 62 additions & 0 deletions tests/MongoDB.Bson.Tests/Jira/PublicDiscriminatorTests.cs
Original file line number Diff line number Diff line change
@@ -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<C>();

var document = BsonSerializer.Deserialize<C>(json);

document.Should().BeOfType<D>();
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<C>(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<string> Discriminator => ["C"];
}

public sealed class D : C
{
public override IReadOnlyList<string> Discriminator => ["C", "D"];
}
}
Loading