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
Expand Up @@ -153,34 +153,52 @@ protected override object CreateAccumulator()
protected override TValue FinalizeResult(object accumulator)
{
// find and call a constructor that we can pass the accumulator to
var accumulatorType = accumulator.GetType();
foreach (var constructorInfo in typeof(TValue).GetTypeInfo().GetConstructors())
var accumulatorCtor = FindSingleArgumentConstructor(accumulator.GetType());
if (accumulatorCtor != null)
{
var parameterInfos = constructorInfo.GetParameters();
if (parameterInfos.Length == 1 && parameterInfos[0].ParameterType.GetTypeInfo().IsAssignableFrom(accumulatorType))
{
return (TValue)constructorInfo.Invoke(new object[] { accumulator });
}
return (TValue)accumulatorCtor.Invoke(new object[] { accumulator });
}

// otherwise try to find a no-argument constructor and an Add method
var valueTypeInfo = typeof(TValue).GetTypeInfo();
var noArgumentConstructorInfo = valueTypeInfo.GetConstructor(new Type[] { });
var addMethodInfo = typeof(TValue).GetTypeInfo().GetMethod("Add", new Type[] { typeof(TItem) });
var noArgumentConstructorInfo = valueTypeInfo.GetConstructor(Type.EmptyTypes);
var addMethodInfo = valueTypeInfo.GetMethod("Add", new Type[] { typeof(TItem) });
if (noArgumentConstructorInfo != null && addMethodInfo != null)
{
var value = (TValue)noArgumentConstructorInfo.Invoke(new Type[] { });
var value = (TValue)noArgumentConstructorInfo.Invoke(null);
foreach (var item in (IEnumerable<TItem>)accumulator)
{
addMethodInfo.Invoke(value, new object[] { item });
}
return value;
}

// last resort: try a constructor that takes an ISet<TItem> (e.g. ReadOnlySet<T>(ISet<T>))
// Note: collapses duplicate elements, which is correct for set-shaped targets.
var setCtor = FindSingleArgumentConstructor(typeof(ISet<TItem>));
if (setCtor != null)
{
var hashSet = new HashSet<TItem>((IEnumerable<TItem>)accumulator);
return (TValue)setCtor.Invoke(new object[] { hashSet });
}

var message = string.Format("Type '{0}' does not have a suitable constructor or Add method.", typeof(TValue).FullName);
throw new BsonSerializationException(message);
}

private static ConstructorInfo FindSingleArgumentConstructor(Type argumentType)
{
foreach (var constructorInfo in typeof(TValue).GetTypeInfo().GetConstructors())
{
var parameterInfos = constructorInfo.GetParameters();
if (parameterInfos.Length == 1 && parameterInfos[0].ParameterType.GetTypeInfo().IsAssignableFrom(argumentType))
{
return constructorInfo;
}
}
return null;
}

// explicit interface implementations
IBsonSerializer IChildSerializerConfigurable.ChildSerializer
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
using System;
using System.Collections;
using System.Collections.Generic;
#if NET9_0_OR_GREATER
using System.Collections.ObjectModel;
#endif
Comment thread
papafe marked this conversation as resolved.
using System.IO;
using FluentAssertions;
using MongoDB.Bson.IO;
Expand Down Expand Up @@ -226,5 +229,76 @@ private IBsonSerializer<C> CreateSubject()
serializerRegistry.RegisterSerializer(typeof(C), subject);
return subject;
}

// target type with both an IEnumerable<T> ctor and an ISet<T> ctor — verifies the
// existing IEnumerable<T> path still wins (otherwise duplicates would silently collapse)
public class BothCtorsEnumerable<T> : IEnumerable<T>
{
private readonly List<T> _items;
public BothCtorsEnumerable(IEnumerable<T> items) { _items = new List<T>(items); }
public BothCtorsEnumerable(ISet<T> items) { _items = new List<T>(items); }
public IEnumerator<T> GetEnumerator() => _items.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

[Fact]
public void Deserialize_should_prefer_enumerable_ctor_over_set_ctor()
{
const string json = "[1, 2, 2, 3]";
var subject = new EnumerableInterfaceImplementerSerializer<BothCtorsEnumerable<int>, int>(__itemSerializer1);

using var reader = new JsonReader(json);
var context = BsonDeserializationContext.CreateRoot(reader);
var result = subject.Deserialize(context);

result.Should().Equal(1, 2, 2, 3);
}

#if NET9_0_OR_GREATER
[Fact]
public void Deserialize_should_use_set_ctor_when_no_other_ctor_matches()
{
const string json = "[1, 2, 3, 4]";
var subject = new EnumerableInterfaceImplementerSerializer<ReadOnlySet<int>, int>(__itemSerializer1);

using var reader = new JsonReader(json);
var context = BsonDeserializationContext.CreateRoot(reader);
var result = subject.Deserialize(context);

result.Should().BeEquivalentTo(new[] { 1, 2, 3, 4 });
}

[Fact]
public void Deserialize_via_set_ctor_should_collapse_duplicates()
{
const string json = "[1, 2, 2, 3]";
var subject = new EnumerableInterfaceImplementerSerializer<ReadOnlySet<int>, int>(__itemSerializer1);

using var reader = new JsonReader(json);
var context = BsonDeserializationContext.CreateRoot(reader);
var result = subject.Deserialize(context);

result.Should().BeEquivalentTo(new[] { 1, 2, 3 });
}

public class ReadOnlySetHolder
{
public ReadOnlySet<int> X { get; set; }
}

[Fact]
public void ReadOnlySet_should_roundtrip_via_BsonSerializer()
{
var original = new ReadOnlySetHolder
{
X = new ReadOnlySet<int>(new HashSet<int> { 1, 2, 3, 4 })
};

var bson = original.ToBson();
var rehydrated = BsonSerializer.Deserialize<ReadOnlySetHolder>(bson);

rehydrated.X.Should().BeEquivalentTo(original.X);
}
#endif
}
}