RicoSuter/NSwag

Remove api prefix from route

Closed this issue · 1 comments

Hi!

I am encountering an issue while generating a C# client using NSwag, where I modify the OpenAPI document to remove the /api prefix from all paths. This is leading to an ArgumentException: An item with the same key has already been added during the client generation process when two paths become duplicates after the prefix is removed.

For example, paths like /api/roles and /roles both map to /roles after the prefix is removed, causing a key collision in the path dictionary. The client generation succeeds for some paths, but for others, I get the following error: System.ArgumentException: An item with the same key has already been added. Key: /roles

I would also like to customize the URL in my C# client because I plan to deploy the solution in the cloud, where the API will be behind Azure API Management (APIM). In my Swagger configuration, I can use the following solution to change the base URL:

// Swagger configuration
file sealed class AppDocumentFilter : IDocumentFilter
{
    public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
    {
        var editedPaths = new OpenApiPaths();
        foreach (var (key, value) in swaggerDoc.Paths)
        {
            var newKey = key.Replace("api/", string.Empty);
            editedPaths.Add(newKey, value);
        }
        swaggerDoc.Paths = editedPaths;
        swaggerDoc.Servers.Add(new OpenApiServer { Url = "/api" });
    }
}

Is there a way to customize the NSwag configuration in a similar way to control the base URL used by the C# client (for instance, using /api prefix locally but customizing it for cloud environments)?

I have tried to configure nswag, but I'm getting duplicate keys error.

public class MyOperationProcessor : IOperationProcessor
{
    public bool Process(OperationProcessorContext context)
    {
        Dictionary<string, NSwag.OpenApiPathItem> editedPaths = [];
        foreach (var (key, value) in context.Document.Paths)
        {
            var newKey = key.Replace("api/", string.Empty);
            editedPaths.Add(newKey, value);
        }

        context.Document.Paths.Clear();
        foreach (var (key, value) in editedPaths)
        {
            context.Document.Paths.Add(key, value);
        }
        context.Document.Servers.Add(new NSwag.OpenApiServer { Url = "/api" });
        return true; // return false to exclude the operation from the document
    }
}

Error

