diff --git a/src/Dapr.Actors/Client/ActorProxyFactory.cs b/src/Dapr.Actors/Client/ActorProxyFactory.cs
index 9fd5edddb..4a8fe3a08 100644
--- a/src/Dapr.Actors/Client/ActorProxyFactory.cs
+++ b/src/Dapr.Actors/Client/ActorProxyFactory.cs
@@ -16,6 +16,7 @@ namespace Dapr.Actors.Client
using System;
using System.Net.Http;
using Dapr.Actors.Builder;
+ using Dapr.Actors.Communication;
using Dapr.Actors.Communication.Client;
///
@@ -79,7 +80,15 @@ public object CreateActorProxy(ActorId actorId, Type actorInterfaceType, string
options ??= this.DefaultOptions;
var daprInteractor = new DaprHttpInteractor(this.handler, options.HttpEndpoint, options.DaprApiToken, options.RequestTimeout);
- var remotingClient = new ActorRemotingClient(daprInteractor);
+
+ // provide a serializer if 'useJsonSerialization' is true and no serialization provider is provided.
+ IActorMessageBodySerializationProvider serializationProvider = null;
+ if (options.UseJsonSerialization)
+ {
+ serializationProvider = new ActorMessageBodyJsonSerializationProvider(options.JsonSerializerOptions);
+ }
+
+ var remotingClient = new ActorRemotingClient(daprInteractor, serializationProvider);
var proxyGenerator = ActorCodeBuilder.GetOrCreateProxyGenerator(actorInterfaceType);
var actorProxy = proxyGenerator.CreateActorProxy();
actorProxy.Initialize(remotingClient, actorId, actorType, options);
diff --git a/src/Dapr.Actors/Client/ActorProxyOptions.cs b/src/Dapr.Actors/Client/ActorProxyOptions.cs
index 808605c70..665a1dced 100644
--- a/src/Dapr.Actors/Client/ActorProxyOptions.cs
+++ b/src/Dapr.Actors/Client/ActorProxyOptions.cs
@@ -62,5 +62,10 @@ public JsonSerializerOptions JsonSerializerOptions
/// The timeout allowed for an actor request. Can be set to System.Threading.Timeout.InfiniteTimeSpan to disable any timeouts.
///
public TimeSpan? RequestTimeout { get; set; } = null;
+
+ ///
+ /// Enable JSON serialization for actor proxy message serialization in both remoting and non-remoting invocations.
+ ///
+ public bool UseJsonSerialization { get; set; }
}
}
diff --git a/src/Dapr.Actors/Communication/ActorMessageBodyDataContractSerializationProvider.cs b/src/Dapr.Actors/Communication/ActorMessageBodyDataContractSerializationProvider.cs
index e1991df26..cf16ee2d8 100644
--- a/src/Dapr.Actors/Communication/ActorMessageBodyDataContractSerializationProvider.cs
+++ b/src/Dapr.Actors/Communication/ActorMessageBodyDataContractSerializationProvider.cs
@@ -17,6 +17,7 @@ namespace Dapr.Actors.Communication
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization;
+ using System.Threading.Tasks;
using System.Xml;
///
@@ -185,21 +186,21 @@ byte[] IActorRequestMessageBodySerializer.Serialize(IActorRequestMessageBody act
return stream.ToArray();
}
- IActorRequestMessageBody IActorRequestMessageBodySerializer.Deserialize(Stream stream)
+ ValueTask IActorRequestMessageBodySerializer.DeserializeAsync(Stream stream)
{
if (stream == null)
{
- return null;
+ return default;
}
if (stream.Length == 0)
{
- return null;
+ return default;
}
stream.Position = 0;
using var reader = this.CreateXmlDictionaryReader(stream);
- return (TRequest)this.serializer.ReadObject(reader);
+ return new ValueTask((TRequest)this.serializer.ReadObject(reader));
}
byte[] IActorResponseMessageBodySerializer.Serialize(IActorResponseMessageBody actorResponseMessageBody)
@@ -217,11 +218,11 @@ byte[] IActorResponseMessageBodySerializer.Serialize(IActorResponseMessageBody a
return stream.ToArray();
}
- IActorResponseMessageBody IActorResponseMessageBodySerializer.Deserialize(Stream messageBody)
+ ValueTask IActorResponseMessageBodySerializer.DeserializeAsync(Stream messageBody)
{
if (messageBody == null)
{
- return null;
+ return default;
}
// TODO check performance
@@ -231,11 +232,11 @@ IActorResponseMessageBody IActorResponseMessageBodySerializer.Deserialize(Stream
if (stream.Capacity == 0)
{
- return null;
+ return default;
}
using var reader = this.CreateXmlDictionaryReader(stream);
- return (TResponse)this.serializer.ReadObject(reader);
+ return new ValueTask((TResponse)this.serializer.ReadObject(reader));
}
///
diff --git a/src/Dapr.Actors/Communication/ActorMessageBodyJsonConverter.cs b/src/Dapr.Actors/Communication/ActorMessageBodyJsonConverter.cs
new file mode 100644
index 000000000..0c77adb10
--- /dev/null
+++ b/src/Dapr.Actors/Communication/ActorMessageBodyJsonConverter.cs
@@ -0,0 +1,95 @@
+// ------------------------------------------------------------------------
+// Copyright 2021 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+using System;
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Dapr.Actors.Communication
+{
+ internal class ActorMessageBodyJsonConverter : JsonConverter
+ {
+ private readonly List methodRequestParameterTypes;
+ private readonly List wrappedRequestMessageTypes;
+ private readonly Type wrapperMessageType;
+
+ public ActorMessageBodyJsonConverter(
+ List methodRequestParameterTypes,
+ List wrappedRequestMessageTypes = null
+ )
+ {
+ this.methodRequestParameterTypes = methodRequestParameterTypes;
+ this.wrappedRequestMessageTypes = wrappedRequestMessageTypes;
+
+ if (this.wrappedRequestMessageTypes != null && this.wrappedRequestMessageTypes.Count == 1)
+ {
+ this.wrapperMessageType = this.wrappedRequestMessageTypes[0];
+ }
+ }
+
+ public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ // Ensure start-of-object, then advance
+ if (reader.TokenType != JsonTokenType.StartObject) throw new JsonException();
+ reader.Read();
+
+ // Ensure property name, then advance
+ if (reader.TokenType != JsonTokenType.PropertyName || reader.GetString() != "value") throw new JsonException();
+ reader.Read();
+
+ // If the value is null, return null.
+ if (reader.TokenType == JsonTokenType.Null)
+ {
+ // Read the end object token.
+ reader.Read();
+ return default;
+ }
+
+ // If the value is an object, deserialize it to wrapper message type
+ if (this.wrapperMessageType != null)
+ {
+ var value = JsonSerializer.Deserialize(ref reader, this.wrapperMessageType, options);
+
+ // Construct a new WrappedMessageBody with the deserialized value.
+ var wrapper = new WrappedMessageBody()
+ {
+ Value = value,
+ };
+
+ // Read the end object token.
+ reader.Read();
+
+ // Coerce the type to T; required because WrappedMessageBody inherits from two separate interfaces, which
+ // cannot both be used as generic constraints
+ return (T)(object)wrapper;
+ }
+
+ return JsonSerializer.Deserialize(ref reader, options);
+ }
+
+ public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
+ {
+ writer.WriteStartObject();
+ writer.WritePropertyName("value");
+
+ if (value is WrappedMessageBody body)
+ {
+ JsonSerializer.Serialize(writer, body.Value, body.Value.GetType(), options);
+ }
+ else
+ writer.WriteNullValue();
+ writer.WriteEndObject();
+ }
+ }
+}
diff --git a/src/Dapr.Actors/Communication/ActorMessageBodyJsonSerializationProvider.cs b/src/Dapr.Actors/Communication/ActorMessageBodyJsonSerializationProvider.cs
new file mode 100644
index 000000000..fd35db8e1
--- /dev/null
+++ b/src/Dapr.Actors/Communication/ActorMessageBodyJsonSerializationProvider.cs
@@ -0,0 +1,175 @@
+// ------------------------------------------------------------------------
+// Copyright 2021 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+namespace Dapr.Actors.Communication
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Text.Json;
+ using System.Threading.Tasks;
+ using System.Xml;
+
+ ///
+ /// This is the implmentation for used by remoting service and client during
+ /// request/response serialization . It uses request Wrapping and data contract for serialization.
+ ///
+ internal class ActorMessageBodyJsonSerializationProvider : IActorMessageBodySerializationProvider
+ {
+ public JsonSerializerOptions Options { get; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ActorMessageBodyJsonSerializationProvider(JsonSerializerOptions options)
+ {
+ Options = options;
+ }
+
+ ///
+ /// Creates a MessageFactory for Wrapped Message Json Remoting Types. This is used to create Remoting Request/Response objects.
+ ///
+ ///
+ /// that provides an instance of the factory for creating
+ /// remoting request and response message bodies.
+ ///
+ public IActorMessageBodyFactory CreateMessageBodyFactory()
+ {
+ return new WrappedRequestMessageFactory();
+ }
+
+ ///
+ /// Creates IActorRequestMessageBodySerializer for a serviceInterface using Wrapped Message Json implementation.
+ ///
+ /// The remoted service interface.
+ /// The union of parameter types of all of the methods of the specified interface.
+ /// Wrapped Request Types for all Methods.
+ ///
+ /// An instance of the that can serialize the service
+ /// actor request message body to a messaging body for transferring over the transport.
+ ///
+ public IActorRequestMessageBodySerializer CreateRequestMessageBodySerializer(
+ Type serviceInterfaceType,
+ IEnumerable methodRequestParameterTypes,
+ IEnumerable wrappedRequestMessageTypes = null)
+ {
+ return new MemoryStreamMessageBodySerializer(Options, serviceInterfaceType, methodRequestParameterTypes, wrappedRequestMessageTypes);
+ }
+
+ ///
+ /// Creates IActorResponseMessageBodySerializer for a serviceInterface using Wrapped Message Json implementation.
+ ///
+ /// The remoted service interface.
+ /// The return types of all of the methods of the specified interface.
+ /// Wrapped Response Types for all remoting methods.
+ ///
+ /// An instance of the that can serialize the service
+ /// actor response message body to a messaging body for transferring over the transport.
+ ///
+ public IActorResponseMessageBodySerializer CreateResponseMessageBodySerializer(
+ Type serviceInterfaceType,
+ IEnumerable methodReturnTypes,
+ IEnumerable wrappedResponseMessageTypes = null)
+ {
+ return new MemoryStreamMessageBodySerializer(Options, serviceInterfaceType, methodReturnTypes, wrappedResponseMessageTypes);
+ }
+
+ ///
+ /// Default serializer for service remoting request and response message body that uses the
+ /// memory stream to create outgoing message buffers.
+ ///
+ private class MemoryStreamMessageBodySerializer :
+ IActorRequestMessageBodySerializer,
+ IActorResponseMessageBodySerializer
+ where TRequest : IActorRequestMessageBody
+ where TResponse : IActorResponseMessageBody
+ {
+ private readonly JsonSerializerOptions serializerOptions;
+
+ public MemoryStreamMessageBodySerializer(
+ JsonSerializerOptions serializerOptions,
+ Type serviceInterfaceType,
+ IEnumerable methodRequestParameterTypes,
+ IEnumerable wrappedRequestMessageTypes = null)
+ {
+ var _methodRequestParameterTypes = new List(methodRequestParameterTypes);
+ var _wrappedRequestMessageTypes = new List(wrappedRequestMessageTypes);
+
+ this.serializerOptions = new(serializerOptions)
+ {
+ // Workaround since WrappedMessageBody creates an object
+ // with parameters as fields
+ IncludeFields = true,
+ };
+
+ this.serializerOptions.Converters.Add(new ActorMessageBodyJsonConverter(_methodRequestParameterTypes, _wrappedRequestMessageTypes));
+ this.serializerOptions.Converters.Add(new ActorMessageBodyJsonConverter(_methodRequestParameterTypes, _wrappedRequestMessageTypes));
+ }
+
+ byte[] IActorRequestMessageBodySerializer.Serialize(IActorRequestMessageBody actorRequestMessageBody)
+ {
+ if (actorRequestMessageBody == null)
+ {
+ return null;
+ }
+
+ return JsonSerializer.SerializeToUtf8Bytes
public class Startup
{
+ bool JsonSerializationEnabled =>
+ System.Linq.Enumerable.Contains(System.Environment.GetCommandLineArgs(), "--json-serialization");
+
///
/// Initializes a new instance of the class.
///
@@ -83,10 +87,12 @@ public void ConfigureServices(IServiceCollection services)
});
services.AddActors(options =>
{
+ options.UseJsonSerialization = JsonSerializationEnabled;
options.Actors.RegisterActor();
options.Actors.RegisterActor();
options.Actors.RegisterActor();
options.Actors.RegisterActor();
+ options.Actors.RegisterActor();
});
}
diff --git a/test/Dapr.E2E.Test/Actors/E2ETests.CustomSerializerTests.cs b/test/Dapr.E2E.Test/Actors/E2ETests.CustomSerializerTests.cs
new file mode 100644
index 000000000..c393f2ef1
--- /dev/null
+++ b/test/Dapr.E2E.Test/Actors/E2ETests.CustomSerializerTests.cs
@@ -0,0 +1,88 @@
+// ------------------------------------------------------------------------
+// Copyright 2021 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+namespace Dapr.E2E.Test
+{
+ using System;
+ using System.Diagnostics;
+ using System.Text.Json;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Dapr.Actors;
+ using Dapr.Actors.Client;
+ using Dapr.E2E.Test.Actors;
+ using Xunit;
+ using Xunit.Abstractions;
+
+ public class CustomSerializerTests : DaprTestAppLifecycle
+ {
+ private readonly Lazy proxyFactory;
+ private IActorProxyFactory ProxyFactory => this.HttpEndpoint == null ? null : this.proxyFactory.Value;
+
+ public CustomSerializerTests(ITestOutputHelper output, DaprTestAppFixture fixture) : base(output, fixture)
+ {
+ base.Configuration = new DaprRunConfiguration
+ {
+ UseAppPort = true,
+ AppId = "serializerapp",
+ AppJsonSerialization = true,
+ TargetProject = "./../../../../../test/Dapr.E2E.Test.App/Dapr.E2E.Test.App.csproj"
+ };
+
+ this.proxyFactory = new Lazy(() =>
+ {
+ Debug.Assert(this.HttpEndpoint != null);
+ return new ActorProxyFactory(new ActorProxyOptions() {
+ HttpEndpoint = this.HttpEndpoint,
+ JsonSerializerOptions = new JsonSerializerOptions()
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ PropertyNameCaseInsensitive = true,
+ WriteIndented = true,
+ },
+ UseJsonSerialization = true,
+ });
+ });
+ }
+
+ [Fact]
+ public async Task ActorCanSupportCustomSerializer()
+ {
+ using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60));
+ var proxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "SerializationActor");
+
+ await ActorRuntimeChecker.WaitForActorRuntimeAsync(this.AppId, this.Output, proxy, cts.Token);
+
+ var payload = new SerializationPayload("hello world")
+ {
+ Value = JsonSerializer.SerializeToElement(new { foo = "bar" }),
+ ExtensionData = new System.Collections.Generic.Dictionary()
+ {
+ { "baz", "qux" },
+ { "count", 42 },
+ }
+ };
+
+ var result = await proxy.SendAsync("test", payload, CancellationToken.None);
+
+ Assert.Equal(payload.Message, result.Message);
+ Assert.Equal(payload.Value.GetRawText(), result.Value.GetRawText());
+ Assert.Equal(payload.ExtensionData.Count, result.ExtensionData.Count);
+
+ foreach (var kvp in payload.ExtensionData)
+ {
+ Assert.True(result.ExtensionData.TryGetValue(kvp.Key, out var value));
+ Assert.Equal(JsonSerializer.Serialize(kvp.Value), JsonSerializer.Serialize(value));
+ }
+ }
+ }
+}
diff --git a/test/Dapr.E2E.Test/DaprRunConfiguration.cs b/test/Dapr.E2E.Test/DaprRunConfiguration.cs
index 9e423205d..fccfbcdd4 100644
--- a/test/Dapr.E2E.Test/DaprRunConfiguration.cs
+++ b/test/Dapr.E2E.Test/DaprRunConfiguration.cs
@@ -1,4 +1,4 @@
-// ------------------------------------------------------------------------
+// ------------------------------------------------------------------------
// Copyright 2021 The Dapr Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -20,9 +20,11 @@ public class DaprRunConfiguration
public string AppId { get; set; }
public string AppProtocol { get; set; }
+
+ public bool AppJsonSerialization { get; set; }
public string ConfigurationPath { get; set; }
public string TargetProject { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/test/Dapr.E2E.Test/DaprTestApp.cs b/test/Dapr.E2E.Test/DaprTestApp.cs
index 0d89be674..d65e21fd6 100644
--- a/test/Dapr.E2E.Test/DaprTestApp.cs
+++ b/test/Dapr.E2E.Test/DaprTestApp.cs
@@ -89,6 +89,11 @@ public DaprTestApp(ITestOutputHelper output, string appId)
arguments.AddRange(new[] { "--urls", $"http://localhost:{appPort.ToString(CultureInfo.InvariantCulture)}", });
}
+ if (configuration.AppJsonSerialization)
+ {
+ arguments.AddRange(new[] { "--json-serialization" });
+ }
+
// TODO: we don't do any quoting right now because our paths are guaranteed not to contain spaces
var daprStart = new DaprCommand(this.testOutput)
{
diff --git a/test/Dapr.E2E.Test/E2ETests.cs b/test/Dapr.E2E.Test/E2ETests.cs
index 94ebbf3df..bc469f715 100644
--- a/test/Dapr.E2E.Test/E2ETests.cs
+++ b/test/Dapr.E2E.Test/E2ETests.cs
@@ -1,4 +1,4 @@
-// ------------------------------------------------------------------------
+// ------------------------------------------------------------------------
// Copyright 2021 The Dapr Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -20,6 +20,8 @@
using Xunit;
using Xunit.Abstractions;
+[assembly: CollectionBehavior(DisableTestParallelization = true)]
+
namespace Dapr.E2E.Test
{
// We're using IClassFixture to manage the state we need across tests.