Skip to content

Commit 6eb1227

Browse files
authored
Add MessagePackFormatter.InternStrings property (#1446)
This allows users to opt-out of string interning functionality. Fixes #1434
2 parents 60495bb + 26fbf66 commit 6eb1227

2 files changed

Lines changed: 119 additions & 7 deletions

File tree

src/StreamJsonRpc/MessagePackFormatter.cs

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,26 @@ public class MessagePackFormatter : FormatterBase, IJsonRpcMessageFormatter, IJs
114114

115115
private readonly ToStringHelper deserializationToStringHelper = new ToStringHelper();
116116

117+
/// <summary>
118+
/// Backing field for the <see cref="InternStrings" /> property.
119+
/// </summary>
120+
private bool internStrings = true;
121+
117122
/// <summary>
118123
/// The options to use for serializing user data (e.g. arguments, return values and errors).
119124
/// </summary>
120125
private MessagePackSerializerOptions userDataSerializationOptions;
121126

127+
/// <summary>
128+
/// The original value supplied to <see cref="SetMessagePackSerializerOptions(MessagePackSerializerOptions)"/>
129+
/// before we mutated it into <see cref="userDataSerializationOptions"/>.
130+
/// </summary>
131+
/// <remarks>
132+
/// This value is useful when we have to rebuild the final serialization options
133+
/// due to some other configuration change.
134+
/// </remarks>
135+
private MessagePackSerializerOptions originalUserDataSerializationOptions;
136+
122137
/// <summary>
123138
/// Initializes a new instance of the <see cref="MessagePackFormatter"/> class.
124139
/// </summary>
@@ -136,6 +151,7 @@ public MessagePackFormatter()
136151
this.exceptionResolver = new MessagePackExceptionResolver(this);
137152

138153
// Set up default user data resolver.
154+
this.originalUserDataSerializationOptions = DefaultUserDataSerializationOptions;
139155
this.userDataSerializationOptions = this.MassageUserDataOptions(DefaultUserDataSerializationOptions);
140156
}
141157

@@ -168,6 +184,28 @@ private interface IJsonRpcMessagePackRetention
168184
set => base.MultiplexingStream = value;
169185
}
170186

