Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,6 @@ public JsonPayloadContentBuilder(ObjectSerializer jsonObjectSerializer)
{
return payload == null ? null : new JsonPayloadMessageContent(payload, _jsonObjectSerializer, typeHint);
}

public ObjectSerializer ObjectSerializer => _jsonObjectSerializer;
}
19 changes: 18 additions & 1 deletion src/Microsoft.Azure.SignalR.Common/Utilities/RestClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public RestClient(IHttpClientFactory httpClientFactory, IPayloadContentBuilder c
_httpClientFactory = httpClientFactory;
_payloadContentBuilder = contentBuilder;
}

// TODO: Test only, will remove later
internal RestClient(IHttpClientFactory httpClientFactory) : this(httpClientFactory, new JsonPayloadContentBuilder(new JsonObjectSerializer()))
{
Expand Down Expand Up @@ -78,6 +78,17 @@ public Task SendMessageWithRetryAsync(
return SendAsyncCore(Constants.HttpClientNames.MessageResilient, api, httpMethod, new InvocationMessage(methodName, args), null, AsAsync(handleExpectedResponse), cancellationToken);
}

public Task SendMessageWithRetryAsync(
RestApiEndpoint api,
HttpMethod httpMethod,
string methodName,
object?[] args,
Func<HttpResponseMessage, Task<bool>>? handleExpectedResponseAsync = null,
CancellationToken cancellationToken = default)
{
return SendAsyncCore(Constants.HttpClientNames.MessageResilient, api, httpMethod, new InvocationMessage(methodName, args), null, handleExpectedResponseAsync, cancellationToken);
}

public Task SendStreamMessageWithRetryAsync(
RestApiEndpoint api,
HttpMethod httpMethod,
Expand All @@ -90,6 +101,12 @@ public Task SendStreamMessageWithRetryAsync(
return SendAsyncCore(Constants.HttpClientNames.MessageResilient, api, httpMethod, new StreamItemMessage(streamId, arg), typeHint, AsAsync(handleExpectedResponse), cancellationToken);
}

public ObjectSerializer ObjectSerializer => _payloadContentBuilder switch
{
JsonPayloadContentBuilder jsonBuilder => jsonBuilder.ObjectSerializer,
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't work if the PayloadContentBuilder is not a JsonPayloadContentBuider. I'd suggest injecting IOptionsMonior<ServiceManagerOptions> into RestClient, using the ServiceManagerOptions.ObjectSerializer or providing a default one when ServiceManagerOptions.ObjectSerializer=null

_ => throw new NotSupportedException("Only JsonPayloadContentBuilder is supported to get the ObjectSerializer.")
};

private static Uri GetUri(string url, IDictionary<string, StringValues>? query)
{
if (query == null || query.Count == 0)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Text.Json.Serialization;

namespace Microsoft.Azure.SignalR.Management.ClientInvocation;
#nullable enable
sealed class InvocationResponse<T>
{
[JsonPropertyName("result")]
Copy link
Member

Choose a reason for hiding this comment

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

It may not work if the object serializer is backed by Newtonsoft.Json.

public T? Result { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using System.ComponentModel;
using System.Linq;
using System.Net.Http;

using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
Expand Down
5 changes: 5 additions & 0 deletions src/Microsoft.Azure.SignalR.Management/RestApiProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,9 @@ private RestApiEndpoint GenerateRestApiEndpoint(string appName, string hubName,
: $"{pathAfterHub}?application={Uri.EscapeDataString(appName.ToLowerInvariant())}&api-version={Version}";
return new RestApiEndpoint($"{requestPrefixWithHub}{pathAfterHub}") { Query = queries };
}

public RestApiEndpoint SendClientInvocation(string appName, string hubName, string connectionId)
{
return GenerateRestApiEndpoint(appName, hubName, $"/connections/{Uri.EscapeDataString(connectionId)}/:invoke");
}
}
75 changes: 74 additions & 1 deletion src/Microsoft.Azure.SignalR.Management/RestHubLifetimeManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,18 @@
using System.Linq;
using System.Net;
using System.Net.Http;
#if NET7_0_OR_GREATER
using System.IO;
#endif
using System.Threading;
using System.Threading.Tasks;

using Azure;

using Microsoft.AspNetCore.SignalR;
#if NET7_0_OR_GREATER
using Microsoft.AspNetCore.SignalR.Protocol;
using Microsoft.Azure.SignalR.Management.ClientInvocation;
#endif
using Microsoft.Azure.SignalR.Protocol;
using Microsoft.Extensions.Primitives;

Expand Down Expand Up @@ -351,6 +357,73 @@ public async Task SendStreamCompletionAsync(string connectionId, string streamId
await _restClient.SendWithRetryAsync(api, HttpMethod.Post, cancellationToken: cancellationToken);
}

#if NET7_0_OR_GREATER
#nullable enable
public override async Task<T> InvokeConnectionAsync<T>(string connectionId, string methodName, object?[] args, CancellationToken cancellationToken = default)
{
// Validate input parameters
if (string.IsNullOrEmpty(methodName))
{
throw new ArgumentException(NullOrEmptyStringErrorMessage, nameof(methodName));
}
if (string.IsNullOrEmpty(connectionId))
{
throw new ArgumentException(NullOrEmptyStringErrorMessage, nameof(connectionId));
}

// Get API endpoint and prepare for the request
var api = _restApiProvider.SendClientInvocation(_appName, _hubName, connectionId);
InvocationResponse<T>? wrapper = null;
string? errorContent = null;
bool isSuccess = false;
// Send request and capture the response
await _restClient.SendMessageWithRetryAsync(
api,
HttpMethod.Post,
methodName,
args,
async response =>
{
isSuccess = response.IsSuccessStatusCode;

if (isSuccess)
{
await using var contentStream = await response.Content.ReadAsStreamAsync(cancellationToken);

var deserialized = await _restClient.ObjectSerializer.DeserializeAsync(
Copy link
Member

Choose a reason for hiding this comment

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

The ObjectSerializer should be used to deserialize the value of `InvocationResponse.Result“ only. How to deserialize the key "result" should be controlled by ourselves.

contentStream,
typeof(InvocationResponse<T>),
cancellationToken);

wrapper = deserialized as InvocationResponse<T>
?? throw new HubException("Failed to deserialize response");
}
else
{
errorContent = await response.Content.ReadAsStringAsync(cancellationToken);
}

return isSuccess || response.StatusCode == HttpStatusCode.BadRequest;
},
cancellationToken);

// Ensure we have a response
if (!isSuccess)
{
throw new HubException(errorContent ?? "Unknown error in response");
Copy link
Member

Choose a reason for hiding this comment

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

Let's throw an AzureSignalRException to keep consistent with other functions.

}

return wrapper != null && wrapper.Result != null ? wrapper.Result : throw new HubException("Result not found in response");
Copy link
Member

Choose a reason for hiding this comment

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

Is a null invocation result invalid?

}

public override Task SetConnectionResultAsync(string connectionId, CompletionMessage result)
{
// This method won't get trigger because in transient we will wait for the returned completion message.
// this is to honor the interface
throw new NotImplementedException();
}
#endif

private static bool FilterExpectedResponse(HttpResponseMessage response, string expectedErrorCode) =>
response.IsSuccessStatusCode
|| (response.StatusCode == HttpStatusCode.NotFound && response.Headers.TryGetValues(Headers.MicrosoftErrorCode, out var errorCodes) && errorCodes.First().Equals(expectedErrorCode, StringComparison.OrdinalIgnoreCase));
Expand Down
Loading
Loading