Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve support for OpenAPI in minimal actions #34906

Merged
merged 20 commits into from
Aug 6, 2021
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
f0d2558
Support setting content types in ProducesResponseTypeAttribute to clo…
captainsafia Jul 27, 2021
e6c4335
Add WithName extension method to resolve #34538
captainsafia Jul 27, 2021
74df1e3
Support setting endpoints on group names to resolve #34541
captainsafia Jul 27, 2021
1eefa01
Add OpenAPI extension methods to resolve #33924
captainsafia Jul 28, 2021
a85aab1
Add tests for new OpenAPI methods
captainsafia Jul 29, 2021
d21964e
Add endpoint metadata attributes
captainsafia Jul 29, 2021
57f36ec
Update PublicAPI files with deltas
captainsafia Jul 29, 2021
5add623
Add support for SuppressApi to close #34068
captainsafia Jul 30, 2021
3940f1f
Update tests to account for supporting setting content types
captainsafia Jul 30, 2021
96cec51
Fix up PublicAPI analyzer warnings
captainsafia Jul 31, 2021
d5e6e73
Clean up source files
captainsafia Jul 31, 2021
14dbfab
Address feedback from API review
captainsafia Aug 2, 2021
5c21deb
Fix typo and update type signature
captainsafia Aug 2, 2021
a914a2d
Apply feedback from second API review
captainsafia Aug 3, 2021
8885db7
Update docstrings
captainsafia Aug 3, 2021
504b9f1
Apply suggestions from code review
captainsafia Aug 4, 2021
a3e4171
Address non-test related feedback
captainsafia Aug 5, 2021
c8a1cd9
Handle setting content types for ProducesResponseType attribute
captainsafia Aug 5, 2021
63d6329
Address feedback from peer review
captainsafia Aug 6, 2021
94260c4
Add test for ProducesResponseType override scenario
captainsafia Aug 6, 2021
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 @@ -118,5 +118,31 @@ public static TBuilder WithMetadata<TBuilder>(this TBuilder builder, params obje

return builder;
}

/// <summary>
/// Sets the <see cref="EndpointNameAttribute"/> for all endpoints produced
/// on the target <see cref="IEndpointConventionBuilder"/>.
captainsafia marked this conversation as resolved.
Show resolved Hide resolved
/// </summary>
/// <param name="builder">The <see cref="IEndpointConventionBuilder"/>.</param>
/// <param name="endpointName">The endpoint name.</param>
/// <returns>The <see cref="IEndpointConventionBuilder"/>.</returns>
public static TBuilder WithName<TBuilder>(this TBuilder builder, string endpointName) where TBuilder : IEndpointConventionBuilder
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

excited about this

{
builder.WithMetadata(new EndpointNameAttribute(endpointName));
return builder;
}

/// <summary>
/// Sets the <see cref="EndpointGroupNameAttribute"/> for all endpoints produced
/// on the target <see cref="IEndpointConventionBuilder"/>.
/// </summary>
/// <param name="builder">The <see cref="IEndpointConventionBuilder"/>.</param>
/// <param name="endpointGroupName">The endpoint group name.</param>
/// <returns>The <see cref="IEndpointConventionBuilder"/>.</returns>
public static TBuilder WithGroupName<TBuilder>(this TBuilder builder, string endpointGroupName) where TBuilder : IEndpointConventionBuilder
{
builder.WithMetadata(new EndpointGroupNameAttribute(endpointGroupName));
return builder;
}
}
}
34 changes: 34 additions & 0 deletions src/Http/Routing/src/EndpointGroupNameAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using Microsoft.AspNetCore.Http;

namespace Microsoft.AspNetCore.Routing
{
/// <summary>
/// Specifies the endpoint group name in <see cref="Microsoft.AspNetCore.Http.Endpoint.Metadata"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Delegate | AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class EndpointGroupNameAttribute : Attribute, IEndpointGroupNameMetadata
captainsafia marked this conversation as resolved.
Show resolved Hide resolved
{
/// <summary>
/// Initializes an instance of the EndpointGroupNameAttribute.
captainsafia marked this conversation as resolved.
Show resolved Hide resolved
/// </summary>
/// <param name="endpointGroupName">The endpoint name.</param>
captainsafia marked this conversation as resolved.
Show resolved Hide resolved
public EndpointGroupNameAttribute(string endpointGroupName)
{
if (endpointGroupName == null)
{
throw new ArgumentNullException(nameof(endpointGroupName));
}

EndpointGroupName = endpointGroupName;
}

/// <summary>
/// The endpoint group name.
/// </summary>
public string EndpointGroupName { get; }
}
}
38 changes: 38 additions & 0 deletions src/Http/Routing/src/EndpointNameAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using Microsoft.AspNetCore.Http;

