Setting up for ApiConventionAttribute analyzers (#7912)

* Setting up for ApiConventionAttribute analyzers
This commit is contained in:
Pranav K 2018-06-20 15:22:53 -07:00 committed by GitHub
parent e7ab81fe0b
commit 4f7e849cc1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 863 additions and 68 deletions

View File

@ -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; }
}
}

View File

@ -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<AttributeData> 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<AttributeData> GetAttributes(this ISymbol symbol, ITypeSymbol attribute)
{
foreach (var declaredAttribute in symbol.GetAttributes())
{
if (attribute.IsAssignableFrom(declaredAttribute.AttributeClass))
{
yield return declaredAttribute;
}
}
}
private static IEnumerable<ITypeSymbol> GetTypeHierarchy(this ITypeSymbol typeSymbol)
{
while (typeSymbol != null)

View File

@ -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<ApiResponseMetadata> GetResponseMetadata(ApiControllerTypeCache typeCache, IMethodSymbol methodSymbol)
{
var responseMetadataAttributes = methodSymbol.GetAttributes(typeCache.ProducesResponseTypeAttribute, inherit: true);
var metadataItems = new List<ApiResponseMetadata>();
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; }
}
}

View File

@ -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";
}
}

View File

@ -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
/// <summary>
/// An <see cref="ActionResult"/> that returns a Accepted (202) response with a Location header.
/// </summary>
[DefaultStatusCode(DefaultStatusCode)]
public class AcceptedAtActionResult : ObjectResult
{
private const int DefaultStatusCode = StatusCodes.Status202Accepted;
/// <summary>
/// Initializes a new instance of the <see cref="AcceptedAtActionResult"/> 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;
}
/// <summary>
@ -91,4 +95,4 @@ namespace Microsoft.AspNetCore.Mvc
context.HttpContext.Response.Headers[HeaderNames.Location] = url;
}
}
}
}

View File

@ -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
{
/// <summary>
/// An <see cref="ActionResult"/> that returns a Accepted (202) response with a Location header.
/// </summary>
[DefaultStatusCode(DefaultStatusCode)]
public class AcceptedAtRouteResult : ObjectResult
{
private const int DefaultStatusCode = StatusCodes.Status202Accepted;
/// <summary>
/// Initializes a new instance of the <see cref="AcceptedAtRouteResult"/> 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;
}
/// <summary>
@ -87,4 +91,4 @@ namespace Microsoft.AspNetCore.Mvc
context.HttpContext.Response.Headers[HeaderNames.Location] = url;
}
}
}
}

View File

@ -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
/// <summary>
/// An <see cref="ActionResult"/> that returns an Accepted (202) response with a Location header.
/// </summary>
[DefaultStatusCode(DefaultStatusCode)]
public class AcceptedResult : ObjectResult
{
private const int DefaultStatusCode = StatusCodes.Status202Accepted;
/// <summary>
/// Initializes a new instance of the <see cref="AcceptedResult"/> class with the values
/// provided.
@ -19,7 +23,7 @@ namespace Microsoft.AspNetCore.Mvc
public AcceptedResult()
: base(value: null)
{
StatusCode = StatusCodes.Status202Accepted;
StatusCode = DefaultStatusCode;
}
/// <summary>
@ -32,7 +36,7 @@ namespace Microsoft.AspNetCore.Mvc
: base(value)
{
Location = location;
StatusCode = StatusCodes.Status202Accepted;
StatusCode = DefaultStatusCode;
}
/// <summary>
@ -59,7 +63,7 @@ namespace Microsoft.AspNetCore.Mvc
Location = locationUri.GetComponents(UriComponents.SerializationInfoString, UriFormat.UriEscaped);
}
StatusCode = StatusCodes.Status202Accepted;
StatusCode = DefaultStatusCode;
}
/// <summary>
@ -83,4 +87,4 @@ namespace Microsoft.AspNetCore.Mvc
}
}
}
}
}

View File

