Skip to content

Commit

Permalink
Merge pull request #948 from AArnott/expandoObject
Browse files Browse the repository at this point in the history
Add support for ExpandoObject
  • Loading branch information
AArnott committed Sep 12, 2020
2 parents 2bb600e + d05ed55 commit 6f5c234
Show file tree
Hide file tree
Showing 8 changed files with 280 additions and 15 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,9 @@ Console.WriteLine(dynamicModel["Name"]); // foobar
Console.WriteLine(dynamicModel["Items"][2]); // 100
```

Exploring object trees using the dictionary indexer syntax is the fastest option for untyped deserialization, but it is tedious to read and write.
Where performance is not as important as code readability, consider deserializing with [ExpandoObject](doc/ExpandoObject.md).

## Object Type Serialization

`StandardResolver` and `ContractlessStandardResolver` can serialize `object`/anonymous typed objects.
Expand Down
25 changes: 25 additions & 0 deletions doc/ExpandoObject.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Using `ExpandoObject` for Javascript-like discovery of messagepack structures

In Javascript an arbitrary JSON object can be parsed into an object graph and then explored with Javascript as easily as a native Javascript object,
since in Javascript all property access is late-bound.
In C# we can do the same thing using the `dynamic` keyword and the `ExpandoObject` type.

By default, deserializing untyped maps results in a `Dictionary<object, object>` being created to store the map.
If you would like to use C# `dynamic` to explore the deserialized object graph more naturally (i.e. the way Javascript would allow),
you can deserialize these maps into .NET `ExpandoObject` and use the C# dynamic keyword:

```cs
dynamic expando = new ExpandoObject();
expando.Name = "George";
expando.Age = 18;
expando.Other = new { OtherProperty = "foo" };

byte[] bin = MessagePackSerializer.Serialize(expando, MessagePackSerializerOptions.Standard);
this.logger.WriteLine(MessagePackSerializer.ConvertToJson(bin)); // {"Name":"George","Age":18,"Other":{"OtherProperty":"foo"}}
dynamic expando2 = MessagePackSerializer.Deserialize<ExpandoObject>(bin, ExpandoObjectResolver.Options);
Assert.Equal(expando.Name, expando2.Name);
Assert.Equal(expando.Age, expando2.Age);
Assert.NotNull(expando2.Other);
Assert.Equal(expando.Other.OtherProperty, expando2.Other.OtherProperty);
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright (c) All contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Collections.Generic;
using System.Dynamic;

