swagger-api / swagger-codegen

swagger-codegen contains a template-driven engine to generate documentation, API clients and server stubs in different languages by parsing your OpenAPI / Swagger definition.
http://swagger.io
Apache License 2.0
17.03k stars 6.03k forks source link

[aspnetcore] Implement c# server through inheritance and abstract override #9967

Open AtosNicoS opened 4 years ago

AtosNicoS commented 4 years ago
Description

I want to write a yaml OpenAPI specification and generate a c# client and server from it (I do not want to generate the yaml for the c# server code.).

The code generator generates something similar to this:

  [ApiController]
    public class PetApiController : ControllerBase, IPetApiController
    { 
        /// <summary>
        /// Deletes a pet
        /// </summary>
        /// <param name="petId">Pet id to delete</param>
        /// <param name="apiKey"></param>
        /// <response code="400">Invalid ID supplied</response>
        /// <response code="404">Pet not found</response>
        [HttpDelete]
        [Route("/v2/pet/{petId}")]
        [ValidateModelState]
        [SwaggerOperation("DeletePet")]
        public virtual IActionResult DeletePet([FromRoute][Required]long? petId, [FromHeader]string apiKey)
        { 
            //TODO: Uncomment the next line to return response 400 or use other options such as return this.NotFound(), return this.BadRequest(..), ...
            // return StatusCode(400);

            //TODO: Uncomment the next line to return response 404 or use other options such as return this.NotFound(), return this.BadRequest(..), ...
            // return StatusCode(404);

            throw new NotImplementedException();
        }
}

What I want to do is to Not touch anything of the generated code, inherit this class and implement it:

    public class TestController : PetApiController
    {
        public override IActionResult DeletePet(long? petId, string apiKey)
        {
            // TODO my new implementation goes here.
            throw new NotImplementedException();
        }
    }

But I will get an error, that the path/route is used twice:

Exception thrown: 'System.IO.FileNotFoundException' in System.Private.CoreLib.dll
'dotnet.exe' (CoreCLR: clrhost): Loaded 'C:\Program Files\dotnet\shared\Microsoft.NETCore.App\2.2.5\System.Reflection.Emit.dll'. 
'dotnet.exe' (CoreCLR: clrhost): Loaded 'Microsoft.GeneratedCode'. 
'dotnet.exe' (CoreCLR: clrhost): Loaded 'C:\Program Files\dotnet\shared\Microsoft.NETCore.App\2.2.5\System.Reflection.Extensions.dll'. 
'dotnet.exe' (CoreCLR: clrhost): Loaded 'C:\Program Files\dotnet\shared\Microsoft.NETCore.App\2.2.5\System.Reflection.TypeExtensions.dll'. 
Exception thrown: 'System.NotSupportedException' in Swashbuckle.AspNetCore.SwaggerGen.dll
Exception thrown: 'System.NotSupportedException' in System.Private.CoreLib.dll
Exception thrown: 'System.NotSupportedException' in System.Private.CoreLib.dll
Exception thrown: 'System.NotSupportedException' in System.Private.CoreLib.dll
Exception thrown: 'System.NotSupportedException' in System.Private.CoreLib.dll
'dotnet.exe' (CoreCLR: clrhost): Loaded 'C:\Program Files\dotnet\shared\Microsoft.NETCore.App\2.2.5\System.Diagnostics.StackTrace.dll'. 
'dotnet.exe' (CoreCLR: clrhost): Loaded 'C:\Program Files\dotnet\shared\Microsoft.NETCore.App\2.2.5\System.Reflection.Metadata.dll'. 
Microsoft.AspNetCore.Server.Kestrel:Error: Connection id "0HLSKRN7E1EUJ", Request id "0HLSKRN7E1EUJ:00000002": An unhandled exception was thrown by the application.

System.NotSupportedException: HTTP method "GET" & path "v2/pet/{petId}" overloaded by actions - IO.Swagger.Controllers.TestController.GetPetById (IO.Swagger),IO.Swagger.Controllers.TestController.GetPetById2 (IO.Swagger). Actions require unique method/path combination for Swagger 2.0. Use ConflictingActionsResolver as a workaround
   at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.CreatePathItem(IEnumerable`1 apiDescriptions, ISchemaRegistry schemaRegistry)
   at System.Linq.Enumerable.ToDictionary[TSource,TKey,TElement](IEnumerable`1 source, Func`2 keySelector, Func`2 elementSelector, IEqualityComparer`1 comparer)
   at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.CreatePathItems(IEnumerable`1 apiDescriptions, ISchemaRegistry schemaRegistry)
   at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GetSwagger(String documentName, String host, String basePath, String[] schemes)
   at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext)
   at Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Builder.RouterMiddleware.Invoke(HttpContext httpContext)
   at Microsoft.AspNetCore.Server.IISIntegration.IISMiddleware.Invoke(HttpContext httpContext)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 336.3171ms 500 
The thread 0x5560 has exited with code 0 (0x0).
The thread 0x56cc has exited with code 0 (0x0).
The thread 0x904 has exited with code 0 (0x0).

Now my Question is: How can I properly override those methods without getting this error? If I change the methods from virtual to abstract, remove the (not relevant, generated) "implementation", the error disappears.

Swagger-codegen version

3.0.15 Windows

Command line used for generation
java -jar swagger-codegen-cli-3.0.15.jar generate -i petstore.yaml -l aspnetcore
Suggest a fix/enhancement

There is very likely something I am not aware of yet, but if that is not the case, we could maybe also provide an abstract option to generate. The interface-only option does not contain any path definitions, so that is not a suitable option.

copperorange commented 4 years ago

The way I solved this was by specifying a templates folder and modifying the templates in that folder that I wanted to change. Specifically, make a change for public abstract class (which I think should be the default btw)

./templates/aspnetcore/2.1/controller.mustache

... namespace {{packageName}}.Controllers { {{#operations}} /// <summary> /// {{description}} /// </summary>{{#description}} [Description("{{description}}")]{{/description}} [ApiController] public **abstract** class {{classname}}Controller : ControllerBase{{#interfaceController}}, I{{classname}}Controller{{/interfaceController}} { {{#operation}} /// <summary> /// {{#summary}}{{summary}}{{/summary}} /// </summary> {{#notes}}/// <remarks>{{notes}}</remarks>{{/notes}}{{#allParams}} /// <param name="{{paramName}}">{{description}}</param>{{/allParams}}{{#responses}} /// <response code="{{code}}">{{message}}</response>{{/responses}} [{{httpMethod}}] ...

Then generated my output

java -jar ./swagger-codegen-cli.jar \ generate \ -i ./api.yaml \ -l aspnetcore --aspnet-core-version 2.1 --interface-controller \ -t ./templates/aspnetcore --template-engine mustache -c ./config.json \ -o ../

my config.json

`{ "packageName": "Api", "modelPackage": "Api.Models", "apiPackage": "Api.Api", "invokerPackage": "Api", "interfaceOnly": true

}`

AtosNicoS commented 4 years ago

In the end we now do not use the generator at all. It does not look production ready yet, so we implement all rest calls manually. Aspdotnet already has so much stuff builtin, that it is not really much more work. But we can remove all the swagger stuff and simply host the predefined yaml file ourself within the aspdotnet server. Another advantage is, that we can now use aspdotnet 3.1, which enables c#8 support and other features.

@copperorange thanks for the hint anyways, I've used your idea to also fix the generated client. This one also uses an outdated rest nuget package, that I've fixed locally. It seems that the overall c# tooling is really bad, unfortunately.

copperorange commented 4 years ago

@AtosNicoS Agreed on all points. That being said we really like the continuous contract-first approach and met in the middle. Overridablitiy somewhat allows this and it seems all languages if supported should have an option to choose the class implementation methodology. Basically we changed all templates to generate the code we wanted. It does feel like there is an opportunity to carefully craft a template that meets all needs and catch up on the 3.1 support. In other cases just maintaining the .swagger-codegen-ignore or limiting to models also meets the objective.