1>EXEC : error : /api/roles
1>System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation.
1> ---> System.ArgumentException: An item with the same key has already been added. Key: /roles
1>   at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior)
1>   at System.Collections.Generic.Dictionary`2.Add(TKey key, TValue value)
1>   at MyProject.Web.Api.App.MyOperationProcessor.Process(OperationProcessorContext context) in C:\EDF\ext\e\Project\src\MyProject\MyProject.Web.Api\App\ConfigureSwagger.cs:line 106
1>   at NSwag.Generation.AspNetCore.AspNetCoreOpenApiDocumentGenerator.RunOperationProcessors(OpenApiDocument document, ApiDescription apiDescription, Type controllerType, MethodInfo methodInfo, OpenApiOperationDescription operationDescription, List`1 allOperations, OpenApiDocumentGenerator generator, OpenApiSchemaResolver schemaResolver)
1>   at NSwag.Generation.AspNetCore.AspNetCoreOpenApiDocumentGenerator.AddOperationDescriptionsToDocument(OpenApiDocument document, Type controllerType, List`1 operations, OpenApiDocumentGenerator swaggerGenerator, OpenApiSchemaResolver schemaResolver)
1>   at NSwag.Generation.AspNetCore.AspNetCoreOpenApiDocumentGenerator.GenerateApiGroups(OpenApiDocumentGenerator generator, OpenApiDocument document, IGrouping`2[] apiGroups, OpenApiSchemaResolver schemaResolver)
1>   at NSwag.Generation.AspNetCore.AspNetCoreOpenApiDocumentGenerator.GenerateAsync(ApiDescriptionGroupCollection apiDescriptionGroups)
1>   at NSwag.Commands.Generation.AspNetCore.AspNetCoreToOpenApiCommand.GenerateDocumentWithDocumentProviderAsync(IServiceProvider serviceProvider) in /_/src/NSwag.Commands/Commands/Generation/AspNetCore/AspNetCoreToOpenApiCommand.cs:line 245
1>   at NSwag.Commands.Generation.AspNetCore.AspNetCoreToOpenApiCommand.GenerateDocumentAsync(IServiceProvider serviceProvider, String currentWorkingDirectory) in /_/src/NSwag.Commands/Commands/Generation/AspNetCore/AspNetCoreToOpenApiCommand.cs:line 239
1>   at NSwag.Commands.Generation.AspNetCore.AspNetCoreToOpenApiGeneratorCommandEntryPoint.Process(String commandContent, String outputFile, String applicationName) in /_/src/NSwag.Commands/Commands/Generation/AspNetCore/AspNetCoreToOpenApiGeneratorCommandEntryPoint.cs:line 29
1>   at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
1>   at System.Reflection.MethodBaseInvoker.InvokeDirectByRefWithFewArgs(Object obj, Span`1 copyOfArgs, BindingFlags invokeAttr)
1>   --- End of inner exception stack trace ---
1>   at System.Reflection.MethodBaseInvoker.InvokeDirectByRefWithFewArgs(Object obj, Span`1 copyOfArgs, BindingFlags invokeAttr)
1>   at System.Reflection.MethodBaseInvoker.InvokeWithFewArgs(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
1>   at System.Reflection.MethodBase.Invoke(Object obj, Object[] parameters)
1>   at NSwag.AspNetCore.Launcher.Program.Main(String[] args) in /_/src/NSwag.AspNetCore.Launcher/Program.cs:line 132
1>System.InvalidOperationException: Swagger generation failed with non-zero exit code '1'.
1>   at NSwag.Commands.Generation.AspNetCore.AspNetCoreToOpenApiCommand.RunAsync(CommandLineProcessor processor, IConsoleHost host) in /_/src/NSwag.Commands/Commands/Generation/AspNetCore/AspNetCoreToOpenApiCommand.cs:line 195
1>   at NSwag.Commands.NSwagDocumentBase.GenerateSwaggerDocumentAsync() in /_/src/NSwag.Commands/NSwagDocumentBase.cs:line 270
1>   at NSwag.Commands.NSwagDocument.ExecuteAsync() in /_/src/NSwag.Commands/NSwagDocument.cs:line 67
1>   at NSwag.Commands.Document.ExecuteDocumentCommand.ExecuteDocumentAsync(IConsoleHost host, String filePath) in /_/src/NSwag.Commands/Commands/Document/ExecuteDocumentCommand.cs:line 76
1>   at NSwag.Commands.Document.ExecuteDocumentCommand.RunAsync(CommandLineProcessor processor, IConsoleHost host) in /_/src/NSwag.Commands/Commands/Document/ExecuteDocumentCommand.cs:line 33
1>   at NConsole.CommandLineProcessor.ProcessSingleAsync(String[] args, Object input)
1>   at NConsole.CommandLineProcessor.ProcessAsync(String[] args, Object input)
1>   at NSwag.Commands.NSwagCommandProcessor.ProcessAsync(String[] args) in /_/src/NSwag.Commands/NSwagCommandProcessor.cs:line 65
1>C:\ext\ed\Project\src\MyProject\MyProject.Web.Api\MyProject.Web.Api.csproj(46,3): error MSB3073: The command "dotnet "C:\Users\myuser\.nuget\packages\nswag.msbuild\14.1.0\buildTransitive\../tools/Net80/dotnet-nswag.dll" run nswag.json /variables:Configuration=Debug" exited with code -1.

NSwag versions:

<PackageReference Include="NSwag.AspNetCore" Version="14.1.0" />
<PackageReference Include="NSwag.MSBuild" Version="14.1.0">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.8.0" />

I found the solution, you just need to use IDocumentProcessor

file sealed class AppDocumentProcessor : IDocumentProcessor
{
    public void Process(DocumentProcessorContext context)
    {
        var editedPaths = context.Document.Paths
            .ToDictionary(item => item.Key.ReplaceApi(), item => item.Value);

        context.Document.Paths.Clear();
        foreach (var (key, value) in editedPaths)
        {
            context.Document.Paths.Add(key, value);
        }
        context.Document.Servers.Add(new NSwag.OpenApiServer { Url = "/api" });
    }
}

Registration:

services.AddOpenApiDocument((settings, _) =>
    settings.DocumentProcessors.Insert(0, new AppDocumentProcessor()));