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()));