Repository: microsoft/kiota (csharp)

Issue:
Models not generating for requests with multiple content types
### What are you generating using Kiota, clients or plugins?

API Client/SDK

### In what context or format are you using Kiota?

Nuget tool

### Client library/SDK language

Csharp

### Describe the bug

I am trying to generate a CSharp api client from an openapi document with kiota 1.14.0 but model files are not being generated when the requestBody content type does not match application/json, or if application/json exists along side multipart/form-data.

Based on the document, JWTObtainPairRequest should have a Model generated, but this is only true when the endpoint request content mime type is only application/json

### Expected behavior

Model files should be generated regardless of mime types.

Based on the example, the models folder should have the type JWTObtainPairRequest

### How to reproduce

` kiota generate -l CSharp -c MyApiClient -n My.ApiClient -d ./test.yaml -o ./MyApiClient  --include-path "**/token/**" --co --cc`

### Open API description file

`openapi: 3.0.3
info:
  title: API
  version: 2.0
paths:
  /api/v2/token/new/:
    post:
      operationId: Obtain new access/refresh token pair
      description: Obtain JWT pair
      tags:
        - token
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/JWTObtainPairRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/JWTObtainPairRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/JWTObtainPairRequest'
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SpectacularJWTObtain'
          description: Obtain JWT response.
components:
  schemas:
    ErrorResponse:
      type: object
      properties:
        summary:
          type: string
        detail:
          type: string
        type:
          type: string
        status_code:
          type: integer
      required:
        - detail
        - status_code
        - summary
    JWTObtainPairRequest:
      type: object
      description: Obtain JWT pair.
      properties:
        secret_id:
          type: string
          minLength: 1
          description: Secret id from /user-secrets/
        secret_key:
          type: string
          minLength: 1
          description: Secret key from /user-secrets/
      required:
        - secret_id
        - secret_key
    SpectacularJWTObtain:
      type: object
      description: Obtain new JWT pair.
      properties:
        access:
          type: string
          readOnly: true
          description: Your access token
        access_expires:
          type: integer
          readOnly: true
          default: 86400
          description: Access token expires in seconds
        refresh:
          type: string
          readOnly: true
          description: Your refresh token
        refresh_expires:
          type: integer
          readOnly: true
          default: 2592000
          description: Refresh token expires in seconds
  securitySchemes:
    jwtAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
security:
  - jwtAuth: [ ]
servers:
  - url: https://api.server.com

`

### Kiota Version

1.14.0

### Latest Kiota version known to work for scenario above?(Not required)

_No response_

### Known Workarounds

Reduce request mime content types to just application/json

### Configuration

- OS: Windows 10
- Arch: x64

### Debug output

<details><summary>Click to expand log</summary>
```
> kiota generate -l CSharp -c MyApiClient -n My.Client -d ./test.yaml -o ./MyApiClient  --include-path "**/token/**" --co --cc --ll debug
info: Kiota.Builder.KiotaBuilder[0]
      Cleaning output directory C:\Project\.\ClientTest
dbug: Kiota.Builder.KiotaBuilder[0]
      kiota version 1.14.0
info: Kiota.Builder.KiotaBuilder[0]
      loaded description from local source
dbug: Kiota.Builder.KiotaBuilder[0]
      step 1 - reading the stream - took 00:00:00.0187292
dbug: Kiota.Builder.KiotaBuilder[0]
      step 2 - parsing the document - took 00:00:00.2263048
dbug: Kiota.Builder.KiotaBuilder[0]
      step 3 - updating generation configuration from kiota extension - took 00:00:00.0001934
dbug: Kiota.Builder.KiotaBuilder[0]
      step 4 - filtering API paths with patterns - took 00:00:00.0348416
dbug: Kiota.Builder.KiotaBuilder[0]
      step 5 - checking whether the output should be updated - took 00:00:00.0281600
dbug: Kiota.Builder.KiotaBuilder[0]
      step 6 - create uri space - took 00:00:00.0048704
dbug: Kiota.Builder.KiotaBuilder[0]
      InitializeInheritanceIndex 00:00:00.0072927
dbug: Kiota.Builder.KiotaBuilder[0]
      CreateRequestBuilderClass 00:00:00
dbug: Kiota.Builder.KiotaBuilder[0]
      MapTypeDefinitions 00:00:00.0181795
dbug: Kiota.Builder.KiotaBuilder[0]
      TrimInheritedModels 00:00:00
dbug: Kiota.Builder.KiotaBuilder[0]
      CleanUpInternalState 00:00:00
dbug: Kiota.Builder.KiotaBuilder[0]
      step 7 - create source model - took 00:00:00.2189239
dbug: Kiota.Builder.KiotaBuilder[0]
      77ms: Language refinement applied
dbug: Kiota.Builder.KiotaBuilder[0]
      step 8 - refine by language - took 00:00:00.0798026
dbug: Kiota.Builder.KiotaBuilder[0]
      step 9 - writing files - took 00:00:00.0837791
info: Kiota.Builder.KiotaBuilder[0]
      loaded description from local source
dbug: Kiota.Builder.KiotaBuilder[0]
      step 10 - writing lock file - took 00:00:00.0277753
Generation completed successfully
dbug: Kiota.Builder.KiotaBuilder[0]
      Api manifest path: C:\Project\apimanifest.json

Hint: use the info command to get the list of dependencies you need to add to your project.
Example: kiota info -d "C:\Project\.\test.yaml" -l CSharp
```
</details>


