diff --git a/src/Microsoft.AspNetCore.Mvc.Api.Analyzers/DeclaredApiResponseMetadata.cs b/src/Microsoft.AspNetCore.Mvc.Api.Analyzers/DeclaredApiResponseMetadata.cs
index a45b1ed573..30819931d0 100644
--- a/src/Microsoft.AspNetCore.Mvc.Api.Analyzers/DeclaredApiResponseMetadata.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Api.Analyzers/DeclaredApiResponseMetadata.cs
@@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
internal readonly struct DeclaredApiResponseMetadata
{
public static DeclaredApiResponseMetadata ImplicitResponse { get; } =
- new DeclaredApiResponseMetadata(statusCode: 0, attributeData: null, attributeSource: null, @implicit: true, @default: false);
+ new DeclaredApiResponseMetadata(statusCode: 200, attributeData: null, attributeSource: null, @implicit: true, @default: false);
public static DeclaredApiResponseMetadata ForProducesResponseType(int statusCode, AttributeData attributeData, IMethodSymbol attributeSource)
{
@@ -41,8 +41,16 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
public IMethodSymbol AttributeSource { get; }
+ ///
+ /// True if this is the implicit 200 associated with an
+ /// action specifying no metadata.
+ ///
public bool IsImplicit { get; }
+ ///
+ /// True if this is from a ProducesDefaultResponseTypeAttribute.
+ /// Matches all failure (400 and above) status codes.
+ ///
public bool IsDefault { get; }
internal static bool Contains(IList declaredApiResponseMetadata, ActualApiResponseMetadata actualMetadata)
diff --git a/test/Mvc.Api.Analyzers.Test/ApiConventionAnalyzerIntegrationTest.cs b/test/Mvc.Api.Analyzers.Test/ApiConventionAnalyzerIntegrationTest.cs
index f611c8d421..5e68cdbe54 100644
--- a/test/Mvc.Api.Analyzers.Test/ApiConventionAnalyzerIntegrationTest.cs
+++ b/test/Mvc.Api.Analyzers.Test/ApiConventionAnalyzerIntegrationTest.cs
@@ -26,6 +26,10 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
public Task NoDiagnosticsAreReturned_ForApiController_WithAllDocumentedStatusCodes()
=> RunNoDiagnosticsAreReturned();
+ [Fact]
+ public Task NoDiagnosticsAreReturned_ForOkResultReturningAction()
+ => RunNoDiagnosticsAreReturned();
+
[Fact]
public Task NoDiagnosticsAreReturned_ForApiController_IfStatusCodesCannotBeInferred()
=> RunNoDiagnosticsAreReturned();
diff --git a/test/Mvc.Api.Analyzers.Test/DeclaredApiResponseMetadataTest.cs b/test/Mvc.Api.Analyzers.Test/DeclaredApiResponseMetadataTest.cs
new file mode 100644
index 0000000000..fcfc65ebef
--- /dev/null
+++ b/test/Mvc.Api.Analyzers.Test/DeclaredApiResponseMetadataTest.cs
@@ -0,0 +1,169 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
+{
+ public class DeclaredApiResponseMetadataTest
+ {
+ private readonly ReturnStatementSyntax ReturnStatement = SyntaxFactory.ReturnStatement();
+ private readonly AttributeData AttributeData = new TestAttributeData();
+
+ [Fact]
+ public void Matches_ReturnsTrue_IfDeclaredMetadataIsImplicit_AndActualMetadataIsDefaultResponse()
+ {
+ // Arrange
+ var declaredMetadata = DeclaredApiResponseMetadata.ImplicitResponse;
+ var actualMetadata = new ActualApiResponseMetadata(ReturnStatement);
+
+ // Act
+ var matches = declaredMetadata.Matches(actualMetadata);
+
+ // Assert
+ Assert.True(matches);
+ }
+
+ [Fact]
+ public void Matches_ReturnsTrue_IfDeclaredMetadataIsImplicit_AndActualMetadataReturns200()
+ {
+ // Arrange
+ var declaredMetadata = DeclaredApiResponseMetadata.ImplicitResponse;
+ var actualMetadata = new ActualApiResponseMetadata(ReturnStatement, 200);
+
+ // Act
+ var matches = declaredMetadata.Matches(actualMetadata);
+
+ // Assert
+ Assert.True(matches);
+ }
+
+ [Fact]
+ public void Matches_ReturnsTrue_IfDeclaredMetadataIs200_AndActualMetadataIsDefaultResponse()
+ {
+ // Arrange
+ var declaredMetadata = DeclaredApiResponseMetadata.ForProducesResponseType(200, AttributeData, Mock.Of());
+ var actualMetadata = new ActualApiResponseMetadata(ReturnStatement);
+
+ // Act
+ var matches = declaredMetadata.Matches(actualMetadata);
+
+ // Assert
+ Assert.True(matches);
+ }
+
+ ///
+ /// [ProducesResponseType(201)]
+ /// public IActionResult SomeAction => new Model();
+ ///
+ [Fact]
+ public void Matches_ReturnsTrue_IfDeclaredMetadataIs201_AndActualMetadataIsDefault()
+ {
+ // Arrange
+ var declaredMetadata = DeclaredApiResponseMetadata.ForProducesResponseType(201, AttributeData, Mock.Of());
+ var actualMetadata = new ActualApiResponseMetadata(ReturnStatement);
+
+ // Act
+ var matches = declaredMetadata.Matches(actualMetadata);
+
+ // Assert
+ Assert.True(matches);
+ }
+
+ ///
+ /// [ProducesResponseType(201)]
+ /// public IActionResult SomeAction => Ok(new Model());
+ ///
+ [Fact]
+ public void Matches_ReturnsFalse_IfDeclaredMetadataIs201_AndActualMetadataIs200()
+ {
+ // Arrange
+ var declaredMetadata = DeclaredApiResponseMetadata.ForProducesResponseType(201, AttributeData, Mock.Of());
+ var actualMetadata = new ActualApiResponseMetadata(ReturnStatement, 200);
+
+ // Act
+ var matches = declaredMetadata.Matches(actualMetadata);
+
+ // Assert
+ Assert.False(matches);
+ }
+
+ [Fact]
+ public void Matches_ReturnsTrue_IfDeclaredMetadataAndActualMetadataHaveSameStatusCode()
+ {
+ // Arrange
+ var declaredMetadata = DeclaredApiResponseMetadata.ForProducesResponseType(302, AttributeData, Mock.Of());
+ var actualMetadata = new ActualApiResponseMetadata(ReturnStatement, 302);
+
+ // Act
+ var matches = declaredMetadata.Matches(actualMetadata);
+
+ // Assert
+ Assert.True(matches);
+ }
+
+ [Theory]
+ [InlineData(400)]
+ [InlineData(409)]
+ [InlineData(500)]
+ public void Matches_ReturnsTrue_IfDeclaredMetadataIsDefault_AndActualMetadataIsErrorStatusCode(int actualStatusCode)
+ {
+ // Arrange
+ var declaredMetadata = DeclaredApiResponseMetadata.ForProducesDefaultResponse(AttributeData, Mock.Of());
+ var actualMetadata = new ActualApiResponseMetadata(ReturnStatement, actualStatusCode);
+
+ // Act
+ var matches = declaredMetadata.Matches(actualMetadata);
+
+ // Assert
+ Assert.True(matches);
+ }
+
+ [Fact]
+ public void Matches_ReturnsFalse_IfDeclaredMetadataIsDefault_AndActualMetadataIsNotErrorStatusCode()
+ {
+ // Arrange
+ var declaredMetadata = DeclaredApiResponseMetadata.ForProducesDefaultResponse(AttributeData, Mock.Of());
+ var actualMetadata = new ActualApiResponseMetadata(ReturnStatement, 204);
+
+ // Act
+ var matches = declaredMetadata.Matches(actualMetadata);
+
+ // Assert
+ Assert.False(matches);
+ }
+
+ [Fact]
+ public void Matches_ReturnsFalse_IfDeclaredMetadataIsDefault_AndActualMetadataIsDefaultResponse()
+ {
+ // Arrange
+ var declaredMetadata = DeclaredApiResponseMetadata.ForProducesDefaultResponse(AttributeData, Mock.Of());
+ var actualMetadata = new ActualApiResponseMetadata(ReturnStatement);
+
+ // Act
+ var matches = declaredMetadata.Matches(actualMetadata);
+
+ // Assert
+ Assert.False(matches);
+ }
+
+ private class TestAttributeData : AttributeData
+ {
+ protected override INamedTypeSymbol CommonAttributeClass => throw new System.NotImplementedException();
+
+ protected override IMethodSymbol CommonAttributeConstructor => throw new System.NotImplementedException();
+
+ protected override SyntaxReference CommonApplicationSyntaxReference => throw new System.NotImplementedException();
+
+ protected override ImmutableArray CommonConstructorArguments => throw new System.NotImplementedException();
+
+ protected override ImmutableArray> CommonNamedArguments => throw new System.NotImplementedException();
+ }
+ }
+}
diff --git a/test/Mvc.Api.Analyzers.Test/SymbolApiResponseMetadataProviderTest.cs b/test/Mvc.Api.Analyzers.Test/SymbolApiResponseMetadataProviderTest.cs
index 483901ffa1..c417a199fe 100644
--- a/test/Mvc.Api.Analyzers.Test/SymbolApiResponseMetadataProviderTest.cs
+++ b/test/Mvc.Api.Analyzers.Test/SymbolApiResponseMetadataProviderTest.cs
@@ -1,11 +1,13 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Analyzer.Testing;
+using Microsoft.AspNetCore.Mvc.Api.Analyzers.TestFiles.SymbolApiResponseMetadataProviderTest;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Xunit;
@@ -433,6 +435,95 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
Assert.True(actualResponseMetadata.Value.IsDefaultResponse);
}
+ [Fact]
+ public async Task TryGetActualResponseMetadata_ActionWithActionResultOfTReturningOkResult()
+ {
+ // Arrange
+ var typeName = typeof(TryGetActualResponseMetadataController).FullName;
+ var methodName = nameof(TryGetActualResponseMetadataController.ActionWithActionResultOfTReturningOkResult);
+
+ // Act
+ var (success, responseMetadatas, _) = await TryGetActualResponseMetadata(typeName, methodName);
+
+ // Assert
+ Assert.True(success);
+ Assert.Collection(
+ responseMetadatas,
+ metadata =>
+ {
+ Assert.False(metadata.IsDefaultResponse);
+ Assert.Equal(200, metadata.StatusCode);
+ });
+ }
+
+ [Fact]
+ public async Task TryGetActualResponseMetadata_ActionWithActionResultOfTReturningModel()
+ {
+ // Arrange
+ var typeName = typeof(TryGetActualResponseMetadataController).FullName;
+ var methodName = nameof(TryGetActualResponseMetadataController.ActionWithActionResultOfTReturningModel);
+
+ // Act
+ var (success, responseMetadatas, _) = await TryGetActualResponseMetadata(typeName, methodName);
+
+ // Assert
+ Assert.True(success);
+ Assert.Collection(
+ responseMetadatas,
+ metadata =>
+ {
+ Assert.True(metadata.IsDefaultResponse);
+ });
+ }
+
+ [Fact]
+ public async Task TryGetActualResponseMetadata_ActionReturningNotFoundAndModel()
+ {
+ // Arrange
+ var typeName = typeof(TryGetActualResponseMetadataController).FullName;
+ var methodName = nameof(TryGetActualResponseMetadataController.ActionReturningNotFoundAndModel);
+
+ // Act
+ var (success, responseMetadatas, testSource) = await TryGetActualResponseMetadata(typeName, methodName);
+
+ // Assert
+ Assert.True(success);
+ Assert.Collection(
+ responseMetadatas,
+ metadata =>
+ {
+ Assert.False(metadata.IsDefaultResponse);
+ Assert.Equal(204, metadata.StatusCode);
+ AnalyzerAssert.DiagnosticLocation(testSource.MarkerLocations["MM1"], metadata.ReturnStatement.GetLocation());
+
+ },
+ metadata =>
+ {
+ Assert.True(metadata.IsDefaultResponse);
+ AnalyzerAssert.DiagnosticLocation(testSource.MarkerLocations["MM2"], metadata.ReturnStatement.GetLocation());
+ });
+ }
+
+ private async Task<(bool result, IList responseMetadatas, TestSource testSource)> TryGetActualResponseMetadata(string typeName, string methodName)
+ {
+ var testSource = MvcTestSource.Read(GetType().Name, "TryGetActualResponseMetadataTests");
+ var project = DiagnosticProject.Create(GetType().Assembly, new[] { testSource.Source });
+
+ var compilation = await GetCompilation("TryGetActualResponseMetadataTests");
+
+ var type = compilation.GetTypeByMetadataName(typeName);
+ var method = (IMethodSymbol)type.GetMembers(methodName).First();
+ var symbolCache = new ApiControllerSymbolCache(compilation);
+
+ var syntaxTree = method.DeclaringSyntaxReferences[0].SyntaxTree;
+ var methodSyntax = (MethodDeclarationSyntax)syntaxTree.GetRoot().FindNode(method.Locations[0].SourceSpan);
+ var semanticModel = compilation.GetSemanticModel(syntaxTree);
+
+ var result = SymbolApiResponseMetadataProvider.TryGetActualResponseMetadata(symbolCache, semanticModel, methodSyntax, CancellationToken.None, out var responseMetadatas);
+
+ return (result, responseMetadatas, testSource);
+ }
+
private async Task RunInspectReturnStatementSyntax([CallerMemberName]string test = null)
{
// Arrange
diff --git a/test/Mvc.Api.Analyzers.Test/TestFiles/ApiConventionAnalyzerIntegrationTest/NoDiagnosticsAreReturned_ForOkResultReturningAction.cs b/test/Mvc.Api.Analyzers.Test/TestFiles/ApiConventionAnalyzerIntegrationTest/NoDiagnosticsAreReturned_ForOkResultReturningAction.cs
new file mode 100644
index 0000000000..e5ae8feb2c
--- /dev/null
+++ b/test/Mvc.Api.Analyzers.Test/TestFiles/ApiConventionAnalyzerIntegrationTest/NoDiagnosticsAreReturned_ForOkResultReturningAction.cs
@@ -0,0 +1,19 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
+{
+ [ApiController]
+ public class NoDiagnosticsAreReturned_ForOkResultReturningAction : ControllerBase
+ {
+ public async Task>> Action()
+ {
+ await Task.Yield();
+ var models = new List();
+
+ return Ok(models);
+ }
+ }
+
+ public class NoDiagnosticsAreReturned_ForOkResultReturningActionModel { }
+}
diff --git a/test/Mvc.Api.Analyzers.Test/TestFiles/SymbolApiResponseMetadataProviderTest/TryGetActualResponseMetadataTests.cs b/test/Mvc.Api.Analyzers.Test/TestFiles/SymbolApiResponseMetadataProviderTest/TryGetActualResponseMetadataTests.cs
new file mode 100644
index 0000000000..38e4bc1a81
--- /dev/null
+++ b/test/Mvc.Api.Analyzers.Test/TestFiles/SymbolApiResponseMetadataProviderTest/TryGetActualResponseMetadataTests.cs
@@ -0,0 +1,38 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.Mvc.Api.Analyzers.TestFiles.SymbolApiResponseMetadataProviderTest
+{
+ public class TryGetActualResponseMetadataController : ControllerBase
+ {
+ public async Task>> ActionWithActionResultOfTReturningOkResult()
+ {
+ await Task.Yield();
+ var models = new List();
+
+ return Ok(models);
+ }
+
+ public async Task>> ActionWithActionResultOfTReturningModel()
+ {
+ await Task.Yield();
+ var models = new List();
+
+ return models;
+ }
+
+ public async Task> ActionReturningNotFoundAndModel(int id)
+ {
+ await Task.Yield();
+
+ if (id == 0)
+ {
+ /*MM1*/return NoContent();
+ }
+
+ /*MM2*/return new TryGetActualResponseMetadataModel();
+ }
+ }
+
+ public class TryGetActualResponseMetadataModel { }
+}