RicoSuter / NSwag

The Swagger/OpenAPI toolchain for .NET, ASP.NET Core and TypeScript.
http://NSwag.org
MIT License
6.73k stars 1.29k forks source link

Typescript Client Generation problem with objects being unrolled #4791

Open awdorrin opened 7 months ago

awdorrin commented 7 months ago

Starting with a C# Controller with a method like this:

[HttpPut("[action]")]
public async Task<IActionResult> PutSubcRtgLink(int subcontractRtgLinkId, SubcRtgLinkDTO subcRtgLink)

Compiling for .Net 6, using NSwag.AspnetCore/NSwag.MSBuild 13.18.2, would generate this typescript:

putSubcRtgLink(subcontractRtgLinkId: number, subcRtgLink: SubcRtgLinkDTO): Observable<FileResponse> {

After upgrading to .Net8 and NSwag.AspnetCore/NSwag.MSBuild 14.0.3, the SubcRtgLinkDTO is being un-rolled into multiple parameters, which ends up repeating the subcontractRtgLinkId parameter, like this:

putSubcRtgLink(subcontractRtgLinkIdQuery: number | undefined, subcontractRtgLinkIdQuery: number | undefined, subcontractId: number | undefined, pmmProgramId: number | undefined, subcontractRtgId: number | undefined, fileName: string | null | undefined, fileDescription: string | null | undefined, linkAddress: string | null | undefined, sipLabel: string | null | undefined, dateAdded: DateTime | undefined, dateModified: DateTime | undefined, modifiedBy: string | null | undefined, file: FileParameter | null | undefined): Observable<FileResponse | null> {

Not sure if this is a bug, or if something changed and I need to add some new parameters to the nswag.json file. Searched but didn't see anything related...

awdorrin commented 7 months ago

Making some progress, as I'm reading through the thread here: https://github.com/RicoSuter/NSwag/issues/4524

Looks like my nswag.json was not being read, which was a large part of the problem. Fixed by updating my csproj file:

<Target Name="NSwag" AfterTargets="Build">
  <Exec Condition="'$(Configuration)' == 'Debug'" EnvironmentVariables="ASPNETCORE_ENVIRONMENT=Development" Command="$(NSwagExe_Net80) run nswag.json /variables:Configuration=$(Configuration)" />
  <Exec Condition="'$(Configuration)' == 'Release'" EnvironmentVariables="ASPNETCORE_ENVIRONMENT=Production" Command="$(NSwagExe_Net80) run nswag.json /variables:Configuration=$(Configuration)" />
</Target>`

Now the vast majority of my method signatures are the same, between NSwag toolchain v13.18.2.0 and v14.0.3.0, discounting all the new | null or | undefined additions.

I now only have one methods, out of several hundred, that is being unrolled... the same one I mentioned in my initial post.

Original: putSubcRtgLink(subcontractRtgLinkId: number, subcRtgLink: SubcRtgLinkDTO): Observable<FileResponse> { Now: putSubcRtgLink(subcontractRtgLinkIdQuery?: number | undefined, subcontractRtgLinkIdQuery?: number | undefined, subcontractId?: number | undefined, pmmProgramId?: number | undefined, subcontractRtgId?: number | undefined, fileName?: string | null | undefined, fileDescription?: string | null | undefined, linkAddress?: string | null | undefined, sipLabel?: string | null | undefined, dateAdded?: DateTime | undefined, dateModified?: DateTime | undefined, modifiedBy?: string | null | undefined, file?: FileParameter | null | undefined): Observable<FileResponse | null> {

Code in the controller:

        [HttpPut("[action]")]
        public async Task<IActionResult> PutSubcRtgLink(int subcontractRtgLinkId, SubcRtgLinkDTO subcRtgLink)

Definition of SubcRtgLinkDTO:

    public partial class SubcRtgLinkDTO
    {
        public int SubcontractRtgLinkId { get; set; }
        public int SubcontractId { get; set; }
        public int PmmProgramId { get; set; }
        public int SubcontractRtgId { get; set; }
        public string FileName { get; set; }
        public string FileDescription { get; set; }
        public string LinkAddress { get; set; }
        public string SipLabel { get; set; }
        public DateTime DateAdded { get; set; }
        public DateTime DateModified { get; set; }
        public string ModifiedBy { get; set; }
        public IFormFile File { get; set; }
        public SubcRtgLinkDTO() { }

I seem to recall running into a similar issue with IFormFile and NSwag in another application, so I'm guessing that may be what is unrolling the SubcRtgLinkDTO object into its parameters...

Comparing the definition of SubcRtgLinkDTO, it does indicate it is an issue related to IFormFile. Older NSwag gave:

export class SubcRtgLinkDTO implements ISubcRtgLinkDTO {
    subcontractRtgLinkId?: number;
    subcontractId?: number;
    pmmProgramId?: number;
    subcontractRtgId?: number;
    fileName?: string | undefined;
    fileDescription?: string | undefined;
    linkAddress?: string | undefined;
    sipLabel?: string | undefined;
    dateAdded?: DateTime;
    dateModified?: DateTime;
    modifiedBy?: string | undefined;
    file?: string | undefined;

while the new NSwag gives:

export class SubcRtgLinkDTO implements ISubcRtgLinkDTO {
    subcontractRtgLinkId!: number;
    subcontractId!: number;
    pmmProgramId!: number;
    subcontractRtgId!: number;
    fileName?: string | undefined;
    fileDescription?: string | undefined;
    linkAddress?: string | undefined;
    sipLabel?: string | undefined;
    dateAdded!: DateTime;
    dateModified!: DateTime;
    modifiedBy?: string | undefined;
    file?: any | undefined;
awdorrin commented 7 months ago

Spoke too soon, I found about a dozen method calls where the order of parameters changed. For example: C#: public async Task<IActionResult> PutMarketStreamEnterpriseLead(int id, MarketStreamEnterpriseLead msel)

Typescript: putMarketStreamEnterpriseLead(msel: MarketStreamEnterpriseLead, id?: number | undefined): Observable<FileResponse | null>

I'm not sure why it is seeing the id parameter as optional/nullable.

awdorrin commented 7 months ago

Work around for the parameter order issue: public async Task<IActionResult> PutMarketStreamEnterpriseLead( [BindRequired] int id, MarketStreamEnterpriseLead msel) I had to add [BindRequired] to 5 put methods in our app, out of 75 total put methods. Not sure why the other 70 work without issue?

lecramr commented 6 months ago

@awdorrin Every fixed this Issue? I have the same one, as soon as I add a IFormFile to the DTO it gets unrolled.

awdorrin commented 6 months ago

We put the .net 8 migration work on hold due to these issues, and the lack of response to this issue (and other issues that have been posted here) I will say that IFormFile always seems to cause issues for us, even prior to .net 8. Sometimes it gets translated to FileParameter, other times it gets unrolled.

awdorrin commented 2 months ago

Finally getting back to looking at this, and to expand on what I had above, the following appears to work.

public async Task<IActionResult> PutMarketStreamEnterpriseLead( 
    [BindRequired] int id,
    [FromBody] MarketStreamEnterpriseLead msel)

The [BindRequired] keeps the key field for HttpPut actions at the front of the list, while [FromBody] keeps the object from being unrolled.

Now I'm going through searching for a few thousand Put/Post methods and inserting these parameters and continuing to test.