Skip to content

Commit

Permalink
Fix verb in route template with gRPC transcoding (dotnet#47123)
Browse files Browse the repository at this point in the history
  • Loading branch information
JamesNK authored Mar 11, 2023
1 parent 53e77ee commit ccb861b
Show file tree
Hide file tree
Showing 7 changed files with 358 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using Grpc.Shared;
using Microsoft.AspNetCore.Http;

Expand Down Expand Up @@ -55,6 +56,8 @@ public static JsonTranscodingRouteAdapter Parse(HttpRoutePattern pattern)
var rewriteActions = new List<Action<HttpContext>>();

var tempSegments = pattern.Segments.ToList();
var haveCatchAll = false;

var i = 0;
while (i < tempSegments.Count)
{
Expand All @@ -63,8 +66,16 @@ public static JsonTranscodingRouteAdapter Parse(HttpRoutePattern pattern)
{
var fullPath = string.Join(".", segmentVariable.FieldPath);

var segmentCount = segmentVariable.EndSegment - segmentVariable.StartSegment;
if (segmentCount == 1)
var remainingSegmentCount = segmentVariable.EndSegment - segmentVariable.StartSegment;

// Handle situation where the last segment is catch all but there is a verb.
if (remainingSegmentCount == 1 && segmentVariable.HasCatchAllPath && pattern.Verb != null)
{
// Move past the catch all so the regex added below just includes the verb.
remainingSegmentCount++;
}

if (remainingSegmentCount == 1)
{
// Single segment parameter. Include in route with its default name.
tempSegments[i] = segmentVariable.HasCatchAllPath
Expand All @@ -77,7 +88,6 @@ public static JsonTranscodingRouteAdapter Parse(HttpRoutePattern pattern)
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)
Expand All @@ -101,15 +111,15 @@ public static JsonTranscodingRouteAdapter Parse(HttpRoutePattern pattern)
case SegmentType.CatchAll:
{
var parameterName = $"__Complex_{fullPath}_{i}";
var suffix = string.Join("/", tempSegments.Skip(i + 1));
catchAllSuffix = string.Join("/", tempSegments.Skip(i + segmentCount - 1));
var suffix = BuildSuffix(tempSegments.Skip(i + 1), pattern.Verb);
catchAllSuffix = BuildSuffix(tempSegments.Skip(i + remainingSegmentCount - 1), pattern.Verb);

// 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;
var constraint = suffix.Length > 0 ? $":regex({Regex.Escape(suffix)}$)" : string.Empty;
tempSegments[i] = $"{{**{parameterName}{constraint}}}";

routeValueFormatTemplateParts.Add($"{{{variableParts.Count}}}");
Expand Down Expand Up @@ -145,7 +155,7 @@ public static JsonTranscodingRouteAdapter Parse(HttpRoutePattern pattern)
// 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);
finalValue = finalValue[..^catchAllSuffix.Length];
}
context.Request.RouteValues[fullPath] = finalValue;
});
Expand All @@ -169,15 +179,43 @@ public static JsonTranscodingRouteAdapter Parse(HttpRoutePattern pattern)
break;
case SegmentType.CatchAll:
// Ignore remaining segment values.
tempSegments[i] = $"{{**__Discard_{i}}}";
if (pattern.Verb != null)
{
tempSegments[i] = $"{{**__Discard_{i}:regex({Regex.Escape($":{pattern.Verb}")}$)}}";
}
else
{
tempSegments[i] = $"{{**__Discard_{i}}}";
}
haveCatchAll = true;
break;
}

i++;
}
}

return new JsonTranscodingRouteAdapter(pattern, "/" + string.Join("/", tempSegments), rewriteActions);
string resolvedRoutePattern = "/" + string.Join("/", tempSegments);
// If the route has a catch all then the verb is included in the catch all regex constraint.
if (pattern.Verb != null && !haveCatchAll)
{
resolvedRoutePattern += ":" + pattern.Verb;
}
return new JsonTranscodingRouteAdapter(pattern, resolvedRoutePattern, rewriteActions);

static string BuildSuffix(IEnumerable<string> segments, string? verb)
{
var pattern = string.Join("/", segments);
if (!string.IsNullOrEmpty(pattern))
{
pattern = "/" + pattern;
}
if (verb != null)
{
pattern += ":" + verb;
}
return pattern;
}
}

private static SegmentType GetSegmentType(string segment)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
// 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;
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;

Expand Down Expand Up @@ -105,4 +104,163 @@ Task<HelloReply> UnaryMethod(ComplextHelloRequest request, ServerCallContext con
// Assert
Assert.Equal("Hello complex_greeter/test2/b last_name!", result.RootElement.GetProperty("message").GetString());
}

[Fact]
public async Task SimpleCatchAllParameter_PrefixSuffixSlashes_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("SayHelloComplexCatchAll4"));

var client = new HttpClient(Fixture.Handler) { BaseAddress = new Uri("http://localhost") };

// Act
var response = await client.GetAsync("/v1/greeter//name/one/two//").DefaultTimeout();
var responseStream = await response.Content.ReadAsStreamAsync();
using var result = await JsonDocument.ParseAsync(responseStream);

// Assert
Assert.Equal("Hello /name/one/two//!", result.RootElement.GetProperty("message").GetString());
}