namespace MessagePack.Formatters
{
public class ExpandoObjectFormatter : IMessagePackFormatter<ExpandoObject>
{
public static readonly IMessagePackFormatter<ExpandoObject> Instance = new ExpandoObjectFormatter();

private ExpandoObjectFormatter()
{
}

public ExpandoObject Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
{
if (reader.TryReadNil())
{
return null;
}

var result = new ExpandoObject();
int count = reader.ReadMapHeader();
if (count > 0)
{
IFormatterResolver resolver = options.Resolver;
IMessagePackFormatter<string> keyFormatter = resolver.GetFormatterWithVerify<string>();
IMessagePackFormatter<object> valueFormatter = resolver.GetFormatterWithVerify<object>();
IDictionary<string, object> dictionary = result;

options.Security.DepthStep(ref reader);
try
{
for (int i = 0; i < count; i++)
{
string key = keyFormatter.Deserialize(ref reader, options);
object value = valueFormatter.Deserialize(ref reader, options);
dictionary.Add(key, value);
}
}
finally
{
reader.Depth--;
}
}

return result;
}

public void Serialize(ref MessagePackWriter writer, ExpandoObject value, MessagePackSerializerOptions options)
{
var dictionaryFormatter = options.Resolver.GetFormatterWithVerify<IDictionary<string, object>>();
dictionaryFormatter.Serialize(ref writer, value, options);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Net.Security;
using System.Reflection;

namespace MessagePack.Formatters
{
public sealed class PrimitiveObjectFormatter : IMessagePackFormatter<object>
public class PrimitiveObjectFormatter : IMessagePackFormatter<object>
{
public static readonly IMessagePackFormatter<object> Instance = new PrimitiveObjectFormatter();

Expand All @@ -32,7 +33,7 @@ public sealed class PrimitiveObjectFormatter : IMessagePackFormatter<object>
{ typeof(byte[]), 14 },
};

private PrimitiveObjectFormatter()
protected PrimitiveObjectFormatter()
{
}

Expand Down Expand Up @@ -304,26 +305,15 @@ public object Deserialize(ref MessagePackReader reader, MessagePackSerializerOpt
{
var length = reader.ReadMapHeader();

IMessagePackFormatter<object> objectFormatter = resolver.GetFormatter<object>();
var hash = new Dictionary<object, object>(length, options.Security.GetEqualityComparer<object>());
options.Security.DepthStep(ref reader);
try
{
for (int i = 0; i < length; i++)
{
var key = objectFormatter.Deserialize(ref reader, options);

var value = objectFormatter.Deserialize(ref reader, options);

hash.Add(key, value);
}
return this.DeserializeMap(ref reader, length, options);
}
finally
{
reader.Depth--;
}

return hash;
}

case MessagePackType.Nil:
Expand All @@ -333,5 +323,19 @@ public object Deserialize(ref MessagePackReader reader, MessagePackSerializerOpt
throw new MessagePackSerializationException("Invalid primitive bytes.");
}
}

protected virtual object DeserializeMap(ref MessagePackReader reader, int length, MessagePackSerializerOptions options)
{
IMessagePackFormatter<object> objectFormatter = options.Resolver.GetFormatter<object>();
var dictionary = new Dictionary<object, object>(length, options.Security.GetEqualityComparer<object>());
for (int i = 0; i < length; i++)
{
var key = objectFormatter.Deserialize(ref reader, options);
var value = objectFormatter.Deserialize(ref reader, options);
dictionary.Add(key, value);
}

return dictionary;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) All contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Collections.Generic;
using System.Dynamic;
using MessagePack.Formatters;

namespace MessagePack.Resolvers
{
/// <summary>
/// A resolver for use when deserializing MessagePack data where the schema is not known at compile-time
/// such that strong-types can be instantiated.
/// Instead, <see cref="ExpandoObject"/> is used wherever a MessagePack <em>map</em> is encountered.
/// </summary>
public static class ExpandoObjectResolver
{
/// <summary>
/// The resolver to use to deserialize into C#'s <c>dynamic</c> keyword.
/// </summary>
/// <remarks>
/// This resolver includes more than just the <see cref="ExpandoObjectFormatter"/>.
/// </remarks>
public static readonly IFormatterResolver Instance = CompositeResolver.Create(
new IMessagePackFormatter[]
{
ExpandoObjectFormatter.Instance,
new PrimitiveObjectWithExpandoMaps(),
},
new IFormatterResolver[] { BuiltinResolver.Instance });

/// <summary>
/// A set of options that includes the <see cref="Instance"/>
/// and puts the deserializer into <see cref="MessagePackSecurity.UntrustedData"/> mode.
/// </summary>
public static readonly MessagePackSerializerOptions Options = MessagePackSerializerOptions.Standard
.WithSecurity(MessagePackSecurity.UntrustedData) // when the schema isn't known beforehand, that generally suggests you don't know/trust the data.
.WithResolver(Instance);

private class PrimitiveObjectWithExpandoMaps : PrimitiveObjectFormatter
{
protected override object DeserializeMap(ref MessagePackReader reader, int length, MessagePackSerializerOptions options)
{
IMessagePackFormatter<string> keyFormatter = options.Resolver.GetFormatterWithVerify<string>();
IMessagePackFormatter<object> objectFormatter = options.Resolver.GetFormatter<object>();
IDictionary<string, object> dictionary = new ExpandoObject();
for (int i = 0; i < length; i++)
{
var key = keyFormatter.Deserialize(ref reader, options);
var value = objectFormatter.Deserialize(ref reader, options);
dictionary.Add(key, value);
}

return dictionary;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ internal static class StandardResolverHelper
MessagePack.Unity.UnityResolver.Instance,
#else
ImmutableCollection.ImmutableCollectionResolver.Instance,
CompositeResolver.Create(ExpandoObjectFormatter.Instance),
#endif

#if !ENABLE_IL2CPP && !NET_STANDARD_2_0
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Copyright (c) All contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Dynamic;
using System.Runtime.Serialization;
using MessagePack.Resolvers;
using Xunit;
using Xunit.Abstractions;

namespace MessagePack.Tests
{
public class ExpandoObjectTests
{
private readonly ITestOutputHelper logger;

public ExpandoObjectTests(ITestOutputHelper logger)
{
this.logger = logger;
}

[Fact]
public void ExpandoObject_Roundtrip()
{
var options = MessagePackSerializerOptions.Standard;

dynamic expando = new ExpandoObject();
expando.Name = "George";
expando.Age = 18;

byte[] bin = MessagePackSerializer.Serialize(expando, options);
this.logger.WriteLine(MessagePackSerializer.ConvertToJson(bin));

dynamic expando2 = MessagePackSerializer.Deserialize<ExpandoObject>(bin, options);
Assert.Equal(expando.Name, expando2.Name);
Assert.Equal(expando.Age, expando2.Age);
}

[Fact]
public void ExpandoObject_DeepGraphContainsAnonymousType()
{
dynamic expando = new ExpandoObject();
expando.Name = "George";
expando.Age = 18;
expando.Other = new { OtherProperty = "foo" };

byte[] bin = MessagePackSerializer.Serialize(expando, MessagePackSerializerOptions.Standard);
this.logger.WriteLine(MessagePackSerializer.ConvertToJson(bin));

dynamic expando2 = MessagePackSerializer.Deserialize<ExpandoObject>(bin, ExpandoObjectResolver.Options);
Assert.Equal(expando.Name, expando2.Name);
Assert.Equal(expando.Age, expando2.Age);
Assert.NotNull(expando2.Other);
Assert.Equal(expando.Other.OtherProperty, expando2.Other.OtherProperty);
}

[Fact]
public void ExpandoObject_DeepGraphContainsCustomTypes()
{
var options = MessagePackSerializerOptions.Standard;
var f = options.Resolver.GetFormatter<string>();

dynamic expando = new ExpandoObject();
expando.Name = "George";
expando.Age = 18;
expando.Other = new CustomObject { OtherProperty = "foo" };

byte[] bin = MessagePackSerializer.Serialize(expando, MessagePackSerializerOptions.Standard);
this.logger.WriteLine(MessagePackSerializer.ConvertToJson(bin));

dynamic expando2 = MessagePackSerializer.Deserialize<ExpandoObject>(bin, ExpandoObjectResolver.Options);
Assert.Equal(expando.Name, expando2.Name);
Assert.Equal(expando.Age, expando2.Age);
Assert.NotNull(expando2.Other);
Assert.Equal(expando.Other.OtherProperty, expando2.Other.OtherProperty);
}

#if !UNITY_2018_3_OR_NEWER

[Fact]
public void ExpandoObject_DeepGraphContainsCustomTypes_TypeAnnotated()
{
var options = MessagePackSerializerOptions.Standard.WithResolver(TypelessObjectResolver.Instance);

dynamic expando = new ExpandoObject();
expando.Name = "George";
expando.Age = 18;
expando.Other = new CustomObject { OtherProperty = "foo" };

byte[] bin = MessagePackSerializer.Serialize(expando, options);
this.logger.WriteLine(MessagePackSerializer.ConvertToJson(bin));

dynamic expando2 = MessagePackSerializer.Deserialize<ExpandoObject>(bin, options);
Assert.Equal(expando.Name, expando2.Name);
Assert.Equal(expando.Age, expando2.Age);
Assert.IsType<CustomObject>(expando2.Other);
Assert.Equal(expando.Other.OtherProperty, expando2.Other.OtherProperty);
}

#endif

[DataContract]
public class CustomObject
{
[DataMember]
public string OtherProperty { get; set; }
}
}
}
11 changes: 10 additions & 1 deletion src/MessagePack/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ MessagePack.Formatters.ByteReadOnlyMemoryFormatter.Serialize(ref MessagePack.Mes
MessagePack.Formatters.ByteReadOnlySequenceFormatter
MessagePack.Formatters.ByteReadOnlySequenceFormatter.Deserialize(ref MessagePack.MessagePackReader reader, MessagePack.MessagePackSerializerOptions options) -> System.Buffers.ReadOnlySequence<byte>
MessagePack.Formatters.ByteReadOnlySequenceFormatter.Serialize(ref MessagePack.MessagePackWriter writer, System.Buffers.ReadOnlySequence<byte> value, MessagePack.MessagePackSerializerOptions options) -> void
MessagePack.Formatters.ExpandoObjectFormatter
MessagePack.Formatters.ExpandoObjectFormatter.Deserialize(ref MessagePack.MessagePackReader reader, MessagePack.MessagePackSerializerOptions options) -> System.Dynamic.ExpandoObject
MessagePack.Formatters.ExpandoObjectFormatter.Serialize(ref MessagePack.MessagePackWriter writer, System.Dynamic.ExpandoObject value, MessagePack.MessagePackSerializerOptions options) -> void
MessagePack.Formatters.MemoryFormatter<T>
MessagePack.Formatters.MemoryFormatter<T>.Deserialize(ref MessagePack.MessagePackReader reader, MessagePack.MessagePackSerializerOptions options) -> System.Memory<T>
MessagePack.Formatters.MemoryFormatter<T>.MemoryFormatter() -> void
MessagePack.Formatters.MemoryFormatter<T>.Serialize(ref MessagePack.MessagePackWriter writer, System.Memory<T> value, MessagePack.MessagePackSerializerOptions options) -> void
MessagePack.Formatters.PrimitiveObjectFormatter.PrimitiveObjectFormatter() -> void
MessagePack.Formatters.ReadOnlyMemoryFormatter<T>
MessagePack.Formatters.ReadOnlyMemoryFormatter<T>.Deserialize(ref MessagePack.MessagePackReader reader, MessagePack.MessagePackSerializerOptions options) -> System.ReadOnlyMemory<T>
MessagePack.Formatters.ReadOnlyMemoryFormatter<T>.ReadOnlyMemoryFormatter() -> void
Expand Down Expand Up @@ -57,6 +61,7 @@ MessagePack.ImmutableCollection.InterfaceImmutableSetFormatter<T>
MessagePack.ImmutableCollection.InterfaceImmutableSetFormatter<T>.InterfaceImmutableSetFormatter() -> void
MessagePack.ImmutableCollection.InterfaceImmutableStackFormatter<T>
MessagePack.ImmutableCollection.InterfaceImmutableStackFormatter<T>.InterfaceImmutableStackFormatter() -> void
MessagePack.Resolvers.ExpandoObjectResolver
override MessagePack.ImmutableCollection.ImmutableDictionaryFormatter<TKey, TValue>.Add(System.Collections.Immutable.ImmutableDictionary<TKey, TValue>.Builder collection, int index, TKey key, TValue value, MessagePack.MessagePackSerializerOptions options) -> void
override MessagePack.ImmutableCollection.ImmutableDictionaryFormatter<TKey, TValue>.Complete(System.Collections.Immutable.ImmutableDictionary<TKey, TValue>.Builder intermediateCollection) -> System.Collections.Immutable.ImmutableDictionary<TKey, TValue>
override MessagePack.ImmutableCollection.ImmutableDictionaryFormatter<TKey, TValue>.Create(int count, MessagePack.MessagePackSerializerOptions options) -> System.Collections.Immutable.ImmutableDictionary<TKey, TValue>.Builder
Expand Down Expand Up @@ -101,5 +106,9 @@ override MessagePack.ImmutableCollection.InterfaceImmutableStackFormatter<T>.Cre
static readonly MessagePack.Formatters.ByteMemoryFormatter.Instance -> MessagePack.Formatters.ByteMemoryFormatter
static readonly MessagePack.Formatters.ByteReadOnlyMemoryFormatter.Instance -> MessagePack.Formatters.ByteReadOnlyMemoryFormatter
static readonly MessagePack.Formatters.ByteReadOnlySequenceFormatter.Instance -> MessagePack.Formatters.ByteReadOnlySequenceFormatter
static readonly MessagePack.Formatters.ExpandoObjectFormatter.Instance -> MessagePack.Formatters.IMessagePackFormatter<System.Dynamic.ExpandoObject>
static readonly MessagePack.Formatters.TypeFormatter<T>.Instance -> MessagePack.Formatters.IMessagePackFormatter<T>
static readonly MessagePack.ImmutableCollection.ImmutableCollectionResolver.Instance -> MessagePack.ImmutableCollection.ImmutableCollectionResolver
static readonly MessagePack.ImmutableCollection.ImmutableCollectionResolver.Instance -> MessagePack.ImmutableCollection.ImmutableCollectionResolver
static readonly MessagePack.Resolvers.ExpandoObjectResolver.Instance -> MessagePack.IFormatterResolver
static readonly MessagePack.Resolvers.ExpandoObjectResolver.Options -> MessagePack.MessagePackSerializerOptions
virtual MessagePack.Formatters.PrimitiveObjectFormatter.DeserializeMap(ref MessagePack.MessagePackReader reader, int length, MessagePack.MessagePackSerializerOptions options) -> object

0 comments on commit 6f5c234

Please sign in to comment.