Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,8 @@ private OpenApiSchema CreateObjectSchema(DataContract dataContract, SchemaReposi
}

applicableDataProperties = applicableDataProperties
.Where(dataProperty => dataProperty.MemberInfo.DeclaringType == dataContract.UnderlyingType);
// if the property is declared on a type other than (the one we just added as a base or one of its parents)
.Where(dataProperty => !baseTypeDataContract.UnderlyingType.IsAssignableTo(dataProperty.MemberInfo.DeclaringType));
}

if (IsBaseTypeWithKnownTypesDefined(dataContract, out var knownTypesDataContracts))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using Microsoft.AspNetCore.Mvc;

namespace Swashbuckle.AspNetCore.SwaggerGen.Test;

public class FakeControllerWithInheritance
{
public void ActionWithDerivedObjectParameter([FromBody] AbcTests_C param)
{ }

public List<AbcTests_A> ActionWithDerivedObjectResponse()
{
return null!;
}

public AbcTests_B ActionWithDerivedObjectResponse_ExcludedFromInheritanceConfig()
{
return null!;
}

// Helper test types for GenerateSchema_PreservesIntermediateBaseProperties_WhenUsingOneOfPolymorphism
public abstract class AbcTests_A
{
public string PropA { get; set; }
}

public class AbcTests_B : AbcTests_A
{
public string PropB { get; set; }
}

public class AbcTests_C : AbcTests_B
{
public string PropC { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,60 @@ public void GenerateSchema_SupportsOption_UseAllOfForPolymorphism()
Assert.Equal(["Property2"], subType2Schema.Properties.Keys);
}

[Fact]
public void GenerateSchema_PreservesIntermediateBaseProperties_WhenUsingOneOfPolymorphism()
{
// Arrange - define a type hierarchy A <- B <- C where only A and C are selected as known subtypes
var subject = Subject(configureGenerator: c =>
{
c.UseOneOfForPolymorphism = true;
c.SubTypesSelector = (type) => type == typeof(AbcTests_A) ? new[] { typeof(AbcTests_C) } : Array.Empty<Type>();
});

var schemaRepository = new SchemaRepository();

// Act
var schema = subject.GenerateSchema(typeof(AbcTests_A), schemaRepository);

// Assert - polymorphic schema should be present
Assert.NotNull(schema.OneOf);

// Ensure base A schema contains PropA
Assert.True(schemaRepository.Schemas.ContainsKey(nameof(AbcTests_A)));
var aSchema = schemaRepository.Schemas[nameof(AbcTests_A)];
Assert.True(aSchema.Properties.ContainsKey(nameof(AbcTests_A.PropA)));

// Find the C schema in the OneOf and assert it preserves B's properties while not duplicating A's
var cRef = schema.OneOf
.OfType<OpenApiSchemaReference>()
.First(r => r.Reference.Id == nameof(AbcTests_C));

var cSchema = schemaRepository.Schemas[nameof(AbcTests_C)];

// C should include PropC and properties declared on intermediate B
Assert.True(cSchema.Properties.ContainsKey(nameof(AbcTests_C.PropC)));
Assert.True(cSchema.Properties.ContainsKey(nameof(AbcTests_B.PropB)));

// A's property should not be in C's inline properties because it's provided by the referenced base schema
Assert.False(cSchema.Properties.ContainsKey(nameof(AbcTests_A.PropA)));
}

// Helper test types for the A/B/C regression
public abstract class AbcTests_A
{
public string PropA { get; set; }
}

public class AbcTests_B : AbcTests_A
{
public string PropB { get; set; }
}

public class AbcTests_C : AbcTests_B
{
public string PropC { get; set; }
}

[Fact]
public void GenerateSchema_SupportsOption_UseAllOfToExtendReferenceSchemas()
{
Expand Down
148 changes: 146 additions & 2 deletions test/Swashbuckle.AspNetCore.SwaggerGen.Test/VerifyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1112,6 +1112,146 @@ public async Task ActionHavingFromFormAttributeWithSwaggerIgnore()
await Verify(document);
}

[Fact]
public async Task GenerateSchema_PreservesIntermediateBaseProperties_WhenUsingOneOfPolymorphism()
{
var subject = Subject(
apiDescriptions:
[
ApiDescriptionFactory.Create<FakeControllerWithInheritance>(
c => nameof(c.ActionWithDerivedObjectParameter),
groupName: "v1",
httpMethod: "POST",
relativePath: "resource",
parameterDescriptions:
[
new ApiParameterDescription
{
Name = "param1",
Source = BindingSource.Body,
Type = typeof(FakeControllerWithInheritance.AbcTests_C), // most derived type
ModelMetadata = ModelMetadataFactory.CreateForType(typeof(FakeControllerWithInheritance.AbcTests_C)),
},
],
supportedRequestFormats:
[
new ApiRequestFormat { MediaType = "application/json" },
]),
ApiDescriptionFactory.Create<FakeControllerWithInheritance>(
c => nameof(c.ActionWithDerivedObjectResponse),
groupName: "v1",
httpMethod: "GET",
relativePath: "resource",
parameterDescriptions: [],
supportedResponseTypes: [
new ApiResponseType
{
ApiResponseFormats = [new ApiResponseFormat { MediaType = "application/json" }],
StatusCode = 200,
Type = typeof(FakeControllerWithInheritance.AbcTests_A),
},
]),
ApiDescriptionFactory.Create<FakeControllerWithInheritance>(
c => nameof(c.ActionWithDerivedObjectResponse_ExcludedFromInheritanceConfig),
groupName: "v1",
httpMethod: "GET",
relativePath: "resourceB",
parameterDescriptions: [],
supportedResponseTypes: [
new ApiResponseType
{
ApiResponseFormats = [new ApiResponseFormat { MediaType = "application/json" }],
StatusCode = 200,
Type = typeof(FakeControllerWithInheritance.AbcTests_B),
},
]),
],
configureSchemaGeneratorOptions: c =>
{
c.UseOneOfForPolymorphism = true;
c.SubTypesSelector =
(type) => (Type[])(
type == typeof(FakeControllerWithInheritance.AbcTests_A)
? [typeof(FakeControllerWithInheritance.AbcTests_C)]
: []
);
}
);
var document = subject.GetSwagger("v1");

await Verify(document);
}

[Fact]
public async Task GenerateSchema_PreservesMultiLevelInheritance()
{
var subject = Subject(
apiDescriptions:
[
ApiDescriptionFactory.Create<FakeControllerWithInheritance>(
c => nameof(c.ActionWithDerivedObjectParameter),
groupName: "v1",
httpMethod: "POST",
relativePath: "resource",
parameterDescriptions:
[
new ApiParameterDescription
{
Name = "param1",
Source = BindingSource.Body,
Type = typeof(FakeControllerWithInheritance.AbcTests_C), // most derived type
ModelMetadata = ModelMetadataFactory.CreateForType(typeof(FakeControllerWithInheritance.AbcTests_C)),
},
],
supportedRequestFormats:
[
new ApiRequestFormat { MediaType = "application/json" },
]),
ApiDescriptionFactory.Create<FakeControllerWithInheritance>(
c => nameof(c.ActionWithDerivedObjectResponse),
groupName: "v1",
httpMethod: "GET",
relativePath: "resource",
parameterDescriptions: [],
supportedResponseTypes: [
new ApiResponseType
{
ApiResponseFormats = [new ApiResponseFormat { MediaType = "application/json" }],
StatusCode = 200,
Type = typeof(FakeControllerWithInheritance.AbcTests_A),
},
]),
ApiDescriptionFactory.Create<FakeControllerWithInheritance>(
c => nameof(c.ActionWithDerivedObjectResponse_ExcludedFromInheritanceConfig),
groupName: "v1",
httpMethod: "GET",
relativePath: "resourceB",
parameterDescriptions: [],
supportedResponseTypes: [
new ApiResponseType
{
ApiResponseFormats = [new ApiResponseFormat { MediaType = "application/json" }],
StatusCode = 200,
Type = typeof(FakeControllerWithInheritance.AbcTests_B),
},
]),
],
configureSchemaGeneratorOptions: c =>
{
c.UseOneOfForPolymorphism = true;
c.SubTypesSelector =
(type) => (Type[])(
type == typeof(FakeControllerWithInheritance.AbcTests_A) ? [typeof(FakeControllerWithInheritance.AbcTests_B), typeof(FakeControllerWithInheritance.AbcTests_C)]
: type == typeof(FakeControllerWithInheritance.AbcTests_B) ? [typeof(FakeControllerWithInheritance.AbcTests_C)]
: []
);
}
);
var document = subject.GetSwagger("v1");

await Verify(document);
}

[Fact]
public async Task GetSwagger_Works_As_Expected_When_FromFormObject()
{
Expand Down Expand Up @@ -1465,12 +1605,16 @@ private static SwaggerGenerator Subject(
IEnumerable<ApiDescription> apiDescriptions,
SwaggerGeneratorOptions options = null,
IEnumerable<AuthenticationScheme> authenticationSchemes = null,
List<ISchemaFilter> schemaFilters = null)
List<ISchemaFilter> schemaFilters = null,
Action<SchemaGeneratorOptions> configureSchemaGeneratorOptions = null)
{
var schemaGeneratorOptions = new SchemaGeneratorOptions() { SchemaFilters = schemaFilters ?? [] };
configureSchemaGeneratorOptions?.Invoke(schemaGeneratorOptions);

return new SwaggerGenerator(
options ?? DefaultOptions,
new FakeApiDescriptionGroupCollectionProvider(apiDescriptions),
new SchemaGenerator(new SchemaGeneratorOptions() { SchemaFilters = schemaFilters ?? [] }, new JsonSerializerDataContractResolver(new JsonSerializerOptions())),
new SchemaGenerator(schemaGeneratorOptions, new JsonSerializerDataContractResolver(new JsonSerializerOptions())),
new FakeAuthenticationSchemeProvider(authenticationSchemes ?? [])
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
{
"openapi": "3.0.4",
"info": {
"title": "Test API",
"version": "V1"
},
"paths": {
"/resource": {
"post": {
"tags": [
"FakeWithInheritance"
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AbcTests_C"
}
}
}
},
"responses": {
"200": {
"description": "OK"
}
}
},
"get": {
"tags": [
"FakeWithInheritance"
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/AbcTests_C"
}
]
}
}
}
}
}
}
}
},
"/resourceB": {
"get": {
"tags": [
"FakeWithInheritance"
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AbcTests_B"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"AbcTests_A": {
"type": "object",
"properties": {
"PropA": {
"type": "string",
"nullable": true
}
},
"additionalProperties": false
},
"AbcTests_B": {
"type": "object",
"properties": {
"PropA": {
"type": "string",
"nullable": true
},
"PropB": {
"type": "string",
"nullable": true
}
},
"additionalProperties": false
},
"AbcTests_C": {
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/AbcTests_A"
}
],
"properties": {
"PropB": {
"type": "string",
"nullable": true
},
"PropC": {
"type": "string",
"nullable": true
}
},
"additionalProperties": false
}
}
},
"tags": [
{
"name": "FakeWithInheritance"
}
]
}
Loading