[Fact]
public async Task ParameterVerb_MatchUrl_SuccessResult()
{
// Arrange
Task<HelloReply> UnaryMethod1(HelloRequest request, ServerCallContext context)
{
return Task.FromResult(new HelloReply { Message = $"Hello {request.Name} one!" });
}
Task<HelloReply> UnaryMethod2(HelloRequest request, ServerCallContext context)
{
return Task.FromResult(new HelloReply { Message = $"Hello {request.Name} two!" });
}
var method1 = Fixture.DynamicGrpc.AddUnaryMethod<HelloRequest, HelloReply>(
UnaryMethod1,
Greeter.Descriptor.FindMethodByName("SayHelloCustomVerbOne"));
var method2 = Fixture.DynamicGrpc.AddUnaryMethod<HelloRequest, HelloReply>(
UnaryMethod2,
Greeter.Descriptor.FindMethodByName("SayHelloCustomVerbTwo"));

var client = new HttpClient(Fixture.Handler) { BaseAddress = new Uri("http://localhost") };

// Act 1
var response1 = await client.GetAsync("/v1/greeter_custom/test:one").DefaultTimeout();
var responseStream1 = await response1.Content.ReadAsStreamAsync();
using var result1 = await JsonDocument.ParseAsync(responseStream1);

// Assert 2
Assert.Equal("Hello test one!", result1.RootElement.GetProperty("message").GetString());

// Act 2
var response2 = await client.GetAsync("/v1/greeter_custom/test:two").DefaultTimeout();
var responseStream2 = await response2.Content.ReadAsStreamAsync();
using var result2 = await JsonDocument.ParseAsync(responseStream2);

// Assert 2
Assert.Equal("Hello test two!", result2.RootElement.GetProperty("message").GetString());

// Act 3
var response3 = await client.GetAsync("/v1/greeter_custom/test").DefaultTimeout();

// Assert 3
Assert.Equal(HttpStatusCode.NotFound, response3.StatusCode);
}

[Fact]
public async Task CatchAllVerb_MatchUrl_SuccessResult()
{
// Arrange
Task<HelloReply> UnaryMethod1(HelloRequest request, ServerCallContext context)
{
return Task.FromResult(new HelloReply { Message = $"Hello {request.Name} one!" });
}
Task<HelloReply> UnaryMethod2(HelloRequest request, ServerCallContext context)
{
return Task.FromResult(new HelloReply { Message = $"Hello {request.Name} two!" });
}
var method1 = Fixture.DynamicGrpc.AddUnaryMethod<HelloRequest, HelloReply>(
UnaryMethod1,
Greeter.Descriptor.FindMethodByName("SayHelloCatchAllCustomVerbOne"));
var method2 = Fixture.DynamicGrpc.AddUnaryMethod<HelloRequest, HelloReply>(
UnaryMethod2,
Greeter.Descriptor.FindMethodByName("SayHelloCatchAllCustomVerbTwo"));

var client = new HttpClient(Fixture.Handler) { BaseAddress = new Uri("http://localhost") };

// Act 1
var response1 = await client.GetAsync("/v1/greeter_customcatchall/test/name:one").DefaultTimeout();
var responseStream1 = await response1.Content.ReadAsStreamAsync();
using var result1 = await JsonDocument.ParseAsync(responseStream1);

// Assert 2
Assert.Equal("Hello test/name one!", result1.RootElement.GetProperty("message").GetString());

// Act 2
var response2 = await client.GetAsync("/v1/greeter_customcatchall/test/name:two").DefaultTimeout();
var responseStream2 = await response2.Content.ReadAsStreamAsync();
using var result2 = await JsonDocument.ParseAsync(responseStream2);

// Assert 2
Assert.Equal("Hello test/name two!", result2.RootElement.GetProperty("message").GetString());

// Act 3
var response3 = await client.GetAsync("/v1/greeter_customcatchall/test/name").DefaultTimeout();

// Assert 3
Assert.Equal(HttpStatusCode.NotFound, response3.StatusCode);
}

[Fact]
public async Task PostVerb_MatchUrl_SuccessResult()
{
// Arrange
Task<HelloReply> UnaryMethod1(HelloRequest request, ServerCallContext context)
{
return Task.FromResult(new HelloReply { Message = $"Hello {request.Name} one!" });
}
Task<HelloReply> UnaryMethod2(HelloRequest request, ServerCallContext context)
{
return Task.FromResult(new HelloReply { Message = $"Hello {request.Name} two!" });
}
var method1 = Fixture.DynamicGrpc.AddUnaryMethod<HelloRequest, HelloReply>(
UnaryMethod1,
Greeter.Descriptor.FindMethodByName("SayHelloPostCustomVerbOne"));
var method2 = Fixture.DynamicGrpc.AddUnaryMethod<HelloRequest, HelloReply>(
UnaryMethod2,
Greeter.Descriptor.FindMethodByName("SayHelloPostCustomVerbTwo"));

var client = new HttpClient(Fixture.Handler) { BaseAddress = new Uri("http://localhost") };

var requestMessage = new HelloRequest { Name = "test" };
var content = new ByteArrayContent(Encoding.UTF8.GetBytes(requestMessage.ToString()));
content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");

// Act 1
var response1 = await client.PostAsync("/v1/greeter_custompost:one", content).DefaultTimeout();
var responseStream1 = await response1.Content.ReadAsStreamAsync();
using var result1 = await JsonDocument.ParseAsync(responseStream1);

// Assert 2
Assert.Equal("Hello test one!", result1.RootElement.GetProperty("message").GetString());

// Act 2
var response2 = await client.PostAsync("/v1/greeter_custompost:two", content).DefaultTimeout();
var responseStream2 = await response2.Content.ReadAsStreamAsync();
using var result2 = await JsonDocument.ParseAsync(responseStream2);

// Assert 2
Assert.Equal("Hello test two!", result2.RootElement.GetProperty("message").GetString());

// Act 3
var response3 = await client.PostAsync("/v1/greeter_custompost", content).DefaultTimeout();

// Assert 3
Assert.Equal(HttpStatusCode.NotFound, response3.StatusCode);
}
}
Loading

0 comments on commit ccb861b

Please sign in to comment.