187+
/// <summary>
188+
/// Gets or sets a value indicating whether user data should be deserialized
189+
/// with string interning.
190+
/// </summary>
191+
/// <value>The default value is <see langword="true" />.</value>
192+
public bool InternStrings
193+
{
194+
get => this.internStrings;
195+
set
196+
{
197+
if (this.internStrings == value)
198+
{
199+
return;
200+
}
201+
202+
this.internStrings = value;
203+
204+
// Reinitialize with the new setting.
205+
this.userDataSerializationOptions = this.MassageUserDataOptions(this.originalUserDataSerializationOptions);
206+
}
207+
}
208+
171209
/// <summary>
172210
/// Gets a value indicating whether the W3C <c>traceparent</c> property
173211
/// should be serialized as a string instead of a more compact binary format.
@@ -189,6 +227,7 @@ public void SetMessagePackSerializerOptions(MessagePackSerializerOptions options
189227
{
190228
Requires.NotNull(options, nameof(options));
191229

230+
this.originalUserDataSerializationOptions = options;
192231
this.userDataSerializationOptions = this.MassageUserDataOptions(options);
193232
}
194233

@@ -340,14 +379,18 @@ private MessagePackSerializerOptions MassageUserDataOptions(MessagePackSerialize
340379
};
341380

342381
// Add our own resolvers to fill in specialized behavior if the user doesn't provide/override it by their own resolver.
343-
var resolvers = new IFormatterResolver[]
344-
{
345-
// Support for marshalled objects.
346-
new RpcMarshalableResolver(this),
382+
List<IFormatterResolver> resolvers =
383+
[
384+
new RpcMarshalableResolver(this), // Support for marshalled objects.
385+
];
347386

348-
// Intern strings to reduce memory usage.
349-
StringInterningResolver,
387+
if (this.InternStrings)
388+
{
389+
resolvers.Add(StringInterningResolver);
390+
}
350391

392+
resolvers.AddRange(
393+
[
351394
userSuppliedOptions.Resolver,
352395

353396
// Add stateless, non-specialized resolvers that help basic functionality to "just work".
@@ -358,7 +401,7 @@ private MessagePackSerializerOptions MassageUserDataOptions(MessagePackSerialize
358401
this.asyncEnumerableFormatterResolver,
359402
this.pipeFormatterResolver,
360403
this.exceptionResolver,
361-
};
404+
]);
362405

363406
// Wrap the resolver in another class as a way to pass information to our custom formatters.
364407
IFormatterResolver userDataResolver = new ResolverWrapper(CompositeResolver.Create(formatters, resolvers), this);

test/StreamJsonRpc.Tests/MessagePackFormatterTests.cs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,55 @@ public void StringValuesOfStandardPropertiesAreInterned()
409409
Assert.Same(request1.Method, request2.Method); // reference equality to ensure it was interned.
410410
}
411411

412+
[Fact]
413+
public void InternStrings_DefaultValueIsTrue()
414+
{
415+
MessagePackFormatter formatter = new();
416+
Assert.True(formatter.InternStrings);
417+
}
418+
419+
[Fact]
420+
public void InternStrings_CanBeDisabledBeforeSetMessagePackSerializerOptions()
421+
{
422+
MessagePackFormatter formatter = new()
423+
{
424+
InternStrings = false,
425+
};
426+
427+
Assert.False(this.AreTwoIdenticalStringsInterned(formatter));
428+
}
429+
430+
[Fact]
431+
public void InternStrings_CanBeDisabledAfterSetMessagePackSerializerOptions()
432+
{
433+
MessagePackFormatter formatter = new();
434+
formatter.SetMessagePackSerializerOptions(MessagePackSerializerOptions.Standard);
435+
formatter.InternStrings = false;
436+
437+
Assert.False(this.AreTwoIdenticalStringsInterned(formatter));
438+
}
439+
440+
[Fact]
441+
public void InternStrings_CanBeReEnabledBeforeSetMessagePackSerializerOptions()
442+
{
443+
MessagePackFormatter formatter = new();
444+
formatter.InternStrings = false;
445+
formatter.InternStrings = true;
446+
447+
Assert.True(this.AreTwoIdenticalStringsInterned(formatter));
448+
}
449+
450+
[Fact]
451+
public void InternStrings_CanBeReEnabledAfterSetMessagePackSerializerOptions()
452+
{
453+
MessagePackFormatter formatter = new();
454+
formatter.InternStrings = false;
455+
formatter.SetMessagePackSerializerOptions(MessagePackSerializerOptions.Standard);
456+
formatter.InternStrings = true;
457+
458+
Assert.True(this.AreTwoIdenticalStringsInterned(formatter));
459+
}
460+
412461
protected override MessagePackFormatter CreateFormatter() => new();
413462

414463
private T Read<T>(object anonymousObject)
@@ -421,6 +470,26 @@ private T Read<T>(object anonymousObject)
421470
return (T)this.Formatter.Deserialize(sequence);
422471
}
423472

473+
private bool AreTwoIdenticalStringsInterned(MessagePackFormatter formatter)
474+
{
475+
var anonymousObject = new
476+
{
477+
jsonrpc = "2.0",
478+
method = "test",
479+
@params = new object[] { "duplicate", "duplicate" },
480+
};
481+
482+
var sequence = new Sequence<byte>();
483+
var writer = new MessagePackWriter(sequence);
484+
MessagePackSerializer.Serialize(ref writer, anonymousObject, MessagePackSerializerOptions.Standard);
485+
writer.Flush();
486+
487+
var request = (JsonRpcRequest)formatter.Deserialize(sequence);
488+
Assert.True(request.TryGetArgumentByNameOrIndex(null, 0, typeof(string), out object? arg1));
489+
Assert.True(request.TryGetArgumentByNameOrIndex(null, 1, typeof(string), out object? arg2));
490+
return ReferenceEquals(arg1, arg2);
491+
}
492+
424493
[DataContract]
425494
private class DataContractWithSubsetOfMembersIncluded
426495
{

0 commit comments

Comments
 (0)