namespace Microsoft.AspNetCore.Routing
{
/// <summary>
/// Specifies the endpoint name in <see cref="Endpoint.Metadata"/>.
/// </summary>
/// <remarks>
/// Endpoint names must be unique within an application, and can be used to unambiguously
/// identify a desired endpoint for URI generation using <see cref="Microsoft.AspNetCore.Routing.LinkGenerator"/>
/// </remarks>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Delegate, Inherited = false, AllowMultiple = false)]
public sealed class EndpointNameAttribute : Attribute, IEndpointNameMetadata
{
/// <summary>
/// Initializes an instance of the EndpointNameAttribute.
/// </summary>
/// <param name="endpointName">The endpoint name.</param>
public EndpointNameAttribute(string endpointName)
{
if (endpointName == null)
{
throw new ArgumentNullException(nameof(endpointName));
}

EndpointName = endpointName;
}

/// <summary>
/// The endpoint name.
/// </summary>
public string EndpointName { get; }
}
}
22 changes: 22 additions & 0 deletions src/Http/Routing/src/ExclueFromApiExplorerAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using Microsoft.AspNetCore.Http;

namespace Microsoft.AspNetCore.Routing
{
/// <summary>
/// Indicates that this <see cref="Endpoint"/> should not be included in the generated API metadata.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Delegate, AllowMultiple = false, Inherited = true)]
public sealed class ExclueFromApiExplorerAttribute : Attribute, IExclueFromApiExplorerMetadata
{
/// <summary>
/// Gets a value indicating whether API explorer
/// data should be excluded for this endpoint. If <see cref="true"/>,
/// API metadata is not emitted.
/// </summary>
public bool ExclueFromApiExplorer => true;
captainsafia marked this conversation as resolved.
Show resolved Hide resolved
}
}
18 changes: 18 additions & 0 deletions src/Http/Routing/src/IEndpointGroupNameMetadata.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// 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.Http;

namespace Microsoft.AspNetCore.Routing
{
/// <summary>
/// Defines a contract used to specify an endpoint group name in <see cref="Endpoint.Metadata"/>.
/// </summary>
public interface IEndpointGroupNameMetadata
{
/// <summary>
/// Gets the endpoint group name.
/// </summary>
string EndpointGroupName { get; }
}
}
20 changes: 20 additions & 0 deletions src/Http/Routing/src/IExclueFromApiExplorerMetadata.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using Microsoft.AspNetCore.Http;

namespace Microsoft.AspNetCore.Routing
{
/// <summary>
/// Indicates wheter or not that API explorer data should be emitted for this endpoint.
captainsafia marked this conversation as resolved.
Show resolved Hide resolved
/// </summary>
public interface IExclueFromApiExplorerMetadata
{
/// <summary>
/// Gets a value indicating whether API explorer
/// data should be emitted for this endpoint.
/// </summary>
bool ExclueFromApiExplorer { get; }
}
}
15 changes: 15 additions & 0 deletions src/Http/Routing/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,18 @@ static Microsoft.AspNetCore.Builder.MinimalActionEndpointRouteBuilderExtensions.
static Microsoft.AspNetCore.Builder.MinimalActionEndpointRouteBuilderExtensions.MapMethods(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! pattern, System.Collections.Generic.IEnumerable<string!>! httpMethods, System.Delegate! action) -> Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder!
static Microsoft.AspNetCore.Builder.MinimalActionEndpointRouteBuilderExtensions.MapPost(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! pattern, System.Delegate! action) -> Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder!
static Microsoft.AspNetCore.Builder.MinimalActionEndpointRouteBuilderExtensions.MapPut(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! pattern, System.Delegate! action) -> Microsoft.AspNetCore.Builder.MinimalActionEndpointConventionBuilder!
Microsoft.AspNetCore.Routing.IEndpointGroupNameMetadata
Microsoft.AspNetCore.Routing.IEndpointGroupNameMetadata.EndpointGroupName.get -> string!
Microsoft.AspNetCore.Routing.EndpointGroupNameAttribute
Microsoft.AspNetCore.Routing.EndpointGroupNameAttribute.EndpointGroupNameAttribute(string! endpointGroupName) -> void
Microsoft.AspNetCore.Routing.EndpointGroupNameAttribute.EndpointGroupName.get -> string!
Microsoft.AspNetCore.Routing.EndpointNameAttribute
Microsoft.AspNetCore.Routing.EndpointNameAttribute.EndpointNameAttribute(string! endpointName) -> void
Microsoft.AspNetCore.Routing.EndpointNameAttribute.EndpointName.get -> string!
static Microsoft.AspNetCore.Builder.RoutingEndpointConventionBuilderExtensions.WithName<TBuilder>(this TBuilder builder, string! endpointName) -> TBuilder
static Microsoft.AspNetCore.Builder.RoutingEndpointConventionBuilderExtensions.WithGroupName<TBuilder>(this TBuilder builder, string! endpointGroupName) -> TBuilder
Microsoft.AspNetCore.Routing.IExclueFromApiExplorerMetadata
Microsoft.AspNetCore.Routing.IExclueFromApiExplorerMetadata.ExclueFromApiExplorer.get -> bool
Microsoft.AspNetCore.Routing.ExclueFromApiExplorerAttribute
Microsoft.AspNetCore.Routing.ExclueFromApiExplorerAttribute.ExclueFromApiExplorerAttribute() -> void
Microsoft.AspNetCore.Routing.ExclueFromApiExplorerAttribute.ExclueFromApiExplorer.get -> bool
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,38 @@ public void WithMetadata_ChainedCall_ReturnedBuilderIsDerivedType()
Assert.True(chainedBuilder.TestProperty);
}