@ -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
/// <summary>
/// An <see cref="ObjectResult"/> that when executed will produce a Bad Request (400) response.
/// </summary>
[DefaultStatusCode(DefaultStatusCode)]
public class BadRequestObjectResult : ObjectResult
{
private const int DefaultStatusCode = StatusCodes.Status400BadRequest;
/// <summary>
/// Creates a new <see cref="BadRequestObjectResult"/> instance.
/// </summary>
@ -19,7 +23,7 @@ namespace Microsoft.AspNetCore.Mvc
public BadRequestObjectResult(object error)
: base(error)
{
StatusCode = StatusCodes.Status400BadRequest;
StatusCode = DefaultStatusCode;
}
/// <summary>
@ -34,7 +38,7 @@ namespace Microsoft.AspNetCore.Mvc
throw new ArgumentNullException(nameof(modelState));
}
StatusCode = StatusCodes.Status400BadRequest;
StatusCode = DefaultStatusCode;
}
}
}

View File

@ -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 <see cref="StatusCodeResult"/> that when
/// executed will produce a Bad Request (400) response.
/// </summary>
[DefaultStatusCode(DefaultStatusCode)]
public class BadRequestResult : StatusCodeResult
{
private const int DefaultStatusCode = StatusCodes.Status400BadRequest;
/// <summary>
/// Creates a new <see cref="BadRequestResult"/> instance.
/// </summary>
public BadRequestResult()
: base(StatusCodes.Status400BadRequest)
: base(DefaultStatusCode)
{
}
}

View File

@ -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
/// <summary>
/// An <see cref="ObjectResult"/> that when executed will produce a Conflict (409) response.
/// </summary>
[DefaultStatusCode(DefaultStatusCode)]
public class ConflictObjectResult : ObjectResult
{
private const int DefaultStatusCode = StatusCodes.Status409Conflict;
/// <summary>
/// Creates a new <see cref="ConflictObjectResult"/> instance.
/// </summary>
@ -19,7 +23,7 @@ namespace Microsoft.AspNetCore.Mvc
public ConflictObjectResult(object error)
: base(error)
{
StatusCode = StatusCodes.Status409Conflict;
StatusCode = DefaultStatusCode;
}
/// <summary>
@ -34,7 +38,7 @@ namespace Microsoft.AspNetCore.Mvc
throw new ArgumentNullException(nameof(modelState));
}
StatusCode = StatusCodes.Status409Conflict;
StatusCode = DefaultStatusCode;
}
}
}

View File

@ -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
{
/// <summary>
/// A <see cref="StatusCodeResult"/> that when executed will produce a Conflict (409) response.
/// </summary>
[DefaultStatusCode(DefaultStatusCode)]
public class ConflictResult : StatusCodeResult
{
private const int DefaultStatusCode = StatusCodes.Status409Conflict;
/// <summary>
/// Creates a new <see cref="ConflictResult"/> instance.
/// </summary>
public ConflictResult()
: base(StatusCodes.Status409Conflict)
: base(DefaultStatusCode)
{
}
}

View File

@ -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
{
/// <summary>
/// An <see cref="ActionResult"/> that returns a Created (201) response with a Location header.
/// </summary>
[DefaultStatusCode(DefaultStatusCode)]
public class CreatedAtActionResult : ObjectResult
{
private const int DefaultStatusCode = StatusCodes.Status201Created;
/// <summary>
/// Initializes a new instance of the <see cref="CreatedAtActionResult"/> 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;
}
/// <summary>

View File

@ -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
{
/// <summary>
/// An <see cref="ActionResult"/> that returns a Created (201) response with a Location header.
/// </summary>
[DefaultStatusCode(DefaultStatusCode)]
public class CreatedAtRouteResult : ObjectResult
{
private const int DefaultStatusCode = StatusCodes.Status201Created;
/// <summary>
/// Initializes a new instance of the <see cref="CreatedAtRouteResult"/> 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;
}
/// <summary>

View File

@ -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
/// <summary>
/// An <see cref="ActionResult"/> that returns a Created (201) response with a Location header.
/// </summary>
[DefaultStatusCode(DefaultStatusCode)]
public class CreatedResult : ObjectResult
{
private const int DefaultStatusCode = StatusCodes.Status201Created;
private string _location;
/// <summary>
@ -29,7 +33,7 @@ namespace Microsoft.AspNetCore.Mvc
}
Location = location;
StatusCode = StatusCodes.Status201Created;
StatusCode = DefaultStatusCode;
}
/// <summary>
@ -55,7 +59,7 @@ namespace Microsoft.AspNetCore.Mvc
Location = location.GetComponents(UriComponents.SerializationInfoString, UriFormat.UriEscaped);
}
StatusCode = StatusCodes.Status201Created;
StatusCode = DefaultStatusCode;
}
/// <summary>
@ -88,4 +92,4 @@ namespace Microsoft.AspNetCore.Mvc
context.HttpContext.Response.Headers[HeaderNames.Location] = Location;
}
}
}
}

View File

@ -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
{
/// <summary>
/// Specifies the default status code associated with an <see cref="ActionResult"/>.
/// </summary>
/// <remarks>
/// This attribute is informational only and does not have any runtime effects.
/// </remarks>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public sealed class DefaultStatusCodeAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of <see cref="DefaultStatusCodeAttribute"/>.
/// </summary>
/// <param name="statusCode">The default status code.</param>
public DefaultStatusCodeAttribute(int statusCode)
{
StatusCode = statusCode;
}
/// <summary>
/// Gets the default status code.
/// </summary>
public int StatusCode { get; }
}
}

View File

@ -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
{
/// <summary>
/// A <see cref="StatusCodeResult"/> that when executed will produce a 204 No Content response.
/// </summary>
[DefaultStatusCode(DefaultStatusCode)]
public class NoContentResult : StatusCodeResult
{
private const int DefaultStatusCode = StatusCodes.Status204NoContent;
/// <summary>
/// Initializes a new <see cref="NoContentResult"/> instance.
/// </summary>
public NoContentResult()
: base(StatusCodes.Status204NoContent)
: base(DefaultStatusCode)
{
}
}

View File

@ -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
{
/// <summary>
/// An <see cref="ObjectResult"/> that when executed will produce a Not Found (404) response.
/// </summary>
[DefaultStatusCode(DefaultStatusCode)]
public class NotFoundObjectResult : ObjectResult
{
private const int DefaultStatusCode = StatusCodes.Status404NotFound;
/// <summary>
/// Creates a new <see cref="NotFoundObjectResult"/> instance.
/// </summary>
@ -17,7 +21,7 @@ namespace Microsoft.AspNetCore.Mvc
public NotFoundObjectResult(object value)
: base(value)
{
StatusCode = StatusCodes.Status404NotFound;
StatusCode = DefaultStatusCode;
}
}
}

View File

@ -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 <see cref="StatusCodeResult"/> that when
/// executed will produce a Not Found (404) response.
/// </summary>
[DefaultStatusCode(DefaultStatusCode)]
public class NotFoundResult : StatusCodeResult
{
private const int DefaultStatusCode = StatusCodes.Status404NotFound;
/// <summary>
/// Creates a new <see cref="NotFoundResult"/> instance.
/// </summary>
public NotFoundResult() : base(StatusCodes.Status404NotFound)
public NotFoundResult() : base(DefaultStatusCode)
{
}
}

View File

@ -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

View File

@ -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 <see cref="ObjectResult"/> that when executed performs content negotiation, formats the entity body, and
/// will produce a <see cref="StatusCodes.Status200OK"/> response if negotiation and formatting succeed.
/// </summary>
[DefaultStatusCode(DefaultStatusCode)]
public class OkObjectResult : ObjectResult
{
private const int DefaultStatusCode = StatusCodes.Status200OK;
/// <summary>
/// Initializes a new instance of the <see cref="OkObjectResult"/> class.
/// </summary>
@ -18,7 +22,7 @@ namespace Microsoft.AspNetCore.Mvc
public OkObjectResult(object value)
: base(value)
{
StatusCode = StatusCodes.Status200OK;
StatusCode = DefaultStatusCode;
}
}
}

View File

