diff --git a/src/Microsoft.AspNetCore.Mvc.Analyzers/ApiControllerTypeCache.cs b/src/Microsoft.AspNetCore.Mvc.Analyzers/ApiControllerTypeCache.cs new file mode 100644 index 0000000000..8f4b2948e9 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Analyzers/ApiControllerTypeCache.cs @@ -0,0 +1,20 @@ +// 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 Microsoft.CodeAnalysis; + +namespace Microsoft.AspNetCore.Mvc.Analyzers +{ + internal readonly struct ApiControllerTypeCache + { + public ApiControllerTypeCache(Compilation compilation) + { + ApiConventionAttribute = compilation.GetTypeByMetadataName(SymbolNames.ApiConventionAttribute); + ProducesResponseTypeAttribute = compilation.GetTypeByMetadataName(SymbolNames.ProducesResponseTypeAttribute); + } + + public INamedTypeSymbol ApiConventionAttribute { get; } + + public INamedTypeSymbol ProducesResponseTypeAttribute { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Analyzers/CodeAnalysisExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Analyzers/CodeAnalysisExtensions.cs index 6c191174ab..3fca8e3851 100644 --- a/src/Microsoft.AspNetCore.Mvc.Analyzers/CodeAnalysisExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Analyzers/CodeAnalysisExtensions.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using Microsoft.CodeAnalysis; namespace Microsoft.AspNetCore.Mvc.Analyzers @@ -31,26 +32,27 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers } public static bool HasAttribute(this IMethodSymbol methodSymbol, ITypeSymbol attribute, bool inherit) + => GetAttributes(methodSymbol, attribute, inherit).Any(); + + public static IEnumerable GetAttributes(this IMethodSymbol methodSymbol, ITypeSymbol attribute, bool inherit) { Debug.Assert(methodSymbol != null); Debug.Assert(attribute != null); - if (!inherit) - { - return HasAttribute(methodSymbol, attribute); - } - while (methodSymbol != null) { - if (methodSymbol.HasAttribute(attribute)) + foreach (var attributeData in GetAttributes(methodSymbol, attribute)) { - return true; + yield return attributeData; + } + + if (!inherit) + { + break; } methodSymbol = methodSymbol.IsOverride ? methodSymbol.OverriddenMethod : null; } - - return false; } public static bool HasAttribute(this IPropertySymbol propertySymbol, ITypeSymbol attribute, bool inherit) @@ -118,6 +120,17 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers return false; } + private static IEnumerable GetAttributes(this ISymbol symbol, ITypeSymbol attribute) + { + foreach (var declaredAttribute in symbol.GetAttributes()) + { + if (attribute.IsAssignableFrom(declaredAttribute.AttributeClass)) + { + yield return declaredAttribute; + } + } + } + private static IEnumerable GetTypeHierarchy(this ITypeSymbol typeSymbol) { while (typeSymbol != null) diff --git a/src/Microsoft.AspNetCore.Mvc.Analyzers/SymbolApiResponseMetadataProvider.cs b/src/Microsoft.AspNetCore.Mvc.Analyzers/SymbolApiResponseMetadataProvider.cs new file mode 100644 index 0000000000..bfc6935572 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Analyzers/SymbolApiResponseMetadataProvider.cs @@ -0,0 +1,90 @@ +// 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; +using System.Collections.Generic; +using Microsoft.CodeAnalysis; + +namespace Microsoft.AspNetCore.Mvc.Analyzers +{ + internal class SymbolApiResponseMetadataProvider + { + private const string StatusCodeProperty = "StatusCode"; + private const string StatusCodeConstructorParameter = "statusCode"; + + internal static IList GetResponseMetadata(ApiControllerTypeCache typeCache, IMethodSymbol methodSymbol) + { + var responseMetadataAttributes = methodSymbol.GetAttributes(typeCache.ProducesResponseTypeAttribute, inherit: true); + var metadataItems = new List(); + foreach (var attribute in responseMetadataAttributes) + { + var statusCode = GetStatusCode(attribute); + var metadata = new ApiResponseMetadata(statusCode, attribute, convention: null); + + metadataItems.Add(metadata); + } + + return metadataItems; + } + + internal static int GetStatusCode(AttributeData attribute) + { + const int DefaultStatusCode = 200; + for (var i = 0; i < attribute.NamedArguments.Length; i++) + { + var namedArgument = attribute.NamedArguments[i]; + var namedArgumentValue = namedArgument.Value; + if (string.Equals(namedArgument.Key, StatusCodeProperty, StringComparison.Ordinal) && + namedArgumentValue.Kind == TypedConstantKind.Primitive && + (namedArgumentValue.Type.SpecialType & SpecialType.System_Int32) == SpecialType.System_Int32 && + namedArgumentValue.Value is int statusCode) + { + return statusCode; + } + } + + if (attribute.AttributeConstructor == null) + { + return DefaultStatusCode; + } + + var constructorParameters = attribute.AttributeConstructor.Parameters; + for (var i = 0; i < constructorParameters.Length; i++) + { + var parameter = constructorParameters[i]; + if (string.Equals(parameter.Name, StatusCodeConstructorParameter, StringComparison.Ordinal) && + (parameter.Type.SpecialType & SpecialType.System_Int32) == SpecialType.System_Int32) + { + if (attribute.ConstructorArguments.Length < i) + { + return DefaultStatusCode; + } + + var argument = attribute.ConstructorArguments[i]; + if (argument.Kind == TypedConstantKind.Primitive && argument.Value is int statusCode) + { + return statusCode; + } + } + } + + return DefaultStatusCode; + } + } + + internal readonly struct ApiResponseMetadata + { + public ApiResponseMetadata(int statusCode, AttributeData attributeData, IMethodSymbol convention) + { + StatusCode = statusCode; + Attribute = attributeData; + Convention = convention; + } + + public int StatusCode { get; } + + public AttributeData Attribute { get; } + + public IMethodSymbol Convention { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Analyzers/SymbolNames.cs b/src/Microsoft.AspNetCore.Mvc.Analyzers/SymbolNames.cs index 9e9065df04..98cd8666be 100644 --- a/src/Microsoft.AspNetCore.Mvc.Analyzers/SymbolNames.cs +++ b/src/Microsoft.AspNetCore.Mvc.Analyzers/SymbolNames.cs @@ -7,6 +7,8 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers { public const string AllowAnonymousAttribute = "Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute"; + public const string ApiConventionAttribute = "Microsoft.AspNetCore.Mvc.ApiConventionAttribute"; + public const string AuthorizeAttribute = "Microsoft.AspNetCore.Authorization.AuthorizeAttribute"; public const string IFilterMetadataType = "Microsoft.AspNetCore.Mvc.Filters.IFilterMetadata"; @@ -15,12 +17,14 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers public const string IHtmlHelperType = "Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper"; + public const string IRouteTemplateProvider = "Microsoft.AspNetCore.Mvc.Routing.IRouteTemplateProvider"; + public const string PageModelAttributeType = "Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageModelAttribute"; public const string PartialMethod = "Partial"; - public const string RenderPartialMethod = "RenderPartial"; + public const string ProducesResponseTypeAttribute = "Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute"; - public const string IRouteTemplateProvider = "Microsoft.AspNetCore.Mvc.Routing.IRouteTemplateProvider"; + public const string RenderPartialMethod = "RenderPartial"; } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/AcceptedAtActionResult.cs b/src/Microsoft.AspNetCore.Mvc.Core/AcceptedAtActionResult.cs index dd0480c39e..291e25930a 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/AcceptedAtActionResult.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/AcceptedAtActionResult.cs @@ -4,6 +4,7 @@ using System; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Core; +using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; @@ -14,8 +15,11 @@ namespace Microsoft.AspNetCore.Mvc /// /// An that returns a Accepted (202) response with a Location header. /// + [DefaultStatusCode(DefaultStatusCode)] public class AcceptedAtActionResult : ObjectResult { + private const int DefaultStatusCode = StatusCodes.Status202Accepted; + /// /// Initializes a new instance of the with the values /// provided. @@ -34,7 +38,7 @@ namespace Microsoft.AspNetCore.Mvc ActionName = actionName; ControllerName = controllerName; RouteValues = routeValues == null ? null : new RouteValueDictionary(routeValues); - StatusCode = StatusCodes.Status202Accepted; + StatusCode = DefaultStatusCode; } /// @@ -91,4 +95,4 @@ namespace Microsoft.AspNetCore.Mvc context.HttpContext.Response.Headers[HeaderNames.Location] = url; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/AcceptedAtRouteResult.cs b/src/Microsoft.AspNetCore.Mvc.Core/AcceptedAtRouteResult.cs index bfde81d3ae..2fb29b505c 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/AcceptedAtRouteResult.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/AcceptedAtRouteResult.cs @@ -8,14 +8,18 @@ using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Net.Http.Headers; +using Microsoft.AspNetCore.Mvc.Infrastructure; namespace Microsoft.AspNetCore.Mvc { /// /// An that returns a Accepted (202) response with a Location header. /// + [DefaultStatusCode(DefaultStatusCode)] public class AcceptedAtRouteResult : ObjectResult { + private const int DefaultStatusCode = StatusCodes.Status202Accepted; + /// /// Initializes a new instance of the class with the values /// provided. @@ -42,7 +46,7 @@ namespace Microsoft.AspNetCore.Mvc { RouteName = routeName; RouteValues = routeValues == null ? null : new RouteValueDictionary(routeValues); - StatusCode = StatusCodes.Status202Accepted; + StatusCode = DefaultStatusCode; } /// @@ -87,4 +91,4 @@ namespace Microsoft.AspNetCore.Mvc context.HttpContext.Response.Headers[HeaderNames.Location] = url; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/AcceptedResult.cs b/src/Microsoft.AspNetCore.Mvc.Core/AcceptedResult.cs index 30b33a4fc5..cf0d383fd3 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/AcceptedResult.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/AcceptedResult.cs @@ -3,6 +3,7 @@ using System; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Mvc @@ -10,8 +11,11 @@ namespace Microsoft.AspNetCore.Mvc /// /// An that returns an Accepted (202) response with a Location header. /// + [DefaultStatusCode(DefaultStatusCode)] public class AcceptedResult : ObjectResult { + private const int DefaultStatusCode = StatusCodes.Status202Accepted; + /// /// Initializes a new instance of the class with the values /// provided. @@ -19,7 +23,7 @@ namespace Microsoft.AspNetCore.Mvc public AcceptedResult() : base(value: null) { - StatusCode = StatusCodes.Status202Accepted; + StatusCode = DefaultStatusCode; } /// @@ -32,7 +36,7 @@ namespace Microsoft.AspNetCore.Mvc : base(value) { Location = location; - StatusCode = StatusCodes.Status202Accepted; + StatusCode = DefaultStatusCode; } /// @@ -59,7 +63,7 @@ namespace Microsoft.AspNetCore.Mvc Location = locationUri.GetComponents(UriComponents.SerializationInfoString, UriFormat.UriEscaped); } - StatusCode = StatusCodes.Status202Accepted; + StatusCode = DefaultStatusCode; } /// @@ -83,4 +87,4 @@ namespace Microsoft.AspNetCore.Mvc } } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/BadRequestObjectResult.cs b/src/Microsoft.AspNetCore.Mvc.Core/BadRequestObjectResult.cs index f31552a3a6..9af96ff7cc 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/BadRequestObjectResult.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/BadRequestObjectResult.cs @@ -3,6 +3,7 @@ using System; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Microsoft.AspNetCore.Mvc @@ -10,8 +11,11 @@ namespace Microsoft.AspNetCore.Mvc /// /// An that when executed will produce a Bad Request (400) response. /// + [DefaultStatusCode(DefaultStatusCode)] public class BadRequestObjectResult : ObjectResult { + private const int DefaultStatusCode = StatusCodes.Status400BadRequest; + /// /// Creates a new instance. /// @@ -19,7 +23,7 @@ namespace Microsoft.AspNetCore.Mvc public BadRequestObjectResult(object error) : base(error) { - StatusCode = StatusCodes.Status400BadRequest; + StatusCode = DefaultStatusCode; } /// @@ -34,7 +38,7 @@ namespace Microsoft.AspNetCore.Mvc throw new ArgumentNullException(nameof(modelState)); } - StatusCode = StatusCodes.Status400BadRequest; + StatusCode = DefaultStatusCode; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Core/BadRequestResult.cs b/src/Microsoft.AspNetCore.Mvc.Core/BadRequestResult.cs index 69025ee49d..5a034b181a 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/BadRequestResult.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/BadRequestResult.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Infrastructure; namespace Microsoft.AspNetCore.Mvc { @@ -9,13 +10,16 @@ namespace Microsoft.AspNetCore.Mvc /// A that when /// executed will produce a Bad Request (400) response. /// + [DefaultStatusCode(DefaultStatusCode)] public class BadRequestResult : StatusCodeResult { + private const int DefaultStatusCode = StatusCodes.Status400BadRequest; + /// /// Creates a new instance. /// public BadRequestResult() - : base(StatusCodes.Status400BadRequest) + : base(DefaultStatusCode) { } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ConflictObjectResult.cs b/src/Microsoft.AspNetCore.Mvc.Core/ConflictObjectResult.cs index 29e72e816b..93927d78f9 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ConflictObjectResult.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ConflictObjectResult.cs @@ -3,6 +3,7 @@ using System; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Microsoft.AspNetCore.Mvc @@ -10,8 +11,11 @@ namespace Microsoft.AspNetCore.Mvc /// /// An that when executed will produce a Conflict (409) response. /// + [DefaultStatusCode(DefaultStatusCode)] public class ConflictObjectResult : ObjectResult { + private const int DefaultStatusCode = StatusCodes.Status409Conflict; + /// /// Creates a new instance. /// @@ -19,7 +23,7 @@ namespace Microsoft.AspNetCore.Mvc public ConflictObjectResult(object error) : base(error) { - StatusCode = StatusCodes.Status409Conflict; + StatusCode = DefaultStatusCode; } /// @@ -34,7 +38,7 @@ namespace Microsoft.AspNetCore.Mvc throw new ArgumentNullException(nameof(modelState)); } - StatusCode = StatusCodes.Status409Conflict; + StatusCode = DefaultStatusCode; } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ConflictResult.cs b/src/Microsoft.AspNetCore.Mvc.Core/ConflictResult.cs index 92eb10385a..a97acb8c31 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ConflictResult.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ConflictResult.cs @@ -2,19 +2,23 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Infrastructure; namespace Microsoft.AspNetCore.Mvc { /// /// A that when executed will produce a Conflict (409) response. /// + [DefaultStatusCode(DefaultStatusCode)] public class ConflictResult : StatusCodeResult { + private const int DefaultStatusCode = StatusCodes.Status409Conflict; + /// /// Creates a new instance. /// public ConflictResult() - : base(StatusCodes.Status409Conflict) + : base(DefaultStatusCode) { } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/CreatedAtActionResult.cs b/src/Microsoft.AspNetCore.Mvc.Core/CreatedAtActionResult.cs index 6ed312a374..f173c2ba9d 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/CreatedAtActionResult.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/CreatedAtActionResult.cs @@ -8,14 +8,18 @@ using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Net.Http.Headers; +using Microsoft.AspNetCore.Mvc.Infrastructure; namespace Microsoft.AspNetCore.Mvc { /// /// An that returns a Created (201) response with a Location header. /// + [DefaultStatusCode(DefaultStatusCode)] public class CreatedAtActionResult : ObjectResult { + private const int DefaultStatusCode = StatusCodes.Status201Created; + /// /// Initializes a new instance of the with the values /// provided. @@ -34,7 +38,7 @@ namespace Microsoft.AspNetCore.Mvc ActionName = actionName; ControllerName = controllerName; RouteValues = routeValues == null ? null : new RouteValueDictionary(routeValues); - StatusCode = StatusCodes.Status201Created; + StatusCode = DefaultStatusCode; } /// diff --git a/src/Microsoft.AspNetCore.Mvc.Core/CreatedAtRouteResult.cs b/src/Microsoft.AspNetCore.Mvc.Core/CreatedAtRouteResult.cs index 6e26789563..ad05b6e4a9 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/CreatedAtRouteResult.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/CreatedAtRouteResult.cs @@ -8,14 +8,18 @@ using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Net.Http.Headers; +using Microsoft.AspNetCore.Mvc.Infrastructure; namespace Microsoft.AspNetCore.Mvc { /// /// An that returns a Created (201) response with a Location header. /// + [DefaultStatusCode(DefaultStatusCode)] public class CreatedAtRouteResult : ObjectResult { + private const int DefaultStatusCode = StatusCodes.Status201Created; + /// /// Initializes a new instance of the class with the values /// provided. @@ -42,7 +46,7 @@ namespace Microsoft.AspNetCore.Mvc { RouteName = routeName; RouteValues = routeValues == null ? null : new RouteValueDictionary(routeValues); - StatusCode = StatusCodes.Status201Created; + StatusCode = DefaultStatusCode; } /// diff --git a/src/Microsoft.AspNetCore.Mvc.Core/CreatedResult.cs b/src/Microsoft.AspNetCore.Mvc.Core/CreatedResult.cs index 5e1d23a628..1ead848ca2 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/CreatedResult.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/CreatedResult.cs @@ -3,6 +3,7 @@ using System; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Mvc @@ -10,8 +11,11 @@ namespace Microsoft.AspNetCore.Mvc /// /// An that returns a Created (201) response with a Location header. /// + [DefaultStatusCode(DefaultStatusCode)] public class CreatedResult : ObjectResult { + private const int DefaultStatusCode = StatusCodes.Status201Created; + private string _location; /// @@ -29,7 +33,7 @@ namespace Microsoft.AspNetCore.Mvc } Location = location; - StatusCode = StatusCodes.Status201Created; + StatusCode = DefaultStatusCode; } /// @@ -55,7 +59,7 @@ namespace Microsoft.AspNetCore.Mvc Location = location.GetComponents(UriComponents.SerializationInfoString, UriFormat.UriEscaped); } - StatusCode = StatusCodes.Status201Created; + StatusCode = DefaultStatusCode; } /// @@ -88,4 +92,4 @@ namespace Microsoft.AspNetCore.Mvc context.HttpContext.Response.Headers[HeaderNames.Location] = Location; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/DefaultStatusCodeAttribute.cs b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/DefaultStatusCodeAttribute.cs new file mode 100644 index 0000000000..f34ffc4d66 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/DefaultStatusCodeAttribute.cs @@ -0,0 +1,31 @@ +// 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; + +namespace Microsoft.AspNetCore.Mvc.Infrastructure +{ + /// + /// Specifies the default status code associated with an . + /// + /// + /// This attribute is informational only and does not have any runtime effects. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] + public sealed class DefaultStatusCodeAttribute : Attribute + { + /// + /// Initializes a new instance of . + /// + /// The default status code. + public DefaultStatusCodeAttribute(int statusCode) + { + StatusCode = statusCode; + } + + /// + /// Gets the default status code. + /// + public int StatusCode { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/NoContentResult.cs b/src/Microsoft.AspNetCore.Mvc.Core/NoContentResult.cs index 09a700398e..f4a23075f9 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/NoContentResult.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/NoContentResult.cs @@ -2,13 +2,23 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Infrastructure; namespace Microsoft.AspNetCore.Mvc { + /// + /// A that when executed will produce a 204 No Content response. + /// + [DefaultStatusCode(DefaultStatusCode)] public class NoContentResult : StatusCodeResult { + private const int DefaultStatusCode = StatusCodes.Status204NoContent; + + /// + /// Initializes a new instance. + /// public NoContentResult() - : base(StatusCodes.Status204NoContent) + : base(DefaultStatusCode) { } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/NotFoundObjectResult.cs b/src/Microsoft.AspNetCore.Mvc.Core/NotFoundObjectResult.cs index 9260784017..c8856473aa 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/NotFoundObjectResult.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/NotFoundObjectResult.cs @@ -2,14 +2,18 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Infrastructure; namespace Microsoft.AspNetCore.Mvc { /// /// An that when executed will produce a Not Found (404) response. /// + [DefaultStatusCode(DefaultStatusCode)] public class NotFoundObjectResult : ObjectResult { + private const int DefaultStatusCode = StatusCodes.Status404NotFound; + /// /// Creates a new instance. /// @@ -17,7 +21,7 @@ namespace Microsoft.AspNetCore.Mvc public NotFoundObjectResult(object value) : base(value) { - StatusCode = StatusCodes.Status404NotFound; + StatusCode = DefaultStatusCode; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Core/NotFoundResult.cs b/src/Microsoft.AspNetCore.Mvc.Core/NotFoundResult.cs index b4b7de1600..ed59417621 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/NotFoundResult.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/NotFoundResult.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Infrastructure; namespace Microsoft.AspNetCore.Mvc { @@ -9,12 +10,15 @@ namespace Microsoft.AspNetCore.Mvc /// Represents an that when /// executed will produce a Not Found (404) response. /// + [DefaultStatusCode(DefaultStatusCode)] public class NotFoundResult : StatusCodeResult { + private const int DefaultStatusCode = StatusCodes.Status404NotFound; + /// /// Creates a new instance. /// - public NotFoundResult() : base(StatusCodes.Status404NotFound) + public NotFoundResult() : base(DefaultStatusCode) { } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ObjectResult.cs b/src/Microsoft.AspNetCore.Mvc.Core/ObjectResult.cs index 1148ceffc1..ded227d22a 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ObjectResult.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ObjectResult.cs @@ -5,7 +5,6 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Mvc diff --git a/src/Microsoft.AspNetCore.Mvc.Core/OkObjectResult.cs b/src/Microsoft.AspNetCore.Mvc.Core/OkObjectResult.cs index 2990ffc843..3f4162b426 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/OkObjectResult.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/OkObjectResult.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Infrastructure; namespace Microsoft.AspNetCore.Mvc { @@ -9,8 +10,11 @@ namespace Microsoft.AspNetCore.Mvc /// An that when executed performs content negotiation, formats the entity body, and /// will produce a response if negotiation and formatting succeed. /// + [DefaultStatusCode(DefaultStatusCode)] public class OkObjectResult : ObjectResult { + private const int DefaultStatusCode = StatusCodes.Status200OK; + /// /// Initializes a new instance of the class. /// @@ -18,7 +22,7 @@ namespace Microsoft.AspNetCore.Mvc public OkObjectResult(object value) : base(value) { - StatusCode = StatusCodes.Status200OK; + StatusCode = DefaultStatusCode; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Core/OkResult.cs b/src/Microsoft.AspNetCore.Mvc.Core/OkResult.cs index 49cd649c99..cb6476b72d 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/OkResult.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/OkResult.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Infrastructure; namespace Microsoft.AspNetCore.Mvc { @@ -9,13 +10,16 @@ namespace Microsoft.AspNetCore.Mvc /// An that when executed will produce an empty /// response. /// + [DefaultStatusCode(DefaultStatusCode)] public class OkResult : StatusCodeResult { + private const int DefaultStatusCode = StatusCodes.Status200OK; + /// /// Initializes a new instance of the class. /// public OkResult() - : base(StatusCodes.Status200OK) + : base(DefaultStatusCode) { } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ProducesAttribute.cs b/src/Microsoft.AspNetCore.Mvc.Core/ProducesAttribute.cs index 202591a1b6..e9346f7330 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ProducesAttribute.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ProducesAttribute.cs @@ -82,9 +82,7 @@ namespace Microsoft.AspNetCore.Mvc throw new ArgumentNullException(nameof(context)); } - var objectResult = context.Result as ObjectResult; - - if (objectResult != null) + if (context.Result is ObjectResult objectResult) { // Check if there are any IFormatFilter in the pipeline, and if any of them is active. If there is one, // do not override the content type value. diff --git a/src/Microsoft.AspNetCore.Mvc.Core/UnauthorizedResult.cs b/src/Microsoft.AspNetCore.Mvc.Core/UnauthorizedResult.cs index a92106ec84..7e3bc7bb7f 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/UnauthorizedResult.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/UnauthorizedResult.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Infrastructure; namespace Microsoft.AspNetCore.Mvc { @@ -9,12 +10,15 @@ namespace Microsoft.AspNetCore.Mvc /// Represents an that when /// executed will produce an Unauthorized (401) response. /// + [DefaultStatusCode(DefaultStatusCode)] public class UnauthorizedResult : StatusCodeResult { + private const int DefaultStatusCode = StatusCodes.Status401Unauthorized; + /// /// Creates a new instance. /// - public UnauthorizedResult() : base(StatusCodes.Status401Unauthorized) + public UnauthorizedResult() : base(DefaultStatusCode) { } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/UnprocessableEntityObjectResult.cs b/src/Microsoft.AspNetCore.Mvc.Core/UnprocessableEntityObjectResult.cs index 002d9d97af..85c21a9594 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/UnprocessableEntityObjectResult.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/UnprocessableEntityObjectResult.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Microsoft.AspNetCore.Mvc @@ -9,8 +10,11 @@ namespace Microsoft.AspNetCore.Mvc /// /// An that when executed will produce a Unprocessable Entity (422) response. /// + [DefaultStatusCode(DefaultStatusCode)] public class UnprocessableEntityObjectResult : ObjectResult { + private const int DefaultStatusCode = StatusCodes.Status422UnprocessableEntity; + /// /// Creates a new instance. /// @@ -27,7 +31,7 @@ namespace Microsoft.AspNetCore.Mvc public UnprocessableEntityObjectResult(object error) : base(error) { - StatusCode = StatusCodes.Status422UnprocessableEntity; + StatusCode = DefaultStatusCode; } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/UnprocessableEntityResult.cs b/src/Microsoft.AspNetCore.Mvc.Core/UnprocessableEntityResult.cs index 0851057499..d82cf642a7 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/UnprocessableEntityResult.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/UnprocessableEntityResult.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Infrastructure; namespace Microsoft.AspNetCore.Mvc { @@ -9,13 +10,16 @@ namespace Microsoft.AspNetCore.Mvc /// A that when /// executed will produce a Unprocessable Entity (422) response. /// + [DefaultStatusCode(DefaultStatusCode)] public class UnprocessableEntityResult : StatusCodeResult { + private const int DefaultStatusCode = StatusCodes.Status422UnprocessableEntity; + /// /// Creates a new instance. /// public UnprocessableEntityResult() - : base(StatusCodes.Status422UnprocessableEntity) + : base(DefaultStatusCode) { } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/UnsupportedMediaTypeResult.cs b/src/Microsoft.AspNetCore.Mvc.Core/UnsupportedMediaTypeResult.cs index 445ded67d0..175c886874 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/UnsupportedMediaTypeResult.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/UnsupportedMediaTypeResult.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Infrastructure; namespace Microsoft.AspNetCore.Mvc { @@ -9,12 +10,15 @@ namespace Microsoft.AspNetCore.Mvc /// A that when /// executed will produce a UnsupportedMediaType (415) response. /// + [DefaultStatusCode(DefaultStatusCode)] public class UnsupportedMediaTypeResult : StatusCodeResult { + private const int DefaultStatusCode = StatusCodes.Status415UnsupportedMediaType; + /// /// Creates a new instance of . /// - public UnsupportedMediaTypeResult() : base(StatusCodes.Status415UnsupportedMediaType) + public UnsupportedMediaTypeResult() : base(DefaultStatusCode) { } } diff --git a/test/Microsoft.AspNetCore.Mvc.Analyzers.Test/CodeAnalysisExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.Analyzers.Test/CodeAnalysisExtensionsTest.cs index e0e9a90d5c..392a16d6bb 100644 --- a/test/Microsoft.AspNetCore.Mvc.Analyzers.Test/CodeAnalysisExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Analyzers.Test/CodeAnalysisExtensionsTest.cs @@ -13,13 +13,123 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers { public class CodeAnalysisExtensionsTest { + private static readonly string Namespace = typeof(CodeAnalysisExtensionsTest).Namespace; + + [Fact] + public async Task GetAttributes_OnMethodWithoutAttributes() + { + // Arrange + var compilation = await GetCompilation(); + var attribute = compilation.GetTypeByMetadataName(typeof(ProducesResponseTypeAttribute).FullName); + var testClass = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetAttributes_OnMethodWithoutAttributesClass)}"); + var method = (IMethodSymbol)testClass.GetMembers(nameof(GetAttributes_OnMethodWithoutAttributesClass.Method)).First(); + + // Act + var attributes = CodeAnalysisExtensions.GetAttributes(method, attribute, inherit: true); + + // Assert + Assert.Empty(attributes); + } + + [Fact] + public async Task GetAttributes_OnNonOverriddenMethod_ReturnsAllAttributesOnCurrentAction() + { + // Arrange + var compilation = await GetCompilation("GetAttributes_WithoutMethodOverridding"); + var attribute = compilation.GetTypeByMetadataName(typeof(ProducesResponseTypeAttribute).FullName); + var testClass = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetAttributes_WithoutMethodOverridding)}"); + var method = (IMethodSymbol)testClass.GetMembers(nameof(GetAttributes_WithoutMethodOverridding.Method)).First(); + + // Act + var attributes = CodeAnalysisExtensions.GetAttributes(method, attribute, inherit: true); + + // Assert + Assert.Collection( + attributes, + attributeData => Assert.Equal(201, attributeData.ConstructorArguments[0].Value)); + } + + [Fact] + public async Task GetAttributes_WithInheritFalse_ReturnsAllAttributesOnCurrentAction() + { + // Arrange + var compilation = await GetCompilation("GetAttributes_WithMethodOverridding"); + var attribute = compilation.GetTypeByMetadataName(typeof(ProducesResponseTypeAttribute).FullName); + var testClass = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetAttributes_WithInheritFalse_ReturnsAllAttributesOnCurrentActionClass)}"); + var method = (IMethodSymbol)testClass.GetMembers(nameof(GetAttributes_WithInheritFalse_ReturnsAllAttributesOnCurrentActionClass.Method)).First(); + + // Act + var attributes = CodeAnalysisExtensions.GetAttributes(method, attribute, inherit: false); + + // Assert + Assert.Collection( + attributes, + attributeData => Assert.Equal(400, attributeData.ConstructorArguments[0].Value)); + } + + [Fact] + public async Task GetAttributes_WithInheritTrue_ReturnsAllAttributesOnCurrentActionAndOverridingMethod() + { + // Arrange + var compilation = await GetCompilation("GetAttributes_WithMethodOverridding"); + var attribute = compilation.GetTypeByMetadataName(typeof(ProducesResponseTypeAttribute).FullName); + var testClass = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetAttributes_WithInheritFalse_ReturnsAllAttributesOnCurrentActionClass)}"); + var method = (IMethodSymbol)testClass.GetMembers(nameof(GetAttributes_WithInheritFalse_ReturnsAllAttributesOnCurrentActionClass.Method)).First(); + + // Act + var attributes = CodeAnalysisExtensions.GetAttributes(method, attribute, inherit: true); + + // Assert + Assert.Collection( + attributes, + attributeData => Assert.Equal(400, attributeData.ConstructorArguments[0].Value), + attributeData => Assert.Equal(200, attributeData.ConstructorArguments[0].Value), + attributeData => Assert.Equal(404, attributeData.ConstructorArguments[0].Value)); + } + + [Fact] + public async Task GetAttributes_OnNewMethodOfVirtualBaseMethod() + { + // Arrange + var compilation = await GetCompilation("GetAttributes_WithNewMethod"); + var attribute = compilation.GetTypeByMetadataName(typeof(ProducesResponseTypeAttribute).FullName); + var testClass = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetAttributes_WithNewMethodDerived)}"); + var method = (IMethodSymbol)testClass.GetMembers(nameof(GetAttributes_WithNewMethodDerived.VirtualMethod)).First(); + + // Act + var attributes = CodeAnalysisExtensions.GetAttributes(method, attribute, inherit: true); + + // Assert + Assert.Collection( + attributes, + attributeData => Assert.Equal(400, attributeData.ConstructorArguments[0].Value)); + } + + [Fact] + public async Task GetAttributes_OnNewMethodOfNonVirtualBaseMethod() + { + // Arrange + var compilation = await GetCompilation("GetAttributes_WithNewMethod"); + var attribute = compilation.GetTypeByMetadataName(typeof(ProducesResponseTypeAttribute).FullName); + var testClass = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetAttributes_WithNewMethodDerived)}"); + var method = (IMethodSymbol)testClass.GetMembers(nameof(GetAttributes_WithNewMethodDerived.NotVirtualMethod)).First(); + + // Act + var attributes = CodeAnalysisExtensions.GetAttributes(method, attribute, inherit: true); + + // Assert + Assert.Collection( + attributes, + attributeData => Assert.Equal(401, attributeData.ConstructorArguments[0].Value)); + } + [Fact] public async Task HasAttribute_ReturnsFalseIfSymbolDoesNotHaveAttribute() { // Arrange var compilation = await GetCompilation(); - var attribute = compilation.GetTypeByMetadataName($"{GetType().Namespace}.HasAttribute_ReturnsFalseIfTypeDoesNotHaveAttribute"); - var testClass = compilation.GetTypeByMetadataName($"{GetType().Namespace}.HasAttribute_ReturnsFalseIfTypeDoesNotHaveAttributeTest"); + var attribute = compilation.GetTypeByMetadataName($"{Namespace}.HasAttribute_ReturnsFalseIfTypeDoesNotHaveAttribute"); + var testClass = compilation.GetTypeByMetadataName($"{Namespace}.HasAttribute_ReturnsFalseIfTypeDoesNotHaveAttributeTest"); var testMethod = (IMethodSymbol)testClass.GetMembers("SomeMethod").First(); var testProperty = (IPropertySymbol)testClass.GetMembers("SomeProperty").First(); @@ -40,7 +150,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers // Arrange var compilation = await GetCompilation(); var attribute = compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Mvc.ControllerAttribute"); - var testClass = compilation.GetTypeByMetadataName($"{GetType().Namespace}.{nameof(HasAttribute_ReturnsTrueIfTypeHasAttribute)}"); + var testClass = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(HasAttribute_ReturnsTrueIfTypeHasAttribute)}"); // Act var hasAttribute = CodeAnalysisExtensions.HasAttribute(testClass, attribute, inherit: false); @@ -55,7 +165,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers // Arrange var compilation = await GetCompilation(); var attribute = compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Mvc.ControllerAttribute"); - var testClass = compilation.GetTypeByMetadataName($"{GetType().Namespace}.{nameof(HasAttribute_ReturnsTrueIfBaseTypeHasAttribute)}"); + var testClass = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(HasAttribute_ReturnsTrueIfBaseTypeHasAttribute)}"); // Act var hasAttributeWithoutInherit = CodeAnalysisExtensions.HasAttribute(testClass, attribute, inherit: false); @@ -71,9 +181,9 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers { // Arrange var compilation = await GetCompilation(); - var @interface = compilation.GetTypeByMetadataName($"{GetType().Namespace}.IHasAttribute_ReturnsTrueForInterfaceContractOnAttribute"); - var testClass = compilation.GetTypeByMetadataName($"{GetType().Namespace}.HasAttribute_ReturnsTrueForInterfaceContractOnAttributeTest"); - var derivedClass = compilation.GetTypeByMetadataName($"{GetType().Namespace}.HasAttribute_ReturnsTrueForInterfaceContractOnAttributeDerived"); + var @interface = compilation.GetTypeByMetadataName($"{Namespace}.IHasAttribute_ReturnsTrueForInterfaceContractOnAttribute"); + var testClass = compilation.GetTypeByMetadataName($"{Namespace}.HasAttribute_ReturnsTrueForInterfaceContractOnAttributeTest"); + var derivedClass = compilation.GetTypeByMetadataName($"{Namespace}.HasAttribute_ReturnsTrueForInterfaceContractOnAttributeDerived"); // Act var hasAttribute = CodeAnalysisExtensions.HasAttribute(testClass, @interface, inherit: true); @@ -89,8 +199,8 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers { // Arrange var compilation = await GetCompilation(); - var attribute = compilation.GetTypeByMetadataName($"{GetType().Namespace}.HasAttribute_ReturnsTrueForAttributesOnMethodsAttribute"); - var testClass = compilation.GetTypeByMetadataName($"{GetType().Namespace}.HasAttribute_ReturnsTrueForAttributesOnMethodsTest"); + var attribute = compilation.GetTypeByMetadataName($"{Namespace}.HasAttribute_ReturnsTrueForAttributesOnMethodsAttribute"); + var testClass = compilation.GetTypeByMetadataName($"{Namespace}.HasAttribute_ReturnsTrueForAttributesOnMethodsTest"); var method = (IMethodSymbol)testClass.GetMembers("SomeMethod").First(); // Act @@ -105,8 +215,8 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers { // Arrange var compilation = await GetCompilation(); - var attribute = compilation.GetTypeByMetadataName($"{GetType().Namespace}.HasAttribute_ReturnsTrueForAttributesOnOverriddenMethodsAttribute"); - var testClass = compilation.GetTypeByMetadataName($"{GetType().Namespace}.HasAttribute_ReturnsTrueForAttributesOnOverriddenMethodsTest"); + var attribute = compilation.GetTypeByMetadataName($"{Namespace}.HasAttribute_ReturnsTrueForAttributesOnOverriddenMethodsAttribute"); + var testClass = compilation.GetTypeByMetadataName($"{Namespace}.HasAttribute_ReturnsTrueForAttributesOnOverriddenMethodsTest"); var method = (IMethodSymbol)testClass.GetMembers("SomeMethod").First(); @@ -124,8 +234,8 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers { // Arrange var compilation = await GetCompilation(); - var attribute = compilation.GetTypeByMetadataName($"{GetType().Namespace}.HasAttribute_ReturnsTrueForAttributesOnPropertiesAttribute"); - var testClass = compilation.GetTypeByMetadataName($"{GetType().Namespace}.HasAttribute_ReturnsTrueForAttributesOnProperties"); + var attribute = compilation.GetTypeByMetadataName($"{Namespace}.HasAttribute_ReturnsTrueForAttributesOnPropertiesAttribute"); + var testClass = compilation.GetTypeByMetadataName($"{Namespace}.HasAttribute_ReturnsTrueForAttributesOnProperties"); var property = (IPropertySymbol)testClass.GetMembers("SomeProperty").First(); // Act @@ -140,8 +250,8 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers { // Arrange var compilation = await GetCompilation(); - var attribute = compilation.GetTypeByMetadataName($"{GetType().Namespace}.HasAttribute_ReturnsTrueForAttributesOnOverriddenPropertiesAttribute"); - var testClass = compilation.GetTypeByMetadataName($"{GetType().Namespace}.HasAttribute_ReturnsTrueForAttributesOnOverriddenProperties"); + var attribute = compilation.GetTypeByMetadataName($"{Namespace}.HasAttribute_ReturnsTrueForAttributesOnOverriddenPropertiesAttribute"); + var testClass = compilation.GetTypeByMetadataName($"{Namespace}.HasAttribute_ReturnsTrueForAttributesOnOverriddenProperties"); var property = (IPropertySymbol)testClass.GetMembers("SomeProperty").First(); // Act @@ -158,8 +268,8 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers { // Arrange var compilation = await GetCompilation(); - var source = compilation.GetTypeByMetadataName($"{GetType().Namespace}.IsAssignable_ReturnsFalseForDifferentTypesA"); - var target = compilation.GetTypeByMetadataName($"{GetType().Namespace}.IsAssignable_ReturnsFalseForDifferentTypesB"); + var source = compilation.GetTypeByMetadataName($"{Namespace}.IsAssignable_ReturnsFalseForDifferentTypesA"); + var target = compilation.GetTypeByMetadataName($"{Namespace}.IsAssignable_ReturnsFalseForDifferentTypesB"); // Act var isAssignableFrom = CodeAnalysisExtensions.IsAssignableFrom(source, target); @@ -173,7 +283,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers { // Arrange var compilation = await GetCompilation(nameof(IsAssignable_ReturnsFalseForDifferentTypes)); - var source = compilation.GetTypeByMetadataName($"{GetType().Namespace}.IsAssignable_ReturnsFalseForDifferentTypesA"); + var source = compilation.GetTypeByMetadataName($"{Namespace}.IsAssignable_ReturnsFalseForDifferentTypesA"); var target = compilation.GetTypeByMetadataName($"System.IDisposable"); // Act @@ -188,8 +298,8 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers { // Arrange var compilation = await GetCompilation(); - var source = compilation.GetTypeByMetadataName($"{GetType().Namespace}.IsAssignable_ReturnsTrueIfTypesAreExact"); - var target = compilation.GetTypeByMetadataName($"{GetType().Namespace}.IsAssignable_ReturnsTrueIfTypesAreExact"); + var source = compilation.GetTypeByMetadataName($"{Namespace}.IsAssignable_ReturnsTrueIfTypesAreExact"); + var target = compilation.GetTypeByMetadataName($"{Namespace}.IsAssignable_ReturnsTrueIfTypesAreExact"); // Act var isAssignableFrom = CodeAnalysisExtensions.IsAssignableFrom(source, target); @@ -203,8 +313,8 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers { // Arrange var compilation = await GetCompilation(); - var source = compilation.GetTypeByMetadataName($"{GetType().Namespace}.IsAssignable_ReturnsTrueIfTypeImplementsInterface"); - var target = compilation.GetTypeByMetadataName($"{GetType().Namespace}.IsAssignable_ReturnsTrueIfTypeImplementsInterfaceTest"); + var source = compilation.GetTypeByMetadataName($"{Namespace}.IsAssignable_ReturnsTrueIfTypeImplementsInterface"); + var target = compilation.GetTypeByMetadataName($"{Namespace}.IsAssignable_ReturnsTrueIfTypeImplementsInterfaceTest"); // Act var isAssignableFrom = CodeAnalysisExtensions.IsAssignableFrom(source, target); @@ -220,8 +330,8 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers { // Arrange var compilation = await GetCompilation(); - var source = compilation.GetTypeByMetadataName($"{GetType().Namespace}.IsAssignable_ReturnsTrueIfAncestorTypeImplementsInterface"); - var target = compilation.GetTypeByMetadataName($"{GetType().Namespace}.IsAssignable_ReturnsTrueIfAncestorTypeImplementsInterfaceTest"); + var source = compilation.GetTypeByMetadataName($"{Namespace}.IsAssignable_ReturnsTrueIfAncestorTypeImplementsInterface"); + var target = compilation.GetTypeByMetadataName($"{Namespace}.IsAssignable_ReturnsTrueIfAncestorTypeImplementsInterfaceTest"); // Act var isAssignableFrom = CodeAnalysisExtensions.IsAssignableFrom(source, target); diff --git a/test/Microsoft.AspNetCore.Mvc.Analyzers.Test/MvcFactsTest.cs b/test/Microsoft.AspNetCore.Mvc.Analyzers.Test/MvcFactsTest.cs index 1d81f7e41d..6cb3a518a7 100644 --- a/test/Microsoft.AspNetCore.Mvc.Analyzers.Test/MvcFactsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Analyzers.Test/MvcFactsTest.cs @@ -28,7 +28,6 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers [Fact] public Task IsController_ReturnsFalseForValueType() => IsControllerReturnsFalse(typeof(ValueTypeController)); - [Fact] public Task IsController_ReturnsFalseForGenericType() => IsControllerReturnsFalse(typeof(OpenGenericController<>)); diff --git a/test/Microsoft.AspNetCore.Mvc.Analyzers.Test/SymbolApiResponseMetadataProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Analyzers.Test/SymbolApiResponseMetadataProviderTest.cs new file mode 100644 index 0000000000..3f7aad5564 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Analyzers.Test/SymbolApiResponseMetadataProviderTest.cs @@ -0,0 +1,311 @@ +// 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.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Analyzer.Testing; +using Microsoft.AspNetCore.Mvc.Analyzers.Infrastructure; +using Microsoft.CodeAnalysis; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Analyzers +{ + public class SymbolApiResponseMetadataProviderTest + { + private static readonly string Namespace = typeof(SymbolApiResponseMetadataProviderTest).Namespace; + + [Fact] + public async Task GetResponseMetadata_ReturnsEmptySequence_IfNoAttributesArePresent_ForGetAction() + { + // Arrange + var compilation = await GetResponseMetadataCompilation(); + var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerWithoutConvention)}"); + var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerWithoutConvention.GetPerson)).First(); + var typeCache = new ApiControllerTypeCache(compilation); + + // Act + var result = SymbolApiResponseMetadataProvider.GetResponseMetadata(typeCache, method); + + // Assert + Assert.Empty(result); + } + + [Fact] + public async Task GetResponseMetadata_ReturnsEmptySequence_IfNoAttributesArePresent_ForPostAction() + { + // Arrange + var compilation = await GetResponseMetadataCompilation(); + var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerWithoutConvention)}"); + var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerWithoutConvention.PostPerson)).First(); + var typeCache = new ApiControllerTypeCache(compilation); + + // Act + var result = SymbolApiResponseMetadataProvider.GetResponseMetadata(typeCache, method); + + // Assert + Assert.Empty(result); + } + + [Fact] + public async Task GetResponseMetadata_IgnoresProducesAttribute() + { + // Arrange + var compilation = await GetResponseMetadataCompilation(); + var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerActionWithAttributes)}"); + var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithProducesAttribute)).First(); + var typeCache = new ApiControllerTypeCache(compilation); + + // Act + var result = SymbolApiResponseMetadataProvider.GetResponseMetadata(typeCache, method); + + // Assert + Assert.Empty(result); + } + + [Fact] + public async Task GetResponseMetadata_ReturnsValueFromProducesResponseType_WhenStatusCodeIsSpecifiedInConstructor() + { + // Arrange + var compilation = await GetResponseMetadataCompilation(); + var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerActionWithAttributes)}"); + var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithProducesResponseType_StatusCodeInConstructor)).First(); + var typeCache = new ApiControllerTypeCache(compilation); + + // Act + var result = SymbolApiResponseMetadataProvider.GetResponseMetadata(typeCache, method); + + // Assert + Assert.Collection( + result, + metadata => + { + Assert.Equal(201, metadata.StatusCode); + Assert.NotNull(metadata.Attribute); + Assert.Null(metadata.Convention); + }); + } + + [Fact] + public async Task GetResponseMetadata_ReturnsValueFromProducesResponseType_WhenStatusCodeIsSpecifiedInConstructorWithResponseType() + { + // Arrange + var compilation = await GetResponseMetadataCompilation(); + var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerActionWithAttributes)}"); + var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithProducesResponseType_StatusCodeAndTypeInConstructor)).First(); + var typeCache = new ApiControllerTypeCache(compilation); + + // Act + var result = SymbolApiResponseMetadataProvider.GetResponseMetadata(typeCache, method); + + // Assert + Assert.Collection( + result, + metadata => + { + Assert.Equal(202, metadata.StatusCode); + Assert.NotNull(metadata.Attribute); + Assert.Null(metadata.Convention); + }); + } + + [Fact] + public async Task GetResponseMetadata_ReturnsValueFromProducesResponseType_WhenStatusCodeIsSpecifiedInConstructorAndProperty() + { + // Arrange + var compilation = await GetResponseMetadataCompilation(); + var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerActionWithAttributes)}"); + var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithProducesResponseType_StatusCodeInConstructorAndProperty)).First(); + var typeCache = new ApiControllerTypeCache(compilation); + + // Act + var result = SymbolApiResponseMetadataProvider.GetResponseMetadata(typeCache, method); + + // Assert + Assert.Collection( + result, + metadata => + { + Assert.Equal(203, metadata.StatusCode); + Assert.NotNull(metadata.Attribute); + Assert.Null(metadata.Convention); + }); + } + + [Fact] + public async Task GetResponseMetadata_ReturnsValueFromProducesResponseType_WhenStatusCodeAndTypeIsSpecifiedInConstructorAndProperty() + { + // Arrange + var compilation = await GetResponseMetadataCompilation(); + var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerActionWithAttributes)}"); + var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithProducesResponseType_StatusCodeAndTypeInConstructorAndProperty)).First(); + var typeCache = new ApiControllerTypeCache(compilation); + + // Act + var result = SymbolApiResponseMetadataProvider.GetResponseMetadata(typeCache, method); + + // Assert + Assert.Collection( + result, + metadata => + { + Assert.Equal(201, metadata.StatusCode); + Assert.NotNull(metadata.Attribute); + Assert.Null(metadata.Convention); + }); + } + + [Fact] + public async Task GetResponseMetadata_ReturnsValueFromCustomProducesResponseType() + { + // Arrange + var compilation = await GetResponseMetadataCompilation(); + var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerActionWithAttributes)}"); + var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithCustomProducesResponseTypeAttributeWithArguments)).First(); + var typeCache = new ApiControllerTypeCache(compilation); + + // Act + var result = SymbolApiResponseMetadataProvider.GetResponseMetadata(typeCache, method); + + // Assert + Assert.Collection( + result, + metadata => + { + Assert.Equal(201, metadata.StatusCode); + Assert.NotNull(metadata.Attribute); + Assert.Null(metadata.Convention); + }); + } + + [Fact] + public async Task GetResponseMetadata_IgnoresCustomResponseTypeMetadataProvider() + { + // Arrange + var compilation = await GetResponseMetadataCompilation(); + var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerActionWithAttributes)}"); + var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithCustomApiResponseMetadataProvider)).First(); + var typeCache = new ApiControllerTypeCache(compilation); + + // Act + var result = SymbolApiResponseMetadataProvider.GetResponseMetadata(typeCache, method); + + // Assert + Assert.Empty(result); + } + + [Fact] + public Task GetResponseMetadata_IgnoresAttributesWithIncorrectStatusCodeType() + { + return GetResponseMetadata_IgnoresInvalidOrUnsupportedAttribues( + nameof(GetResponseMetadata_ControllerActionWithAttributes), + nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithProducesResponseTypeWithIncorrectStatusCodeType)); + } + + [Fact] + public Task GetResponseMetadata_IgnoresDerivedAttributesWithoutPropertyOnConstructorArguments() + { + return GetResponseMetadata_IgnoresInvalidOrUnsupportedAttribues( + nameof(GetResponseMetadata_ControllerActionWithAttributes), + nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithCustomProducesResponseTypeAttributeWithoutArguments)); + } + + private async Task GetResponseMetadata_IgnoresInvalidOrUnsupportedAttribues(string typeName, string methodName) + { + // Arrange + var compilation = await GetResponseMetadataCompilation(); + var controller = compilation.GetTypeByMetadataName($"{Namespace}.{typeName}"); + var method = (IMethodSymbol)controller.GetMembers(methodName).First(); + var typeCache = new ApiControllerTypeCache(compilation); + + // Act + var result = SymbolApiResponseMetadataProvider.GetResponseMetadata(typeCache, method); + + // Assert + Assert.Collection( + result, + metadata => + { + Assert.Equal(200, metadata.StatusCode); + Assert.NotNull(metadata.Attribute); + Assert.Null(metadata.Convention); + }); + } + + [Fact] + public Task GetStatusCode_ReturnsValueFromConstructor() + { + // Arrange + var actionName = nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithProducesResponseType_StatusCodeInConstructor); + var expected = 201; + + // Act & Assert + return GetStatusCodeTest(actionName, expected); + } + + [Fact] + public Task GetStatusCode_ReturnsValueFromProperty() + { + // Arrange + var actionName = nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithProducesResponseType_StatusCodeAndTypeInConstructorAndProperty); + var expected = 201; + + // Act & Assert + return GetStatusCodeTest(actionName, expected); + } + + [Fact] + public Task GetStatusCode_ReturnsValueFromConstructor_WhenTypeIsSpecified() + { + // Arrange + var actionName = nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithProducesResponseType_StatusCodeAndTypeInConstructor); + var expected = 202; + + // Act & Assert + return GetStatusCodeTest(actionName, expected); + } + + [Fact] + public Task GetStatusCode_Returns200_IfTypeIsNotInteger() + { + // Arrange + var actionName = nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithProducesResponseTypeWithIncorrectStatusCodeType); + var expected = 200; + + // Act & Assert + return GetStatusCodeTest(actionName, expected); + } + + [Fact] + public Task GetStatusCode_ReturnsValueFromDerivedAttributes() + { + // Arrange + var actionName = nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithCustomProducesResponseTypeAttributeWithArguments); + var expected = 201; + + // Act & Assert + return GetStatusCodeTest(actionName, expected); + } + + private async Task GetStatusCodeTest(string actionName, int expected) + { + var compilation = await GetResponseMetadataCompilation(); + var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerActionWithAttributes)}"); + var method = (IMethodSymbol)controller.GetMembers(actionName).First(); + var attribute = method.GetAttributes().First(); + + var statusCode = SymbolApiResponseMetadataProvider.GetStatusCode(attribute); + + Assert.Equal(expected, statusCode); + } + + private Task GetResponseMetadataCompilation() => GetCompilation("GetResponseMetadataTests"); + + private Task GetCompilation(string test) + { + var testSource = MvcTestSource.Read(GetType().Name, test); + var project = DiagnosticProject.Create(GetType().Assembly, new[] { testSource.Source }); + + return project.GetCompilationAsync(); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Analyzers.Test/TestFiles/CodeAnalysisExtensionsTest/GetAttributes_OnMethodWithoutAttributes.cs b/test/Microsoft.AspNetCore.Mvc.Analyzers.Test/TestFiles/CodeAnalysisExtensionsTest/GetAttributes_OnMethodWithoutAttributes.cs new file mode 100644 index 0000000000..9b28469109 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Analyzers.Test/TestFiles/CodeAnalysisExtensionsTest/GetAttributes_OnMethodWithoutAttributes.cs @@ -0,0 +1,7 @@ +namespace Microsoft.AspNetCore.Mvc.Analyzers +{ + public class GetAttributes_OnMethodWithoutAttributesClass + { + public void Method() { } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Analyzers.Test/TestFiles/CodeAnalysisExtensionsTest/GetAttributes_WithMethodOverridding.cs b/test/Microsoft.AspNetCore.Mvc.Analyzers.Test/TestFiles/CodeAnalysisExtensionsTest/GetAttributes_WithMethodOverridding.cs new file mode 100644 index 0000000000..43b2710d58 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Analyzers.Test/TestFiles/CodeAnalysisExtensionsTest/GetAttributes_WithMethodOverridding.cs @@ -0,0 +1,15 @@ +namespace Microsoft.AspNetCore.Mvc.Analyzers +{ + public class GetAttributes_WithInheritFalse_ReturnsAllAttributesOnCurrentActionBase + { + [ProducesResponseType(200)] + [ProducesResponseType(404)] + public virtual void Method() { } + } + + public class GetAttributes_WithInheritFalse_ReturnsAllAttributesOnCurrentActionClass : GetAttributes_WithInheritFalse_ReturnsAllAttributesOnCurrentActionBase + { + [ProducesResponseType(400)] + public override void Method() { } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Analyzers.Test/TestFiles/CodeAnalysisExtensionsTest/GetAttributes_WithNewMethod.cs b/test/Microsoft.AspNetCore.Mvc.Analyzers.Test/TestFiles/CodeAnalysisExtensionsTest/GetAttributes_WithNewMethod.cs new file mode 100644 index 0000000000..3455cc21dd --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Analyzers.Test/TestFiles/CodeAnalysisExtensionsTest/GetAttributes_WithNewMethod.cs @@ -0,0 +1,22 @@ +namespace Microsoft.AspNetCore.Mvc.Analyzers +{ + public class GetAttributes_WithNewMethodBase + { + [ProducesResponseType(200)] + [ProducesResponseType(404)] + public virtual void VirtualMethod() { } + + [ProducesResponseType(200)] + [ProducesResponseType(404)] + public virtual void NotVirtualMethod() { } + } + + public class GetAttributes_WithNewMethodDerived : GetAttributes_WithNewMethodBase + { + [ProducesResponseType(400)] + public new void VirtualMethod() { } + + [ProducesResponseType(401)] + public new void NotVirtualMethod() { } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Analyzers.Test/TestFiles/CodeAnalysisExtensionsTest/GetAttributes_WithoutMethodOverridding.cs b/test/Microsoft.AspNetCore.Mvc.Analyzers.Test/TestFiles/CodeAnalysisExtensionsTest/GetAttributes_WithoutMethodOverridding.cs new file mode 100644 index 0000000000..653abeb19b --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Analyzers.Test/TestFiles/CodeAnalysisExtensionsTest/GetAttributes_WithoutMethodOverridding.cs @@ -0,0 +1,8 @@ +namespace Microsoft.AspNetCore.Mvc.Analyzers +{ + public class GetAttributes_WithoutMethodOverridding + { + [ProducesResponseType(201)] + public void Method() { } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Analyzers.Test/TestFiles/SymbolApiResponseMetadataProviderTest/GetResponseMetadataTests.cs b/test/Microsoft.AspNetCore.Mvc.Analyzers.Test/TestFiles/SymbolApiResponseMetadataProviderTest/GetResponseMetadataTests.cs new file mode 100644 index 0000000000..34e3b87237 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Analyzers.Test/TestFiles/SymbolApiResponseMetadataProviderTest/GetResponseMetadataTests.cs @@ -0,0 +1,86 @@ +// 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; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.AspNetCore.Mvc.Formatters; + +namespace Microsoft.AspNetCore.Mvc.Analyzers +{ + public class GetResponseMetadata_ControllerWithoutConvention : ControllerBase + { + public ActionResult GetPerson(int id) => null; + + public ActionResult PostPerson(Person person) => null; + } + + public class GetResponseMetadata_ControllerActionWithAttributes : ControllerBase + { + [Produces(typeof(Person))] + public IActionResult ActionWithProducesAttribute(int id) => null; + + [ProducesResponseType(201)] + public IActionResult ActionWithProducesResponseType_StatusCodeInConstructor() => null; + + [ProducesResponseType(typeof(Person), 202)] + public IActionResult ActionWithProducesResponseType_StatusCodeAndTypeInConstructor() => null; + + [ProducesResponseType(200, StatusCode = 203)] + public IActionResult ActionWithProducesResponseType_StatusCodeInConstructorAndProperty() => null; + + [ProducesResponseType(typeof(object), 200, Type = typeof(Person), StatusCode = 201)] + public IActionResult ActionWithProducesResponseType_StatusCodeAndTypeInConstructorAndProperty() => null; + + [CustomResponseType(Type = typeof(Person), StatusCode = 204)] + public IActionResult ActionWithCustomApiResponseMetadataProvider() => null; + + [Produces201ResponseType] + public IActionResult ActionWithCustomProducesResponseTypeAttributeWithoutArguments() => null; + + [Produces201ResponseType(201)] + public IActionResult ActionWithCustomProducesResponseTypeAttributeWithArguments() => null; + + [CustomInvalidProducesResponseType(Type = typeof(Person), StatusCode = "204")] + public IActionResult ActionWithProducesResponseTypeWithIncorrectStatusCodeType() => null; + } + + public class Person { } + + public class CustomResponseTypeAttribute : Attribute, IApiResponseMetadataProvider + { + public Type Type { get; set; } + + public int StatusCode { get; set; } + + public void SetContentTypes(MediaTypeCollection contentTypes) + { + } + } + + public class Produces201ResponseTypeAttribute : ProducesResponseTypeAttribute + { + public Produces201ResponseTypeAttribute() : base(201) { } + + public Produces201ResponseTypeAttribute(int statusCode) : base(statusCode) { } + } + + public class CustomInvalidProducesResponseTypeAttribute : ProducesResponseTypeAttribute + { + private string _statusCode; + + public CustomInvalidProducesResponseTypeAttribute() + : base(0) + { + } + + public new string StatusCode + { + get => _statusCode; + set + { + _statusCode = value; + base.StatusCode = int.Parse(value); + } + } + } +}