Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[IDP-1271] Fallback on method name for operationId #3

Merged
Merged
Show file tree
Hide file tree
Changes from 14 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
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,7 @@ jobs:
shell: pwsh
env:
NUGET_SOURCE: ${{ secrets.NUGET_GSOFTDEV_FEED_URL }}
NUGET_API_KEY: ${{ secrets.GSOFT_NUGET_API_KEY }}
NUGET_API_KEY: ${{ secrets.GSOFT_NUGET_API_KEY }}

- run: src/tests/RunSystemTest.ps1
heqianwang marked this conversation as resolved.
Show resolved Hide resolved
shell: pwsh
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,22 @@

## Getting started

TODO
This library provides helpers and guidelines for API endpoints annotations during OpenAPI spec generation. As such, we provide the following features:

- Fallback to use controller name as OperationId when there is no OperationId explicitly defined for the endpoint.

### How to use it
heqianwang marked this conversation as resolved.
Show resolved Hide resolved

Install the package Workleap.Extensions.OpenAPI in your .NET API project. Then you may use the following method to register the required service. Here is a code snippet on how to register this and to enable the operationId fallback feature in your application.

```
public void ConfigureServices(IServiceCollection services)
{
// [...]
services.AddOpenApi().FallbackOnMethodNameForOperationId();
// [...]
}
```

## Building, releasing and versioning

Expand Down
7 changes: 7 additions & 0 deletions src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Workleap.DotNet.CodingStandards" Version="0.3.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
heqianwang marked this conversation as resolved.
Show resolved Hide resolved
<PrivateAssets>all</PrivateAssets>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Workleap.Extensions.OpenAPI.OperationId;

namespace Workleap.Extensions.OpenAPI.Tests.OperationId;