### Other information

_No response_

Pull request:
Fix #4734 

This PR covers an issue when multipart/form-data is present.

Added an additional method IsMultipartTopMimeType next to IsMultipartFormDataSchema.
This method checks for:
- Presence of "multipart/form-data" in the current RequestBody Content
- If "multipart/form-data" is the only RequestBody Content Type
- If "multipart/form-data" is first in structuredMimeTypes

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8a91a69048..34e10fa314 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -45,6 +45,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Fixes a bug where request executors would be missing Untyped parameters in dotnet [#4692](https://github.com/microsoft/kiota/issues/4692)
 - Fixes a bug where indexers in include/exclude patters were not normalized if the indexer was the last segment without a slash at the end [#4715](https://github.com/microsoft/kiota/issues/4715)
 - Fixes a bug where CLI generation doesnot handle parameters of type string array. [#4707](https://github.com/microsoft/kiota/issues/4707)
+- Fixed a bug where models would not be created when a multipart content schema existed with no encoding [#4734](https://github.com/microsoft/kiota/issues/4734)
 
 ## [1.14.0] - 2024-05-02
 
diff --git a/src/Kiota.Builder/Extensions/OpenApiOperationExtensions.cs b/src/Kiota.Builder/Extensions/OpenApiOperationExtensions.cs
index 6d14c22441..aca62940e2 100644
--- a/src/Kiota.Builder/Extensions/OpenApiOperationExtensions.cs
+++ b/src/Kiota.Builder/Extensions/OpenApiOperationExtensions.cs
@@ -47,6 +47,15 @@ internal static bool IsMultipartFormDataSchema(this IDictionary<string, OpenApiM
         return source.GetValidSchemas(structuredMimeTypes).FirstOrDefault() is OpenApiSchema schema &&
         source.GetValidSchemas(multipartMimeTypes).FirstOrDefault() == schema;
     }
+    internal static bool IsMultipartTopMimeType(this IDictionary<string, OpenApiMediaType> source, StructuredMimeTypesCollection structuredMimeTypes)
+    {
+        ArgumentNullException.ThrowIfNull(source);
+        ArgumentNullException.ThrowIfNull(structuredMimeTypes);
+        if (structuredMimeTypes.Count == 0) return false;
+        if (!source.ContainsKey(multipartMimeTypes.First())) return false;
+        if (source.Count == 1) return true;
+        return structuredMimeTypes.First() == multipartMimeTypes.First();
+    }
     internal static IEnumerable<OpenApiSchema> GetValidSchemas(this IDictionary<string, OpenApiMediaType> source, StructuredMimeTypesCollection structuredMimeTypes)
     {
         ArgumentNullException.ThrowIfNull(source);
diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs
index ef05625247..81613e912f 100644
--- a/src/Kiota.Builder/KiotaBuilder.cs
+++ b/src/Kiota.Builder/KiotaBuilder.cs
@@ -1473,26 +1473,37 @@ private void AddRequestBuilderMethodParameters(OpenApiUrlTreeNode currentNode, O
         if (operation.GetRequestSchema(config.StructuredMimeTypes) is OpenApiSchema requestBodySchema)
         {
             CodeTypeBase requestBodyType;
-            if (operation.RequestBody.Content.IsMultipartFormDataSchema(config.StructuredMimeTypes))
+            if (operation.RequestBody.Content.IsMultipartFormDataSchema(config.StructuredMimeTypes)
+                && operation.RequestBody.Content.IsMultipartTopMimeType(config.StructuredMimeTypes))
             {
-                requestBodyType = new CodeType
-                {
-                    Name = "MultipartBody",
-                    IsExternal = true,
-                };
                 var mediaType = operation.RequestBody.Content.First(x => x.Value.Schema == requestBodySchema).Value;
-                foreach (var encodingEntry in mediaType.Encoding
-                                                        .Where(x => !string.IsNullOrEmpty(x.Value.ContentType) &&
-                                                                config.StructuredMimeTypes.Contains(x.Value.ContentType)))
+                if (mediaType.Encoding.Any())
                 {
-                    if (CreateModelDeclarations(currentNode, requestBodySchema.Properties[encodingEntry.Key], operation, method, $"{operationType}RequestBody", isRequestBody: true) is CodeType propertyType &&
-                        propertyType.TypeDefinition is not null)
-                        multipartPropertiesModels.TryAdd(propertyType.TypeDefinition, true);
+                    requestBodyType = new CodeType { Name = "MultipartBody", IsExternal = true, };
+                    foreach (var encodingEntry in mediaType.Encoding
+                                 .Where(x => !string.IsNullOrEmpty(x.Value.ContentType) &&
+                                             config.StructuredMimeTypes.Contains(x.Value.ContentType)))
+                    {
+                        if (CreateModelDeclarations(currentNode, requestBodySchema.Properties[encodingEntry.Key],
+                                operation, method, $"{operationType}RequestBody",
+                                isRequestBody: true) is CodeType propertyType &&
+                            propertyType.TypeDefinition is not null)
+                            multipartPropertiesModels.TryAdd(propertyType.TypeDefinition, true);
+                    }
+                }
+                else
+                {
+                    requestBodyType = CreateModelDeclarations(currentNode, requestBodySchema, operation, method,
+                                          $"{operationType}RequestBody", isRequestBody: true) ??
+                                      throw new InvalidSchemaException();
                 }
             }
             else
-                requestBodyType = CreateModelDeclarations(currentNode, requestBodySchema, operation, method, $"{operationType}RequestBody", isRequestBody: true) ??
-                    throw new InvalidSchemaException();
+            {
+                requestBodyType = CreateModelDeclarations(currentNode, requestBodySchema, operation, method,
+                                      $"{operationType}RequestBody", isRequestBody: true) ??
+                                  throw new InvalidSchemaException();
+            }
             method.AddParameter(new CodeParameter
             {
                 Name = "body",
@@ -1550,6 +1561,7 @@ private void AddRequestBuilderMethodParameters(OpenApiUrlTreeNode currentNode, O
                     PossibleValues = contentTypes.ToList()
                 });
         }
+
         method.AddParameter(new CodeParameter
         {
             Name = "requestConfiguration",


Test patch:
diff --git a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs
index 19c2d8a249..e70aff7d30 100644
--- a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs
+++ b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs
@@ -7180,7 +7180,7 @@ public async Task CleanupSymbolNameDoesNotCauseNameConflictsInQueryParameters()
         Assert.Equal("int64", select.Type.Name);
     }
     [Fact]
-    public async Task SupportsMultiPartFormAsRequestBody()
+    public async Task SupportsMultiPartFormAsRequestBodyWithDefaultMimeTypes()
     {
         var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName());
         await using var fs = await GetDocumentStream(@"openapi: 3.0.1
@@ -7246,6 +7246,285 @@ public async Task SupportsMultiPartFormAsRequestBody()
         Assert.NotNull(addressClass);
     }
     [Fact]
+    public async Task SupportsMultiPartFormAsRequestBodyWithoutEncodingWithDefaultMimeTypes()
+    {
+        var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName());
+        await using var fs = await GetDocumentStream(@"openapi: 3.0.1
+info:
+  title: Example
+  description: Example
+  version: 1.0.1
+servers:
+  - url: https://example.org
+paths:
+  /directoryObject:
+    post:
+      requestBody:
+        content:
+          multipart/form-data:
+            schema:
+              type: object
+              properties:
+                id:
+                  type: string
+                  format: uuid
+                address:
+                  $ref: '#/components/schemas/address'
+                profileImage:
+                  type: string
+                  format: binary
+        responses:
+          '204':
+            content:
+              application/json:
+                schema:
+                  type: string
+components:
+  schemas:
+    address:
+      type: object
+      properties:
+        street:
+          type: string
+        city:
+          type: string");
+        var mockLogger = new Mock<ILogger<KiotaBuilder>>();
+        var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", OpenAPIFilePath = tempFilePath, IncludeAdditionalData = false }, _httpClient);
+        var document = await builder.CreateOpenApiDocumentAsync(fs);
+        var node = builder.CreateUriSpace(document);
+        var codeModel = builder.CreateSourceModel(node);
+        Assert.NotNull(codeModel);
+        var rbClass = codeModel.FindChildByName<CodeClass>("directoryObjectRequestBuilder");
+        Assert.NotNull(rbClass);
+        var postMethod = rbClass.FindChildByName<CodeMethod>("Post", false);
+        Assert.NotNull(postMethod);
+        var bodyParameter = postMethod.Parameters.FirstOrDefault(static x => x.IsOfKind(CodeParameterKind.RequestBody));
+        Assert.NotNull(bodyParameter);
+        Assert.Equal("directoryObjectPostRequestBody", bodyParameter.Type.Name, StringComparer.OrdinalIgnoreCase);
+        var addressClass = codeModel.FindChildByName<CodeClass>("Address");
+        Assert.NotNull(addressClass);
+    }
+    [Fact]
+    public async Task SupportsMultipleContentTypesAsRequestBodyWithDefaultMimeTypes()
+    {
+        var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName());
+        await using var fs = await GetDocumentStream(@"openapi: 3.0.1
+info:
+  title: Example
+  description: Example
+  version: 1.0.1
+servers:
+  - url: https://example.org
+paths:
+  /directoryObject:
+    post:
+      requestBody:
+        content:
+          multipart/form-data:
+            schema:
+              type: object
+              properties:
+                id:
+                  type: string
+                  format: uuid
+                address:
+                  $ref: '#/components/schemas/address'
+                profileImage:
+                  type: string
+                  format: binary
+          application/json:
+            schema:
+              type: object
+              properties:
+                id:
+                  type: string
+                  format: uuid
+                address:
+                  $ref: '#/components/schemas/address'
+                profileImage:
+                  type: string
+                  format: binary
+        responses:
+          '204':
+            content:
+              application/json:
+                schema:
+                  type: string
+components:
+  schemas:
+    address:
+      type: object
+      properties:
+        street:
+          type: string
+        city:
+          type: string");
+        var mockLogger = new Mock<ILogger<KiotaBuilder>>();
+        var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", OpenAPIFilePath = tempFilePath, IncludeAdditionalData = false }, _httpClient);
+        var document = await builder.CreateOpenApiDocumentAsync(fs);
+        var node = builder.CreateUriSpace(document);
+        var codeModel = builder.CreateSourceModel(node);
+        Assert.NotNull(codeModel);
+        var rbClass = codeModel.FindChildByName<CodeClass>("directoryObjectRequestBuilder");
+        Assert.NotNull(rbClass);
+        var postMethod = rbClass.FindChildByName<CodeMethod>("Post", false);
+        Assert.NotNull(postMethod);
+        var bodyParameter = postMethod.Parameters.FirstOrDefault(static x => x.IsOfKind(CodeParameterKind.RequestBody));
+        Assert.NotNull(bodyParameter);
+        Assert.Equal("directoryObjectPostRequestBody", bodyParameter.Type.Name, StringComparer.OrdinalIgnoreCase);
+        var addressClass = codeModel.FindChildByName<CodeClass>("Address");
+        Assert.NotNull(addressClass);
+    }
+    [Fact]
+    public async Task SupportsMultipleContentTypesAsRequestBodyWithMultipartPriorityNoEncoding()
+    {
+        var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName());
+        await using var fs = await GetDocumentStream(@"openapi: 3.0.1
+info:
+  title: Example
+  description: Example
+  version: 1.0.1
+servers:
+  - url: https://example.org
+paths:
+  /directoryObject:
+    post:
+      requestBody:
+        content:
+          multipart/form-data:
+            schema:
+              type: object
+              properties:
+                id:
+                  type: string
+                  format: uuid
+                address:
+                  $ref: '#/components/schemas/address'
+                profileImage:
+                  type: string
+                  format: binary
+          application/json:
+            schema:
+              type: object
+              properties:
+                id:
+                  type: string
+                  format: uuid
+                address:
+                  $ref: '#/components/schemas/address'
+                profileImage:
+                  type: string
+                  format: binary
+        responses:
+          '204':
+            content:
+              application/json:
+                schema:
+                  type: string
+components:
+  schemas:
+    address:
+      type: object
+      properties:
+        street:
+          type: string
+        city:
+          type: string");
+        var mockLogger = new Mock<ILogger<KiotaBuilder>>();
+        var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", OpenAPIFilePath = tempFilePath, IncludeAdditionalData = false, StructuredMimeTypes = new StructuredMimeTypesCollection { "multipart/form-data;q=1", "application/json;q=0.1" } }, _httpClient);
+        var document = await builder.CreateOpenApiDocumentAsync(fs);
+        var node = builder.CreateUriSpace(document);
+        var codeModel = builder.CreateSourceModel(node);
+        Assert.NotNull(codeModel);
+        var rbClass = codeModel.FindChildByName<CodeClass>("directoryObjectRequestBuilder");
+        Assert.NotNull(rbClass);
+        var postMethod = rbClass.FindChildByName<CodeMethod>("Post", false);
+        Assert.NotNull(postMethod);
+        var bodyParameter = postMethod.Parameters.FirstOrDefault(static x => x.IsOfKind(CodeParameterKind.RequestBody));
+        Assert.NotNull(bodyParameter);
+        Assert.Equal("directoryObjectPostRequestBody", bodyParameter.Type.Name, StringComparer.OrdinalIgnoreCase);
+        var addressClass = codeModel.FindChildByName<CodeClass>("Address");
+        Assert.NotNull(addressClass);
+    }
+    [Fact]
+    public async Task SupportsMultipleContentTypesAsRequestBodyWithMultipartPriorityAndEncoding()
+    {
+        var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName());
+        await using var fs = await GetDocumentStream(@"openapi: 3.0.1
+info:
+  title: Example
+  description: Example
+  version: 1.0.1
+servers:
+  - url: https://example.org
+paths:
+  /directoryObject:
+    post:
+      requestBody:
+        content:
+          multipart/form-data:
+            schema:
+              type: object
+              properties:
+                id:
+                  type: string
+                  format: uuid
+                address:
+                  $ref: '#/components/schemas/address'
+                profileImage:
+                  type: string
+                  format: binary
+            encoding:
+              id:
+                contentType: text/plain
+              address:
+                contentType: application/json
+              profileImage:
+                contentType: image/png
+          application/json:
+            schema:
+              type: object
+              properties:
+                id:
+                  type: string
+                  format: uuid
+                address:
+                  $ref: '#/components/schemas/address'
+                profileImage:
+                  type: string
+                  format: binary
+        responses:
+          '204':
+            content:
+              application/json:
+                schema:
+                  type: string
+components:
+  schemas:
+    address:
+      type: object
+      properties:
+        street:
+          type: string
+        city:
+          type: string");
+        var mockLogger = new Mock<ILogger<KiotaBuilder>>();
+        var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", OpenAPIFilePath = tempFilePath, IncludeAdditionalData = false, StructuredMimeTypes = new StructuredMimeTypesCollection { "multipart/form-data;q=1", "application/json;q=0.1" } }, _httpClient);
+        var document = await builder.CreateOpenApiDocumentAsync(fs);
+        var node = builder.CreateUriSpace(document);
+        var codeModel = builder.CreateSourceModel(node);
+        Assert.NotNull(codeModel);
+        var rbClass = codeModel.FindChildByName<CodeClass>("directoryObjectRequestBuilder");
+        Assert.NotNull(rbClass);
+        var postMethod = rbClass.FindChildByName<CodeMethod>("Post", false);
+        Assert.NotNull(postMethod);
+        var bodyParameter = postMethod.Parameters.FirstOrDefault(static x => x.IsOfKind(CodeParameterKind.RequestBody));
+        Assert.NotNull(bodyParameter);
+        Assert.Equal("MultipartBody", bodyParameter.Type.Name, StringComparer.OrdinalIgnoreCase);
+        var addressClass = codeModel.FindChildByName<CodeClass>("Address");
+        Assert.NotNull(addressClass);
+    }
+    [Fact]
     public async Task ComplexInheritanceStructures()
     {
         var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName());