1- using Microsoft . OpenApi . Models ;
1+ using Microsoft . OpenApi ;
22using Swashbuckle . AspNetCore . SwaggerGen ;
33
44namespace Workleap . Extensions . OpenAPI . Ordering ;
@@ -8,31 +8,62 @@ namespace Workleap.Extensions.OpenAPI.Ordering;
88/// </summary>
99internal sealed class OrderResponseFilter : IDocumentFilter
1010{
11+ // In Microsoft.OpenApi v2, operation keys are no longer the OperationType enum (where we could cast to int),
12+ // they are objects with a string Method property. This dictionary preserves explicit ordering.
13+ private static readonly Dictionary < string , int > HttpMethodOrder = new ( StringComparer . OrdinalIgnoreCase )
14+ {
15+ { "GET" , 0 } ,
16+ { "POST" , 1 } ,
17+ { "PUT" , 2 } ,
18+ { "PATCH" , 3 } ,
19+ { "DELETE" , 4 } ,
20+ { "OPTIONS" , 5 } ,
21+ { "HEAD" , 6 } ,
22+ { "TRACE" , 7 }
23+ } ;
24+
1125 public void Apply ( OpenApiDocument document , DocumentFilterContext context )
1226 {
27+ if ( document . Paths == null )
28+ {
29+ return ;
30+ }
31+
1332 var paths = document . Paths . ToList ( ) ;
1433 document . Paths . Clear ( ) ;
1534 document . Paths = new OpenApiPaths ( ) ;
1635 foreach ( var path in paths )
1736 {
1837 document . Paths . Add ( path . Key , path . Value ) ;
1938
20- var sortedOperations = path . Value . Operations . OrderBy ( op => ( int ) op . Key ) . ToList ( ) ;
21- path . Value . Operations . Clear ( ) ;
39+ if ( path . Value is not OpenApiPathItem pathItem || pathItem . Operations == null )
40+ {
41+ continue ;
42+ }
43+
44+ var sortedOperations = pathItem . Operations
45+ . OrderBy ( op => HttpMethodOrder . TryGetValue ( op . Key . Method , out var order ) ? order : 99 )
46+ . ToList ( ) ;
47+ pathItem . Operations . Clear ( ) ;
2248 foreach ( var operation in sortedOperations )
2349 {
24- path . Value . Operations . Add ( operation . Key , operation . Value ) ;
50+ pathItem . Operations . Add ( operation . Key , operation . Value ) ;
51+
52+ if ( operation . Value is not OpenApiOperation openApiOperation || openApiOperation . Responses == null )
53+ {
54+ continue ;
55+ }
2556
2657 // Sort responses by status code (200, 400, 403, 404, 500, etc.)
2758 // This is critical because responses from both controller-level ProducesResponseType
2859 // and method-level attributes are added in the order they're processed, not by status code.
2960 // Without sorting, a 403 from a controller-level attribute might appear before a 200
3061 // from the method-level TypedResult, causing unpredictable ordering and noisy diffs.
31- var sortedResponse = operation . Value . Responses . OrderBy ( responseKvp => responseKvp . Key , StringComparer . Ordinal ) . ToList ( ) ;
32- operation . Value . Responses . Clear ( ) ;
62+ var sortedResponse = openApiOperation . Responses . OrderBy ( responseKvp => responseKvp . Key , StringComparer . Ordinal ) . ToList ( ) ;
63+ openApiOperation . Responses . Clear ( ) ;
3364 foreach ( var response in sortedResponse )
3465 {
35- operation . Value . Responses . Add ( response . Key , response . Value ) ;
66+ openApiOperation . Responses . Add ( response . Key , response . Value ) ;
3667 }
3768 }
3869 }
0 commit comments