Skip to content

Commit 922cd0b

Browse files
author
Mirroring
committed
Merge commit 'b439f9bd782f2889978471480da21de1bcaabbf1'
2 parents d6cd455 + b439f9b commit 922cd0b

File tree

6 files changed

+175
-5
lines changed

6 files changed

+175
-5
lines changed

src/OpenApi/src/Comparers/OpenApiSchemaComparer.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ public bool Equals(OpenApiSchema? x, OpenApiSchema? y)
2424
return true;
2525
}
2626

27+
// If a local reference is present, we can't compare the schema directly
28+
// and should instead use the schema ID as a type-check to assert if the schemas are
29+
// equivalent.
30+
if ((x.Reference != null && y.Reference == null)
31+
|| (x.Reference == null && y.Reference != null))
32+
{
33+
return SchemaIdEquals(x, y);
34+
}
35+
2736
// Compare property equality in an order that should help us find inequality faster
2837
return
2938
x.Type == y.Type &&

src/OpenApi/src/Services/OpenApiDocumentService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,7 @@ private async Task<OpenApiResponse> GetResponseAsync(
411411
"Query" => ParameterLocation.Query,
412412
"Header" => ParameterLocation.Header,
413413
"Path" => ParameterLocation.Path,
414-
_ => throw new InvalidOperationException($"Unsupported parameter source: {parameter.Source.Id}")
414+
_ => ParameterLocation.Query
415415
},
416416
Required = IsRequired(parameter),
417417
Schema = await _componentService.GetOrCreateSchemaAsync(GetTargetType(description, parameter), scopedServiceProvider, schemaTransformers, parameter, cancellationToken: cancellationToken),

src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ internal sealed class OpenApiSchemaService(
3232
IOptionsMonitor<OpenApiOptions> optionsMonitor)
3333
{
3434
private readonly OpenApiSchemaStore _schemaStore = serviceProvider.GetRequiredKeyedService<OpenApiSchemaStore>(documentName);
35-
private readonly OpenApiJsonSchemaContext _jsonSchemaContext = new OpenApiJsonSchemaContext(new(jsonOptions.Value.SerializerOptions));
35+
private readonly OpenApiJsonSchemaContext _jsonSchemaContext = new(new(jsonOptions.Value.SerializerOptions));
3636
private readonly JsonSerializerOptions _jsonSerializerOptions = new(jsonOptions.Value.SerializerOptions)
3737
{
3838
// In order to properly handle the `RequiredAttribute` on type properties, add a modifier to support
@@ -102,7 +102,7 @@ internal sealed class OpenApiSchemaService(
102102
// "nested": "#/properties/nested" becomes "nested": "#/components/schemas/NestedType"
103103
if (jsonPropertyInfo.PropertyType == jsonPropertyInfo.DeclaringType)
104104
{
105-
return new JsonObject { [OpenApiSchemaKeywords.RefKeyword] = context.TypeInfo.GetSchemaReferenceId() };
105+
return new JsonObject { [OpenApiSchemaKeywords.RefKeyword] = createSchemaReferenceId(context.TypeInfo) };
106106
}
107107
schema.ApplyNullabilityContextInfo(jsonPropertyInfo);
108108
}
@@ -212,7 +212,13 @@ private async Task InnerApplySchemaTransformersAsync(OpenApiSchema schema,
212212
}
213213
}
214214
}
215-
}
215+
216+
if (schema is { AdditionalPropertiesAllowed: true, AdditionalProperties: not null } && jsonTypeInfo.ElementType is not null)
217+
{
218+
var elementTypeInfo = _jsonSerializerOptions.GetTypeInfo(jsonTypeInfo.ElementType);
219+
await InnerApplySchemaTransformersAsync(schema.AdditionalProperties, elementTypeInfo, null, context, transformer, cancellationToken);
220+
}
221+
}
216222

