diff --git a/src/OpenApi/sample/Program.cs b/src/OpenApi/sample/Program.cs
index 90e039c29f86..0ce2d85244ec 100644
--- a/src/OpenApi/sample/Program.cs
+++ b/src/OpenApi/sample/Program.cs
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using Microsoft.AspNetCore.Mvc;
using Microsoft.OpenApi.Models;
using Sample.Transformers;
@@ -21,6 +22,7 @@
});
});
builder.Services.AddOpenApi("responses");
+builder.Services.AddOpenApi("forms");
var app = builder.Build();
@@ -30,6 +32,18 @@
app.MapSwaggerUi();
}
+var forms = app.MapGroup("forms")
+ .WithGroupName("forms");
+
+if (app.Environment.IsDevelopment())
+{
+ forms.DisableAntiforgery();
+}
+
+forms.MapPost("/form-file", (IFormFile resume) => Results.Ok(resume.FileName));
+forms.MapPost("/form-files", (IFormFileCollection files) => Results.Ok(files.Count));
+forms.MapPost("/form-todo", ([FromForm] Todo todo) => Results.Ok(todo));
+
var v1 = app.MapGroup("v1")
.WithGroupName("v1");
var v2 = app.MapGroup("v2")
diff --git a/src/OpenApi/src/Extensions/ApiDescriptionExtensions.cs b/src/OpenApi/src/Extensions/ApiDescriptionExtensions.cs
index 814475a82cb6..9e134604641a 100644
--- a/src/OpenApi/src/Extensions/ApiDescriptionExtensions.cs
+++ b/src/OpenApi/src/Extensions/ApiDescriptionExtensions.cs
@@ -2,6 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
using System.Text;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.ModelBinding;
@@ -81,4 +83,34 @@ public static bool IsRequestBodyParameter(this ApiParameterDescription apiParame
apiParameterDescription.Source == BindingSource.Body ||
apiParameterDescription.Source == BindingSource.FormFile ||
apiParameterDescription.Source == BindingSource.Form;
+
+ ///
+ /// Retrieves the form parameters from the ApiDescription, if they exist.
+ ///
+ /// The ApiDescription to resolve form parameters from.
+ /// A list of associated with the form parameters.
+ /// if form parameters were found, otherwise.
+ public static bool TryGetFormParameters(this ApiDescription apiDescription, out IEnumerable formParameters)
+ {
+ formParameters = apiDescription.ParameterDescriptions.Where(parameter => parameter.Source == BindingSource.Form || parameter.Source == BindingSource.FormFile);
+ return formParameters.Any();
+ }
+
+ ///
+ /// Retrieves the body parameter from the ApiDescription, if it exists.
+ ///
+ /// The ApiDescription to resolve the body parameter from.
+ /// The associated with the body parameter.
+ /// if a single body parameter was found, otherwise.
+ public static bool TryGetBodyParameter(this ApiDescription apiDescription, [NotNullWhen(true)] out ApiParameterDescription? bodyParameter)
+ {
+ bodyParameter = null;
+ var bodyParameters = apiDescription.ParameterDescriptions.Where(parameter => parameter.Source == BindingSource.Body);
+ if (bodyParameters.Count() == 1)
+ {
+ bodyParameter = bodyParameters.Single();
+ return true;
+ }
+ return false;
+ }
}
diff --git a/src/OpenApi/src/Services/OpenApiComponentService.cs b/src/OpenApi/src/Services/OpenApiComponentService.cs
index f327ee1c34b3..e949dc6f8236 100644
--- a/src/OpenApi/src/Services/OpenApiComponentService.cs
+++ b/src/OpenApi/src/Services/OpenApiComponentService.cs
@@ -1,6 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Collections.Concurrent;
+using Microsoft.AspNetCore.Http;
+using Microsoft.OpenApi.Models;
+
namespace Microsoft.AspNetCore.OpenApi;
///
@@ -10,4 +14,25 @@ namespace Microsoft.AspNetCore.OpenApi;
///
internal sealed class OpenApiComponentService
{
+ private readonly ConcurrentDictionary _schemas = new()
+ {
+ // Pre-populate OpenAPI schemas for well-defined types in ASP.NET Core.
+ [typeof(IFormFile)] = new OpenApiSchema { Type = "string", Format = "binary" },
+ [typeof(IFormFileCollection)] = new OpenApiSchema
+ {
+ Type = "array",
+ Items = new OpenApiSchema { Type = "string", Format = "binary" }
+ },
+ };
+
+ internal OpenApiSchema GetOrCreateSchema(Type type)
+ {
+ return _schemas.GetOrAdd(type, _ => CreateSchema());
+ }
+
+ // TODO: Implement this method to create a schema for a given type.
+ private static OpenApiSchema CreateSchema()
+ {
+ return new OpenApiSchema { Type = "string" };
+ }
}
diff --git a/src/OpenApi/src/Services/OpenApiDocumentService.cs b/src/OpenApi/src/Services/OpenApiDocumentService.cs
index faca9d9b759d..d96ec88905c8 100644
--- a/src/OpenApi/src/Services/OpenApiDocumentService.cs
+++ b/src/OpenApi/src/Services/OpenApiDocumentService.cs
@@ -28,6 +28,9 @@ internal sealed class OpenApiDocumentService(
IServiceProvider serviceProvider)
{
private readonly OpenApiOptions _options = optionsMonitor.Get(documentName);
+ private readonly OpenApiComponentService _componentService = serviceProvider.GetRequiredKeyedService(documentName);
+
+ private static readonly OpenApiEncoding _defaultFormEncoding = new OpenApiEncoding { Style = ParameterStyle.Form, Explode = true };
///
/// Cache of instances keyed by the
@@ -124,7 +127,7 @@ private Dictionary GetOperations(IGrouping capturedTags)
+ private OpenApiOperation GetOperation(ApiDescription description, HashSet capturedTags)
{
var tags = GetTags(description);
if (tags != null)
@@ -140,6 +143,7 @@ private static OpenApiOperation GetOperation(ApiDescription description, HashSet
Description = GetDescription(description),
Responses = GetResponses(description),
Parameters = GetParameters(description),
+ RequestBody = GetRequestBody(description),
Tags = tags,
};
return operation;
@@ -256,4 +260,78 @@ private static OpenApiResponse GetResponse(ApiDescription apiDescription, int st
}
return parameters;
}
+
+ private OpenApiRequestBody? GetRequestBody(ApiDescription description)
+ {
+ // Only one parameter can be bound from the body in each request.
+ if (description.TryGetBodyParameter(out var bodyParameter))
+ {
+ return GetJsonRequestBody(description.SupportedRequestFormats, bodyParameter);
+ }
+ // If there are no body parameters, check for form parameters.
+ // Note: Form parameters and body parameters cannot exist simultaneously
+ // in the same endpoint.
+ if (description.TryGetFormParameters(out var formParameters))
+ {
+ return GetFormRequestBody(description.SupportedRequestFormats, formParameters);
+ }
+ return null;
+ }
+
+ private OpenApiRequestBody GetFormRequestBody(IList supportedRequestFormats, IEnumerable formParameters)
+ {
+ if (supportedRequestFormats.Count == 0)
+ {
+ // Assume "application/x-www-form-urlencoded" as the default media type
+ // to match the default assumed in IFormFeature.
+ supportedRequestFormats = [new ApiRequestFormat { MediaType = "application/x-www-form-urlencoded" }];
+ }
+
+ var requestBody = new OpenApiRequestBody
+ {
+ Required = formParameters.Any(parameter => parameter.IsRequired),
+ Content = new Dictionary()
+ };
+
+ // Forms are represented as objects with properties for each form field.
+ var schema = new OpenApiSchema { Type = "object", Properties = new Dictionary() };
+ foreach (var parameter in formParameters)
+ {
+ schema.Properties[parameter.Name] = _componentService.GetOrCreateSchema(parameter.Type);
+ }
+
+ foreach (var requestFormat in supportedRequestFormats)
+ {
+ var contentType = requestFormat.MediaType;
+ requestBody.Content[contentType] = new OpenApiMediaType
+ {
+ Schema = schema,
+ Encoding = new Dictionary() { [contentType] = _defaultFormEncoding }
+ };
+ }
+
+ return requestBody;
+ }
+
+ private static OpenApiRequestBody GetJsonRequestBody(IList supportedRequestFormats, ApiParameterDescription bodyParameter)
+ {
+ if (supportedRequestFormats.Count == 0)
+ {
+ supportedRequestFormats = [new ApiRequestFormat { MediaType = "application/json" }];
+ }
+
+ var requestBody = new OpenApiRequestBody
+ {
+ Required = bodyParameter.IsRequired,
+ Content = new Dictionary()
+ };
+
+ foreach (var requestForm in supportedRequestFormats)
+ {
+ var contentType = requestForm.MediaType;
+ requestBody.Content[contentType] = new OpenApiMediaType();
+ }
+
+ return requestBody;
+ }
}
diff --git a/src/OpenApi/test/Services/OpenApiDocumentServiceTests.Info.cs b/src/OpenApi/test/Services/OpenApiDocumentServiceTests.Info.cs
index 3145eb5c4553..3f201381c0f7 100644
--- a/src/OpenApi/test/Services/OpenApiDocumentServiceTests.Info.cs
+++ b/src/OpenApi/test/Services/OpenApiDocumentServiceTests.Info.cs
@@ -23,7 +23,7 @@ public void GetOpenApiInfo_RespectsHostEnvironmentName()
new Mock().Object,
hostEnvironment,
new Mock>().Object,
- new Mock().Object);
+ new Mock().Object);
// Act
var info = docService.GetOpenApiInfo();
@@ -45,7 +45,7 @@ public void GetOpenApiInfo_RespectsDocumentName()
new Mock().Object,
hostEnvironment,
new Mock>().Object,
- new Mock().Object);
+ new Mock().Object);
// Act
var info = docService.GetOpenApiInfo();
diff --git a/src/OpenApi/test/Services/OpenApiDocumentServiceTests.RequestBody.cs b/src/OpenApi/test/Services/OpenApiDocumentServiceTests.RequestBody.cs
new file mode 100644
index 000000000000..16604a533fe8
--- /dev/null
+++ b/src/OpenApi/test/Services/OpenApiDocumentServiceTests.RequestBody.cs
@@ -0,0 +1,391 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.OpenApi.Models;
+
+public partial class OpenApiDocumentServiceTests : OpenApiDocumentServiceTestBase
+{
+ [Fact]
+ public async Task GetRequestBody_VerifyDefaultFormEncoding()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+
+ // Act
+ builder.MapPost("/", (IFormFile formFile) => { });
+
+ // Assert -- The defaults for form encoding are Explode = true and Style = Form
+ // which align with the encoding formats that are used by ASP.NET Core's binding layer.
+ await VerifyOpenApiDocument(builder, document =>
+ {
+ var paths = Assert.Single(document.Paths.Values);
+ var operation = paths.Operations[OperationType.Post];
+ Assert.NotNull(operation.RequestBody);
+ Assert.NotNull(operation.RequestBody.Content);
+ var content = Assert.Single(operation.RequestBody.Content);
+ Assert.Equal("multipart/form-data", content.Key);
+ var encoding = content.Value.Encoding["multipart/form-data"];
+ Assert.True(encoding.Explode);
+ Assert.Equal(ParameterStyle.Form, encoding.Style);
+ });
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task GetRequestBody_HandlesIFormFile(bool withAttribute)
+ {
+ // Arrange
+ var builder = CreateBuilder();
+
+ // Act
+ if (withAttribute)
+ {
+ builder.MapPost("/", ([FromForm] IFormFile formFile) => { });
+ }
+ else
+ {
+ builder.MapPost("/", (IFormFile formFile) => { });
+ }
+
+ // Assert
+ await VerifyOpenApiDocument(builder, document =>
+ {
+ var paths = Assert.Single(document.Paths.Values);
+ var operation = paths.Operations[OperationType.Post];
+ Assert.NotNull(operation.RequestBody);
+ Assert.False(operation.RequestBody.Required);
+ Assert.NotNull(operation.RequestBody.Content);
+ var content = Assert.Single(operation.RequestBody.Content);
+ Assert.Equal("multipart/form-data", content.Key);
+ Assert.Equal("object", content.Value.Schema.Type);
+ Assert.NotNull(content.Value.Schema.Properties);
+ Assert.Contains("formFile", content.Value.Schema.Properties);
+ var formFileProperty = content.Value.Schema.Properties["formFile"];
+ Assert.Equal("string", formFileProperty.Type);
+ Assert.Equal("binary", formFileProperty.Format);
+ });
+ }
+
+#nullable enable
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task GetRequestBody_HandlesIFormFileOptionality(bool isOptional)
+ {
+ // Arrange
+ var builder = CreateBuilder();
+
+ // Act
+ if (isOptional)
+ {
+ builder.MapPost("/", (IFormFile? formFile) => { });
+ }
+ else
+ {
+ builder.MapPost("/", (IFormFile formFile) => { });
+ }
+
+ // Assert
+ await VerifyOpenApiDocument(builder, document =>
+ {
+ var paths = Assert.Single(document.Paths.Values);
+ var operation = paths.Operations[OperationType.Post];
+ Assert.NotNull(operation.RequestBody);
+ Assert.Equal(!isOptional, operation.RequestBody.Required);
+ });
+ }
+#nullable restore
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task GetRequestBody_HandlesIFormFileCollection(bool withAttribute)
+ {
+ // Arrange
+ var builder = CreateBuilder();
+
+ // Act
+ if (withAttribute)
+ {
+ builder.MapPost("/", ([FromForm] IFormFileCollection formFileCollection) => { });
+ }
+ else
+ {
+ builder.MapPost("/", (IFormFileCollection formFileCollection) => { });
+ }
+
+ // Assert
+ await VerifyOpenApiDocument(builder, document =>
+ {
+ var paths = Assert.Single(document.Paths.Values);
+ var operation = paths.Operations[OperationType.Post];
+ Assert.NotNull(operation.RequestBody);
+ Assert.False(operation.RequestBody.Required);
+ Assert.NotNull(operation.RequestBody.Content);
+ var content = Assert.Single(operation.RequestBody.Content);
+ Assert.Equal("multipart/form-data", content.Key);
+ Assert.Equal("object", content.Value.Schema.Type);
+ Assert.NotNull(content.Value.Schema.Properties);
+ Assert.Contains("formFileCollection", content.Value.Schema.Properties);
+ var formFileProperty = content.Value.Schema.Properties["formFileCollection"];
+ Assert.Equal("array", formFileProperty.Type);
+ Assert.Equal("string", formFileProperty.Items.Type);
+ Assert.Equal("binary", formFileProperty.Items.Format);
+ });
+ }
+
+#nullable enable
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task GetRequestBody_HandlesIFormFileCollectionOptionality(bool isOptional)
+ {
+ // Arrange
+ var builder = CreateBuilder();
+
+ // Act
+ if (isOptional)
+ {
+ builder.MapPost("/", (IFormFileCollection? formFile) => { });
+ }
+ else
+ {
+ builder.MapPost("/", (IFormFileCollection formFile) => { });
+ }
+
+ // Assert
+ await VerifyOpenApiDocument(builder, document =>
+ {
+ var paths = Assert.Single(document.Paths.Values);
+ var operation = paths.Operations[OperationType.Post];
+ Assert.NotNull(operation.RequestBody);
+ Assert.Equal(!isOptional, operation.RequestBody.Required);
+ });
+ }
+#nullable restore
+
+ [Fact]
+ public async Task GetRequestBody_MultipleFormFileParameters()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+
+ // Act
+ builder.MapPost("/", (IFormFile formFile1, IFormFile formFile2) => { });
+
+ // Assert
+ await VerifyOpenApiDocument(builder, document =>
+ {
+ var paths = Assert.Single(document.Paths.Values);
+ var operation = paths.Operations[OperationType.Post];
+ Assert.NotNull(operation.RequestBody);
+ Assert.NotNull(operation.RequestBody.Content);
+ var content = Assert.Single(operation.RequestBody.Content);
+ Assert.Equal("multipart/form-data", content.Key);
+ Assert.Equal("object", content.Value.Schema.Type);
+ Assert.NotNull(content.Value.Schema.Properties);
+ Assert.Contains("formFile1", content.Value.Schema.Properties);
+ Assert.Contains("formFile2", content.Value.Schema.Properties);
+ var formFile1Property = content.Value.Schema.Properties["formFile1"];
+ Assert.Equal("string", formFile1Property.Type);
+ Assert.Equal("binary", formFile1Property.Format);
+ var formFile2Property = content.Value.Schema.Properties["formFile2"];
+ Assert.Equal("string", formFile2Property.Type);
+ Assert.Equal("binary", formFile2Property.Format);
+ });
+ }
+
+ [Fact]
+ public async Task GetRequestBody_IFormFileHandlesAcceptsMetadata()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+
+ // Act
+ builder.MapPost("/", (IFormFile formFile) => { }).Accepts(typeof(IFormFile), "application/magic-foo-content-type");
+
+ // Assert
+ await VerifyOpenApiDocument(builder, document =>
+ {
+ var paths = Assert.Single(document.Paths.Values);
+ var operation = paths.Operations[OperationType.Post];
+ Assert.NotNull(operation.RequestBody);
+ Assert.NotNull(operation.RequestBody.Content);
+ var content = Assert.Single(operation.RequestBody.Content);
+ Assert.Equal("application/magic-foo-content-type", content.Key);
+ Assert.Equal("object", content.Value.Schema.Type);
+ Assert.NotNull(content.Value.Schema.Properties);
+ Assert.Contains("formFile", content.Value.Schema.Properties);
+ var formFileProperty = content.Value.Schema.Properties["formFile"];
+ Assert.Equal("string", formFileProperty.Type);
+ Assert.Equal("binary", formFileProperty.Format);
+ });
+ }
+
+ [Fact]
+ public async Task GetRequestBody_IFormFileHandlesConsumesAttribute()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+
+ // Act
+ builder.MapPost("/", [Consumes(typeof(IFormFile), "application/magic-foo-content-type")] (IFormFile formFile) => { });
+
+ // Assert
+ await VerifyOpenApiDocument(builder, document =>
+ {
+ var paths = Assert.Single(document.Paths.Values);
+ var operation = paths.Operations[OperationType.Post];
+ Assert.NotNull(operation.RequestBody);
+ Assert.NotNull(operation.RequestBody.Content);
+ var content = Assert.Single(operation.RequestBody.Content);
+ Assert.Equal("application/magic-foo-content-type", content.Key);
+ Assert.Equal("object", content.Value.Schema.Type);
+ Assert.NotNull(content.Value.Schema.Properties);
+ Assert.Contains("formFile", content.Value.Schema.Properties);
+ var formFileProperty = content.Value.Schema.Properties["formFile"];
+ Assert.Equal("string", formFileProperty.Type);
+ Assert.Equal("binary", formFileProperty.Format);
+ });
+ }
+
+ [Fact]
+ public async Task GetRequestBody_HandlesJsonBody()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+
+ // Act
+ builder.MapPost("/", (TodoWithDueDate name) => { });
+
+ // Assert
+ await VerifyOpenApiDocument(builder, document =>
+ {
+ var paths = Assert.Single(document.Paths.Values);
+ var operation = paths.Operations[OperationType.Post];
+ Assert.NotNull(operation.RequestBody);
+ Assert.False(operation.RequestBody.Required);
+ Assert.NotNull(operation.RequestBody.Content);
+ var content = Assert.Single(operation.RequestBody.Content);
+ Assert.Equal("application/json", content.Key);
+ });
+ }
+
+#nullable enable
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task GetRequestBody_HandlesJsonBodyOptionality(bool isOptional)
+ {
+ // Arrange
+ var builder = CreateBuilder();
+
+ // Act
+ if (isOptional)
+ {
+ builder.MapPost("/", (TodoWithDueDate? name) => { });
+ }
+ else
+ {
+ builder.MapPost("/", (TodoWithDueDate name) => { });
+ }
+
+ // Assert
+ await VerifyOpenApiDocument(builder, document =>
+ {
+ var paths = Assert.Single(document.Paths.Values);
+ var operation = paths.Operations[OperationType.Post];
+ Assert.NotNull(operation.RequestBody);
+ Assert.Equal(!isOptional, operation.RequestBody.Required);
+ });
+
+ }
+#nullable restore
+
+ [Fact]
+ public async Task GetRequestBody_HandlesJsonBodyWithAttribute()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+
+ // Act
+ builder.MapPost("/", ([FromBody] string name) => { });
+
+ // Assert
+ await VerifyOpenApiDocument(builder, document =>
+ {
+ var paths = Assert.Single(document.Paths.Values);
+ var operation = paths.Operations[OperationType.Post];
+ Assert.NotNull(operation.RequestBody);
+ Assert.False(operation.RequestBody.Required);
+ Assert.NotNull(operation.RequestBody.Content);
+ var content = Assert.Single(operation.RequestBody.Content);
+ Assert.Equal("application/json", content.Key);
+ });
+ }
+
+ [Fact]
+ public async Task GetRequestBody_HandlesJsonBodyWithAcceptsMetadata()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+
+ // Act
+ builder.MapPost("/", (string name) => { }).Accepts(typeof(string), "application/magic-foo-content-type");
+
+ // Assert
+ await VerifyOpenApiDocument(builder, document =>
+ {
+ var paths = Assert.Single(document.Paths.Values);
+ var operation = paths.Operations[OperationType.Post];
+ Assert.NotNull(operation.RequestBody);
+ Assert.NotNull(operation.RequestBody.Content);
+ var content = Assert.Single(operation.RequestBody.Content);
+ Assert.Equal("application/magic-foo-content-type", content.Key);
+ });
+ }
+
+ [Fact]
+ public async Task GetRequestBody_HandlesJsonBodyWithConsumesAttribute()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+
+ // Act
+ builder.MapPost("/", [Consumes(typeof(string), "application/magic-foo-content-type")] (string name) => { });
+
+ // Assert
+ await VerifyOpenApiDocument(builder, document =>
+ {
+ var paths = Assert.Single(document.Paths.Values);
+ var operation = paths.Operations[OperationType.Post];
+ Assert.NotNull(operation.RequestBody);
+ Assert.NotNull(operation.RequestBody.Content);
+ var content = Assert.Single(operation.RequestBody.Content);
+ Assert.Equal("application/magic-foo-content-type", content.Key);
+ });
+ }
+
+ [Fact]
+ public async Task GetOpenApiRequestBody_SetsNullRequestBodyWithNoParameters()
+ {
+ // Arrange
+ var builder = CreateBuilder();
+
+ // Act
+ builder.MapPost("/", (string name) => { });
+
+ // Assert
+ await VerifyOpenApiDocument(builder, document =>
+ {
+ var paths = Assert.Single(document.Paths.Values);
+ var operation = paths.Operations[OperationType.Post];
+ Assert.Null(operation.RequestBody);
+ });
+ }
+
+}
diff --git a/src/OpenApi/test/Services/OpenApiDocumentServiceTestsBase.cs b/src/OpenApi/test/Services/OpenApiDocumentServiceTestsBase.cs
index c517dd7e9bd2..ac3bd3b6e3d4 100644
--- a/src/OpenApi/test/Services/OpenApiDocumentServiceTestsBase.cs
+++ b/src/OpenApi/test/Services/OpenApiDocumentServiceTestsBase.cs
@@ -91,6 +91,7 @@ private class TestServiceProvider : IServiceProvider, IKeyedServiceProvider
public static TestServiceProvider Instance { get; } = new TestServiceProvider();
private IKeyedServiceProvider _serviceProvider;
internal OpenApiDocumentService TestDocumentService { get; set; }
+ internal OpenApiComponentService TestComponentService { get; set; } = new OpenApiComponentService();
public void SetInternalServiceProvider(IServiceCollection serviceCollection)
{
@@ -103,6 +104,10 @@ public object GetKeyedService(Type serviceType, object serviceKey)
{
return TestDocumentService;
}
+ if (serviceType == typeof(OpenApiComponentService))
+ {
+ return TestComponentService;
+ }
return _serviceProvider.GetKeyedService(serviceType, serviceKey);
}
@@ -113,6 +118,10 @@ public object GetRequiredKeyedService(Type serviceType, object serviceKey)
{
return TestDocumentService;
}
+ if (serviceType == typeof(OpenApiComponentService))
+ {
+ return TestComponentService;
+ }
return _serviceProvider.GetRequiredKeyedService(serviceType, serviceKey);
}