[Fact]
public void WithName_SetsEndpointName()
{
// Arrange
var builder = CreateBuilder();

// Act
builder.WithName("SomeEndpointName");

// Assert
var endpoint = builder.Build();

var endpointName = endpoint.Metadata.GetMetadata<IEndpointNameMetadata>();
Assert.Equal("SomeEndpointName", endpointName.EndpointName);
}

[Fact]
public void WithGroupName_SetsEndpointGroupName()
{
// Arrange
var builder = CreateBuilder();

// Act
builder.WithGroupName("SomeEndpointGroupName");

// Assert
var endpoint = builder.Build();

var endpointGroupName = endpoint.Metadata.GetMetadata<IEndpointGroupNameMetadata>();
Assert.Equal("SomeEndpointGroupName", endpointGroupName.EndpointGroupName);
}

private TestEndpointConventionBuilder CreateBuilder()
{
var conventionBuilder = new DefaultEndpointConventionBuilder(new RouteEndpointBuilder(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ public void OnProvidersExecuting(ApiDescriptionProviderContext context)
{
if (endpoint is RouteEndpoint routeEndpoint &&
routeEndpoint.Metadata.GetMetadata<MethodInfo>() is { } methodInfo &&
routeEndpoint.Metadata.GetMetadata<IHttpMethodMetadata>() is { } httpMethodMetadata)
routeEndpoint.Metadata.GetMetadata<IHttpMethodMetadata>() is { } httpMethodMetadata &&
(routeEndpoint.Metadata.GetMetadata<IExclueFromApiExplorerMetadata>() == null ||
routeEndpoint.Metadata.GetMetadata<IExclueFromApiExplorerMetadata>() is { ExclueFromApiExplorer: false} ))
{
// REVIEW: Should we add an ApiDescription for endpoints without IHttpMethodMetadata? Swagger doesn't handle
// a null HttpMethod even though it's nullable on ApiDescription, so we'd need to define "default" HTTP methods.
Expand Down Expand Up @@ -89,6 +91,7 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string
var apiDescription = new ApiDescription
{
HttpMethod = httpMethod,
GroupName = routeEndpoint.Metadata.GetMetadata<IEndpointGroupNameMetadata>()?.EndpointGroupName,
RelativePath = routeEndpoint.RoutePattern.RawText?.TrimStart('/'),
ActionDescriptor = new ActionDescriptor
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -567,7 +567,7 @@ public void GetApiDescription_ReturnsActionResultWithProduces_And_ProducesConten
// Arrange
var action = CreateActionDescriptor(methodName, controllerType);
action.FilterDescriptors = filterDescriptors;
var expectedMediaTypes = new[] { "application/json", "text/json" };
var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" };

// Act
var descriptions = GetApiDescriptions(action);
Expand Down Expand Up @@ -677,7 +677,7 @@ public void GetApiDescription_ReturnsVoidWithProducesContentType(
// Arrange
var action = CreateActionDescriptor(methodName, controllerType);
action.FilterDescriptors = filterDescriptors;
var expectedMediaTypes = new[] { "application/json", "text/json" };
var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" };

// Act
var descriptions = GetApiDescriptions(action);
Expand Down Expand Up @@ -740,7 +740,7 @@ public void GetApiDescription_ReturnsActionResultOfTWithProducesContentType(
new ProducesResponseTypeAttribute(typeof(ErrorDetails), 500),
FilterScope.Action)
};
var expectedMediaTypes = new[] { "application/json", "text/json" };
var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" };

// Act
var descriptions = GetApiDescriptions(action);
Expand Down Expand Up @@ -810,7 +810,7 @@ public void GetApiDescription_ReturnsActionResultOfTWithProducesContentType_ForS
new ProducesResponseTypeAttribute(typeof(ErrorDetails), 500),
FilterScope.Action)
};
var expectedMediaTypes = new[] { "application/json", "text/json" };
var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" };

// Act
var descriptions = GetApiDescriptions(action);
Expand Down Expand Up @@ -880,7 +880,7 @@ public void GetApiDescription_ReturnsActionResultOfSequenceOfTWithProducesConten
new ProducesResponseTypeAttribute(typeof(ErrorDetails), 500),
FilterScope.Action)
};
var expectedMediaTypes = new[] { "application/json", "text/json" };
var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" };

// Act
var descriptions = GetApiDescriptions(action);
Expand Down
Loading