217223
private JsonNode CreateSchema(OpenApiSchemaKey key)
218224
=> JsonSchemaExporter.GetJsonSchemaAsNode(_jsonSerializerOptions, key.Type, _configuration);

src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentService/OpenApiDocumentServiceTests.Parameters.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Microsoft.AspNetCore.Builder;
55
using Microsoft.AspNetCore.Http;
66
using Microsoft.AspNetCore.Mvc;
7+
using Microsoft.AspNetCore.Mvc.ModelBinding;
78
using Microsoft.OpenApi.Models;
89

910
public partial class OpenApiDocumentServiceTests : OpenApiDocumentServiceTestBase
@@ -190,4 +191,29 @@ await VerifyOpenApiDocument(builder, document =>
190191
Assert.Null(document.Paths["/api/content-type-lower"].Operations[OperationType.Get].Parameters);
191192
});
192193
}
194+
195+
[Fact]
196+
public async Task GetOpenApiParameters_ToleratesCustomBindingSource()
197+
{
198+
var action = CreateActionDescriptor(nameof(ActionWithCustomBinder));
199+
200+
await VerifyOpenApiDocument(action, document =>
201+
{
202+
var operation = document.Paths["/custom-binding"].Operations[OperationType.Get];
203+
var parameter = Assert.Single(operation.Parameters);
204+
Assert.Equal("model", parameter.Name);
205+
Assert.Equal(ParameterLocation.Query, parameter.In);
206+
});
207+
}
208+
209+
[Route("/custom-binding")]
210+
private void ActionWithCustomBinder([ModelBinder(BinderType = typeof(CustomBinder))] Todo model) { }
211+
212+
public class CustomBinder : IModelBinder
213+
{
214+
public Task BindModelAsync(ModelBindingContext bindingContext)
215+
{
216+
return Task.CompletedTask;
217+
}
218+
}
193219
}

