Skip to content

Commit 3fd8923

Browse files
authored
CSHARP-4495: Add conventions and attributes to configure ObjectSerial… (#1545)
1 parent 1a43b71 commit 3fd8923

File tree

6 files changed

+559
-5
lines changed

6 files changed

+559
-5
lines changed

src/MongoDB.Bson/Serialization/Conventions/EnumRepresentationConvention.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

1616
using System;
1717
using System.Reflection;
18-
using MongoDB.Bson.Serialization.Options;
1918

2019
namespace MongoDB.Bson.Serialization.Conventions
2120
{
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
/* Copyright 2010-present MongoDB Inc.
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System;
17+
using System.Collections.Generic;
18+
using System.Linq;
19+
using System.Reflection;
20+
using MongoDB.Bson.Serialization.Serializers;
21+
22+
namespace MongoDB.Bson.Serialization.Conventions
23+
{
24+
/// <summary>
25+
/// A convention that allows to set the types that can be safely serialized and deserialized with the <see cref="ObjectSerializer"/>.
26+
/// </summary>
27+
public sealed class ObjectSerializerAllowedTypesConvention : ConventionBase, IMemberMapConvention
28+
{
29+
// static properties
30+
31+
/// <summary>
32+
/// A predefined <see cref="ObjectSerializerAllowedTypesConvention"/> where all types are allowed for both serialization and deserialization.
33+
/// </summary>
34+
public static ObjectSerializerAllowedTypesConvention AllowAllTypes { get; } = new(ObjectSerializer.AllAllowedTypes);
35+
36+
/// <summary>
37+
/// A predefined <see cref="ObjectSerializerAllowedTypesConvention"/> where no types are allowed for both serialization and deserialization.
38+
/// </summary>
39+
public static ObjectSerializerAllowedTypesConvention AllowNoTypes { get; } =
40+
new(ObjectSerializer.NoAllowedTypes) { AllowDefaultFrameworkTypes = false };
41+
42+
/// <summary>
43+
/// A predefined <see cref="ObjectSerializerAllowedTypesConvention"/> where only default framework types are allowed for both serialization and deserialization.
44+
/// </summary>
45+
public static ObjectSerializerAllowedTypesConvention AllowOnlyDefaultFrameworkTypes { get; } = new();
46+
47+
//static methods
48+
49+
/// <summary>
50+
/// Builds a predefined <see cref="ObjectSerializerAllowedTypesConvention"/> where all calling assembly types and default framework types are allowed for both serialization and deserialization.
51+
/// </summary>
52+
public static ObjectSerializerAllowedTypesConvention GetAllowAllCallingAssemblyAndDefaultFrameworkTypesConvention() => new(Assembly.GetCallingAssembly());
53+
54+
// private fields
55+
private readonly Func<Type, bool> _allowedDeserializationTypes;
56+
private readonly Func<Type, bool> _allowedSerializationTypes;
57+
private readonly bool _allowDefaultFrameworkTypes = true;
58+
private readonly Lazy<Func<Type, bool>> _effectiveAllowedDeserializationTypes;
59+
private readonly Lazy<Func<Type, bool>> _effectiveAllowedSerializationTypes;
60+
61+
/// <summary>
62+
/// Initializes a new instance of the <see cref="ObjectSerializerAllowedTypesConvention"/> class.
63+
/// </summary>
64+
/// <param name="allowedTypes">A delegate that determines what types are allowed to be serialized and deserialized.</param>
65+
public ObjectSerializerAllowedTypesConvention(Func<Type, bool> allowedTypes)
66+
: this(allowedTypes, allowedTypes)
67+
{
68+
}
69+
70+
/// <summary>
71+
/// Initializes a new instance of the <see cref="ObjectSerializerAllowedTypesConvention"/> class.
72+
/// </summary>
73+
/// <param name="allowedDeserializationTypes">A delegate that determines what types are allowed to be deserialized.</param>
74+
/// <param name="allowedSerializationTypes">A delegate that determines what types are allowed to be serialized.</param>
75+
public ObjectSerializerAllowedTypesConvention(Func<Type, bool> allowedDeserializationTypes, Func<Type, bool> allowedSerializationTypes)
76+
: this()
77+
{
78+
_allowedDeserializationTypes = allowedDeserializationTypes;
79+
_allowedSerializationTypes = allowedSerializationTypes;
80+
}
81+
82+
/// <summary>
83+
/// Initializes a new instance of the <see cref="ObjectSerializerAllowedTypesConvention"/> class.
84+
/// </summary>
85+
/// <param name="allowedTypes">A collection of the allowed types for both serialization and deserialization.</param>
86+
public ObjectSerializerAllowedTypesConvention(IEnumerable<Type> allowedTypes)
87+
: this()
88+
{
89+
var allowedTypesArray = allowedTypes.ToArray();
90+
91+
_allowedDeserializationTypes = allowedTypesArray.Contains;
92+
_allowedSerializationTypes = allowedTypesArray.Contains;
93+
}
94+
95+
/// <summary>
96+
/// Initializes a new instance of the <see cref="ObjectSerializerAllowedTypesConvention"/> class.
97+
/// </summary>
98+
/// <param name="allowedDeserializationTypes">A collection of the allowed types for deserialization.</param>
99+
/// <param name="allowedSerializationTypes">A collection of the allowed types for serialization.</param>
100+
public ObjectSerializerAllowedTypesConvention(IEnumerable<Type> allowedDeserializationTypes, IEnumerable<Type> allowedSerializationTypes)
101+
: this()
102+
{
103+
var allowedDeserializationTypesArray = allowedDeserializationTypes.ToArray();
104+
var allowedSerializationTypesArray = allowedSerializationTypes.ToArray();
105+
106+
_allowedDeserializationTypes = allowedDeserializationTypesArray.Contains;
107+
_allowedSerializationTypes = allowedSerializationTypesArray.Contains;
108+
}
109+
110+
/// <summary>
111+
/// Initializes a new instance of the <see cref="ObjectSerializerAllowedTypesConvention"/> class.
112+
/// </summary>
113+
/// <param name="allowedAssemblies">A collection of allowed assemblies whose types can be serialized and deserialized.</param>
114+
public ObjectSerializerAllowedTypesConvention(params Assembly[] allowedAssemblies)
115+
: this()
116+
{
117+
_allowedDeserializationTypes = _allowedSerializationTypes = t => allowedAssemblies.Contains(t.Assembly);
118+
}
119+
120+
/// <summary>
121+
/// Initializes a new instance of the <see cref="ObjectSerializerAllowedTypesConvention"/> class.
122+
/// </summary>
123+
public ObjectSerializerAllowedTypesConvention()
124+
{
125+
_effectiveAllowedDeserializationTypes = new Lazy<Func<Type, bool>>(() => CreateEffectiveAllowedTypes(_allowedDeserializationTypes));
126+
_effectiveAllowedSerializationTypes = new Lazy<Func<Type, bool>>(() => CreateEffectiveAllowedTypes(_allowedSerializationTypes));
127+
128+
Func<Type, bool> CreateEffectiveAllowedTypes(Func<Type, bool> allowedTypes)
129+
{
130+
return (allowedTypes, _allowDefaultFrameworkTypes) switch
131+
{
132+
(null, false) => ObjectSerializer.NoAllowedTypes,
133+
(null, true) => ObjectSerializer.DefaultAllowedTypes,
134+
(not null, false) => allowedTypes,
135+
(not null, true) => t => allowedTypes(t) || ObjectSerializer.DefaultAllowedTypes(t)
136+
};
137+
}
138+
}
139+
140+
/// <summary>
141+
/// Indicates whether default framework types are included for serialization and deserialization. Defaults to true.
142+
/// </summary>
143+
public bool AllowDefaultFrameworkTypes
144+
{
145+
get => _allowDefaultFrameworkTypes;
146+
init => _allowDefaultFrameworkTypes = value;
147+
}
148+
149+
/// <summary>
150+
/// Applies a modification to the member map.
151+
/// </summary>
152+
/// <param name="memberMap">The member map.</param>
153+
public void Apply(BsonMemberMap memberMap)
154+
{
155+
var serializer = memberMap.GetSerializer();
156+
157+
var reconfiguredSerializer = Reconfigure(serializer);
158+
if (reconfiguredSerializer is not null)
159+
{
160+
memberMap.SetSerializer(reconfiguredSerializer);
161+
}
162+
}
163+
164+
private IBsonSerializer Reconfigure(IBsonSerializer serializer)
165+
{
166+
if (serializer is IChildSerializerConfigurable childSerializerConfigurable)
167+
{
168+
var childSerializer = childSerializerConfigurable.ChildSerializer;
169+
var reconfiguredChildSerializer = Reconfigure(childSerializer);
170+
return reconfiguredChildSerializer is null ? null : childSerializerConfigurable.WithChildSerializer(reconfiguredChildSerializer);
171+
}
172+
173+
if (serializer.ValueType == typeof(object) && serializer is ObjectSerializer objectSerializer)
174+
{
175+
return objectSerializer.WithAllowedTypes(_effectiveAllowedDeserializationTypes.Value, _effectiveAllowedSerializationTypes.Value);
176+
}
177+
178+
return null;
179+
}
180+
}
181+
}

src/MongoDB.Bson/Serialization/Serializers/ObjectSerializer.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,17 @@ public ObjectSerializer WithDiscriminatorConvention(IDiscriminatorConvention dis
348348
return new ObjectSerializer(discriminatorConvention, _guidRepresentation, _allowedDeserializationTypes, _allowedSerializationTypes);
349349
}
350350

351+
/// <summary>
352+
/// Returns a new ObjectSerializer configured the same but with the specified allowed types delegates.
353+
/// </summary>
354+
/// <param name="allowedDeserializationTypes">A delegate that determines what types are allowed to be deserialized.</param>
355+
/// <param name="allowedSerializationTypes">A delegate that determines what types are allowed to be serialized.</param>
356+
/// <returns></returns>
357+
public ObjectSerializer WithAllowedTypes(Func<Type, bool> allowedDeserializationTypes, Func<Type, bool> allowedSerializationTypes)
358+
{
359+
return new ObjectSerializer(_discriminatorConvention, _guidRepresentation, allowedDeserializationTypes, allowedSerializationTypes);
360+
}
361+
351362
// private methods
352363
private object DeserializeDiscriminatedValue(BsonDeserializationContext context, BsonDeserializationArgs args)
353364
{

tests/MongoDB.Bson.Tests/Serialization/Conventions/EnumRepresentationConventionTests.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ public void Apply_should_configure_serializer_when_member_is_an_enum(BsonType re
4848
serializer.Representation.Should().Be(representation);
4949
}
5050

51-
5251
[Theory]
5352
[InlineData(BsonType.Int32)]
5453
[InlineData(BsonType.Int64)]

tests/MongoDB.Bson.Tests/Serialization/Conventions/MemberDefaultValueConventionTests.cs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
* limitations under the License.
1414
*/
1515

16+
using System;
1617
using MongoDB.Bson.Serialization;
1718
using MongoDB.Bson.Serialization.Attributes;
1819
using MongoDB.Bson.Serialization.Conventions;
@@ -37,42 +38,51 @@ private class B
3738
[Fact]
3839
public void TestMappingUsesMemberDefaultValueConvention()
3940
{
41+
var conventionName = Guid.NewGuid().ToString();
4042
var pack = new ConventionPack();
4143
pack.Add(new MemberDefaultValueConvention(typeof(int), 1));
42-
ConventionRegistry.Register("test", pack, t => t == typeof(A));
44+
ConventionRegistry.Register(conventionName, pack, t => t == typeof(A));
4345

4446
var classMap = new BsonClassMap<A>(cm => cm.AutoMap());
4547

4648
var defaultValue = classMap.GetMemberMap("Match").DefaultValue;
4749
Assert.IsType<int>(defaultValue);
4850
Assert.Equal(1, defaultValue);
51+
52+
ConventionRegistry.Remove(conventionName);
4953
}
5054

5155
[Fact]
5256
public void TestMappingUsesMemberDefaultValueConventionDoesNotMatchWrongProperty()
5357
{
58+
var conventionName = Guid.NewGuid().ToString();
5459
var pack = new ConventionPack();
5560
pack.Add(new MemberDefaultValueConvention(typeof(int), 1));
56-
ConventionRegistry.Register("test", pack, t => t == typeof(A));
61+
ConventionRegistry.Register(conventionName, pack, t => t == typeof(A));
5762

5863
var classMap = new BsonClassMap<A>(cm => cm.AutoMap());
5964

6065
var defaultValue = classMap.GetMemberMap("NoMatch").DefaultValue;
6166
Assert.Equal(0L, defaultValue);
67+
68+
ConventionRegistry.Remove(conventionName);
6269
}
6370

6471
[Fact]
6572
public void TestMappingUsesMemberDefaultValueConventionDoesNotOverrideAttribute()
6673
{
74+
var conventionName = Guid.NewGuid().ToString();
6775
var pack = new ConventionPack();
6876
pack.Add(new MemberDefaultValueConvention(typeof(int), 1));
69-
ConventionRegistry.Register("test", pack, t => t == typeof(B));
77+
ConventionRegistry.Register(conventionName, pack, t => t == typeof(B));
7078

7179
var classMap = new BsonClassMap<B>(cm => cm.AutoMap());
7280

7381
var defaultValue = classMap.GetMemberMap("Match").DefaultValue;
7482
Assert.IsType<int>(defaultValue);
7583
Assert.Equal(2, defaultValue);
84+
85+
ConventionRegistry.Remove(conventionName);
7686
}
7787
}
7888
}

0 commit comments

Comments
 (0)