Regression in C# contract generation with oneOf/allOf combination since v1.27.0 (also in v1.28.0)
Nobody has claimed this yet.
- Dominant language
- C#
- Stars
- 3.8k
- Forks
- 333
- Avg merge
- 16h 29m
- Merged PRs (30d)
- 116
Description
### 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
There seems to be a regression in C# contract generation going from v1.26.1 to v1.27.0 and which is also present in v1.28.0.
The linked OpenAPI specification generates the following code for `v1.26.1` where we can see that the class is inheriting from `ComponentCommon` and it has a property `One`:
```csharp
//
#pragma warning disable CS0618
using Microsoft.Kiota.Abstractions.Extensions;
using Microsoft.Kiota.Abstractions.Serialization;
using System.Collections.Generic;
using System.IO;
using System;
namespace ApiSdk.Models
{
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
#pragma warning disable CS1591
public partial class ExampleWithSingleOneOfWithTypeObject : global::ApiSdk.Models.ComponentCommon, IParsable
#pragma warning restore CS1591
{
/// The one property
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public string? One { get; set; }
#nullable restore
#else
public string One { get; set; }
#endif
///
/// Creates a new instance of the appropriate class based on discriminator value
///
/// A
/// The parse node to use to read the discriminator value and create the object
public static new global::ApiSdk.Models.ExampleWithSingleOneOfWithTypeObject CreateFromDiscriminatorValue(IParseNode parseNode)
{
_ = parseNode ?? throw new ArgumentNullException(nameof(parseNode));
return new global::ApiSdk.Models.ExampleWithSingleOneOfWithTypeObject();
}
///
/// The deserialization information for the current model
///
/// A IDictionary<string, Action<IParseNode>>
public override IDictionary> GetFieldDeserializers()
{
return new Dictionary>(base.GetFieldDeserializers())
{
{ "one", n => { One = n.GetStringValue(); } },
};
}
///
/// Serializes information the current object
///
/// Serialization writer to use to serialize this model
public override void Serialize(ISerializationWriter writer)
{
_ = writer ?? throw new ArgumentNullException(nameof(writer));
base.Serialize(writer);
writer.WriteStringValue("one", One);
}
}
}
#pragma warning restore CS0618
```
Using the same contract but using version `v1.28.0` (happens also with `v1.27.0`) generates the following:
**NOTE** The class is no longer inheriting from the base class, nor does it have a property anymore.
```csharp
//
#pragma warning disable CS0618
using Microsoft.Kiota.Abstractions.Extensions;
using Microsoft.Kiota.Abstractions.Serialization;
using System.Collections.Generic;
using System.IO;
using System;
namespace ApiSdk.Models
{
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
#pragma warning disable CS1591
public partial class ExampleWithSingleOneOfWithTypeObject : IAdditionalDataHolder, IParsable
#pragma warning restore CS1591
{
/// Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well.
public IDictionary AdditionalData { get; set; }
///
/// Instantiates a new and sets the default values.
///
public ExampleWithSingleOneOfWithTypeObject()
{
AdditionalData = new Dictionary();
}
///
/// Creates a new instance of the appropriate class based on discriminator value
///
/// A
/// The parse node to use to read the discriminator value and create the object
public static global::ApiSdk.Models.ExampleWithSingleOneOfWithTypeObject CreateFromDiscriminatorValue(IParseNode parseNode)
{
_ = parseNode ?? throw new ArgumentNullException(nameof(parseNode));
return new global::ApiSdk.Models.ExampleWithSingleOneOfWithTypeObject();
}
///
/// The deserialization information for the current model
///
/// A IDictionary<string, Action<IParseNode>>
public virtual IDictionary> GetFieldDeserializers()
{
return new Dictionary>
{
};
}
///
/// Serializes information the current object
///
/// Serialization writer to use to serialize this model
public virtual void Serialize(ISerializationWriter writer)
{
_ = writer ?? throw new ArgumentNullException(nameof(writer));
writer.WriteAdditionalData(AdditionalData);
}
}
}
#pragma warning restore CS0618
```
### Expected behavior
The contract should generate the same output for both v1.26.1, v1.27.0 and v1.28.0.
### How to reproduce
The following unit test example can be used in `KiotaBuilderTests.cs`, which succeeds when built & run `v1.26.1` but fails from `v1.27.0` (I believe the exact commit might be `707d36ca05080c6170a4beae8af14ad011590e02`)
```csharp
[Fact]
public async Task InclusiveUnionIntersectionEntriesMergingRegressionAsync()
{
var tempFilePath = Path.GetTempFileName();
await using var fs = await GetDocumentStreamAsync(
"""
openapi: 3.0.0
info:
title: "Generator not generating oneOf if the containing schema has type: object"
version: "1.0.0"
servers:
- url: https://mytodos.doesnotexist/
paths:
/uses-components:
post:
description: Return something
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/UsesComponents"
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/UsesComponents"
components:
schemas:
ExampleWithSingleOneOfWithTypeObject:
type: object
oneOf:
- $ref: "#/components/schemas/Component1"
discriminator:
propertyName: objectType
ExampleWithSingleOneOfWithoutTypeObject:
oneOf:
- $ref: "#/components/schemas/Component2"
discriminator:
propertyName: objectType
UsesComponents:
type: object
properties:
component_with_single_oneof_with_type_object:
$ref: "#/components/schemas/ExampleWithSingleOneOfWithTypeObject"
component_with_single_oneof_without_type_object:
$ref: "#/components/schemas/ExampleWithSingleOneOfWithoutTypeObject"
ComponentCommon:
type: object
required:
- objectType
properties:
objectType:
type: string
common:
type: string
Component1:
type: object
allOf:
- $ref: "#/components/schemas/ComponentCommon"
- type: object
properties:
one:
type: string
Component2:
type: object
allOf:
- $ref: "#/components/schemas/ComponentCommon"
- type: object
properties:
two:
type: string
""");
var mockLogger = new Mock>();
var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", OpenAPIFilePath = tempFilePath }, _httpClient);
var document = await builder.CreateOpenApiDocumentAsync(fs);
var node = builder.CreateUriSpace(document);
var codeModel = builder.CreateSourceModel(node);
// Verify both scenarios have all the properties available from all schemas
var withObjectClass = codeModel.FindChildByName("ExampleWithSingleOneOfWithTypeObject");
Assert.NotNull(withObjectClass);
var withObjectClassOneProperty = withObjectClass.FindChildByName("one", false);
Assert.NotNull(withObjectClassOneProperty);
// ExampleWithSingleOneOfWithTypeObject inherits from ComponentCommon
Assert.Equal("ComponentCommon", withObjectClass.BaseClass?.Name);
var withoutObjectClass = codeModel.FindChildByName("Component2");
Assert.NotNull(withoutObjectClass);
var withoutObjectClassTwoProperty = withoutObjectClass.FindChildByName("two", false);
Assert.NotNull(withoutObjectClassTwoProperty);
// Component2 inherits from ComponentCommon
Assert.Equal("ComponentCommon", withoutObjectClass.BaseClass?.Name);
}
```
### Open API description file
[https://github.com/vipentti/kiota-type-object-one-of-issue/blob/main/DiscriminatorProblemSampleHierarchy.yaml](https://github.com/vipentti/kiota-type-object-one-of-issue/blob/main/DiscriminatorProblemSampleHierarchy.yaml)
### Kiota Version
1.28.0+57130b1b1db3bc5c060498682f41e20c8ae089f2
### Latest Kiota version known to work for scenario above?(Not required)
1.26.1+a5df7c03bab621bb2d4dc728516fc9c488cabadb
### Known Workarounds
_No response_
### Configuration
_No response_
### Debug output
Click to expand log
```
info: Kiota.Builder.KiotaBuilder[0]
Cleaning output directory \.\Generated\Hierarchy\CSharp\
dbug: Kiota.Builder.KiotaBuilder[0]
kiota version 1.28.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.0051335
dbug: Kiota.Builder.KiotaBuilder[0]
step 2 - parsing the document - took 00:00:00.0627746
dbug: Kiota.Builder.KiotaBuilder[0]
step 3 - updating generation configuration from kiota extension - took 00:00:00.0000747
dbug: Kiota.Builder.KiotaBuilder[0]
step 4 - filtering API paths with patterns - took 00:00:00.0037156
info: Kiota.Builder.KiotaBuilder[0]
Client root URL set to https://mytodos.doesnotexist
dbug: Kiota.Builder.KiotaBuilder[0]
step 5 - checking whether the output should be updated - took 00:00:00.0116493
dbug: Kiota.Builder.KiotaBuilder[0]
step 6 - create uri space - took 00:00:00.0020482
dbug: Kiota.Builder.KiotaBuilder[0]
InitializeInheritanceIndex 00:00:00.0021495
dbug: Kiota.Builder.KiotaBuilder[0]
CreateRequestBuilderClass 00:00:00
dbug: Kiota.Builder.KiotaBuilder[0]
MapTypeDefinitions 00:00:00.0032270
info: Kiota.Builder.KiotaBuilder[0]
Removing unused model Component1 as it is not referenced by the client API surface
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.0477832
dbug: Kiota.Builder.KiotaBuilder[0]
14ms: Language refinement applied
dbug: Kiota.Builder.KiotaBuilder[0]
step 8 - refine by language - took 00:00:00.0145327
dbug: Kiota.Builder.KiotaBuilder[0]
step 9 - writing files - took 00:00:00.0201648
info: Kiota.Builder.KiotaBuilder[0]
loaded description from local source
dbug: Kiota.Builder.KiotaBuilder[0]
step 10 - writing lock file - took 00:00:00.0078204
Generation completed successfully
Client base url set to https://mytodos.doesnotexist
dbug: Kiota.Builder.KiotaBuilder[0]
Api manifest path: \apimanifest.json
Hint: use the info command to get the list of dependencies you need to add to your project.
Example: kiota info -d "\.\DiscriminatorProblemSampleHierarchy.yaml" -l CSharp
Hint: use the --include-path and --exclude-path options with glob patterns to filter the paths generated.
Example: kiota generate --include-path "**/foo" -d "\.\DiscriminatorProblemSampleHierarchy.yaml"
```
### Other information
I believe the exact commit which started failing to be `707d36ca05080c6170a4beae8af14ad011590e02`
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the InclusiveUnionIntersectionEntriesMergingRegressionAsync test in KiotaBuilderTests.cs and run it against the supplied OpenAPI contract. Trace KiotaBuilder.CreateOpenApiDocumentAsync, CreateUriSpace, and CreateSourceModel to see where the oneOf/allOf model information is lost. Done means the test finds the one property and ComponentCommon base class, while Component2 retains its two property and base class.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- backend-api-design, tooling
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 55/100