src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/Implementations/OpenApiSchemaReferenceTransformerTests.cs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,4 +477,126 @@ await VerifyOpenApiDocument(builder, options, document =>
477477
Assert.Equal(ReferenceType.Link, responseSchema.Reference.Type);
478478
});
479479
}
480+
481+
[Fact]
482+
public async Task SupportsNestedSchemasWithSelfReference()
483+
{
484+
// Arrange
485+
var builder = CreateBuilder();
486+
487+
builder.MapPost("/", (LocationContainer item) => { });
488+
489+
await VerifyOpenApiDocument(builder, document =>
490+
{
491+
var operation = document.Paths["/"].Operations[OperationType.Post];
492+
var requestSchema = operation.RequestBody.Content["application/json"].Schema;
493+
494+
// Assert $ref used for top-level
495+
Assert.Equal("LocationContainer", requestSchema.Reference.Id);
496+
497+
// Assert that $ref is used for nested LocationDto
498+
var locationContainerSchema = requestSchema.GetEffective(document);
499+
Assert.Equal("LocationDto", locationContainerSchema.Properties["location"].Reference.Id);
500+
501+
// Assert that $ref is used for nested AddressDto
502+
var locationSchema = locationContainerSchema.Properties["location"].GetEffective(document);
503+
Assert.Equal("AddressDto", locationSchema.Properties["address"].Reference.Id);
504+
505+
// Assert that $ref is used for related LocationDto
506+
var addressSchema = locationSchema.Properties["address"].GetEffective(document);
507+
Assert.Equal("LocationDto", addressSchema.Properties["relatedLocation"].Reference.Id);
508+
});
509+
}
510+
511+
[Fact]
512+
public async Task SupportsListNestedSchemasWithSelfReference()
513+
{
514+
// Arrange
515+
var builder = CreateBuilder();
516+
517+
builder.MapPost("/", (ParentObject item) => { });
518+
519+
await VerifyOpenApiDocument(builder, document =>
520+
{
521+
var operation = document.Paths["/"].Operations[OperationType.Post];
522+
var requestSchema = operation.RequestBody.Content["application/json"].Schema;
523+
524+
// Assert $ref used for top-level
525+
Assert.Equal("ParentObject", requestSchema.Reference.Id);
526+
527+
// Assert that $ref is used for nested Children
528+
var parentSchema = requestSchema.GetEffective(document);
529+
Assert.Equal("ChildObject", parentSchema.Properties["children"].Items.Reference.Id);
530+
531+
// Assert that $ref is used for nested Parent
532+
var childSchema = parentSchema.Properties["children"].Items.GetEffective(document);
533+
Assert.Equal("ParentObject", childSchema.Properties["parent"].Reference.Id);
534+
});
535+
}
536+
537+
[Fact]
538+
public async Task SupportsMultiplePropertiesWithSameType()
539+
{
540+
// Arrange
541+
var builder = CreateBuilder();
542+
543+
builder.MapPost("/", (Root item) => { });
544+
545+
await VerifyOpenApiDocument(builder, document =>
546+
{
547+
var operation = document.Paths["/"].Operations[OperationType.Post];
548+
var requestSchema = operation.RequestBody.Content["application/json"].Schema;
549+
550+
// Assert $ref used for top-level
551+
Assert.Equal("Root", requestSchema.Reference.Id);
552+
553+
// Assert that $ref is used for nested Item1
554+
var rootSchema = requestSchema.GetEffective(document);
555+
Assert.Equal("Item", rootSchema.Properties["item1"].Reference.Id);
556+
557+
// Assert that $ref is used for nested Item2
558+
Assert.Equal("Item", rootSchema.Properties["item2"].Reference.Id);
559+
});
560+
}
561+
562+
private class Root
563+
{
564+
public Item Item1 { get; set; } = null!;
565+
public Item Item2 { get; set; } = null!;
566+
}
567+
568+
private class Item
569+
{
570+
public string[] Name { get; set; } = null!;
571+
public int value { get; set; }
572+
}
573+
574+
private class LocationContainer
575+
{
576+
public LocationDto Location { get; set; }
577+
}
578+
579+
private class LocationDto
580+
{
581+
public AddressDto Address { get; set; }
582+
}
583+
584+
private class AddressDto
585+
{
586+
public LocationDto RelatedLocation { get; set; }
587+
}
588+
589+
#nullable enable
590+
private class ParentObject
591+
{
592+
public int Id { get; set; }
593+
public List<ChildObject> Children { get; set; } = [];
594+
}
595+
596+
private class ChildObject
597+
{
598+
public int Id { get; set; }
599+
public required ParentObject Parent { get; set; }
600+
}
480601
}
602+
#nullable restore

src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/SchemaTransformerTests.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,7 @@ public async Task SchemaTransformer_CanModifyItemTypesInADocument()
444444

445445
builder.MapGet("/list", () => new List<int> { 1, 2, 3, 4 });
446446
builder.MapGet("/single", () => 1);
447+
builder.MapGet("/dictionary", () => new Dictionary<string, int> {{ "key", 1 }});
447448

448449
var options = new OpenApiOptions();
449450
options.AddSchemaTransformer((schema, context, cancellationToken) =>
@@ -469,7 +470,13 @@ await VerifyOpenApiDocument(builder, options, document =>
469470
getOperation = path.Operations[OperationType.Get];
470471
responseSchema = getOperation.Responses["200"].Content["application/json"].Schema.GetEffective(document);
471472
Assert.Equal("modified-number-format", responseSchema.Format);
472-
});
473+
474+
// Assert that the schema represent dictionary values has been modified
475+
path = document.Paths["/dictionary"];
476+
getOperation = path.Operations[OperationType.Get];
477+
responseSchema = getOperation.Responses["200"].Content["application/json"].Schema.GetEffective(document);
478+
Assert.Equal("modified-number-format", responseSchema.AdditionalProperties.Format);
479+
});
473480
}
474481

475482
[Fact]

0 commit comments

Comments
 (0)