Skip to content

Commit

Permalink
Make ActorId deserializable (#457)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
rynowak authored Nov 5, 2020
1 parent 29d0f7c commit 7d1fa13
Show file tree
Hide file tree
Showing 3 changed files with 162 additions and 0 deletions.
3 changes: 3 additions & 0 deletions src/Dapr.Actors/ActorId.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
51 changes: 51 additions & 0 deletions src/Dapr.Actors/Serialization/ActorIdJsonConverter.cs
Original file line number Diff line number Diff line change
@@ -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());
}
}
}
}
108 changes: 108 additions & 0 deletions test/Dapr.Actors.Test/Serialization/ActorIdJsonConverterTest.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
}
}

0 comments on commit 7d1fa13

Please sign in to comment.