Skip to content

Commit 1bf6028

Browse files
Mahdiglnbaywet
authored andcommitted
feat(document): add GetOperationById to search operations across path… (#3025)
* feat(document): add GetOperationById to search operations across paths and webhooks * refactor(document): replace Concat with dedicated GetAllOperations iterator * refactor(document): extract GetOperationsFromPathItems to eliminate duplicate iteration * refactor(document): simplify GetOperationById using single-pass helper Signed-off-by: Vincent Biret <vibiret@microsoft.com>
1 parent ce1ac2d commit 1bf6028

3 files changed

Lines changed: 222 additions & 0 deletions

File tree

src/Microsoft.OpenApi/Models/OpenApiDocument.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -815,6 +815,34 @@ static bool AddToDictionary<TValue>(IDictionary<string, TValue> dict, string key
815815
// Register only if it was actually added to the collection
816816
return added && (Workspace?.RegisterComponentForDocument(this, componentToRegister, id) ?? false);
817817
}
818+
819+
/// <summary>
820+
/// Finds an operation in the document by its operation ID.
821+
/// </summary>
822+
/// <param name="operationId">The operation ID to search for.</param>
823+
/// <returns>The matching <see cref="OpenApiOperation"/>, or <see langword="null"/> if not found.</returns>
824+
public OpenApiOperation? GetOperationById(string operationId)
825+
{
826+
Utils.CheckArgumentNullOrEmpty(operationId);
827+
828+
return GetOperationByIdFromPathItems(Paths, operationId) ??
829+
(Webhooks is not null ?
830+
GetOperationByIdFromPathItems(Webhooks, operationId) : null);
831+
}
832+
833+
private static OpenApiOperation? GetOperationByIdFromPathItems(IDictionary<string, IOpenApiPathItem> pathItems, string operationId)
834+
{
835+
foreach (var pathItem in pathItems.Values)
836+
{
837+
if (pathItem.Operations is null) continue;
838+
foreach (var operation in pathItem.Operations.Values)
839+
{
840+
if (string.Equals(operation.OperationId, operationId, StringComparison.Ordinal))
841+
return operation;
842+
}
843+
}
844+
return null;
845+
}
818846
}
819847

820848
internal class FindSchemaReferences : OpenApiVisitorBase
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
#nullable enable
2+
Microsoft.OpenApi.OpenApiDocument.GetOperationById(string! operationId) -> Microsoft.OpenApi.OpenApiOperation?

