OpenAPI data source generates invalid C# code

Hi,

I am trying to infer a schema from my ASP.NET API via OpenAPI in Radzen. It seems to work so far, but there is one problem.

In my code I am using a default value in the method siganture (see the bool "useSample"):

[ApiController]
[Route("[controller]")]
public class TestController : ControllerBase
{
    private readonly ILogger<TestController> _logger;
    public TestController(ILogger<TestController> logger)
    {
        _logger = logger;
    }
    [HttpGet("Test/{value}")]
    [ProducesResponseType(typeof(Model), StatusCodes.Status200OK)]
    public IActionResult GetTest(string value, bool useSample = true)
    {
        var model = new Model
        {
            Name = "Test Name",
            Age = 30
        };
        if (useSample)
            model.Name = "Another Sample Name";
        return Ok(model);
    }
    public class Model
    {
        public string? Name { get; set; }
        public int Age { get; set; }
    }
}

The generated output is invalid C# code (the word "True" is written with a capital T but it must be lower case!):

  public partial class MyServiceService
    {
        ...
        public async Task<Test2.Models.MyService.Model> GetTestTestValue(string value, bool useSample = True)
        {
            ...
        }
    }

Is this a bug?

What is the json schema file generated for this API? We need it in order to reproduce the problem.

Hi,

I simply generate the OpenAPI spec automatically with .NET 9 and the webapi template:

builder.Services.AddOpenApi();
app.MapOpenApi();

Do you want me to upload the entire sample application?

Best regards,

Joe

Maybe it would be easier to provide us the schema JSON file instead of the app. But the app would work too.

Hi @korchev,

sorry for the delay. Here is the JSON schema file:

{
  "openapi": "3.0.1",
  "info": {
    "title": "WebApplication1 | v1",
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "https://localhost:7142/"
    }
  ],
  "paths": {
    "/Test/Test/{value}": {
      "get": {
        "tags": [
          "Test"
        ],
        "parameters": [
          {
            "name": "value",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "useSample",
            "in": "query",
            "schema": {
              "type": "boolean",
              "default": true
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/Model"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Model"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/Model"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Model": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "nullable": true
          },
          "age": {
            "type": "integer",
            "format": "int32"
          }
        }
      }
    }
  },
  "tags": [
    {
      "name": "Test"
    }
  ]
}

The file looks ok but the generated C# stub by Radzen has the "typo" with the upper case "T" in "true" which has to be lower case.

Thanks @JustJoe,

We will investigate and try to address this problem for the next release.

The problem should be fixed in the latest version 1.47.4.

Thanks a lot! This works now as expected. :slight_smile:

However, I found another, related bug which you will come accross if you have methods with default parameters. In C#, default parameters always have to be at the end of the method signature! E.g.:

public void Test(int number, string atext = "some text")

Now, when the OpenAPI json file is created for such a method, the order to the parameters may change (I assume these get ordered alphabetically!).

When you generate the C# stub from the JSON file back then, you stick to the order in the JSON file which can result in the following ouput:

public void Test(string atext = "some text", int number)

This is invalid C# code. So it would be required when you generate the C# stubs that you split the parameters into a list with default and one without default parameters and output the ones without first.

You are correct. This isn't a supported case yet. We will add support for it with the next release.

Perfect, thanks a lot! Looking forward to it.

One more thing, since I'm taking a closer look at this feature: You offer the option of providing an API key.

This key is stored in the appsettings.json file, and then hard-copied into the generated C# file. However, this is not a good approach since credentials should never be hardcoded.

In my setup, I work with multiple appsettings.json files for different environments (dev, staging, and production). Having the API key hard-coded makes it impossible for me to switch environments.

This is handled better for SQL data sources since you grab the credentials from the connection string in the appsettings.json file at runtime via configuration.

Ideally, you could use this approach for the OpenAPI data source as well.

Thanks a lot!

P.S.: The same applies also for the service address/URL, which is also hard coded in program.cs at the moment - it would generally be a better approach to have the service URL and the API key as settings in the appconfig.json and use the Configutaration system to get the values in the actual code.

