Make ActorId deserializable (#457)

Fixes: #444

ActorId doesn't define a default constructor, and so it's not
deserializable via System.Text.Json.

This change implements a converter so that ActorId's API shape can work
with the serializer properly.

I added an integration test for ActorReference as well, but no library
changes we needed for that, ActorId was the blocker.
This commit is contained in:
Ryan Nowak 2020-11-05 15:10:38 -08:00 committed by GitHub
parent 29d0f7c38c
commit 7d1fa13101
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 162 additions and 0 deletions

View File

@ -7,10 +7,13 @@ namespace Dapr.Actors
{
using System;
using System.Runtime.Serialization;
using System.Text.Json.Serialization;
using Dapr.Actors.Seralization;
/// <summary>
/// The ActorId represents the identity of an actor within an actor service.
/// </summary>
[JsonConverter(typeof(ActorIdJsonConverter))]
[DataContract(Name = "ActorId")]
public class ActorId
{

View File

@ -0,0 +1,51 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
// ------------------------------------------------------------
namespace Dapr.Actors.Seralization
{
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
// Converter for ActorId - will be serialized as a JSON string
internal class ActorIdJsonConverter : JsonConverter<ActorId>
{
public override ActorId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (typeToConvert != typeof(ActorId))
{
throw new ArgumentException( $"Conversion to the type '{typeToConvert}' is not supported.", nameof(typeToConvert));
}
// Note - we generate random Guids for Actor Ids when we're generating them randomly
// but we don't actually enforce a format. Ids could be a number, or a date, or whatever,
// we don't really care. However we always **represent** Ids in JSON as strings.
if (reader.TokenType == JsonTokenType.String &&
reader.GetString() is string text &&
!string.IsNullOrWhiteSpace(text))
{
return new ActorId(text);
}
else if (reader.TokenType == JsonTokenType.Null)
{
return null;
}
throw new JsonException(); // The serializer will provide a default error message.
}
public override void Write(Utf8JsonWriter writer, ActorId value, JsonSerializerOptions options)
{
if (value is null)
{
writer.WriteNullValue();
}
else
{
writer.WriteStringValue(value.GetId());
}
}
}
}

View File

@ -0,0 +1,108 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
// ------------------------------------------------------------
using System.Text.Json;
using System.Text.Json.Serialization;
using Xunit;
namespace Dapr.Actors.Serialization
{
public class ActorIdJsonConverterTest
{
[Fact]
public void CanSerializeActorId()
{
var id = ActorId.CreateRandom();
var document = new { actor = id, };
// We use strings for ActorId - the result should be the same as passing the Id directly.
var expected = JsonSerializer.Serialize(new { actor = id.GetId(), });
var serialized = JsonSerializer.Serialize(document);
Assert.Equal(expected, serialized);
}
[Fact]
public void CanSerializeNullActorId()
{
var document = new { actor = (ActorId)null, };
var expected = JsonSerializer.Serialize(new { actor = (string)null, });
var serialized = JsonSerializer.Serialize(document);
Assert.Equal(expected, serialized);
}
[Fact]
public void CanDeserializeActorId()
{
var id = ActorId.CreateRandom().GetId();
var document = $@"
{{
""actor"": ""{id}""
}}";
var deserialized = JsonSerializer.Deserialize<ActorHolder>(document);
Assert.Equal(id, deserialized.Actor.GetId());
}
[Fact]
public void CanDeserializeNullActorId()
{
var id = ActorId.CreateRandom().GetId();
var document = $@"
{{
""actor"": null
}}";
var deserialized = JsonSerializer.Deserialize<ActorHolder>(document);
Assert.Null(deserialized.Actor);
}
[Theory]
[InlineData("{ \"actor\": ")]
[InlineData("{ \"actor\": \"hi")]
[InlineData("{ \"actor\": }")]
[InlineData("{ \"actor\": 3 }")]
[InlineData("{ \"actor\": \"\"}")]
[InlineData("{ \"actor\": \" \"}")]
public void CanReportErrorsFromInvalidData(string document)
{
// The error messages are provided by the serializer, don't test them here
// that would be fragile.
Assert.Throws<JsonException>(() =>
{
JsonSerializer.Deserialize<ActorHolder>(document);
});
}
// Regression test for #444
[Fact]
public void CanRoundTripActorReference()
{
var reference = new ActorReference()
{
ActorId = ActorId.CreateRandom(),
ActorType = "TestActor",
};
var serialized = JsonSerializer.Serialize(reference);
var deserialized = JsonSerializer.Deserialize<ActorReference>(serialized);
Assert.Equal(reference.ActorId.GetId(), deserialized.ActorId.GetId());
Assert.Equal(reference.ActorType, deserialized.ActorType);
}
private class ActorHolder
{
[JsonPropertyName("actor")]
public ActorId Actor { get; set; }
}
}
}