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 @@ -178,9 +178,43 @@ private static string ResolvePath(HttpRoutePattern httpRoutePattern, Dictionary<
var routeParameter = routeParameters.SingleOrDefault(kvp => kvp.Value.RouteVariable.StartSegment == i).Value;
if (routeParameter != null)
{
sb.Append('{');
sb.Append(routeParameter.JsonPath);
sb.Append('}');
// For variables that span multiple segments (e.g., {name=widgets/*}),
// include the literal segments to make unique OpenAPI paths.
var startSegment = routeParameter.RouteVariable.StartSegment;
var endSegment = routeParameter.RouteVariable.EndSegment;

if (endSegment - startSegment > 1)
{
// Multi-segment variable - include literal segments for uniqueness
for (var j = startSegment; j < endSegment; j++)
{
if (j > startSegment)
{
sb.Append('/');
}

var segment = httpRoutePattern.Segments[j];
if (segment == "*" || segment == "**")
{
// Replace wildcard with parameter
sb.Append('{');
sb.Append(routeParameter.JsonPath);
sb.Append('}');
}
else
{
// Include literal segment as-is
sb.Append(segment);
}
}
}
else
{
// Single-segment variable
sb.Append('{');
sb.Append(routeParameter.JsonPath);
sb.Append('}');
}

// Skip segments if variable is multiple segment.
i = routeParameter.RouteVariable.EndSegment - 1;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Grpc.Swagger.Tests.Infrastructure;
using Microsoft.AspNetCore.Grpc.Swagger.Tests.Services;
using Microsoft.OpenApi.Models;
using Xunit.Abstractions;

namespace Microsoft.AspNetCore.Grpc.Swagger.Tests.Binding;

public class ResourcePathTests
{
private readonly ITestOutputHelper _testOutputHelper;

public ResourcePathTests(ITestOutputHelper testOutputHelper)
{
_testOutputHelper = testOutputHelper;
}

[Fact]
public void ConflictingPaths_DifferentLiteralSegments_GenerateUniquePaths()
{
// Arrange & Act
var swagger = OpenApiTestHelpers.GetOpenApiDocument<ResourceService>(_testOutputHelper);

// Assert - Should have 3 distinct paths
Assert.Equal(3, swagger.Paths.Count);

// Path 1: /v1/widgets/{name}
Assert.True(swagger.Paths.ContainsKey("/v1/widgets/{name}"));
var widgetPath = swagger.Paths["/v1/widgets/{name}"];
Assert.True(widgetPath.Operations.TryGetValue(OperationType.Get, out var widgetOperation));
Assert.Single(widgetOperation.Parameters);
Assert.Equal(ParameterLocation.Path, widgetOperation.Parameters[0].In);
Assert.Equal("name", widgetOperation.Parameters[0].Name);

// Path 2: /v1/things/{name}
Assert.True(swagger.Paths.ContainsKey("/v1/things/{name}"));
var thingPath = swagger.Paths["/v1/things/{name}"];
Assert.True(thingPath.Operations.TryGetValue(OperationType.Get, out var thingOperation));
Assert.Single(thingOperation.Parameters);
Assert.Equal(ParameterLocation.Path, thingOperation.Parameters[0].In);
Assert.Equal("name", thingOperation.Parameters[0].Name);

// Path 3: /v1/gadgets/{name}/items/{name} (nested path)
Assert.True(swagger.Paths.ContainsKey("/v1/gadgets/{name}/items/{name}"));
var gadgetPath = swagger.Paths["/v1/gadgets/{name}/items/{name}"];
Assert.True(gadgetPath.Operations.TryGetValue(OperationType.Get, out var gadgetOperation));
Assert.Single(gadgetOperation.Parameters);
Assert.Equal(ParameterLocation.Path, gadgetOperation.Parameters[0].In);
Assert.Equal("name", gadgetOperation.Parameters[0].Name);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<Protobuf Include="Proto\xmldoc.proto" GrpcServices="Both" />
<Protobuf Include="Proto\greeter.proto" GrpcServices="Both" />
<Protobuf Include="Proto\messages.proto" GrpcServices="Both" />
<Protobuf Include="Proto\resources.proto" GrpcServices="Both" />

<Reference Include="Microsoft.AspNetCore.Grpc.Swagger" />
<Reference Include="Grpc.Tools" PrivateAssets="All" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

syntax = "proto3";

import "google/api/annotations.proto";

package resources;

// Service for testing Google AIP-131 style resource patterns
service ResourceService {
// Get a widget by name
rpc GetWidget (WidgetRequest) returns (WidgetResponse) {
option (google.api.http) = {
get: "/v1/{name=widgets/*}"
};
}

// Get a thing by name
rpc GetThing (ThingRequest) returns (ThingResponse) {
option (google.api.http) = {
get: "/v1/{name=things/*}"
};
}

// Get a gadget with nested path
rpc GetGadget (GadgetRequest) returns (GadgetResponse) {
option (google.api.http) = {
get: "/v1/{name=gadgets/*/items/*}"
};
}
}

message WidgetRequest {
string name = 1;
}

message WidgetResponse {
string id = 1;
string name = 2;
}

message ThingRequest {
string name = 1;
}

message ThingResponse {
string id = 1;
string name = 2;
}

message GadgetRequest {
string name = 1;
}

message GadgetResponse {
string id = 1;
string name = 2;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.Grpc.Swagger.Tests.Services;

public class ResourceService : Resources.ResourceServiceBase
{
}
Loading