Of course the generated code can easily be modified afterwards but in case I do a new infer of the data source beause e.g. I updated my API with new methods, all the changes will get lost...

The problem is appsettings.json doesn't work for WASM (unless the OpenaAPI data source is proxied via the server).

That is correct. However, the current approach is not very flexible at all.

It would help if re-infering the API would not overwrite the authentication method or if these kind of methods as well as the service API could be declared in a way that one could overwrite it with custom code in a partial class.

Maybe you have idea for that...

This is not possible I am afraid. Code generation overwrites everything. This is why partial classes can be useful in such context.

Yes, but partial does not help here, because on the one hand you have the

private async Task AuthorizeRequest(HttpRequestMessage request)

method and in program.cs this line:

builder.Services.AddHttpClient("MyAPI", client => client.BaseAddress = new Uri("https://localhost:7039"));

Both contain information form the actual configuration that needs to be replaced and will be overwritten on every infer.

A partial class or implementation does not help in this case.

But I have a better idea since you mentioned that using the settings from the Configutration is not an option due to compatibilty to Wasm:

Why don't you add a checkbox/bool option to the infer dialog (only available on re-infer) which prevents the code in program.cs and the AuthorizeRequest method from being overwritten, so somthing like "Only infer API and not update access information"?

That would help since I can implement my own, custom code after the first infer and when doing a re-infer, the code remains and does not get overwritten... what do you think?

That's probably a solution but definitely not trivial to implement. It would still work only for non-wasm cases which is a bit limiting. If Microsoft provides some equivalent to appsettings.json and Configuration for WASM we would definitely use it as we do for everything else.

Well, generally you are right but at the moment, this is the only approach I see and it also would work with Wasm since if you tick the checkbox, the original code does not get overwritten and it would still work. Actually the only difference compared to the current state is that some parts of the generated code do not get updated, if the option is checked. By default it is not checked and everything will behave like it does right now. So I do not see a limitation here...

The limitation is that this is brand new unplanned functionality that needs to be implemented :slight_smile: I don't think we can squeeze that in any time soon.

I ran some tests but couldn't reproduce the problem. For example I tested with this JSON:

  "paths": {
    "/Test/Test": {
      "get": {
        "tags": [
          "Test"
        ],
        "parameters": [
          {
            "name": "atext",
            "in": "query",
            "schema": {
              "type": "string",
	          "default": "some text"
            }
          },
          {
            "name": "number",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          }
        ],

And it generated the following method:

Test(string atext = "some text", int? number = default)

When I added "required": true to the "number" parameter the method signature changed to:

Test(int number, string atext = "some text")

Can you please send me the JSON which leads to?

public void Test(string atext = "some text", int number)

Here is the code to reproduce the issue. This is the original API method:

 [HttpPost("Info")]
 public IActionResult PostTest([FromBody] List<int> accounts, int? relatedNo = 0)
 {
  return Ok();
 }

And this is the Open API json file:

{
  "openapi": "3.0.1",
  "info": {
    "title": "WebApplication1 | v1",
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "https://localhost:7142/"
    }
  ],
  "paths": {
    "/Test/Info": {
      "post": {
        "tags": [
          "Test"
        ],
        "parameters": [
          {
            "name": "relatedNo",
            "in": "query",
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 0
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "array",
                "items": {
                  "type": "integer",
                  "format": "int32"
                }
              }
            },
            "text/json": {
              "schema": {
                "type": "array",
                "items": {
                  "type": "integer",
                  "format": "int32"
                }
              }
            },
            "application/*+json": {
              "schema": {
                "type": "array",
                "items": {
                  "type": "integer",
                  "format": "int32"
                }
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    }
  },
  "components": { },
  "tags": [
    {
      "name": "Test"
    }
  ]
}

As a result, Radzen will generate the method stub with the parameters in the wrong order which causes invalid C# code as described before.

Let me know if you need any further information to reproduce.