@ -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 <see cref="StatusCodeResult"/> that when executed will produce an empty
/// <see cref="StatusCodes.Status200OK"/> response.
/// </summary>
[DefaultStatusCode(DefaultStatusCode)]
public class OkResult : StatusCodeResult
{
private const int DefaultStatusCode = StatusCodes.Status200OK;
/// <summary>
/// Initializes a new instance of the <see cref="OkResult"/> class.
/// </summary>
public OkResult()
: base(StatusCodes.Status200OK)
: base(DefaultStatusCode)
{
}
}

View File

@ -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.

View File

@ -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 <see cref="UnauthorizedResult"/> that when
/// executed will produce an Unauthorized (401) response.
/// </summary>
[DefaultStatusCode(DefaultStatusCode)]
public class UnauthorizedResult : StatusCodeResult
{
private const int DefaultStatusCode = StatusCodes.Status401Unauthorized;
/// <summary>
/// Creates a new <see cref="UnauthorizedResult"/> instance.
/// </summary>
public UnauthorizedResult() : base(StatusCodes.Status401Unauthorized)
public UnauthorizedResult() : base(DefaultStatusCode)
{
}
}

View File

@ -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
/// <summary>
/// An <see cref="ObjectResult"/> that when executed will produce a Unprocessable Entity (422) response.
/// </summary>
[DefaultStatusCode(DefaultStatusCode)]
public class UnprocessableEntityObjectResult : ObjectResult
{
private const int DefaultStatusCode = StatusCodes.Status422UnprocessableEntity;
/// <summary>
/// Creates a new <see cref="UnprocessableEntityObjectResult"/> instance.
/// </summary>
@ -27,7 +31,7 @@ namespace Microsoft.AspNetCore.Mvc
public UnprocessableEntityObjectResult(object error)
: base(error)
{
StatusCode = StatusCodes.Status422UnprocessableEntity;
StatusCode = DefaultStatusCode;
}
}
}

View File

@ -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 <see cref="StatusCodeResult"/> that when
/// executed will produce a Unprocessable Entity (422) response.
/// </summary>
[DefaultStatusCode(DefaultStatusCode)]
public class UnprocessableEntityResult : StatusCodeResult
{
private const int DefaultStatusCode = StatusCodes.Status422UnprocessableEntity;
/// <summary>
/// Creates a new <see cref="UnprocessableEntityResult"/> instance.
/// </summary>
public UnprocessableEntityResult()
: base(StatusCodes.Status422UnprocessableEntity)
: base(DefaultStatusCode)
{
}
}

View File

@ -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 <see cref="StatusCodeResult"/> that when
/// executed will produce a UnsupportedMediaType (415) response.
/// </summary>
[DefaultStatusCode(DefaultStatusCode)]
public class UnsupportedMediaTypeResult : StatusCodeResult
{
private const int DefaultStatusCode = StatusCodes.Status415UnsupportedMediaType;
/// <summary>
/// Creates a new instance of <see cref="UnsupportedMediaTypeResult"/>.
/// </summary>
public UnsupportedMediaTypeResult() : base(StatusCodes.Status415UnsupportedMediaType)
public UnsupportedMediaTypeResult() : base(DefaultStatusCode)
{
}
}

View File

@ -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);

View File

@ -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<>));

View File

@ -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<Compilation> GetResponseMetadataCompilation() => GetCompilation("GetResponseMetadataTests");
private Task<Compilation> GetCompilation(string test)
{
var testSource = MvcTestSource.Read(GetType().Name, test);
var project = DiagnosticProject.Create(GetType().Assembly, new[] { testSource.Source });
return project.GetCompilationAsync();
}
}
}

View File

@ -0,0 +1,7 @@
namespace Microsoft.AspNetCore.Mvc.Analyzers
{
public class GetAttributes_OnMethodWithoutAttributesClass
{
public void Method() { }
}
}

View File

@ -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() { }
}
}

View File

@ -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() { }
}
}

View File

@ -0,0 +1,8 @@
namespace Microsoft.AspNetCore.Mvc.Analyzers
{
public class GetAttributes_WithoutMethodOverridding
{
[ProducesResponseType(201)]
public void Method() { }
}
}

View File

@ -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<Person> GetPerson(int id) => null;
public ActionResult<Person> 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);
}
}
}
}