test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2419,5 +2419,198 @@ public async Task SerializeDocumentWithSecurityRequirementAsJsonWorks(OpenApiSpe
24192419
Assert.NotNull(actualSecurity);
24202420
Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), actualSecurity));
24212421
}
2422+
2423+
[Fact]
2424+
public void GetOperationById_ReturnsMatchingOperation()
2425+
{
2426+
var operation = new OpenApiOperation { OperationId = "getUser" };
2427+
var doc = new OpenApiDocument
2428+
{
2429+
Info = new OpenApiInfo { Title = "Test", Version = "1.0" },
2430+
Paths = new OpenApiPaths
2431+
{
2432+
["/users/{id}"] = new OpenApiPathItem
2433+
{
2434+
Operations = new Dictionary<HttpMethod, OpenApiOperation>
2435+
{
2436+
[HttpMethod.Get] = operation
2437+
}
2438+
}
2439+
}
2440+
};
2441+
2442+
var result = doc.GetOperationById("getUser");
2443+
2444+
Assert.Same(operation, result);
2445+
}
2446+
2447+
[Fact]
2448+
public void GetOperationById_ReturnsNullWhenNotFound()
2449+
{
2450+
var doc = new OpenApiDocument
2451+
{
2452+
Info = new OpenApiInfo { Title = "Test", Version = "1.0" },
2453+
Paths = new OpenApiPaths
2454+
{
2455+
["/users"] = new OpenApiPathItem
2456+
{
2457+
Operations = new Dictionary<HttpMethod, OpenApiOperation>
2458+
{
2459+
[HttpMethod.Get] = new OpenApiOperation { OperationId = "listUsers" }
2460+
}
2461+
}
2462+
}
2463+
};
2464+
2465+
var result = doc.GetOperationById("nonExistentId");
2466+
2467+
Assert.Null(result);
2468+
}
2469+
2470+
[Fact]
2471+
public void GetOperationById_SearchesWebhooks()
2472+
{
2473+
var webhookOperation = new OpenApiOperation { OperationId = "onUserCreated" };
2474+
var doc = new OpenApiDocument
2475+
{
2476+
Info = new OpenApiInfo { Title = "Test", Version = "1.0" },
2477+
Paths = [],
2478+
Webhooks = new Dictionary<string, IOpenApiPathItem>
2479+
{
2480+
["userCreated"] = new OpenApiPathItem
2481+
{
2482+
Operations = new Dictionary<HttpMethod, OpenApiOperation>
2483+
{
2484+
[HttpMethod.Post] = webhookOperation
2485+
}
2486+
}
2487+
}
2488+
};
2489+
2490+
var result = doc.GetOperationById("onUserCreated");
2491+
2492+
Assert.Same(webhookOperation, result);
2493+
}
2494+
2495+
[Fact]
2496+
public void GetOperationById_IsCaseSensitive()
2497+
{
2498+
var doc = new OpenApiDocument
2499+
{
2500+
Info = new OpenApiInfo { Title = "Test", Version = "1.0" },
2501+
Paths = new OpenApiPaths
2502+
{
2503+
["/users"] = new OpenApiPathItem
2504+
{
2505+
Operations = new Dictionary<HttpMethod, OpenApiOperation>
2506+
{
2507+
[HttpMethod.Get] = new OpenApiOperation { OperationId = "getUser" }
2508+
}
2509+
}
2510+
}
2511+
};
2512+
2513+
Assert.NotNull(doc.GetOperationById("getUser"));
2514+
Assert.Null(doc.GetOperationById("GetUser"));
2515+
Assert.Null(doc.GetOperationById("GETUSER"));
2516+
}
2517+
2518+
[Fact]
2519+
public void GetOperationById_ResolvesOperationThroughPathItemReference()
2520+
{
2521+
const string yaml = """
2522+
openapi: '3.1.0'
2523+
info:
2524+
title: Test
2525+
version: 1.0.0
2526+
paths:
2527+
/users:
2528+
$ref: '#/components/pathItems/userPathItem'
2529+
components:
2530+
pathItems:
2531+
userPathItem:
2532+
get:
2533+
operationId: listUsers
2534+
responses:
2535+
'200':
2536+
description: OK
2537+
""";
2538+
2539+
var doc = OpenApiDocument.Parse(yaml, OpenApiConstants.Yaml, SettingsFixture.ReaderSettings).Document;
2540+
doc.Workspace.RegisterComponents(doc);
2541+
2542+
var result = doc.GetOperationById("listUsers");
2543+
2544+
Assert.NotNull(result);
2545+
Assert.Equal("listUsers", result.OperationId);
2546+
}
2547+
2548+
[Fact]
2549+
public void GetOperationById_DuplicateIdReturnsFirstMatch()
2550+
{
2551+
// operationId must be unique per spec, but if not, Paths takes priority over Webhooks
2552+
var pathsOperation = new OpenApiOperation { OperationId = "duplicateId" };
2553+
var webhooksOperation = new OpenApiOperation { OperationId = "duplicateId" };
2554+
var doc = new OpenApiDocument
2555+
{
2556+
Info = new OpenApiInfo { Title = "Test", Version = "1.0" },
2557+
Paths = new OpenApiPaths
2558+
{
2559+
["/users"] = new OpenApiPathItem
2560+
{
2561+
Operations = new Dictionary<HttpMethod, OpenApiOperation>
2562+
{
2563+
[HttpMethod.Get] = pathsOperation
2564+
}
2565+
}
2566+
},
2567+
Webhooks = new Dictionary<string, IOpenApiPathItem>
2568+
{
2569+
["userEvent"] = new OpenApiPathItem
2570+
{
2571+
Operations = new Dictionary<HttpMethod, OpenApiOperation>
2572+
{
2573+
[HttpMethod.Post] = webhooksOperation
2574+
}
2575+
}
2576+
}
2577+
};
2578+
2579+
var result = doc.GetOperationById("duplicateId");
2580+
2581+
Assert.Same(pathsOperation, result);
2582+
}
2583+
2584+
[Fact]
2585+
public void GetOperationById_UnresolvedPathItemReferenceIsSkipped()
2586+
{
2587+
// An unresolved $ref has Target = null, so Operations = null — should be skipped gracefully
2588+
var unresolvedRef = new OpenApiPathItemReference("nonExistentPathItem", null);
2589+
var doc = new OpenApiDocument
2590+
{
2591+
Info = new OpenApiInfo { Title = "Test", Version = "1.0" },
2592+
Paths = new OpenApiPaths
2593+
{
2594+
["/users"] = unresolvedRef
2595+
}
2596+
};
2597+
2598+
var result = doc.GetOperationById("anyId");
2599+
2600+
Assert.Null(result);
2601+
}
2602+
2603+
[Fact]
2604+
public void GetOperationById_ThrowsOnNullOrEmptyId()
2605+
{
2606+
var doc = new OpenApiDocument
2607+
{
2608+
Info = new OpenApiInfo { Title = "Test", Version = "1.0" },
2609+
Paths = []
2610+
};
2611+
2612+
Assert.Throws<ArgumentNullException>(() => doc.GetOperationById(null!));
2613+
Assert.Throws<ArgumentNullException>(() => doc.GetOperationById(string.Empty));
2614+
}
24222615
}
24232616
}

0 commit comments

Comments
 (0)