Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/dotnet/aspnetcore.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/Binding/JsonTranscodingProviderServiceBinder.cs19
-rw-r--r--src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/CallHandlers/CallHandlerDescriptorInfo.cs5
-rw-r--r--src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/CallHandlers/ServerCallHandlerBase.cs5
-rw-r--r--src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/JsonRequestHelpers.cs2
-rw-r--r--src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/JsonTranscodingRouteAdapter.cs210
-rw-r--r--src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Microsoft.AspNetCore.Grpc.JsonTranscoding.csproj2
-rw-r--r--src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.Swagger/Internal/GrpcJsonTranscodingDescriptionProvider.cs3
-rw-r--r--src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.Swagger/Microsoft.AspNetCore.Grpc.Swagger.csproj2
-rw-r--r--src/Grpc/JsonTranscoding/src/Shared/HttpRoutePattern.cs35
-rw-r--r--src/Grpc/JsonTranscoding/src/Shared/HttpRoutePatternParser.cs349
-rw-r--r--src/Grpc/JsonTranscoding/src/Shared/ServiceDescriptorHelpers.cs35
-rw-r--r--src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.IntegrationTests/Infrastructure/TestHelpers.cs5
-rw-r--r--src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.IntegrationTests/RouteTests.cs108
-rw-r--r--src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/HttpRoutePatternParserTests.cs325
-rw-r--r--src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/Infrastructure/TestHelpers.cs5
-rw-r--r--src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/JsonTranscodingRouteAdapterTests.cs223
-rw-r--r--src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/JsonTranscodingServerCallContextTests.cs4
-rw-r--r--src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/JsonTranscodingServiceMethodProviderTests.cs3
-rw-r--r--src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/UnaryServerCallHandlerTests.cs18
-rw-r--r--src/Grpc/JsonTranscoding/test/testassets/IntegrationTestsWebsite/Protos/greet.proto28
20 files changed, 1335 insertions, 51 deletions
diff --git a/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/Binding/JsonTranscodingProviderServiceBinder.cs b/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/Binding/JsonTranscodingProviderServiceBinder.cs
index 3941ea7b5e..4ea56f445d 100644
--- a/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/Binding/JsonTranscodingProviderServiceBinder.cs
+++ b/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/Binding/JsonTranscodingProviderServiceBinder.cs
@@ -2,6 +2,7 @@
// 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;
@@ -228,20 +229,15 @@ internal sealed partial class JsonTranscodingProviderServiceBinder<TService> : S
private static (RoutePattern routePattern, CallHandlerDescriptorInfo descriptorInfo) ParseRoute(string pattern, string body, string responseBody, MethodDescriptor methodDescriptor)
{
- if (!pattern.StartsWith('/'))
- {
- // This validation is consistent with grpc-gateway code generation.
- // We should match their validation to be a good member of the eco-system.
- throw new InvalidOperationException($"Path template '{pattern}' must start with a '/'.");
- }
+ var httpRoutePattern = HttpRoutePattern.Parse(pattern);
+ var adapter = JsonTranscodingRouteAdapter.Parse(httpRoutePattern);
- var routePattern = RoutePatternFactory.Parse(pattern);
- return (RoutePatternFactory.Parse(pattern), CreateDescriptorInfo(body, responseBody, methodDescriptor, routePattern));
+ return (RoutePatternFactory.Parse(adapter.ResolvedRouteTemplate), CreateDescriptorInfo(body, responseBody, methodDescriptor, adapter));
}
- private static CallHandlerDescriptorInfo CreateDescriptorInfo(string body, string responseBody, MethodDescriptor methodDescriptor, RoutePattern routePattern)
+ private static CallHandlerDescriptorInfo CreateDescriptorInfo(string body, string responseBody, MethodDescriptor methodDescriptor, JsonTranscodingRouteAdapter routeAdapter)
{
- var routeParameterDescriptors = ServiceDescriptorHelpers.ResolveRouteParameterDescriptors(routePattern, methodDescriptor.InputType);
+ var routeParameterDescriptors = ServiceDescriptorHelpers.ResolveRouteParameterDescriptors(routeAdapter.HttpRoutePattern.Variables.Select(v => v.FieldPath).ToList(), methodDescriptor.InputType);
var bodyDescriptor = ServiceDescriptorHelpers.ResolveBodyDescriptor(body, typeof(TService), methodDescriptor);
@@ -260,7 +256,8 @@ internal sealed partial class JsonTranscodingProviderServiceBinder<TService> : S
bodyDescriptor?.Descriptor,
bodyDescriptor?.IsDescriptorRepeated ?? false,
bodyDescriptor?.FieldDescriptors,
- routeParameterDescriptors);
+ routeParameterDescriptors,
+ routeAdapter);
return descriptorInfo;
}
diff --git a/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/CallHandlers/CallHandlerDescriptorInfo.cs b/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/CallHandlers/CallHandlerDescriptorInfo.cs
index fc31a28f01..022ff483fd 100644
--- a/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/CallHandlers/CallHandlerDescriptorInfo.cs
+++ b/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/CallHandlers/CallHandlerDescriptorInfo.cs
@@ -15,13 +15,15 @@ internal sealed class CallHandlerDescriptorInfo
MessageDescriptor? bodyDescriptor,
bool bodyDescriptorRepeated,
List<FieldDescriptor>? bodyFieldDescriptors,
- Dictionary<string, List<FieldDescriptor>> routeParameterDescriptors)
+ Dictionary<string, List<FieldDescriptor>> routeParameterDescriptors,
+ JsonTranscodingRouteAdapter routeAdapter)
{
ResponseBodyDescriptor = responseBodyDescriptor;
BodyDescriptor = bodyDescriptor;
BodyDescriptorRepeated = bodyDescriptorRepeated;
BodyFieldDescriptors = bodyFieldDescriptors;
RouteParameterDescriptors = routeParameterDescriptors;
+ RouteAdapter = routeAdapter;
if (BodyFieldDescriptors != null)
{
BodyFieldDescriptorsPath = string.Join('.', BodyFieldDescriptors.Select(d => d.Name));
@@ -35,6 +37,7 @@ internal sealed class CallHandlerDescriptorInfo
public bool BodyDescriptorRepeated { get; }
public List<FieldDescriptor>? BodyFieldDescriptors { get; }
public Dictionary<string, List<FieldDescriptor>> RouteParameterDescriptors { get; }
+ public JsonTranscodingRouteAdapter RouteAdapter { get; }
public ConcurrentDictionary<string, List<FieldDescriptor>?> PathDescriptorsCache { get; }
public string? BodyFieldDescriptorsPath { get; }
}
diff --git a/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/CallHandlers/ServerCallHandlerBase.cs b/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/CallHandlers/ServerCallHandlerBase.cs
index aeb030dd77..5cfe984ac1 100644
--- a/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/CallHandlers/ServerCallHandlerBase.cs
+++ b/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/CallHandlers/ServerCallHandlerBase.cs
@@ -36,6 +36,11 @@ internal abstract class ServerCallHandlerBase<TService, TRequest, TResponse>
public Task HandleCallAsync(HttpContext httpContext)
{
+ foreach (var rewriteAction in DescriptorInfo.RouteAdapter.RewriteVariableActions)
+ {
+ rewriteAction(httpContext);
+ }
+
var serverCallContext = new JsonTranscodingServerCallContext(httpContext, MethodInvoker.Options, MethodInvoker.Method, DescriptorInfo, Logger);
httpContext.Features.Set<IServerCallContextFeature>(serverCallContext);
diff --git a/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/JsonRequestHelpers.cs b/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/JsonRequestHelpers.cs
index 3507363c96..1dff5bb15d 100644
--- a/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/JsonRequestHelpers.cs
+++ b/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/JsonRequestHelpers.cs
@@ -338,7 +338,7 @@ internal static class JsonRequestHelpers
{
return serverCallContext.DescriptorInfo.PathDescriptorsCache.GetOrAdd(path, p =>
{
- ServiceDescriptorHelpers.TryResolveDescriptors(requestMessage.Descriptor, p, out var pathDescriptors);
+ ServiceDescriptorHelpers.TryResolveDescriptors(requestMessage.Descriptor, p.Split('.'), out var pathDescriptors);
return pathDescriptors;
});
}
diff --git a/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/JsonTranscodingRouteAdapter.cs b/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/JsonTranscodingRouteAdapter.cs
new file mode 100644
index 0000000000..af9d3b8dbd
--- /dev/null
+++ b/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/JsonTranscodingRouteAdapter.cs
@@ -0,0 +1,210 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Globalization;
+using System.Linq;
+using Grpc.Shared;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal;
+
+/// <summary>
+/// Routes on HTTP rule are similar to ASP.NET Core routes but add and remove some features.
+/// - Constraints aren't supported.
+/// - Optional parameters aren't supported.
+/// - Parameters spanning multiple segments are supported.
+///
+/// The purpose of this type is to add support for parameters spanning multiple segments and
+/// anonymous any or catch-all segments. This type transforms an HTTP route into an ASP.NET Core
+/// route by rewritting it to a compatible format and providing actions to reconstruct parameters
+/// that span multiple segments.
+///
+/// For example, consider a multi-segment parameter route:
+/// - Before: /v1/{book.name=shelves/*/books/*}
+/// - After: /v1/shelves/{__Complex_book.name_2}/books/{__Complex_book.name_4}
+///
+/// It is rewritten so that any * or ** segments become ASP.NET Core route parameters. These parameter
+/// names are never used by the user, and instead they're reconstructed into the final value by the
+/// adapter and then added to the HttpRequest.RouteValues collection.
+/// - Request URL: /v1/shelves/example-shelf/books/example-book
+/// - Route parameter: book.name = shelves/example-self/books/example-book
+/// </summary>
+internal sealed class JsonTranscodingRouteAdapter
+{
+ public HttpRoutePattern HttpRoutePattern { get; }
+ public string ResolvedRouteTemplate { get; }
+ public List<Action<HttpContext>> RewriteVariableActions { get; }
+
+ private JsonTranscodingRouteAdapter(HttpRoutePattern httpRoutePattern, string resolvedRoutePattern, List<Action<HttpContext>> rewriteVariableActions)
+ {
+ HttpRoutePattern = httpRoutePattern;
+ ResolvedRouteTemplate = resolvedRoutePattern;
+ RewriteVariableActions = rewriteVariableActions;
+ }
+
+ public static JsonTranscodingRouteAdapter Parse(HttpRoutePattern pattern)
+ {
+ var rewriteActions = new List<Action<HttpContext>>();
+
+ var tempSegments = pattern.Segments.ToList();
+ var i = 0;
+ while (i < tempSegments.Count)
+ {
+ var segmentVariable = GetVariable(pattern, i);
+ if (segmentVariable != null)
+ {
+ var fullPath = string.Join(".", segmentVariable.FieldPath);
+
+ var segmentCount = segmentVariable.EndSegment - segmentVariable.StartSegment;
+ if (segmentCount == 1)
+ {
+ // Single segment parameter. Include in route with its default name.
+ tempSegments[i] = segmentVariable.HasCatchAllPath
+ ? $"{{**{fullPath}}}"
+ : $"{{{fullPath}}}";
+ i++;
+ }
+ else
+ {
+ var routeParameterParts = new List<string>();
+ var routeValueFormatTemplateParts = new List<string>();
+ var variableParts = new List<string>();
+ var haveCatchAll = false;
+ var catchAllSuffix = string.Empty;
+
+ while (i < segmentVariable.EndSegment && !haveCatchAll)
+ {
+ var segment = tempSegments[i];
+ var segmentType = GetSegmentType(segment);
+ switch (segmentType)
+ {
+ case SegmentType.Literal:
+ routeValueFormatTemplateParts.Add(segment);
+ break;
+ case SegmentType.Any:
+ {
+ var parameterName = $"__Complex_{fullPath}_{i}";
+ tempSegments[i] = $"{{{parameterName}}}";
+
+ routeValueFormatTemplateParts.Add($"{{{variableParts.Count}}}");
+ variableParts.Add(parameterName);
+ break;
+ }
+ case SegmentType.CatchAll:
+ {
+ var parameterName = $"__Complex_{fullPath}_{i}";
+ var suffix = string.Join("/", tempSegments.Skip(i + 1));
+ catchAllSuffix = string.Join("/", tempSegments.Skip(i + segmentCount - 1));
+
+ // It's possible to have multiple routes with catch-all parameters that have different suffixes.
+ // For example:
+ // - /{name=v1/**/b}/one
+ // - /{name=v1/**/b}/two
+ // The suffix is added as a route constraint to avoid matching multiple routes to a request.
+ var constraint = suffix.Length > 0 ? $":regex({suffix}$)" : string.Empty;
+ tempSegments[i] = $"{{**{parameterName}{constraint}}}";
+
+ routeValueFormatTemplateParts.Add($"{{{variableParts.Count}}}");
+ variableParts.Add(parameterName);
+ haveCatchAll = true;
+
+ // Remove remaining segments. They have been added in the route constraint.
+ while (i < tempSegments.Count - 1)
+ {
+ tempSegments.RemoveAt(tempSegments.Count - 1);
+ }
+ break;
+ }
+ }
+ i++;
+ }
+
+ var routeValueFormatTemplate = string.Join("/", routeValueFormatTemplateParts);
+
+ // Add an action to reconstruct the multiple segment parameter from ASP.NET Core
+ // request route values. This should be called when the request is received.
+ rewriteActions.Add(context =>
+ {
+ var values = new object?[variableParts.Count];
+ for (var i = 0; i < values.Length; i++)
+ {
+ values[i] = context.Request.RouteValues[variableParts[i]];
+ }
+ var finalValue = string.Format(CultureInfo.InvariantCulture, routeValueFormatTemplate, values);
+
+ // Catch-all route parameter is always the last parameter. The original HTTP pattern could specify a
+ // literal suffix after the catch-all, e.g. /{param=**}/suffix. Because ASP.NET Core routing provides
+ // the entire remainder of the URL in the route value, we must trim the suffix from that route value.
+ if (!string.IsNullOrEmpty(catchAllSuffix))
+ {
+ finalValue = finalValue.Substring(0, finalValue.Length - catchAllSuffix.Length - 1);
+ }
+ context.Request.RouteValues[fullPath] = finalValue;
+ });
+ }
+ }
+ else
+ {
+ // HTTP route can match any value in a segment without a parameter.
+ // For example, v1/*/books. Add a parameter to match this behavior logic.
+ // Parameter value is never used.
+
+ var segmentType = GetSegmentType(tempSegments[i]);
+ switch (segmentType)
+ {
+ case SegmentType.Literal:
+ // Literal is unchanged.
+ break;
+ case SegmentType.Any:
+ // Ignore any segment value.
+ tempSegments[i] = $"{{__Discard_{i}}}";
+ break;
+ case SegmentType.CatchAll:
+ // Ignore remaining segment values.
+ tempSegments[i] = $"{{**__Discard_{i}}}";
+ break;
+ }
+
+ i++;
+ }
+ }
+
+ return new JsonTranscodingRouteAdapter(pattern, "/" + string.Join("/", tempSegments), rewriteActions);
+ }
+
+ private static SegmentType GetSegmentType(string segment)
+ {
+ if (segment.StartsWith("**", StringComparison.Ordinal))
+ {
+ return SegmentType.CatchAll;
+ }
+ else if (segment.StartsWith("*", StringComparison.Ordinal))
+ {
+ return SegmentType.Any;
+ }
+ else
+ {
+ return SegmentType.Literal;
+ }
+ }
+
+ private enum SegmentType
+ {
+ Literal,
+ Any,
+ CatchAll
+ }
+
+ private static HttpRouteVariable? GetVariable(HttpRoutePattern pattern, int i)
+ {
+ foreach (var variable in pattern.Variables)
+ {
+ if (i >= variable.StartSegment && i < variable.EndSegment)
+ {
+ return variable;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Microsoft.AspNetCore.Grpc.JsonTranscoding.csproj b/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Microsoft.AspNetCore.Grpc.JsonTranscoding.csproj
index 956777c1e3..260bb02b3b 100644
--- a/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Microsoft.AspNetCore.Grpc.JsonTranscoding.csproj
+++ b/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Microsoft.AspNetCore.Grpc.JsonTranscoding.csproj
@@ -23,6 +23,8 @@
<Compile Include="..\Shared\AuthContextHelpers.cs" Link="Internal\Shared\AuthContextHelpers.cs" />
<Compile Include="..\Shared\ServiceDescriptorHelpers.cs" Link="Internal\Shared\ServiceDescriptorHelpers.cs" />
<Compile Include="..\Shared\X509CertificateHelpers.cs" Link="Internal\Shared\X509CertificateHelpers.cs" />
+ <Compile Include="..\Shared\HttpRoutePattern.cs" Link="Internal\Shared\HttpRoutePattern.cs" />
+ <Compile Include="..\Shared\HttpRoutePatternParser.cs" Link="Internal\Shared\HttpRoutePatternParser.cs" />
<Compile Include="$(SharedSourceRoot)ValueTaskExtensions\**\*.cs" LinkBase="Internal\Shared" />
<Protobuf Include="Internal\Protos\errors.proto" Access="Internal" />
diff --git a/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.Swagger/Internal/GrpcJsonTranscodingDescriptionProvider.cs b/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.Swagger/Internal/GrpcJsonTranscodingDescriptionProvider.cs
index 1cc787de88..cff40e9d68 100644
--- a/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.Swagger/Internal/GrpcJsonTranscodingDescriptionProvider.cs
+++ b/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.Swagger/Internal/GrpcJsonTranscodingDescriptionProvider.cs
@@ -84,7 +84,8 @@ internal sealed class GrpcJsonTranscodingDescriptionProvider : IApiDescriptionPr
}
var methodMetadata = routeEndpoint.Metadata.GetMetadata<GrpcMethodMetadata>()!;
- var routeParameters = ServiceDescriptorHelpers.ResolveRouteParameterDescriptors(routeEndpoint.RoutePattern, methodDescriptor.InputType);
+ var httpRoutePattern = HttpRoutePattern.Parse(pattern);
+ var routeParameters = ServiceDescriptorHelpers.ResolveRouteParameterDescriptors(httpRoutePattern.Variables.Select(v => v.FieldPath).ToList(), methodDescriptor.InputType);
foreach (var routeParameter in routeParameters)
{
diff --git a/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.Swagger/Microsoft.AspNetCore.Grpc.Swagger.csproj b/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.Swagger/Microsoft.AspNetCore.Grpc.Swagger.csproj
index 4ad6d024b9..f102223658 100644
--- a/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.Swagger/Microsoft.AspNetCore.Grpc.Swagger.csproj
+++ b/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.Swagger/Microsoft.AspNetCore.Grpc.Swagger.csproj
@@ -11,6 +11,8 @@
<InternalsVisibleTo Include="Microsoft.AspNetCore.Grpc.Swagger.Tests" />
<Compile Include="..\Shared\ServiceDescriptorHelpers.cs" Link="Internal\Shared\ServiceDescriptorHelpers.cs" />
+ <Compile Include="..\Shared\HttpRoutePattern.cs" Link="Internal\Shared\HttpRoutePattern.cs" />
+ <Compile Include="..\Shared\HttpRoutePatternParser.cs" Link="Internal\Shared\HttpRoutePatternParser.cs" />
<Reference Include="Microsoft.AspNetCore.Grpc.JsonTranscoding" />
<Reference Include="Swashbuckle.AspNetCore" />
diff --git a/src/Grpc/JsonTranscoding/src/Shared/HttpRoutePattern.cs b/src/Grpc/JsonTranscoding/src/Shared/HttpRoutePattern.cs
new file mode 100644
index 0000000000..0110856608
--- /dev/null
+++ b/src/Grpc/JsonTranscoding/src/Shared/HttpRoutePattern.cs
@@ -0,0 +1,35 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Grpc.Shared;
+
+internal sealed class HttpRoutePattern
+{
+ public List<string> Segments { get; }
+ public string? Verb { get; }
+ public List<HttpRouteVariable> Variables { get; }
+
+ private HttpRoutePattern(List<string> segments, string? verb, List<HttpRouteVariable> variables)
+ {
+ Segments = segments;
+ Verb = verb;
+ Variables = variables;
+ }
+
+ public static HttpRoutePattern Parse(string pattern)
+ {
+ var p = new HttpRoutePatternParser(pattern);
+ p.Parse();
+
+ return new HttpRoutePattern(p.Segments, p.Verb, p.Variables);
+ }
+}
+
+internal sealed class HttpRouteVariable
+{
+ public int Index { get; set; }
+ public int StartSegment { get; set; }
+ public int EndSegment { get; set; }
+ public List<string> FieldPath { get; } = new List<string>();
+ public bool HasCatchAllPath { get; set; }
+}
diff --git a/src/Grpc/JsonTranscoding/src/Shared/HttpRoutePatternParser.cs b/src/Grpc/JsonTranscoding/src/Shared/HttpRoutePatternParser.cs
new file mode 100644
index 0000000000..d32e4d863e
--- /dev/null
+++ b/src/Grpc/JsonTranscoding/src/Shared/HttpRoutePatternParser.cs
@@ -0,0 +1,349 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+
+namespace Grpc.Shared;
+
+// HTTP Template Grammar:
+//
+// Template = "/" | "/" Segments [ Verb ] ;
+// Segments = Segment { "/" Segment } ;
+// Segment = "*" | "**" | LITERAL | Variable ;
+// Variable = "{" FieldPath [ "=" Segments ] "}" ;
+// FieldPath = IDENT { "." IDENT } ;
+// Verb = ":" LITERAL ;
+internal class HttpRoutePatternParser
+{
+ private readonly string _input;
+
+ // Token delimiter indexes
+ private int _tokenStart;
+ private int _tokenEnd;
+
+ private bool _inVariable;
+
+ private readonly List<string> _segments;
+ private string? _verb;
+ private readonly List<HttpRouteVariable> _variables;
+ private bool _hasCatchAllSegment;
+
+ public List<string> Segments => _segments;
+ public string? Verb => _verb;
+ public List<HttpRouteVariable> Variables => _variables;
+
+ public HttpRoutePatternParser(string input)
+ {
+ _input = input;
+ _segments = new List<string>();
+ _variables = new List<HttpRouteVariable>();
+ }
+
+ public void Parse()
+ {
+ try
+ {
+ ParseTemplate();
+
+ if (_tokenStart < _input.Length)
+ {
+ throw new InvalidOperationException("Path template wasn't parsed to the end.");
+ }
+ }
+ catch (Exception ex)
+ {
+ throw new InvalidOperationException($"Error parsing path template '{_input}'.", ex);
+ }
+ }
+
+ // Template = "/" Segments [ Verb ] ;
+ private void ParseTemplate()
+ {
+ if (!Consume('/'))
+ {
+ throw new InvalidOperationException("Path template must start with a '/'.");
+ }
+ ParseSegments();
+
+ if (EnsureCurrent())
+ {
+ if (CurrentChar != ':')
+ {
+ throw new InvalidOperationException("Path segment must end with a '/'.");
+ }
+ ParseVerb();
+ }
+ }
+
+ // Segments = Segment { "/" Segment } ;
+ private void ParseSegments()
+ {
+ while (true)
+ {
+ if (!ParseSegment())
+ {
+ // Support '/' template.
+ if (_segments.Count > 0)
+ {
+ throw new InvalidOperationException("Route template shouldn't end with a '/'.");
+ }
+ }
+ if (!Consume('/'))
+ {
+ break;
+ }
+ }
+ }
+
+ // Segment = "*" | "**" | LITERAL | Variable ;
+ private bool ParseSegment()
+ {
+ if (!EnsureCurrent())
+ {
+ return false;
+ }
+ switch (CurrentChar)
+ {
+ case '*':
+ {
+ if (_hasCatchAllSegment)
+ {
+ throw new InvalidOperationException("Only literal segments can follow a catch-all segment.");
+ }
+
+ ConsumeAndAssert('*');
+
+ // Check for '**'
+ if (Consume('*'))
+ {
+ _segments.Add("**");
+ _hasCatchAllSegment = true;
+ if (_inVariable)
+ {
+ CurrentVariable.HasCatchAllPath = true;
+ }
+ return true;
+ }
+ else
+ {
+ _segments.Add("*");
+ return true;
+ }
+ }
+
+ case '{':
+ if (_hasCatchAllSegment)
+ {
+ throw new InvalidOperationException("Only literal segments can follow a catch-all segment.");
+ }
+
+ ParseVariable();
+ return true;
+ default:
+ ParseLiteralSegment();
+ return true;
+ }
+ }
+
+ // Variable = "{" FieldPath [ "=" Segments ] "}" ;
+ private void ParseVariable()
+ {
+ ConsumeAndAssert('{');
+ StartVariable();
+ ParseFieldPath();
+ if (Consume('='))
+ {
+ ParseSegments();
+ }
+ else
+ {
+ _segments.Add("*");
+ }
+ EndVariable();
+ ConsumeAndAssert('}');
+ }
+
+ private void ParseLiteralSegment()
+ {
+ if (!TryParseLiteral(out var literal))
+ {
+ throw new InvalidOperationException("Empty literal segment.");
+ }
+ _segments.Add(literal);
+ }
+
+ // FieldPath = IDENT { "." IDENT } ;
+ private void ParseFieldPath()
+ {
+ do
+ {
+ if (!ParseIdentifier())
+ {
+ throw new InvalidOperationException("Incomplete or empty field path.");
+ }
+ }
+ while (Consume('.'));
+ }
+
+ // Verb = ":" LITERAL ;
+ private void ParseVerb()
+ {
+ ConsumeAndAssert(':');
+ if (!TryParseLiteral(out _verb))
+ {
+ throw new InvalidOperationException("Empty verb.");
+ }
+ }
+
+ private bool ParseIdentifier()
+ {
+ var identifier = string.Empty;
+ var hasEndChar = false;
+
+ while (!hasEndChar && NextChar())
+ {
+ var c = CurrentChar;
+ switch (c)
+ {
+ case '.':
+ case '}':
+ case '=':
+ hasEndChar = true;
+ break;
+ default:
+ Consume(c);
+ identifier += c;
+ break;
+ }
+ }
+
+ if (string.IsNullOrEmpty(identifier))
+ {
+ return false;
+ }
+
+ CurrentVariable.FieldPath.Add(identifier);
+ return true;
+ }
+
+ private bool TryParseLiteral([NotNullWhen(true)] out string? literal)
+ {
+ literal = null;
+
+ if (!EnsureCurrent())
+ {
+ return false;
+ }
+
+ // Initialize to false in case we encounter an empty literal.
+ var result = false;
+
+ while (true)
+ {
+ var c = CurrentChar;
+ switch (c)
+ {
+ case '/':
+ case ':':
+ case '}':
+ if (!result)
+ {
+ throw new InvalidOperationException("Path template has an empty segment.");
+ }
+ return result;
+ default:
+ Consume(c);
+ literal += c;
+ break;
+ }
+
+ result = true;
+
+ if (!NextChar())
+ {
+ break;
+ }
+ }
+
+ return result;
+ }
+
+ private void ConsumeAndAssert(char? c)
+ {
+ if (!Consume(c))
+ {
+ throw new InvalidOperationException($"Expected '{c}' when parsing path template.");
+ }
+ }
+
+ private bool Consume(char? c)
+ {
+ if (!EnsureCurrent())
+ {
+ return false;
+ }
+ if (CurrentChar != c)
+ {
+ return false;
+ }
+ _tokenStart++;
+ return true;
+ }
+
+ private bool EnsureCurrent() => _tokenStart < _tokenEnd || NextChar();
+
+ private bool NextChar()
+ {
+ if (_tokenEnd < _input.Length)
+ {
+ _tokenEnd++;
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ private char? CurrentChar => _tokenStart < _tokenEnd && _tokenEnd <= _input.Length ? _input[_tokenEnd - 1] : null;
+
+ private HttpRouteVariable CurrentVariable
+ {
+ get
+ {
+ if (!_inVariable || _variables.LastOrDefault() is not HttpRouteVariable variable)
+ {
+ throw new InvalidOperationException("Unexpected error when updating variable.");
+ }
+
+ return variable;
+ }
+
+ }
+
+ private void StartVariable()
+ {
+ if (_inVariable)
+ {
+ throw new InvalidOperationException("Variable can't be nested.");
+ }
+
+ _variables.Add(new HttpRouteVariable());
+ _inVariable = true;
+ CurrentVariable.StartSegment = _segments.Count;
+ CurrentVariable.HasCatchAllPath = false;
+ }
+
+ private void EndVariable()
+ {
+ CurrentVariable.EndSegment = _segments.Count;
+
+ Debug.Assert(CurrentVariable.FieldPath.Any());
+ Debug.Assert(CurrentVariable.StartSegment < CurrentVariable.EndSegment);
+ Debug.Assert(CurrentVariable.EndSegment <= _segments.Count);
+
+ _inVariable = false;
+ }
+}
diff --git a/src/Grpc/JsonTranscoding/src/Shared/ServiceDescriptorHelpers.cs b/src/Grpc/JsonTranscoding/src/Shared/ServiceDescriptorHelpers.cs
index f18b0a2aec..07b68b74d6 100644
--- a/src/Grpc/JsonTranscoding/src/Shared/ServiceDescriptorHelpers.cs
+++ b/src/Grpc/JsonTranscoding/src/Shared/ServiceDescriptorHelpers.cs
@@ -25,6 +25,7 @@ using Google.Api;
using Google.Protobuf;
using Google.Protobuf.Reflection;
using Google.Protobuf.WellKnownTypes;
+using Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal;
using Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.Json;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.Extensions.Primitives;
@@ -65,28 +66,13 @@ internal static class ServiceDescriptorHelpers
throw new InvalidOperationException($"Get not find Descriptor property on {serviceReflectionType.Name}.");
}
- public static bool TryResolveDescriptors(MessageDescriptor messageDescriptor, string variable, [NotNullWhen(true)]out List<FieldDescriptor>? fieldDescriptors)
+ public static bool TryResolveDescriptors(MessageDescriptor messageDescriptor, IList<string> path, [NotNullWhen(true)]out List<FieldDescriptor>? fieldDescriptors)
{
fieldDescriptors = null;
- var path = variable.AsSpan();
MessageDescriptor? currentDescriptor = messageDescriptor;
- while (path.Length > 0)
+ foreach (var fieldName in path)
{
- var separator = path.IndexOf('.');
-
- string fieldName;
- if (separator != -1)
- {
- fieldName = path.Slice(0, separator).ToString();
- path = path.Slice(separator + 1);
- }
- else
- {
- fieldName = path.ToString();
- path = ReadOnlySpan<char>.Empty;
- }
-
var field = currentDescriptor?.FindFieldByName(fieldName);
if (field == null)
{
@@ -108,7 +94,6 @@ internal static class ServiceDescriptorHelpers
{
currentDescriptor = null;
}
-
}
return fieldDescriptors != null;
@@ -303,17 +288,18 @@ internal static class ServiceDescriptorHelpers
}
}
- public static Dictionary<string, List<FieldDescriptor>> ResolveRouteParameterDescriptors(RoutePattern pattern, MessageDescriptor messageDescriptor)
+ public static Dictionary<string, List<FieldDescriptor>> ResolveRouteParameterDescriptors(List<List<string>> parameters, MessageDescriptor messageDescriptor)
{
var routeParameterDescriptors = new Dictionary<string, List<FieldDescriptor>>(StringComparer.Ordinal);
- foreach (var routeParameter in pattern.Parameters)
+ foreach (var routeParameter in parameters)
{
- if (!TryResolveDescriptors(messageDescriptor, routeParameter.Name, out var fieldDescriptors))
+ var completeFieldPath = string.Join(".", routeParameter);
+ if (!TryResolveDescriptors(messageDescriptor, routeParameter, out var fieldDescriptors))
{
- throw new InvalidOperationException($"Couldn't find matching field for route parameter '{routeParameter.Name}' on {messageDescriptor.Name}.");
+ throw new InvalidOperationException($"Couldn't find matching field for route parameter '{completeFieldPath}' on {messageDescriptor.Name}.");
}
- routeParameterDescriptors.Add(routeParameter.Name, fieldDescriptors);
+ routeParameterDescriptors.Add(completeFieldPath, fieldDescriptors);
}
return routeParameterDescriptors;
@@ -325,7 +311,8 @@ internal static class ServiceDescriptorHelpers
{
if (!string.Equals(body, "*", StringComparison.Ordinal))
{
- if (!TryResolveDescriptors(methodDescriptor.InputType, body, out var bodyFieldDescriptors))
+ var bodyFieldPath = body.Split('.');
+ if (!TryResolveDescriptors(methodDescriptor.InputType, bodyFieldPath, out var bodyFieldDescriptors))
{
throw new InvalidOperationException($"Couldn't find matching field for body '{body}' on {methodDescriptor.InputType.Name}.");
}
diff --git a/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.IntegrationTests/Infrastructure/TestHelpers.cs b/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.IntegrationTests/Infrastructure/TestHelpers.cs
index c2319e38bf..adc3c9e111 100644
--- a/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.IntegrationTests/Infrastructure/TestHelpers.cs
+++ b/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.IntegrationTests/Infrastructure/TestHelpers.cs
@@ -5,6 +5,8 @@ using System.Net;
using Google.Protobuf.Reflection;
using Grpc.AspNetCore.Server;
using Grpc.Core.Interceptors;
+using Grpc.Shared;
+using Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal;
using Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.CallHandlers;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
@@ -67,6 +69,7 @@ internal static class TestHelpers
bodyDescriptor,
bodyDescriptorRepeated ?? false,
bodyFieldDescriptors,
- routeParameterDescriptors ?? new Dictionary<string, List<FieldDescriptor>>());
+ routeParameterDescriptors ?? new Dictionary<string, List<FieldDescriptor>>(),
+ JsonTranscodingRouteAdapter.Parse(HttpRoutePattern.Parse("/")));
}
}
diff --git a/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.IntegrationTests/RouteTests.cs b/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.IntegrationTests/RouteTests.cs
new file mode 100644
index 0000000000..6ccaeffd65
--- /dev/null
+++ b/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.IntegrationTests/RouteTests.cs
@@ -0,0 +1,108 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Text;
+using System.Text.Json;
+using Grpc.Core;
+using IntegrationTestsWebsite;
+using Microsoft.AspNetCore.Grpc.JsonTranscoding.IntegrationTests.Infrastructure;
+using Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal;
+using Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests.Infrastructure;
+using Microsoft.AspNetCore.Testing;
+using Xunit.Abstractions;
+
+namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.IntegrationTests;
+
+public class RouteTests : IntegrationTestBase
+{
+ public RouteTests(GrpcTestFixture<Startup> fixture, ITestOutputHelper outputHelper)
+ : base(fixture, outputHelper)
+ {
+ }
+
+ [Fact]
+ public async Task ComplexParameter_MatchUrl_SuccessResult()
+ {
+ // Arrange
+ Task<HelloReply> UnaryMethod(HelloRequest request, ServerCallContext context)
+ {
+ return Task.FromResult(new HelloReply { Message = $"Hello {request.Name}!" });
+ }
+ var method = Fixture.DynamicGrpc.AddUnaryMethod<HelloRequest, HelloReply>(
+ UnaryMethod,
+ Greeter.Descriptor.FindMethodByName("SayHelloComplex"));
+
+ var client = new HttpClient(Fixture.Handler) { BaseAddress = new Uri("http://localhost") };
+
+ // Act
+ var response = await client.GetAsync("/v1/greeter/from/test").DefaultTimeout();
+ var responseStream = await response.Content.ReadAsStreamAsync();
+ using var result = await JsonDocument.ParseAsync(responseStream);
+
+ // Assert
+ Assert.Equal("Hello from/test!", result.RootElement.GetProperty("message").GetString());
+ }
+
+ [Fact]
+ public async Task MultipleComplexCatchAll_MatchUrl_SuccessResult()
+ {
+ // Arrange
+ Task<HelloReply> UnaryMethod1(HelloRequest request, ServerCallContext context)
+ {
+ return Task.FromResult(new HelloReply { Message = $"One - Hello {request.Name}!" });
+ }
+ Task<HelloReply> UnaryMethod2(HelloRequest request, ServerCallContext context)
+ {
+ return Task.FromResult(new HelloReply { Message = $"Two - Hello {request.Name}!" });
+ }
+ var method1 = Fixture.DynamicGrpc.AddUnaryMethod<HelloRequest, HelloReply>(
+ UnaryMethod1,
+ Greeter.Descriptor.FindMethodByName("SayHelloComplexCatchAll1"));
+ var method2 = Fixture.DynamicGrpc.AddUnaryMethod<HelloRequest, HelloReply>(
+ UnaryMethod2,
+ Greeter.Descriptor.FindMethodByName("SayHelloComplexCatchAll2"));
+
+ var client = new HttpClient(Fixture.Handler) { BaseAddress = new Uri("http://localhost") };
+
+ // Act 1
+ var response1 = await client.GetAsync("/v1/greeter/test1/b/c/d/one").DefaultTimeout();
+ var responseStream1 = await response1.Content.ReadAsStreamAsync();
+ using var result1 = await JsonDocument.ParseAsync(responseStream1);
+
+ // Assert 1
+ Assert.Equal("One - Hello v1/greeter/test1/b/c!", result1.RootElement.GetProperty("message").GetString());
+
+ // Act 2
+ var response2 = await client.GetAsync("/v1/greeter/test2/b/c/d/two").DefaultTimeout();
+ var responseStream2 = await response2.Content.ReadAsStreamAsync();
+ using var result2 = await JsonDocument.ParseAsync(responseStream2);
+
+ // Assert 2
+ Assert.Equal("Two - Hello v1/greeter/test2/b/c!", result2.RootElement.GetProperty("message").GetString());
+ }
+
+ [Fact]
+ public async Task ComplexCatchAllParameter_NestedField_MatchUrl_SuccessResult()
+ {
+ // Arrange
+ Task<HelloReply> UnaryMethod(ComplextHelloRequest request, ServerCallContext context)
+ {
+ return Task.FromResult(new HelloReply { Message = $"Hello {request.Name.FirstName} {request.Name.LastName}!" });
+ }
+ var method = Fixture.DynamicGrpc.AddUnaryMethod<ComplextHelloRequest, HelloReply>(
+ UnaryMethod,
+ Greeter.Descriptor.FindMethodByName("SayHelloComplexCatchAll3"));
+
+ var client = new HttpClient(Fixture.Handler) { BaseAddress = new Uri("http://localhost") };
+
+ // Act
+ var response = await client.GetAsync("/v1/last_name/complex_greeter/test2/b/c/d/two").DefaultTimeout();
+ var responseStream = await response.Content.ReadAsStreamAsync();
+ using var result = await JsonDocument.ParseAsync(responseStream);
+
+ // Assert
+ Assert.Equal("Hello complex_greeter/test2/b last_name!", result.RootElement.GetProperty("message").GetString());
+ }
+}
diff --git a/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/HttpRoutePatternParserTests.cs b/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/HttpRoutePatternParserTests.cs
new file mode 100644
index 0000000000..3fb4a80d1f
--- /dev/null
+++ b/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/HttpRoutePatternParserTests.cs
@@ -0,0 +1,325 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Grpc.Shared;
+using Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal;
+
+namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests;
+
+public class HttpRoutePatternParserTests
+{
+ [Fact]
+ public void ParseMultipleVariables()
+ {
+ var pattern = HttpRoutePattern.Parse("/shelves/{shelf}/books/{book}");
+ Assert.Null(pattern.Verb);
+ Assert.Collection(
+ pattern.Segments,
+ s => Assert.Equal("shelves", s),
+ s => Assert.Equal("*", s),
+ s => Assert.Equal("books", s),
+ s => Assert.Equal("*", s));
+ Assert.Collection(
+ pattern.Variables,
+ v =>
+ {
+ Assert.Equal(1, v.StartSegment);
+ Assert.Equal(2, v.EndSegment);
+ Assert.Equal("shelf", string.Join(".", v.FieldPath));
+ Assert.False(v.HasCatchAllPath);
+ },
+ v =>
+ {
+ Assert.Equal(3, v.StartSegment);
+ Assert.Equal(4, v.EndSegment);
+ Assert.Equal("book", string.Join(".", v.FieldPath));
+ Assert.False(v.HasCatchAllPath);
+ });
+ }
+
+ [Fact]
+ public void ParseComplexVariable()
+ {
+ var pattern = HttpRoutePattern.Parse("/v1/{book.name=shelves/*/books/*}");
+ Assert.Null(pattern.Verb);
+ Assert.Collection(
+ pattern.Segments,
+ s => Assert.Equal("v1", s),
+ s => Assert.Equal("shelves", s),
+ s => Assert.Equal("*", s),
+ s => Assert.Equal("books", s),
+ s => Assert.Equal("*", s));
+ Assert.Collection(
+ pattern.Variables,
+ v =>
+ {
+ Assert.Equal(1, v.StartSegment);
+ Assert.Equal(5, v.EndSegment);
+ Assert.Equal("book.name", string.Join(".", v.FieldPath));
+ Assert.False(v.HasCatchAllPath);
+ });
+ }
+
+ [Fact]
+ public void ParseCatchAllSegment()
+ {
+ var pattern = HttpRoutePattern.Parse("/shelves/**");
+ Assert.Collection(
+ pattern.Segments,
+ s => Assert.Equal("shelves", s),
+ s => Assert.Equal("**", s));
+ Assert.Empty(pattern.Variables);
+ }
+
+ [Fact]
+ public void ParseCatchAllSegment2()
+ {
+ var pattern = HttpRoutePattern.Parse("/**");
+ Assert.Collection(
+ pattern.Segments,
+ s => Assert.Equal("**", s));
+ Assert.Empty(pattern.Variables);
+ }
+
+ [Fact]
+ public void ParseAnySegment()
+ {
+ var pattern = HttpRoutePattern.Parse("/*");
+ Assert.Collection(
+ pattern.Segments,
+ s => Assert.Equal("*", s));
+ Assert.Empty(pattern.Variables);
+ }
+
+ [Fact]
+ public void ParseSlash()
+ {
+ var pattern = HttpRoutePattern.Parse("/");
+ Assert.Empty(pattern.Segments);
+ Assert.Empty(pattern.Variables);
+ }
+
+ [Fact]
+ public void ParseVerb()
+ {
+ var pattern = HttpRoutePattern.Parse("/a:foo");
+ Assert.Equal("foo", pattern.Verb);
+ Assert.Collection(
+ pattern.Segments,
+ s => Assert.Equal("a", s));
+ Assert.Empty(pattern.Variables);
+ }
+
+ [Fact]
+ public void ParseAnyAndCatchAllSegment()
+ {
+ var pattern = HttpRoutePattern.Parse("/*/**");
+ Assert.Collection(
+ pattern.Segments,
+ s => Assert.Equal("*", s),
+ s => Assert.Equal("**", s));
+ Assert.Empty(pattern.Variables);
+ }
+
+ [Fact]
+ public void ParseAnyAndCatchAllSegment2()
+ {
+ var pattern = HttpRoutePattern.Parse("/*/a/**");
+ Assert.Collection(
+ pattern.Segments,
+ s => Assert.Equal("*", s),
+ s => Assert.Equal("a", s),
+ s => Assert.Equal("**", s));
+ Assert.Empty(pattern.Variables);
+ }
+
+ [Fact]
+ public void ParseNestedFieldPath()
+ {
+ var pattern = HttpRoutePattern.Parse("/a/{a.b.c}");
+ Assert.Collection(
+ pattern.Segments,
+ s => Assert.Equal("a", s),
+ s => Assert.Equal("*", s));
+ Assert.Collection(
+ pattern.Variables,
+ v =>
+ {
+ Assert.Equal(1, v.StartSegment);
+ Assert.Equal(2, v.EndSegment);
+ Assert.Equal("a.b.c", string.Join(".", v.FieldPath));
+ Assert.False(v.HasCatchAllPath);
+ });
+ }
+
+ [Fact]
+ public void ParseComplexNestedFieldPath()
+ {
+ var pattern = HttpRoutePattern.Parse("/a/{a.b.c=*}");
+ Assert.Collection(
+ pattern.Segments,
+ s => Assert.Equal("a", s),
+ s => Assert.Equal("*", s));
+ Assert.Collection(
+ pattern.Variables,
+ v =>
+ {
+ Assert.Equal(1, v.StartSegment);
+ Assert.Equal(2, v.EndSegment);
+ Assert.Equal("a.b.c", string.Join(".", v.FieldPath));
+ Assert.False(v.HasCatchAllPath);
+ });
+ }
+
+ [Fact]
+ public void ParseComplexCatchAll()
+ {
+ var pattern = HttpRoutePattern.Parse("/a/{b=**}");
+ Assert.Collection(
+ pattern.Segments,
+ s => Assert.Equal("a", s),
+ s => Assert.Equal("**", s));
+ Assert.Collection(
+ pattern.Variables,
+ v =>
+ {
+ Assert.Equal(1, v.StartSegment);
+ Assert.Equal(2, v.EndSegment);
+ Assert.Equal("b", string.Join(".", v.FieldPath));
+ Assert.True(v.HasCatchAllPath);
+ });
+ }
+
+ [Fact]
+ public void ParseComplexPrefixSegment()
+ {
+ var pattern = HttpRoutePattern.Parse("/a/{b=c/*}");
+ Assert.Collection(
+ pattern.Segments,
+ s => Assert.Equal("a", s),
+ s => Assert.Equal("c", s),
+ s => Assert.Equal("*", s));
+ Assert.Collection(
+ pattern.Variables,
+ v =>
+ {
+ Assert.Equal(1, v.StartSegment);
+ Assert.Equal(3, v.EndSegment);
+ Assert.Equal("b", string.Join(".", v.FieldPath));
+ Assert.False(v.HasCatchAllPath);
+ });
+ }
+
+ [Fact]
+ public void ParseComplexPrefixSuffixSegment()
+ {
+ var pattern = HttpRoutePattern.Parse("/a/{b=c/*/d}");
+ Assert.Collection(
+ pattern.Segments,
+ s => Assert.Equal("a", s),
+ s => Assert.Equal("c", s),
+ s => Assert.Equal("*", s),
+ s => Assert.Equal("d", s));
+ Assert.Collection(
+ pattern.Variables,
+ v =>
+ {
+ Assert.Equal(1, v.StartSegment);
+ Assert.Equal(4, v.EndSegment);
+ Assert.Equal("b", string.Join(".", v.FieldPath));
+ Assert.False(v.HasCatchAllPath);
+ });
+ }
+
+ [Fact]
+ public void ParseComplexPathCatchAll()
+ {
+ var pattern = HttpRoutePattern.Parse("/a/{b=c/**}");
+ Assert.Collection(
+ pattern.Segments,
+ s => Assert.Equal("a", s),
+ s => Assert.Equal("c", s),
+ s => Assert.Equal("**", s));
+ Assert.Collection(
+ pattern.Variables,
+ v =>
+ {
+ Assert.Equal(1, v.StartSegment);
+ Assert.Equal(3, v.EndSegment);
+ Assert.Equal("b", string.Join(".", v.FieldPath));
+ Assert.True(v.HasCatchAllPath);
+ });
+ }
+
+ [Fact]
+ public void ParseComplexPrefixSuffixCatchAll()
+ {
+ var pattern = HttpRoutePattern.Parse("/{x.y.z=a/**/b}/c/d");
+ Assert.Collection(
+ pattern.Segments,
+ s => Assert.Equal("a", s),
+ s => Assert.Equal("**", s),
+ s => Assert.Equal("b", s),
+ s => Assert.Equal("c", s),
+ s => Assert.Equal("d", s));
+ Assert.Collection(
+ pattern.Variables,
+ v =>
+ {
+ Assert.Equal(0, v.StartSegment);
+ Assert.Equal(3, v.EndSegment);
+ Assert.Equal("x.y.z", string.Join(".", v.FieldPath));
+ Assert.True(v.HasCatchAllPath);
+ });
+ }
+
+ [Fact]
+ public void ParseCatchAllVerb()
+ {
+ var pattern = HttpRoutePattern.Parse("/a/{b=*}/**:verb");
+ Assert.Equal("verb", pattern.Verb);
+ Assert.Collection(
+ pattern.Segments,
+ s => Assert.Equal("a", s),
+ s => Assert.Equal("*", s),
+ s => Assert.Equal("**", s));
+ Assert.Collection(
+ pattern.Variables,
+ v =>
+ {
+ Assert.Equal(1, v.StartSegment);
+ Assert.Equal(2, v.EndSegment);
+ Assert.Equal("b", string.Join(".", v.FieldPath));
+ Assert.False(v.HasCatchAllPath);
+ });
+ }
+
+ [Theory]
+ [InlineData("", "Path template must start with a '/'.")]
+ [InlineData("//", "Path template has an empty segment.")]
+ [InlineData("/{}", "Incomplete or empty field path.")]
+ [InlineData("/a/", "Route template shouldn't end with a '/'.")]
+ [InlineData(":verb", "Path template must start with a '/'.")]
+ [InlineData(":", "Path template must start with a '/'.")]
+ [InlineData("/:", "Path template has an empty segment.")]
+ [InlineData("/{var}:", "Empty verb.")]
+ [InlineData("/{", "Incomplete or empty field path.")]
+ [InlineData("/a{x}", "Path segment must end with a '/'.")]
+ [InlineData("/{x}a", "Path segment must end with a '/'.")]
+ [InlineData("/{x}{y}", "Path segment must end with a '/'.")]
+ [InlineData("/{var=a/{nested=b}}", "Variable can't be nested.")]
+ [InlineData("/{x=**}/*", "Only literal segments can follow a catch-all segment.")]
+ [InlineData("/{x=}", "Path template has an empty segment.")]
+ [InlineData("/**/*", "Only literal segments can follow a catch-all segment.")]
+ [InlineData("/{x", "Expected '}' when parsing path template.")]
+ public void Error(string pattern, string errorMessage)
+ {
+ var ex = Assert.Throws<InvalidOperationException>(() => HttpRoutePattern.Parse(pattern));
+ Assert.Equal(errorMessage, ex.InnerException!.Message);
+ }
+}
diff --git a/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/Infrastructure/TestHelpers.cs b/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/Infrastructure/TestHelpers.cs
index 03a26331d6..85550a7a82 100644
--- a/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/Infrastructure/TestHelpers.cs
+++ b/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/Infrastructure/TestHelpers.cs
@@ -5,6 +5,8 @@ using System.Net;
using Google.Protobuf.Reflection;
using Grpc.AspNetCore.Server;
using Grpc.Core.Interceptors;
+using Grpc.Shared;
+using Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal;
using Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.CallHandlers;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
@@ -68,6 +70,7 @@ internal static class TestHelpers
bodyDescriptor,
bodyDescriptorRepeated ?? false,
bodyFieldDescriptors,
- routeParameterDescriptors ?? new Dictionary<string, List<FieldDescriptor>>());
+ routeParameterDescriptors ?? new Dictionary<string, List<FieldDescriptor>>(),
+ JsonTranscodingRouteAdapter.Parse(HttpRoutePattern.Parse("/")));
}
}
diff --git a/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/JsonTranscodingRouteAdapterTests.cs b/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/JsonTranscodingRouteAdapterTests.cs
new file mode 100644
index 0000000000..c27ffe47e6
--- /dev/null
+++ b/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/JsonTranscodingRouteAdapterTests.cs
@@ -0,0 +1,223 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Grpc.Shared;
+using Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.FileSystemGlobbing.Internal;
+
+namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests;
+
+public class JsonTranscodingRouteAdapterTests
+{
+ [Fact]
+ public void ParseMultipleVariables()
+ {
+ var pattern = HttpRoutePattern.Parse("/shelves/{shelf}/books/{book}");
+ var adapter = JsonTranscodingRouteAdapter.Parse(pattern);
+
+ Assert.Equal("/shelves/{shelf}/books/{book}", adapter.ResolvedRouteTemplate);
+ Assert.Empty(adapter.RewriteVariableActions);
+ }
+
+ [Fact]
+ public void ParseComplexVariable()
+ {
+ var route = HttpRoutePattern.Parse("/v1/{book.name=shelves/*/books/*}");
+ var adapter = JsonTranscodingRouteAdapter.Parse(route);
+
+ Assert.Equal("/v1/shelves/{__Complex_book.name_2}/books/{__Complex_book.name_4}", adapter.ResolvedRouteTemplate);
+ Assert.Single(adapter.RewriteVariableActions);
+
+ var httpContext = new DefaultHttpContext();
+ httpContext.Request.RouteValues = new RouteValueDictionary
+ {
+ { "__Complex_book.name_2", "first" },
+ { "__Complex_book.name_4", "second" }
+ };
+
+ adapter.RewriteVariableActions[0](httpContext);
+
+ Assert.Equal("shelves/first/books/second", httpContext.Request.RouteValues["book.name"]);
+ }
+
+ [Fact]
+ public void ParseCatchAllSegment()
+ {
+ var pattern = HttpRoutePattern.Parse("/shelves/**");
+ var adapter = JsonTranscodingRouteAdapter.Parse(pattern);
+
+ Assert.Equal("/shelves/{**__Discard_1}", adapter.ResolvedRouteTemplate);
+ Assert.Empty(adapter.RewriteVariableActions);
+ }
+
+ [Fact]
+ public void ParseAnySegment()
+ {
+ var pattern = HttpRoutePattern.Parse("/*")!;
+ var adapter = JsonTranscodingRouteAdapter.Parse(pattern);
+
+ Assert.Equal("/{__Discard_0}", adapter.ResolvedRouteTemplate);
+ Assert.Empty(adapter.RewriteVariableActions);
+ }
+
+ [Fact]
+ public void ParseVerb()
+ {
+ var pattern = HttpRoutePattern.Parse("/a:foo");
+ var adapter = JsonTranscodingRouteAdapter.Parse(pattern);
+
+ Assert.Equal("/a", adapter.ResolvedRouteTemplate);
+ Assert.Empty(adapter.RewriteVariableActions);
+ }
+
+ [Fact]
+ public void ParseAnyAndCatchAllSegment()
+ {
+ var pattern = HttpRoutePattern.Parse("/*/**");
+ var adapter = JsonTranscodingRouteAdapter.Parse(pattern);
+
+ Assert.Equal("/{__Discard_0}/{**__Discard_1}", adapter.ResolvedRouteTemplate);
+ Assert.Empty(adapter.RewriteVariableActions);
+ }
+
+ [Fact]
+ public void ParseAnyAndCatchAllSegment2()
+ {
+ var pattern = HttpRoutePattern.Parse("/*/a/**");
+ var adapter = JsonTranscodingRouteAdapter.Parse(pattern);
+
+ Assert.Equal("/{__Discard_0}/a/{**__Discard_2}", adapter.ResolvedRouteTemplate);
+ Assert.Empty(adapter.RewriteVariableActions);
+ }
+
+ [Fact]
+ public void ParseNestedFieldPath()
+ {
+ var pattern = HttpRoutePattern.Parse("/a/{a.b.c}");
+ var adapter = JsonTranscodingRouteAdapter.Parse(pattern);
+
+ Assert.Equal("/a/{a.b.c}", adapter.ResolvedRouteTemplate);
+ Assert.Empty(adapter.RewriteVariableActions);
+ }
+
+ [Fact]
+ public void ParseComplexNestedFieldPath()
+ {
+ var pattern = HttpRoutePattern.Parse("/a/{a.b.c=*}");
+ var adapter = JsonTranscodingRouteAdapter.Parse(pattern);
+
+ Assert.Equal("/a/{a.b.c}", adapter.ResolvedRouteTemplate);
+ Assert.Empty(adapter.RewriteVariableActions);
+ }
+
+ [Fact]
+ public void ParseComplexCatchAll()
+ {
+ var pattern = HttpRoutePattern.Parse("/a/{b=**}");
+ var adapter = JsonTranscodingRouteAdapter.Parse(pattern);
+
+ Assert.Equal("/a/{**b}", adapter.ResolvedRouteTemplate);
+ Assert.Empty(adapter.RewriteVariableActions);
+ }
+
+ [Fact]
+ public void ParseComplexPrefixSuffixCatchAll()
+ {
+ var pattern = HttpRoutePattern.Parse("/{x.y.z=a/**/b}/c/d");
+ var adapter = JsonTranscodingRouteAdapter.Parse(pattern);
+
+ Assert.Equal("/a/{**__Complex_x.y.z_1:regex(b/c/d$)}", adapter.ResolvedRouteTemplate);
+
+ var httpContext = new DefaultHttpContext();
+ httpContext.Request.RouteValues = new RouteValueDictionary
+ {
+ { "__Complex_x.y.z_1", "my/value/b/c/d" }
+ };
+
+ adapter.RewriteVariableActions[0](httpContext);
+
+ Assert.Equal("a/my/value/b", httpContext.Request.RouteValues["x.y.z"]);
+ }
+
+ [Fact]
+ public void ParseComplexPrefixSegment()
+ {
+ var pattern = HttpRoutePattern.Parse("/a/{b=c/*}");
+ var adapter = JsonTranscodingRouteAdapter.Parse(pattern);
+
+ Assert.Equal("/a/c/{__Complex_b_2}", adapter.ResolvedRouteTemplate);
+
+ var httpContext = new DefaultHttpContext();
+ httpContext.Request.RouteValues = new RouteValueDictionary
+ {
+ { "__Complex_b_2", "value" }
+ };
+
+ adapter.RewriteVariableActions[0](httpContext);
+
+ Assert.Equal("c/value", httpContext.Request.RouteValues["b"]);
+ }
+
+ [Fact]
+ public void ParseComplexPrefixSuffixSegment()
+ {
+ var pattern = HttpRoutePattern.Parse("/a/{b=c/*/d}");
+ var adapter = JsonTranscodingRouteAdapter.Parse(pattern);
+
+ Assert.Equal("/a/c/{__Complex_b_2}/d", adapter.ResolvedRouteTemplate);
+
+ var httpContext = new DefaultHttpContext();
+ httpContext.Request.RouteValues = new RouteValueDictionary
+ {
+ { "__Complex_b_2", "value" }
+ };
+
+ adapter.RewriteVariableActions[0](httpContext);
+
+ Assert.Equal("c/value/d", httpContext.Request.RouteValues["b"]);
+ }
+
+ [Fact]
+ public void ParseComplexPathCatchAll()
+ {
+ var pattern = HttpRoutePattern.Parse("/a/{b=c/**}");
+ var adapter = JsonTranscodingRouteAdapter.Parse(pattern);
+
+ Assert.Equal("/a/c/{**__Complex_b_2}", adapter.ResolvedRouteTemplate);
+
+ var httpContext = new DefaultHttpContext();
+ httpContext.Request.RouteValues = new RouteValueDictionary
+ {
+ { "__Complex_b_2", "value" }
+ };
+
+ adapter.RewriteVariableActions[0](httpContext);
+
+ Assert.Equal("c/value", httpContext.Request.RouteValues["b"]);
+ }
+
+ [Fact]
+ public void ParseManyVariables()
+ {
+ var pattern = HttpRoutePattern.Parse("/{a}/{b}/{c}");
+ var adapter = JsonTranscodingRouteAdapter.Parse(pattern);
+
+ Assert.Equal("/{a}/{b}/{c}", adapter.ResolvedRouteTemplate);
+ }
+
+ [Fact]
+ public void ParseCatchAllVerb()
+ {
+ var pattern = HttpRoutePattern.Parse("/a/{b=*}/**:verb");
+ var adapter = JsonTranscodingRouteAdapter.Parse(pattern);
+
+ Assert.Equal("/a/{b}/{**__Discard_2}", adapter.ResolvedRouteTemplate);
+ }
+}
diff --git a/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/JsonTranscodingServerCallContextTests.cs b/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/JsonTranscodingServerCallContextTests.cs
index 2e3d6def02..9365412e9d 100644
--- a/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/JsonTranscodingServerCallContextTests.cs
+++ b/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/JsonTranscodingServerCallContextTests.cs
@@ -5,6 +5,7 @@ using System.Net;
using Google.Protobuf.Reflection;
using Grpc.AspNetCore.Server;
using Grpc.Core;
+using Grpc.Shared;
using Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal;
using Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.CallHandlers;
using Microsoft.AspNetCore.Http;
@@ -99,7 +100,8 @@ public class JsonTranscodingServerCallContextTests
null,
false,
null,
- new Dictionary<string, List<FieldDescriptor>>()),
+ new Dictionary<string, List<FieldDescriptor>>(),
+ JsonTranscodingRouteAdapter.Parse(HttpRoutePattern.Parse("/")!)),
NullLogger.Instance);
}
}
diff --git a/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/JsonTranscodingServiceMethodProviderTests.cs b/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/JsonTranscodingServiceMethodProviderTests.cs
index 470db8b06d..4c1e3c65e9 100644
--- a/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/JsonTranscodingServiceMethodProviderTests.cs
+++ b/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/JsonTranscodingServiceMethodProviderTests.cs
@@ -173,7 +173,8 @@ public class JsonTranscodingServiceMethodProviderTests
// Assert
Assert.Equal("Error binding gRPC service 'JsonTranscodingInvalidPatternGreeterService'.", ex.Message);
Assert.Equal("Error binding BadPattern on JsonTranscodingInvalidPatternGreeterService to HTTP API.", ex.InnerException!.InnerException!.Message);
- Assert.Equal("Path template 'v1/greeter/{name}' must start with a '/'.", ex.InnerException!.InnerException!.InnerException!.Message);
+ Assert.Equal("Error parsing path template 'v1/greeter/{name}'.", ex.InnerException!.InnerException!.InnerException!.Message);
+ Assert.Equal("Path template must start with a '/'.", ex.InnerException!.InnerException!.InnerException!.InnerException!.Message);
}
private static RouteEndpoint FindGrpcEndpoint(IReadOnlyList<Endpoint> endpoints, string methodName)
diff --git a/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/UnaryServerCallHandlerTests.cs b/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/UnaryServerCallHandlerTests.cs
index 21855fb25f..722a9410eb 100644
--- a/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/UnaryServerCallHandlerTests.cs
+++ b/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/UnaryServerCallHandlerTests.cs
@@ -222,7 +222,7 @@ public class UnaryServerCallHandlerTests : LoggedTest
return Task.FromResult(new HelloReply { Message = $"Hello {r.Name}" });
};
- ServiceDescriptorHelpers.TryResolveDescriptors(HelloRequest.Descriptor, "sub", out var bodyFieldDescriptors);
+ ServiceDescriptorHelpers.TryResolveDescriptors(HelloRequest.Descriptor, new[] { "sub" }, out var bodyFieldDescriptors);
var descriptorInfo = TestHelpers.CreateDescriptorInfo(
bodyDescriptor: HelloRequest.Types.SubMessage.Descriptor,
@@ -264,7 +264,7 @@ public class UnaryServerCallHandlerTests : LoggedTest
return Task.FromResult(new HelloReply { Message = $"Hello {r.Name}" });
};
- ServiceDescriptorHelpers.TryResolveDescriptors(HelloRequest.Descriptor, "repeated_strings", out var bodyFieldDescriptors);
+ ServiceDescriptorHelpers.TryResolveDescriptors(HelloRequest.Descriptor, new[] { "repeated_strings" }, out var bodyFieldDescriptors);
var descriptorInfo = TestHelpers.CreateDescriptorInfo(
bodyDescriptor: HelloRequest.Types.SubMessage.Descriptor,
@@ -318,7 +318,7 @@ public class UnaryServerCallHandlerTests : LoggedTest
return Task.FromResult(new HelloReply { Message = $"Hello {r.Name}" });
};
- ServiceDescriptorHelpers.TryResolveDescriptors(HelloRequest.Descriptor, "sub.subfields", out var bodyFieldDescriptors);
+ ServiceDescriptorHelpers.TryResolveDescriptors(HelloRequest.Descriptor, new[] { "sub", "subfields" }, out var bodyFieldDescriptors);
var descriptorInfo = TestHelpers.CreateDescriptorInfo(
bodyDescriptor: HelloRequest.Types.SubMessage.Descriptor,
@@ -463,7 +463,7 @@ public class UnaryServerCallHandlerTests : LoggedTest
return Task.FromResult(new HelloReply());
};
- ServiceDescriptorHelpers.TryResolveDescriptors(HelloRequest.Descriptor, "repeated_strings", out var bodyFieldDescriptors);
+ ServiceDescriptorHelpers.TryResolveDescriptors(HelloRequest.Descriptor, new[] { "repeated_strings" }, out var bodyFieldDescriptors);
var descriptorInfo = TestHelpers.CreateDescriptorInfo(
bodyDescriptor: HelloRequest.Types.SubMessage.Descriptor,
@@ -503,7 +503,7 @@ public class UnaryServerCallHandlerTests : LoggedTest
invoker,
descriptorInfo: TestHelpers.CreateDescriptorInfo(bodyDescriptor: HelloRequest.Descriptor));
var httpContext = TestHelpers.CreateHttpContext();
- httpContext.Request.Body = new MemoryStream("{}"u8);
+ httpContext.Request.Body = new MemoryStream("{}"u8.ToArray());
httpContext.Request.ContentType = contentType;
// Act
await unaryServerCallHandler.HandleCallAsync(httpContext);
@@ -707,7 +707,7 @@ public class UnaryServerCallHandlerTests : LoggedTest
var httpContext = TestHelpers.CreateHttpContext();
httpContext.Request.ContentType = "application/json";
- httpContext.Request.Body = new MemoryStream("null"u8);
+ httpContext.Request.Body = new MemoryStream("null"u8.ToArray());
// Act
await unaryServerCallHandler.HandleCallAsync(httpContext);
@@ -733,7 +733,7 @@ public class UnaryServerCallHandlerTests : LoggedTest
return Task.FromResult(new HelloReply());
};
- Assert.True(ServiceDescriptorHelpers.TryResolveDescriptors(HelloRequest.Descriptor, "wrappers.float_value", out var bodyFieldDescriptors));
+ Assert.True(ServiceDescriptorHelpers.TryResolveDescriptors(HelloRequest.Descriptor, new[] { "wrappers", "float_value" }, out var bodyFieldDescriptors));
var descriptorInfo = TestHelpers.CreateDescriptorInfo(
bodyDescriptor: FloatValue.Descriptor,
@@ -795,7 +795,7 @@ public class UnaryServerCallHandlerTests : LoggedTest
return Task.FromResult(new HelloReply { Message = $"Hello {r.Name}" });
};
- ServiceDescriptorHelpers.TryResolveDescriptors(HttpBodySubField.Descriptor, "sub", out var bodyFieldDescriptors);
+ ServiceDescriptorHelpers.TryResolveDescriptors(HttpBodySubField.Descriptor, new[] { "sub" }, out var bodyFieldDescriptors);
var descriptorInfo = TestHelpers.CreateDescriptorInfo(
bodyDescriptor: HttpBody.Descriptor,
@@ -834,7 +834,7 @@ public class UnaryServerCallHandlerTests : LoggedTest
return Task.FromResult(new HelloReply { Message = $"Hello {r.Name}" });
};
- ServiceDescriptorHelpers.TryResolveDescriptors(NestedHttpBodySubField.Descriptor, "sub.sub", out var bodyFieldDescriptors);
+ ServiceDescriptorHelpers.TryResolveDescriptors(NestedHttpBodySubField.Descriptor, new[] { "sub", "sub" }, out var bodyFieldDescriptors);
var descriptorInfo = TestHelpers.CreateDescriptorInfo(
bodyDescriptor: HttpBody.Descriptor,
diff --git a/src/Grpc/JsonTranscoding/test/testassets/IntegrationTestsWebsite/Protos/greet.proto b/src/Grpc/JsonTranscoding/test/testassets/IntegrationTestsWebsite/Protos/greet.proto
index 97f8543145..3038d68f95 100644
--- a/src/Grpc/JsonTranscoding/test/testassets/IntegrationTestsWebsite/Protos/greet.proto
+++ b/src/Grpc/JsonTranscoding/test/testassets/IntegrationTestsWebsite/Protos/greet.proto
@@ -20,6 +20,26 @@ service Greeter {
body: "*"
};
}
+ rpc SayHelloComplex (HelloRequest) returns (HelloReply) {
+ option (google.api.http) = {
+ get: "/v1/greeter/{name=from/*}"
+ };
+ }
+ rpc SayHelloComplexCatchAll1 (HelloRequest) returns (HelloReply) {
+ option (google.api.http) = {
+ get: "/{name=v1/greeter/**/b}/c/d/one"
+ };
+ }
+ rpc SayHelloComplexCatchAll2 (HelloRequest) returns (HelloReply) {
+ option (google.api.http) = {
+ get: "/{name=v1/greeter/**/b}/c/d/two"
+ };
+ }
+ rpc SayHelloComplexCatchAll3 (ComplextHelloRequest) returns (HelloReply) {
+ option (google.api.http) = {
+ get: "/v1/{name.last_name}/{name.first_name=complex_greeter/**/b}/c/d/two"
+ };
+ }
}
// The request message containing the user's name.
@@ -27,6 +47,14 @@ message HelloRequest {
string name = 1;
}
+message ComplextHelloRequest {
+ NameDetail name = 1;
+ message NameDetail {
+ string first_name = 1;
+ string last_name = 2;
+ }
+}
+
// The response message containing the greetings.
message HelloReply {
string message = 1;