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 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
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,5 @@ 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 }}

4 changes: 2 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,5 @@ jobs:
- run: ./Build.ps1
shell: pwsh
env:
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
NUGET_API_KEY: ${{ secrets.WORKLEAP_NUGET_API_KEY }}
NUGET_SOURCE: ${{ secrets.NUGET_GSOFTDEV_FEED_URL }}
heqianwang marked this conversation as resolved.
Show resolved Hide resolved
NUGET_API_KEY: ${{ secrets.GSOFT_NUGET_API_KEY }}
35 changes: 32 additions & 3 deletions Build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,47 @@ Process {
throw ("An error occurred while executing command: {0}" -f $Command)
}
}


function Compare-GeneratedAndExpectedFiles {
param(
[Parameter(Mandatory=$true)]
[string]$generatedFilePath,

[Parameter(Mandatory=$true)]
[string]$expectedFilePath
)

# 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."
}
}

$workingDir = Join-Path $PSScriptRoot "src"
$outputDir = Join-Path $PSScriptRoot ".output"
$nupkgsPath = Join-Path $outputDir "*.nupkg"

$projectPath = Join-Path $workingDir "tests/WebApi.OpenAPI.SystemTest"
$generatedFilePath = Join-Path $projectPath "openapi-v1.yaml"
$expectedFilePath = Join-Path $workingDir "tests/expected-openapi-document.yaml"


try {
Push-Location $workingDir
Remove-Item $outputDir -Force -Recurse -ErrorAction SilentlyContinue

Exec { & dotnet clean -c Release }
Exec { & dotnet build -c Release }
Exec { & dotnet test -c Release --no-build --results-directory "$outputDir" --no-restore -l "trx" -l "console;verbosity=detailed" }
Exec { & Compare-GeneratedAndExpectedFiles -generatedFilePath $generatedFilePath -expectedFilePath $expectedFilePath }
Exec { & dotnet pack -c Release --no-build -o "$outputDir" }

if (($null -ne $env:NUGET_SOURCE ) -and ($null -ne $env:NUGET_API_KEY)) {
Expand All @@ -32,4 +61,4 @@ Process {
finally {
Pop-Location
}
}
}
24 changes: 21 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,29 @@
[![nuget](https://img.shields.io/nuget/v/Workleap.Extensions.OpenAPI.svg?logo=nuget)](https://www.nuget.org/packages/Workleap.Extensions.OpenAPI/)
[![build](https://img.shields.io/github/actions/workflow/status/gsoft-inc/wl-extensions-openapi/publish.yml?logo=github&branch=main)](https://github.com/gsoft-inc/wl-extensions-openapi/actions/workflows/publish.yml)

The `Workleap.Extensions.OpenAPI` library is designed to help generate better OpenApi document with less effort.

## Value proposition and features overview

The library offers an opinionated configuration of OpenAPI document generation and SwaggerUI.

As such, we provide the following features:

- Display OperationId in SwaggerUI
- (Optional) Fallback to use controller name as OperationId when there is no OperationId explicitly defined for the endpoint.

## Getting started

TODO
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.

```cs
public void ConfigureServices(IServiceCollection services)
{
// [...]
services.AddOpenApi()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You forgot to update to readme after renaming the method in the code

.FallbackOnMethodNameForOperationId();
}
```

## Building, releasing and versioning

Expand All @@ -16,7 +35,6 @@ A new *preview* NuGet package is **automatically published** on any new commit o

When you are ready to **officially release** a stable NuGet package by following the [SemVer guidelines](https://semver.org/), simply **manually create a tag** with the format `x.y.z`. This will automatically create and publish a NuGet package for this version.


## License

Copyright © 2024, Workleap This code is licensed under the Apache License, Version 2.0. You may obtain a copy of this license at https://github.com/gsoft-inc/gsoft-license/blob/master/LICENSE.
Copyright © 2024, Workleap This code is licensed under the Apache License, Version 2.0. You may obtain a copy of this license at [License](https://github.com/gsoft-inc/gsoft-license/blob/master/LICENSE).
5 changes: 4 additions & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<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="GitVersion.MsBuild" Version="5.12.0" Condition=" '$(Configuration)' == 'Release' ">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
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")]
heqianwang marked this conversation as resolved.
Show resolved Hide resolved
[InlineData("GetAsyncDataasync", "GetAsyncData")]
public void Given_Method_Name_When_Cleanup_Then_Clean_Name(string methodName, string expectedOutput)
{
// When
var result = FallbackOperationIdToMethodNameFilter.GenerateOperationIdFromMethodName(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 sealed class OpenApiBuilder
{
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 GenerateMissingOperationId()
{
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 ConfigureOpenApiGeneration(this IServiceCollection services)
{
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 sealed class DisplayOperationIdInSwaggerUiOptions : IConfigureOptions<SwaggerUIOptions>
{
public void Configure(SwaggerUIOptions options)
{
options.DisplayOperationId();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;

namespace Workleap.Extensions.OpenAPI.OperationId;

internal sealed 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 enforce explicit declaration
if (IsMinimalApi(context))
{
return;
}

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

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

internal static string GenerateOperationIdFromMethodName(string methodName)
{
if (methodName.EndsWith("Async", StringComparison.OrdinalIgnoreCase))
{
return methodName[..^"Async".Length];
}

return methodName;
}
}
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.ConfigureOpenApiGeneration(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Workleap.Extensions.OpenAPI.Builder.OpenApiBuilder!
Workleap.Extensions.OpenAPI.Builder.OpenApiBuilder
Workleap.Extensions.OpenAPI.Builder.OpenApiBuilder.GenerateMissingOperationId() -> Workleap.Extensions.OpenAPI.Builder.OpenApiBuilder!
Workleap.Extensions.OpenAPI.OpenApiServiceCollectionExtensions
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>true</IsPackable>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
heqianwang marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -9,17 +9,21 @@
<AssemblyOriginatorKeyFile>../Workleap.Extensions.OpenAPI.snk</AssemblyOriginatorKeyFile>
</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
Loading