public class FallbackOperationIdToMethodNameFilterTests
{
[Theory]
[InlineData("GetData", "GetData")]
[InlineData("GetDataAsync", "GetData")]
[InlineData("GetDataasync", "GetData")]
heqianwang marked this conversation as resolved.
Show resolved Hide resolved
public void Given_Method_Name_When_Cleanup_Then_Clean_Name(string methodName, string expectedOutput)
{
// When
var result = FallbackOperationIdToMethodNameFilter.CleanupName(methodName);

// Then
Assert.Equal(expectedOutput, result);
}
}
10 changes: 0 additions & 10 deletions src/Workleap.Extensions.OpenAPI.Tests/UnitTest1.cs

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
heqianwang marked this conversation as resolved.
Show resolved Hide resolved
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<SignAssembly>true</SignAssembly>
Expand All @@ -13,10 +13,6 @@

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="Workleap.DotNet.CodingStandards" Version="0.3.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="xunit" Version="2.7.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.8">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
17 changes: 16 additions & 1 deletion src/Workleap.Extensions.OpenAPI.sln
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,18 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "files", "files", "{3B3625CC-919B-4216-9B50-BCFE297AA184}"
ProjectSection(SolutionItems) = preProject
Directory.Build.props = Directory.Build.props
.editorconfig = .editorconfig
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Workleap.Extensions.OpenAPI.Tests", "Workleap.Extensions.OpenAPI.Tests\Workleap.Extensions.OpenAPI.Tests.csproj", "{2A5429CC-E179-47E7-85BB-C1E33E2AFD8A}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{1868781E-E97B-447A-AEB7-507739F5FA88}"
ProjectSection(SolutionItems) = preProject
tests\RunSystemTest.ps1 = tests\RunSystemTest.ps1
tests\expected-openapi-document.yaml = tests\expected-openapi-document.yaml
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApi.OpenAPI.SystemTest", "tests\WebApi.OpenAPI.SystemTest\WebApi.OpenAPI.SystemTest.csproj", "{F7C54BE5-D538-4D2B-9B07-C77567CEB82A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -30,5 +37,13 @@ Global
{2A5429CC-E179-47E7-85BB-C1E33E2AFD8A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2A5429CC-E179-47E7-85BB-C1E33E2AFD8A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2A5429CC-E179-47E7-85BB-C1E33E2AFD8A}.Release|Any CPU.Build.0 = Release|Any CPU
{F7C54BE5-D538-4D2B-9B07-C77567CEB82A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F7C54BE5-D538-4D2B-9B07-C77567CEB82A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F7C54BE5-D538-4D2B-9B07-C77567CEB82A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F7C54BE5-D538-4D2B-9B07-C77567CEB82A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{F7C54BE5-D538-4D2B-9B07-C77567CEB82A} = {1868781E-E97B-447A-AEB7-507739F5FA88}
{2A5429CC-E179-47E7-85BB-C1E33E2AFD8A} = {1868781E-E97B-447A-AEB7-507739F5FA88}
EndGlobalSection
EndGlobal
40 changes: 40 additions & 0 deletions src/Workleap.Extensions.OpenAPI/Builder/OpenApiBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Swashbuckle.AspNetCore.SwaggerUI;
using Workleap.Extensions.OpenAPI.OperationId;

namespace Workleap.Extensions.OpenAPI.Builder;

/// <summary>
/// Provides methods to configure Swagger/OpenAPI opinionated settings for the application.
/// </summary>
public class OpenApiBuilder
heqianwang marked this conversation as resolved.
Show resolved Hide resolved
{
private readonly IServiceCollection _services;

internal OpenApiBuilder(IServiceCollection services)
{
this._services = services;

this._services.AddSingleton<IConfigureOptions<SwaggerUIOptions>, DisplayOperationIdInSwaggerUiOptions>();
}

/// <summary>
/// Configures the Swagger generator to fallback on the method name as the operation ID if no explicit operation ID is specified.
/// </summary>
/// <remarks>
/// This method adds a custom operation filter to the Swagger generator.
/// </remarks>
/// <returns>
/// The same <see cref="OpenApiBuilder"/> instance so that multiple configuration calls can be chained.
/// </returns>
public OpenApiBuilder FallbackOnMethodNameForOperationId()
heqianwang marked this conversation as resolved.
Show resolved Hide resolved
{
this._services.ConfigureSwaggerGen(options =>
{
options.OperationFilter<FallbackOperationIdToMethodNameFilter>();
});

return this;
}
}
5 changes: 0 additions & 5 deletions src/Workleap.Extensions.OpenAPI/Class1.cs

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Microsoft.Extensions.DependencyInjection;
using Workleap.Extensions.OpenAPI.Builder;

namespace Workleap.Extensions.OpenAPI;

/// <summary>
/// Provides extension methods to the <see cref="IServiceCollection"/> for configuring OpenAPI/Swagger services.
/// </summary>
public static class OpenApiServiceCollectionExtensions
{
/// <summary>
/// Configures OpenAPI/Swagger document generation and SwaggerUI.
/// </summary>
public static OpenApiBuilder AddOpenApi(this IServiceCollection services)
PrincessMadMath marked this conversation as resolved.
Show resolved Hide resolved
heqianwang marked this conversation as resolved.
Show resolved Hide resolved
{
ArgumentNullException.ThrowIfNull(services);

return new OpenApiBuilder(services);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Options;
using Swashbuckle.AspNetCore.SwaggerUI;

namespace Workleap.Extensions.OpenAPI.OperationId;

internal class DisplayOperationIdInSwaggerUiOptions : IConfigureOptions<SwaggerUIOptions>
PrincessMadMath marked this conversation as resolved.
Show resolved Hide resolved
{
public void Configure(SwaggerUIOptions options)
{
options.DisplayOperationId();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;

namespace Workleap.Extensions.OpenAPI.OperationId;

internal class FallbackOperationIdToMethodNameFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
if (!string.IsNullOrEmpty(operation.OperationId))
{
return;
}

// Method name for Minimal API is not the best choice for OperationId so we want to force explicit declaration
PrincessMadMath marked this conversation as resolved.
Show resolved Hide resolved
if (IsMinimalApi(context))
{
return;
}

operation.OperationId = CleanupName(context.MethodInfo.Name);
}

private static bool IsMinimalApi(OperationFilterContext context)
{
return !typeof(ControllerBase).IsAssignableFrom(context.MethodInfo.DeclaringType);
}

internal static string CleanupName(string methodName)
PrincessMadMath marked this conversation as resolved.
Show resolved Hide resolved
heqianwang marked this conversation as resolved.
Show resolved Hide resolved
{
return methodName.Replace("Async", string.Empty, StringComparison.InvariantCultureIgnoreCase);
heqianwang marked this conversation as resolved.
Show resolved Hide resolved
}
}
6 changes: 5 additions & 1 deletion src/Workleap.Extensions.OpenAPI/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
#nullable enable
#nullable enable
static Workleap.Extensions.OpenAPI.OpenApiServiceCollectionExtensions.AddOpenApi(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Workleap.Extensions.OpenAPI.Builder.OpenApiBuilder!
Workleap.Extensions.OpenAPI.Builder.OpenApiBuilder
Workleap.Extensions.OpenAPI.Builder.OpenApiBuilder.FallbackOnMethodNameForOperationId() -> Workleap.Extensions.OpenAPI.Builder.OpenApiBuilder!
Workleap.Extensions.OpenAPI.OpenApiServiceCollectionExtensions
Original file line number Diff line number Diff line change
@@ -1,25 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
<IsPackable>true</IsPackable>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
heqianwang marked this conversation as resolved.
Show resolved Hide resolved
<PackageReadmeFile>README.md</PackageReadmeFile>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>../Workleap.Extensions.OpenAPI.snk</AssemblyOriginatorKeyFile>
<OpenApiGenerateDocumentsOnBuild>false</OpenApiGenerateDocumentsOnBuild>
heqianwang marked this conversation as resolved.
Show resolved Hide resolved
</PropertyGroup>

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Workleap.DotNet.CodingStandards" Version="0.3.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup>

<ItemGroup>
<None Include="..\..\README.md" Link="README.md" Pack="true" PackagePath="\" />
</ItemGroup>
Expand Down
31 changes: 31 additions & 0 deletions src/tests/RunSystemTest.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Stop the script when a cmdlet or a native command fails
$ErrorActionPreference = 'Stop'
$PSNativeCommandUseErrorActionPreference = $true

# Define the paths
$projectPath = Join-Path $PSScriptRoot "WebApi.OpenAPI.SystemTest"
$generatedFilePath = Join-Path $projectPath "openapi-v1.yaml"
$expectedFilePath = Join-Path $PSScriptRoot "expected-openapi-document.yaml"

# Compile the project
dotnet build $projectPath -c Release

# Check if the build was successful
if ($LASTEXITCODE -ne 0) {
Write-Host "Build failed."
exit $LASTEXITCODE
}

# Compare the generated file with the expected file
$generatedFileContent = Get-Content -Path $generatedFilePath
$expectedFileContent = Get-Content -Path $expectedFilePath

$diff = Compare-Object -ReferenceObject $generatedFileContent -DifferenceObject $expectedFileContent

if ($diff) {
$diff | Format-Table
Write-Error "The generated file does not match the expected file."
exit 1
} else {
Write-Host "The generated file matches the expected file."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;

namespace WebApi.OpenAPI.SystemTest.OperationId;

[ApiController]
[Route("OperationId")]
public class OperationIdController : ControllerBase
{
[HttpGet("/explicitOperationIdInName", Name = "GetExplicitOperationIdInName")]
public IActionResult GetExplicitOperationIdInName()
{
return this.Ok();
}

[HttpGet]
[SwaggerOperation(OperationId = "GetExplicitOperationIdInSwagger")]
[Route("/explicitOperationIdInSwagger")]
public IActionResult GetExplicitOperationIdInSwagger()
{
return this.Ok();
}

[HttpGet]
[Route("/noOperationId")]
public IActionResult GetNotOperationId()
{
return this.Ok();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace WebApi.OpenAPI.SystemTest.OperationId;

public static class OperationIdMinimalApis
{
public static void AddEndpointsForOperationId(this WebApplication app)
{
app.MapGet("minimal-endpoint-with-name", (() => Results.Ok()))
.WithName("GetMinimalApiWithName")
.WithTags("OperationId")
.WithOpenApi();

app.MapGet("minimal-endpoint-with-no-name", () => Results.Ok())
.WithTags("OperationId")
.WithOpenApi();
}
}

public class Test
{
PrincessMadMath marked this conversation as resolved.
Show resolved Hide resolved
public int Count { get; set; }
}
Loading