diff --git a/src/Dapr.Actors/ActorId.cs b/src/Dapr.Actors/ActorId.cs index b5774b98c..c466cb76f 100644 --- a/src/Dapr.Actors/ActorId.cs +++ b/src/Dapr.Actors/ActorId.cs @@ -7,10 +7,13 @@ namespace Dapr.Actors { using System; using System.Runtime.Serialization; + using System.Text.Json.Serialization; + using Dapr.Actors.Seralization; /// /// The ActorId represents the identity of an actor within an actor service. /// + [JsonConverter(typeof(ActorIdJsonConverter))] [DataContract(Name = "ActorId")] public class ActorId { diff --git a/src/Dapr.Actors/Serialization/ActorIdJsonConverter.cs b/src/Dapr.Actors/Serialization/ActorIdJsonConverter.cs new file mode 100644 index 000000000..61f33c09b --- /dev/null +++ b/src/Dapr.Actors/Serialization/ActorIdJsonConverter.cs @@ -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 + { + 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()); + } + } + } +} diff --git a/test/Dapr.Actors.Test/Serialization/ActorIdJsonConverterTest.cs b/test/Dapr.Actors.Test/Serialization/ActorIdJsonConverterTest.cs new file mode 100644 index 000000000..4e8117b80 --- /dev/null +++ b/test/Dapr.Actors.Test/Serialization/ActorIdJsonConverterTest.cs @@ -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(document); + + Assert.Equal(id, deserialized.Actor.GetId()); + } + + [Fact] + public void CanDeserializeNullActorId() + { + var id = ActorId.CreateRandom().GetId(); + var document = $@" +{{ + ""actor"": null +}}"; + + var deserialized = JsonSerializer.Deserialize(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(() => + { + JsonSerializer.Deserialize(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(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; } + } + } +}