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

gRPC JSON transcoding: Fix not supporting JSON name in querystring names #46624

Merged
merged 8 commits into from
Feb 18, 2023
Merged
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 @@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Google.Api;
using Google.Protobuf.Reflection;
using Grpc.AspNetCore.Server;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ private static async ValueTask<byte[]> ReadDataAsync(JsonTranscodingServerCallCo
{
return serverCallContext.DescriptorInfo.PathDescriptorsCache.GetOrAdd(path, p =>
{
ServiceDescriptorHelpers.TryResolveDescriptors(requestMessage.Descriptor, p.Split('.'), out var pathDescriptors);
ServiceDescriptorHelpers.TryResolveDescriptors(requestMessage.Descriptor, p.Split('.'), allowJsonName: true, out var pathDescriptors);
return pathDescriptors;
});
}
Expand Down
48 changes: 40 additions & 8 deletions src/Grpc/JsonTranscoding/src/Shared/ServiceDescriptorHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,26 +63,30 @@ internal static bool IsWrapperType(DescriptorBase m) =>
throw new InvalidOperationException($"Get not find Descriptor property on {serviceReflectionType.Name}.");
}

public static bool TryResolveDescriptors(MessageDescriptor messageDescriptor, IList<string> path, [NotNullWhen(true)]out List<FieldDescriptor>? fieldDescriptors)
public static bool TryResolveDescriptors(MessageDescriptor messageDescriptor, IList<string> path, bool allowJsonName, [NotNullWhen(true)]out List<FieldDescriptor>? fieldDescriptors)
{
fieldDescriptors = null;
MessageDescriptor? currentDescriptor = messageDescriptor;

foreach (var fieldName in path)
{
var field = currentDescriptor?.FindFieldByName(fieldName);
if (field == null)
FieldDescriptor? field = null;
if (currentDescriptor != null)
{
fieldDescriptors = null;
return false;
field = allowJsonName
? GetFieldByName(currentDescriptor, fieldName)
: currentDescriptor.FindFieldByName(fieldName);
}

if (fieldDescriptors == null)
if (field == null)
{
fieldDescriptors = new List<FieldDescriptor>();
fieldDescriptors = null;
return false;
}

fieldDescriptors ??= new List<FieldDescriptor>();
fieldDescriptors.Add(field);

if (field.FieldType == FieldType.Message)
{
currentDescriptor = field.MessageType;
Expand All @@ -96,6 +100,34 @@ public static bool TryResolveDescriptors(MessageDescriptor messageDescriptor, IL
return fieldDescriptors != null;
}

private static FieldDescriptor? GetFieldByName(MessageDescriptor messageDescriptor, string fieldName)
{
// Search fields by field name and JSON name. Both names can be referenced.
// If there are conflicts, then the last field with a name wins.
// This logic matches how properties are used in JSON serialization's MessageTypeInfoResolver.
var fields = messageDescriptor.Fields.InFieldNumberOrder();

FieldDescriptor? fieldDescriptor = null;
for (var i = fields.Count - 1; i >= 0; i--)
{
// We're checking JSON name first, in reverse order through fields.
// That means the method can exit early on match because the match has the highest precedence.
mgravell marked this conversation as resolved.
Show resolved Hide resolved
var field = fields[i];
if (field.JsonName == fieldName)
{
fieldDescriptor = field;
break;
}
else if (field.Name == fieldName)
{
fieldDescriptor = field;
break;
}
}

return fieldDescriptor;
}

private static object? ConvertValue(object? value, FieldDescriptor descriptor)
{
switch (descriptor.FieldType)
Expand Down Expand Up @@ -298,7 +330,7 @@ public static Dictionary<string, RouteParameter> ResolveRouteParameterDescriptor
foreach (var variable in variables)
{
var path = variable.FieldPath;
if (!TryResolveDescriptors(messageDescriptor, path, out var fieldDescriptors))
if (!TryResolveDescriptors(messageDescriptor, path, allowJsonName: false, out var fieldDescriptors))
{
throw new InvalidOperationException($"Couldn't find matching field for route parameter '{string.Join(".", path)}' on {messageDescriptor.Name}.");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,23 @@ public JsonConverterReadTests(ITestOutputHelper output)
public void NonJsonName()
{
var json = @"{
""hiding_field_name"": ""A field name""
}";

var m = AssertReadJson<HelloRequest>(json);
Assert.Equal("A field name", m.HidingFieldName);
}

[Fact]
public void HidingJsonName()
{
var json = @"{
""field_name"": ""A field name""
}";

var m = AssertReadJson<HelloRequest>(json);
Assert.Equal("A field name", m.FieldName);
Assert.Equal("", m.FieldName);
Assert.Equal("A field name", m.HidingFieldName);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ message HelloRequest {
google.protobuf.FieldMask field_mask_value = 21;
string field_name = 22 [json_name="json_customized_name"];
google.protobuf.FloatValue float_value = 23;
string hiding_field_name = 24 [json_name="field_name"];
}

message HelloReply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,85 @@ public async Task HandleCallAsync_MatchingQueryStringValues_SetOnRequestMessage(
Assert.Equal("TestSubfield!", request!.Sub.Subfield);
}

[Fact]
public async Task HandleCallAsync_MatchingQueryStringValues_JsonName_SetOnRequestMessage()
{
// Arrange
HelloRequest? request = null;
UnaryServerMethod<JsonTranscodingGreeterService, HelloRequest, HelloReply> invoker = (s, r, c) =>
{
request = r;
return Task.FromResult(new HelloReply());
};

var unaryServerCallHandler = CreateCallHandler(invoker);
var httpContext = TestHelpers.CreateHttpContext();
httpContext.Request.Query = new QueryCollection(new Dictionary<string, StringValues>
{
["json_customized_name"] = "TestName!"
});

// Act
await unaryServerCallHandler.HandleCallAsync(httpContext);

// Assert
Assert.NotNull(request);
Assert.Equal("TestName!", request!.FieldName);
}

[Fact]
public async Task HandleCallAsync_MatchingQueryStringValues_JsonNameAndValueObject_SetOnRequestMessage()
{
// Arrange
HelloRequest? request = null;
UnaryServerMethod<JsonTranscodingGreeterService, HelloRequest, HelloReply> invoker = (s, r, c) =>
{
request = r;
return Task.FromResult(new HelloReply());
};

var unaryServerCallHandler = CreateCallHandler(invoker);
var httpContext = TestHelpers.CreateHttpContext();
httpContext.Request.Query = new QueryCollection(new Dictionary<string, StringValues>
{
["float_value"] = "1.1"
});

// Act
await unaryServerCallHandler.HandleCallAsync(httpContext);

// Assert
Assert.NotNull(request);
Assert.Equal(1.1f, request!.FloatValue);
}

[Fact]
public async Task HandleCallAsync_MatchingQueryStringValues_JsonNameHidesFieldName_SetOnRequestMessage()
{
// Arrange
HelloRequest? request = null;
UnaryServerMethod<JsonTranscodingGreeterService, HelloRequest, HelloReply> invoker = (s, r, c) =>
{
request = r;
return Task.FromResult(new HelloReply());
};

var unaryServerCallHandler = CreateCallHandler(invoker);
var httpContext = TestHelpers.CreateHttpContext();
httpContext.Request.Query = new QueryCollection(new Dictionary<string, StringValues>
{
["field_name"] = "TestName!"
});

// Act
await unaryServerCallHandler.HandleCallAsync(httpContext);

// Assert
Assert.NotNull(request);
Assert.Equal("", request!.FieldName);
Assert.Equal("TestName!", request!.HidingFieldName);
}

[Fact]
public async Task HandleCallAsync_SuccessfulResponse_DefaultValuesInResponseJson()
{
Expand Down Expand Up @@ -414,7 +493,7 @@ public async Task HandleCallAsync_MalformedRequestBody_RepeatedBody_BadRequestRe
return Task.FromResult(new HelloReply());
};

ServiceDescriptorHelpers.TryResolveDescriptors(HelloRequest.Descriptor, new[] { "repeated_strings" }, out var bodyFieldDescriptors);
ServiceDescriptorHelpers.TryResolveDescriptors(HelloRequest.Descriptor, new[] { "repeated_strings" }, allowJsonName: false, out var bodyFieldDescriptors);

var descriptorInfo = TestHelpers.CreateDescriptorInfo(
bodyDescriptor: HelloRequest.Types.SubMessage.Descriptor,
Expand Down