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
27 changes: 27 additions & 0 deletions src/Microsoft.OpenApi/Models/OpenApiDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -857,6 +857,33 @@ static bool AddToDictionary<TValue>(IDictionary<string, TValue> dict, string key
// Register only if it was actually added to the collection
return added && (Workspace?.RegisterComponentForDocument(this, componentToRegister, id) ?? false);
}

/// <summary>
/// Finds an operation in the document by its operation ID.
/// </summary>
/// <param name="operationId">The operation ID to search for.</param>
/// <returns>The matching <see cref="OpenApiOperation"/>, or <see langword="null"/> if not found.</returns>
public OpenApiOperation? GetOperationById(string operationId)
{
Utils.CheckArgumentNullOrEmpty(operationId);

var allPathItems = Webhooks is not null
? Paths.Values.Concat(Webhooks.Values)
: Paths.Values;

foreach (var pathItem in allPathItems)
{
if (pathItem.Operations is not null)
{
foreach (var operation in pathItem.Operations.Values)
{
if (string.Equals(operation.OperationId, operationId, StringComparison.Ordinal))
return operation;
}
}
}
return null;
}
}

internal class FindSchemaReferences : OpenApiVisitorBase
Expand Down
1 change: 1 addition & 0 deletions src/Microsoft.OpenApi/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
#nullable enable
Microsoft.OpenApi.OpenApiDocument.GetOperationById(string! operationId) -> Microsoft.OpenApi.OpenApiOperation?
193 changes: 193 additions & 0 deletions test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2504,5 +2504,198 @@ public async Task SerializeDocumentWithSelfPropertyAsV30WritesAsExtension()
// Assert
Assert.Equal(expected.MakeLineBreaksEnvironmentNeutral(), actual.MakeLineBreaksEnvironmentNeutral());
}

[Fact]
public void GetOperationById_ReturnsMatchingOperation()
{
var operation = new OpenApiOperation { OperationId = "getUser" };
var doc = new OpenApiDocument
{
Info = new OpenApiInfo { Title = "Test", Version = "1.0" },
Paths = new OpenApiPaths
{
["/users/{id}"] = new OpenApiPathItem
{
Operations = new Dictionary<HttpMethod, OpenApiOperation>
{
[HttpMethod.Get] = operation
}
}
}
};

var result = doc.GetOperationById("getUser");

Assert.Same(operation, result);
}

[Fact]
public void GetOperationById_ReturnsNullWhenNotFound()
{
var doc = new OpenApiDocument
{
Info = new OpenApiInfo { Title = "Test", Version = "1.0" },
Paths = new OpenApiPaths
{
["/users"] = new OpenApiPathItem
{
Operations = new Dictionary<HttpMethod, OpenApiOperation>
{
[HttpMethod.Get] = new OpenApiOperation { OperationId = "listUsers" }
}
}
}
};

var result = doc.GetOperationById("nonExistentId");

Assert.Null(result);
}

[Fact]
public void GetOperationById_SearchesWebhooks()
{
var webhookOperation = new OpenApiOperation { OperationId = "onUserCreated" };
var doc = new OpenApiDocument
{
Info = new OpenApiInfo { Title = "Test", Version = "1.0" },
Paths = [],
Webhooks = new Dictionary<string, IOpenApiPathItem>
{
["userCreated"] = new OpenApiPathItem
{
Operations = new Dictionary<HttpMethod, OpenApiOperation>
{
[HttpMethod.Post] = webhookOperation
}
}
}
};

var result = doc.GetOperationById("onUserCreated");

Assert.Same(webhookOperation, result);
}

[Fact]
public void GetOperationById_IsCaseSensitive()
{
var doc = new OpenApiDocument
{
Info = new OpenApiInfo { Title = "Test", Version = "1.0" },
Paths = new OpenApiPaths
{
["/users"] = new OpenApiPathItem
{
Operations = new Dictionary<HttpMethod, OpenApiOperation>
{
[HttpMethod.Get] = new OpenApiOperation { OperationId = "getUser" }
}
}
}
};

Assert.NotNull(doc.GetOperationById("getUser"));
Assert.Null(doc.GetOperationById("GetUser"));
Assert.Null(doc.GetOperationById("GETUSER"));
}

[Fact]
public void GetOperationById_ResolvesOperationThroughPathItemReference()
{
const string yaml = """
openapi: '3.1.0'
info:
title: Test
version: 1.0.0
paths:
/users:
$ref: '#/components/pathItems/userPathItem'
components:
pathItems:
userPathItem:
get:
operationId: listUsers
responses:
'200':
description: OK
""";

var doc = OpenApiDocument.Parse(yaml, OpenApiConstants.Yaml, SettingsFixture.ReaderSettings).Document;
doc.Workspace.RegisterComponents(doc);

var result = doc.GetOperationById("listUsers");

Assert.NotNull(result);
Assert.Equal("listUsers", result.OperationId);
}

[Fact]
public void GetOperationById_DuplicateIdReturnsFirstMatch()
{
// operationId must be unique per spec, but if not, Paths takes priority over Webhooks
var pathsOperation = new OpenApiOperation { OperationId = "duplicateId" };
var webhooksOperation = new OpenApiOperation { OperationId = "duplicateId" };
var doc = new OpenApiDocument
{
Info = new OpenApiInfo { Title = "Test", Version = "1.0" },
Paths = new OpenApiPaths
{
["/users"] = new OpenApiPathItem
{
Operations = new Dictionary<HttpMethod, OpenApiOperation>
{
[HttpMethod.Get] = pathsOperation
}
}
},
Webhooks = new Dictionary<string, IOpenApiPathItem>
{
["userEvent"] = new OpenApiPathItem
{
Operations = new Dictionary<HttpMethod, OpenApiOperation>
{
[HttpMethod.Post] = webhooksOperation
}
}
}
};

var result = doc.GetOperationById("duplicateId");

Assert.Same(pathsOperation, result);
}

[Fact]
public void GetOperationById_UnresolvedPathItemReferenceIsSkipped()
{
// An unresolved $ref has Target = null, so Operations = null — should be skipped gracefully
var unresolvedRef = new OpenApiPathItemReference("nonExistentPathItem", null);
var doc = new OpenApiDocument
{
Info = new OpenApiInfo { Title = "Test", Version = "1.0" },
Paths = new OpenApiPaths
{
["/users"] = unresolvedRef
}
};

var result = doc.GetOperationById("anyId");

Assert.Null(result);
}

[Fact]
public void GetOperationById_ThrowsOnNullOrEmptyId()
{
var doc = new OpenApiDocument
{
Info = new OpenApiInfo { Title = "Test", Version = "1.0" },
Paths = []
};

Assert.Throws<ArgumentNullException>(() => doc.GetOperationById(null!));
Assert.Throws<ArgumentNullException>(() => doc.GetOperationById(string.Empty));
}
}
}