Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug/476 multiple methods per interface with JSON serialization doesn´t work #1343

Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
7ab916c
update devcontainer
paule96 Aug 27, 2024
5412da5
update test setup
paule96 Aug 27, 2024
4dc9fc3
Now the json serialization should work with multiple methods in an in…
paule96 Aug 28, 2024
a31531a
fixed devcontainer to run actors
paule96 Aug 28, 2024
a940c1c
fix bugs with the current implementation
paule96 Aug 28, 2024
a03ff46
add a test that checks excatly the behavior
paule96 Aug 28, 2024
a4222c9
fix devcontainer post creatd command
paule96 Aug 28, 2024
7be9077
change the default to dotnet 8.0
paule96 Sep 3, 2024
78764aa
Merge branch 'master' into bug/476_multiple_methods_per_interface_wit…
yaron2 Sep 26, 2024
2d8d533
Merge branch 'master' into bug/476_multiple_methods_per_interface_wit…
WhitWaldo Oct 10, 2024
24ce360
Merge branch 'master' into bug/476_multiple_methods_per_interface_wit…
WhitWaldo Oct 17, 2024
e95a7f2
Merge branch 'master' into bug/476_multiple_methods_per_interface_wit…
WhitWaldo Oct 18, 2024
8a721be
Merge branch 'master' into bug/476_multiple_methods_per_interface_wit…
WhitWaldo Oct 22, 2024
698b837
I don't know what is different but we commit.
paule96 Oct 23, 2024
96ba8ad
make it easier to see why the application of an E2E test couldn't start
paule96 Oct 23, 2024
a7f0a92
make the exception in E2E more percise
paule96 Oct 24, 2024
9e34381
Merge branch 'master' into bug/476_multiple_methods_per_interface_wit…
WhitWaldo Oct 28, 2024
00ad7d7
Merge branch 'master' into bug/476_multiple_methods_per_interface_wit…
WhitWaldo Nov 4, 2024
de167bf
Merge branch 'master' into bug/476_multiple_methods_per_interface_wit…
WhitWaldo Nov 14, 2024
d3e961c
Merge branch 'master' into bug/476_multiple_methods_per_interface_wit…
WhitWaldo Nov 30, 2024
9fc6fcd
fix exception message
paule96 Dec 3, 2024
9a74614
Merge remote-tracking branch 'origin/bug/476_multiple_methods_per_int…
paule96 Dec 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@
"ghcr.io/devcontainers/features/azure-cli:1": {
"version": "2.38"
},
"ghcr.io/devcontainers/features/docker-from-docker:1": {
"version": "20.10"
"ghcr.io/devcontainers/features/docker-in-docker": {
"version": "latest"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, this should be pinned to a particular version in case breaking changes are introduced later on.

},
"ghcr.io/devcontainers/features/dotnet:1": {
"version": "6.0"
"ghcr.io/devcontainers/features/dotnet": {
"version": "8.0",
"additionalVersions": [
"6.0",
"7.0"
]
},
"ghcr.io/devcontainers/features/github-cli:1": {
"version": "2"
Expand All @@ -32,7 +36,8 @@
"ms-dotnettools.csharp",
"ms-dotnettools.vscode-dotnet-runtime",
"ms-azuretools.vscode-dapr",
"GitHub.copilot"
"GitHub.copilot",
"ms-dotnettools.csdevkit"
],
"forwardPorts": [
3000,
Expand All @@ -42,10 +47,9 @@
5000,
5007
],
"postCreateCommand": ".devcontainer/localinit.sh",
"postCreateCommand": "chmod +x .devcontainer/localinit.sh && .devcontainer/localinit.sh",
"remoteUser": "vscode",
"hostRequirements": {
"memory": "8gb"
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,10 @@ public MemoryStreamMessageBodySerializer(
{
var _methodRequestParameterTypes = new List<Type>(methodRequestParameterTypes);
var _wrappedRequestMessageTypes = new List<Type>(wrappedRequestMessageTypes);

if(_wrappedRequestMessageTypes.Count > 1){
throw new NotSupportedException("JSON serialisation should always provide the actor method (or nothing), that was called" +
" to support (de)serialisation. This is a dapr sdk error, open an issue on github.");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"dapr" -> "Dapr"
"github" -> "GitHub"

}
this.serializerOptions = new(serializerOptions)
{
// Workaround since WrappedMessageBody creates an object
Expand Down
44 changes: 33 additions & 11 deletions src/Dapr.Actors/Communication/ActorMessageSerializersManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@ namespace Dapr.Actors.Communication
{
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Dapr.Actors.Builder;

internal class ActorMessageSerializersManager
{
private readonly ConcurrentDictionary<int, CacheEntry> cachedBodySerializers;
private readonly ConcurrentDictionary<(int, string), CacheEntry> cachedBodySerializers;
private readonly IActorMessageHeaderSerializer headerSerializer;
private readonly IActorMessageBodySerializationProvider serializationProvider;

Expand All @@ -38,7 +41,7 @@ public ActorMessageSerializersManager(
}

this.serializationProvider = serializationProvider;
this.cachedBodySerializers = new ConcurrentDictionary<int, CacheEntry>();
this.cachedBodySerializers = new ConcurrentDictionary<(int, string), CacheEntry>();
this.headerSerializer = headerSerializer;
}

Expand All @@ -52,19 +55,19 @@ public IActorMessageHeaderSerializer GetHeaderSerializer()
return this.headerSerializer;
}

public IActorRequestMessageBodySerializer GetRequestMessageBodySerializer(int interfaceId)
public IActorRequestMessageBodySerializer GetRequestMessageBodySerializer(int interfaceId, [AllowNull] string methodName = null)
{
return this.cachedBodySerializers.GetOrAdd(interfaceId, this.CreateSerializers).RequestMessageBodySerializer;
return this.cachedBodySerializers.GetOrAdd((interfaceId, methodName), this.CreateSerializers).RequestMessageBodySerializer;
}

public IActorResponseMessageBodySerializer GetResponseMessageBodySerializer(int interfaceId)
public IActorResponseMessageBodySerializer GetResponseMessageBodySerializer(int interfaceId, [AllowNull] string methodName = null)
{
return this.cachedBodySerializers.GetOrAdd(interfaceId, this.CreateSerializers).ResponseMessageBodySerializer;
return this.cachedBodySerializers.GetOrAdd((interfaceId, methodName), this.CreateSerializers).ResponseMessageBodySerializer;
}

internal CacheEntry CreateSerializers(int interfaceId)
internal CacheEntry CreateSerializers((int interfaceId, string methodName) data)
{
var interfaceDetails = this.GetInterfaceDetails(interfaceId);
var interfaceDetails = this.GetInterfaceDetails(data.interfaceId);

// get the service interface type from the code gen layer
var serviceInterfaceType = interfaceDetails.ServiceInterfaceType;
Expand All @@ -74,10 +77,29 @@ internal CacheEntry CreateSerializers(int interfaceId)

// get the known types from the codegen layer
var responseBodyTypes = interfaceDetails.ResponseKnownTypes;
if (data.methodName is null)
{
// Path is mainly used for XML serialization
return new CacheEntry(
this.serializationProvider.CreateRequestMessageBodySerializer(serviceInterfaceType, requestBodyTypes, interfaceDetails.RequestWrappedKnownTypes),
this.serializationProvider.CreateResponseMessageBodySerializer(serviceInterfaceType, responseBodyTypes, interfaceDetails.ResponseWrappedKnownTypes));
}
else
{
// This path should be used for JSON serialization
var requestWrapperTypeAsList = interfaceDetails.RequestWrappedKnownTypes.Where(r => r.Name == $"{data.methodName}ReqBody").ToList();
if(requestWrapperTypeAsList.Count > 1){
throw new NotSupportedException($"More then one wrappertype was found for {data.methodName}");
}
var responseWrapperTypeAsList = interfaceDetails.ResponseWrappedKnownTypes.Where(r => r.Name == $"{data.methodName}RespBody").ToList();
if(responseWrapperTypeAsList.Count > 1){
throw new NotSupportedException($"More then one wrappertype was found for {data.methodName}");
}
return new CacheEntry(
this.serializationProvider.CreateRequestMessageBodySerializer(serviceInterfaceType, requestBodyTypes, requestWrapperTypeAsList),
this.serializationProvider.CreateResponseMessageBodySerializer(serviceInterfaceType, responseBodyTypes, responseWrapperTypeAsList));
}

return new CacheEntry(
this.serializationProvider.CreateRequestMessageBodySerializer(serviceInterfaceType, requestBodyTypes, interfaceDetails.RequestWrappedKnownTypes),
this.serializationProvider.CreateResponseMessageBodySerializer(serviceInterfaceType, responseBodyTypes, interfaceDetails.ResponseWrappedKnownTypes));
}

internal InterfaceDetails GetInterfaceDetails(int interfaceId)
Expand Down
4 changes: 2 additions & 2 deletions src/Dapr.Actors/DaprHttpInteractor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ public async Task<IActorResponseMessage> InvokeActorMethodWithRemotingAsync(Acto
var serializedHeader = serializersManager.GetHeaderSerializer()
.SerializeRequestHeader(remotingRequestRequestMessage.GetHeader());

var msgBodySeriaizer = serializersManager.GetRequestMessageBodySerializer(interfaceId);
var msgBodySeriaizer = serializersManager.GetRequestMessageBodySerializer(interfaceId, methodName);
var serializedMsgBody = msgBodySeriaizer.Serialize(remotingRequestRequestMessage.GetBody());

// Send Request
Expand Down Expand Up @@ -170,7 +170,7 @@ HttpRequestMessage RequestFunc()

// Deserialize Actor Response Message Body
// Deserialize to ActorInvokeException when there is response header otherwise normal path
var responseBodySerializer = serializersManager.GetResponseMessageBodySerializer(interfaceId);
var responseBodySerializer = serializersManager.GetResponseMessageBodySerializer(interfaceId, methodName);

// actorResponseMessageHeader is not null, it means there is remote exception
if (actorResponseMessageHeader != null)
Expand Down
10 changes: 5 additions & 5 deletions src/Dapr.Actors/Runtime/ActorManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,8 @@ internal async Task<Tuple<string, byte[]>> DispatchWithRemotingAsync(ActorId act
var interfaceId = actorMessageHeader.InterfaceId;

// Get the deserialized Body.
var msgBodySerializer = this.serializersManager.GetRequestMessageBodySerializer(actorMessageHeader.InterfaceId);

var msgBodySerializer = this.serializersManager.GetRequestMessageBodySerializer(actorMessageHeader.InterfaceId, actorMethodContext.MethodName);
IActorRequestMessageBody actorMessageBody;
using (var stream = new MemoryStream())
{
Expand All @@ -130,7 +130,7 @@ async Task<Tuple<string, byte[]>> RequestFunc(Actor actor, CancellationToken ct)
this.messageBodyFactory,
ct);

return this.CreateResponseMessage(responseMsgBody, interfaceId);
return this.CreateResponseMessage(responseMsgBody, interfaceId, actorMethodContext.MethodName);
}

return await this.DispatchInternalAsync(actorId, actorMethodContext, RequestFunc, cancellationToken);
Expand Down Expand Up @@ -387,12 +387,12 @@ private async Task<T> DispatchInternalAsync<T>(ActorId actorId, ActorMethodConte
return retval;
}

private Tuple<string, byte[]> CreateResponseMessage(IActorResponseMessageBody msgBody, int interfaceId)
private Tuple<string, byte[]> CreateResponseMessage(IActorResponseMessageBody msgBody, int interfaceId, string methodName)
{
var responseMsgBodyBytes = Array.Empty<byte>();
if (msgBody != null)
{
var responseSerializer = this.serializersManager.GetResponseMessageBodySerializer(interfaceId);
var responseSerializer = this.serializersManager.GetResponseMessageBodySerializer(interfaceId, methodName);
responseMsgBodyBytes = responseSerializer.Serialize(msgBody);
}

Expand Down
2 changes: 2 additions & 0 deletions test/Dapr.E2E.Test.Actors/ISerializationActor.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
Expand All @@ -10,6 +11,7 @@ namespace Dapr.E2E.Test.Actors
public interface ISerializationActor : IActor, IPingActor
{
Task<SerializationPayload> SendAsync(string name, SerializationPayload payload, CancellationToken cancellationToken = default);
Task<DateTime> AnotherMethod(DateTime payload);
}

public record SerializationPayload(string Message)
Expand Down
5 changes: 5 additions & 0 deletions test/Dapr.E2E.Test.App/Actors/SerializationActor.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@

using System;
using System.Threading;
using System.Threading.Tasks;
using Dapr.Actors.Runtime;
Expand All @@ -22,5 +23,9 @@ public Task<SerializationPayload> SendAsync(string name,
{
return Task.FromResult(payload);
}

public Task<DateTime> AnotherMethod(DateTime payload){
return Task.FromResult(payload);
}
}
}
27 changes: 27 additions & 0 deletions test/Dapr.E2E.Test/Actors/E2ETests.CustomSerializerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,5 +84,32 @@ public async Task ActorCanSupportCustomSerializer()
Assert.Equal(JsonSerializer.Serialize(kvp.Value), JsonSerializer.Serialize(value));
}
}

/// <summary>
/// This was actually a problem that is why the test exists.
/// It just checks, if the interface of the actor has more than one method defined,
/// that if can call it and serialize the payload correctly.
/// </summary>
/// <remarks>
/// More than one methods means here, that in the exact interface must be two methods defined.
/// That excludes hirachies.
/// So <see cref="IPingActor.Ping"/> wouldn't count here, because it's not directly defined in
/// <see cref="ISerializationActor"/>. (it's defined in the base of it.)
/// That why <see cref="ISerializationActor.AnotherMethod(DateTime)"/> was created,
/// so there are now more then one method.
/// </remark>
[Fact]
public async Task ActorCanSupportCustomSerializerAndCallMoreThenOneDefinedMethod()
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60));
var proxy = this.ProxyFactory.CreateActorProxy<ISerializationActor>(ActorId.CreateRandom(), "SerializationActor");

await ActorRuntimeChecker.WaitForActorRuntimeAsync(this.AppId, this.Output, proxy, cts.Token);

var payload = DateTime.MinValue;
var result = await proxy.AnotherMethod(payload);

Assert.Equal(payload, result);
}
}
}
Loading