Skip to content

Commit c76e108

Browse files
authored
Support DefaultStatusCode annotation on methods (#39255)
* Support DefaultStatusCode annotation on methods * Address feedback from peer review
1 parent 1e26857 commit c76e108

File tree

6 files changed

+68
-2
lines changed

6 files changed

+68
-2
lines changed

src/Mvc/Mvc.Api.Analyzers/src/ActualApiResponseMetadataFactory.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ void AnalyzeResponseExpression(IReturnOperation returnOperation)
6161
IReturnOperation returnOperation)
6262
{
6363
var returnedValue = returnOperation.ReturnedValue;
64+
var defaultStatusCodeAttributeSymbol = symbolCache.DefaultStatusCodeAttribute;
65+
6466
if (returnedValue is null || returnedValue is IInvalidOperation)
6567
{
6668
return null;
@@ -82,9 +84,23 @@ void AnalyzeResponseExpression(IReturnOperation returnOperation)
8284
}
8385

8486
var defaultStatusCodeAttribute = statementReturnType
85-
.GetAttributes(symbolCache.DefaultStatusCodeAttribute, inherit: true)
87+
.GetAttributes(defaultStatusCodeAttributeSymbol, inherit: true)
8688
.FirstOrDefault();
8789

90+
// If the type is not annotated with a default status code, then examine
91+
// the attributes on any invoked method returning the type.
92+
if (defaultStatusCodeAttribute is null && returnedValue.Syntax is InvocationExpressionSyntax targetInvocation)
93+
{
94+
var methodOperation = returnOperation.SemanticModel.GetSymbolInfo(targetInvocation);
95+
var methodSymbol = methodOperation.Symbol ?? methodOperation.CandidateSymbols.FirstOrDefault();
96+
if (methodSymbol is not null)
97+
{
98+
defaultStatusCodeAttribute = methodSymbol
99+
.GetAttributes(defaultStatusCodeAttributeSymbol)
100+
.FirstOrDefault();
101+
}
102+
}
103+
88104
var statusCode = GetDefaultStatusCode(defaultStatusCodeAttribute);
89105

90106
ITypeSymbol? returnType = null;

src/Mvc/Mvc.Api.Analyzers/test/AddResponseTypeAttributeCodeFixProviderIntegrationTest.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ public class AddResponseTypeAttributeCodeFixProviderIntegrationTest
5555
[Fact]
5656
public Task CodeFixWorksOnExpressionBodiedMethod() => RunTest();
5757

58+
[Fact]
59+
public Task CodeFixWorksWithValidationProblem() => RunTest();
60+
5861
private async Task RunTest([CallerMemberName] string testMethod = "")
5962
{
6063
// Arrange
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Http;
5+
6+
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._INPUT_
7+
{
8+
[ApiController]
9+
[Route("[controller]/[action]")]
10+
public class CodeFixWorksWithValidationProblem : ControllerBase
11+
{
12+
public IActionResult GetProblem()
13+
{
14+
return ValidationProblem();
15+
}
16+
}
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Http;
5+
6+
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._OUTPUT_
7+
{
8+
[ApiController]
9+
[Route("[controller]/[action]")]
10+
public class CodeFixWorksWithValidationProblem : ControllerBase
11+
{
12+
[ProducesResponseType(StatusCodes.Status400BadRequest)]
13+
[ProducesDefaultResponseType]
14+
public IActionResult GetProblem()
15+
{
16+
return ValidationProblem();
17+
}
18+
}
19+
}

src/Mvc/Mvc.Core/src/ControllerBase.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1920,6 +1920,7 @@ public virtual ObjectResult Problem(
19201920
/// </summary>
19211921
/// <returns>The created <see cref="BadRequestObjectResult"/> for the response.</returns>
19221922
[NonAction]
1923+
[DefaultStatusCode(StatusCodes.Status400BadRequest)]
19231924
public virtual ActionResult ValidationProblem([ActionResultObjectValue] ValidationProblemDetails descriptor)
19241925
{
19251926
if (descriptor == null)
@@ -1937,6 +1938,7 @@ public virtual ActionResult ValidationProblem([ActionResultObjectValue] Validati
19371938
/// <param name="modelStateDictionary">The <see cref="ModelStateDictionary"/>.</param>
19381939
/// <returns>The created <see cref="BadRequestObjectResult"/> for the response.</returns>
19391940
[NonAction]
1941+
[DefaultStatusCode(StatusCodes.Status400BadRequest)]
19401942
public virtual ActionResult ValidationProblem([ActionResultObjectValue] ModelStateDictionary modelStateDictionary)
19411943
=> ValidationProblem(detail: null, modelStateDictionary: modelStateDictionary);
19421944

@@ -1947,6 +1949,7 @@ public virtual ActionResult ValidationProblem([ActionResultObjectValue] ModelSta
19471949
/// </summary>
19481950
/// <returns>The created <see cref="ActionResult"/> for the response.</returns>
19491951
[NonAction]
1952+
[DefaultStatusCode(StatusCodes.Status400BadRequest)]
19501953
public virtual ActionResult ValidationProblem()
19511954
=> ValidationProblem(ModelState);
19521955

@@ -1963,6 +1966,7 @@ public virtual ActionResult ValidationProblem()
19631966
/// When <see langword="null"/> uses <see cref="ModelState"/>.</param>
19641967
/// <returns>The created <see cref="ActionResult"/> for the response.</returns>
19651968
[NonAction]
1969+
[DefaultStatusCode(StatusCodes.Status400BadRequest)]
19661970
public virtual ActionResult ValidationProblem(
19671971
string? detail = null,
19681972
string? instance = null,

src/Mvc/Mvc.Core/src/Infrastructure/DefaultStatusCodeAttribute.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,15 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure;
1111
/// </summary>
1212
/// <remarks>
1313
/// This attribute is informational only and does not have any runtime effects.
14+
/// Applying the attribute on a class indicates that the <see cref="ActionResult"/>
15+
/// represented by that class uses a particular status code by default. Applying the
16+
/// attribute to a method indicates that the <see cref="ActionResult"/> returned by the
17+
/// method uses that status code by default. The later is helpful in scenarios where we
18+
/// need to specify that a method modifies the status code that an <see cref="ActionResult"/>
19+
/// uses by default in its logic or for specifying the status code for consumption in
20+
/// the API analyzers.
1421
/// </remarks>
15-
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
22+
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
1623
public sealed class DefaultStatusCodeAttribute : Attribute
1724
{
1825
/// <summary>

0 commit comments

Comments
 (0)