Change DefaultApiConventions (#7939)
* Change DefaultApiConventions * Introduce attributes for matching by name and type. * Move discovery of ApiConventionAttribute to ApiBehaviorApplicationModelProvider. This is required for us to detect during startup if the convention is incorrectly authored.
This commit is contained in:
parent
53857d052f
commit
94a7c83998
|
|
@ -4,7 +4,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
|
|
@ -39,57 +38,19 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
|||
var runtimeReturnType = GetRuntimeReturnType(declaredReturnType);
|
||||
|
||||
var responseMetadataAttributes = GetResponseMetadataAttributes(action);
|
||||
if (responseMetadataAttributes.Length == 0)
|
||||
if (responseMetadataAttributes.Count == 0 &&
|
||||
action.Properties.TryGetValue(typeof(ApiConventionResult), out var result))
|
||||
{
|
||||
// Action does not have any conventions. Look for conventions on the type.
|
||||
responseMetadataAttributes = GetResponseMetadataAttributesFromConventions(action);
|
||||
// Action does not have any conventions. Use conventions on it if present.
|
||||
var apiConventionResult = (ApiConventionResult)result;
|
||||
responseMetadataAttributes = apiConventionResult.ResponseMetadataProviders;
|
||||
}
|
||||
|
||||
var apiResponseTypes = GetApiResponseTypes(responseMetadataAttributes, runtimeReturnType);
|
||||
return apiResponseTypes;
|
||||
}
|
||||
|
||||
private IApiResponseMetadataProvider[] GetResponseMetadataAttributesFromConventions(ControllerActionDescriptor action)
|
||||
{
|
||||
if (action.FilterDescriptors == null)
|
||||
{
|
||||
return Array.Empty<IApiResponseMetadataProvider>();
|
||||
}
|
||||
|
||||
foreach (var filterDescriptor in action.FilterDescriptors)
|
||||
{
|
||||
if (!(filterDescriptor.Filter is ApiConventionAttribute apiConventionAttribute))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var method = GetConventionMethod(action.MethodInfo, apiConventionAttribute.ConventionType);
|
||||
if (method != null)
|
||||
{
|
||||
return method.GetCustomAttributes(inherit: false)
|
||||
.OfType<IApiResponseMetadataProvider>()
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
return Array.Empty<IApiResponseMetadataProvider>();
|
||||
}
|
||||
|
||||
private MethodInfo GetConventionMethod(MethodInfo methodInfo, Type conventions)
|
||||
{
|
||||
var conventionMethods = conventions.GetMethods(BindingFlags.Public | BindingFlags.Static);
|
||||
for (var i = 0; i < conventionMethods.Length; i++)
|
||||
{
|
||||
if (IsMatch(methodInfo, conventionMethods[i]))
|
||||
{
|
||||
return conventionMethods[i];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private IApiResponseMetadataProvider[] GetResponseMetadataAttributes(ControllerActionDescriptor action)
|
||||
private IReadOnlyList<IApiResponseMetadataProvider> GetResponseMetadataAttributes(ControllerActionDescriptor action)
|
||||
{
|
||||
if (action.FilterDescriptors == null)
|
||||
{
|
||||
|
|
@ -107,7 +68,7 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
|||
}
|
||||
|
||||
private IList<ApiResponseType> GetApiResponseTypes(
|
||||
IApiResponseMetadataProvider[] responseMetadataAttributes,
|
||||
IReadOnlyList<IApiResponseMetadataProvider> responseMetadataAttributes,
|
||||
Type type)
|
||||
{
|
||||
var results = new List<ApiResponseType>();
|
||||
|
|
@ -240,95 +201,5 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
|||
|
||||
return declaredReturnType;
|
||||
}
|
||||
|
||||
internal static bool IsMatch(MethodInfo methodInfo, MethodInfo conventionMethod)
|
||||
{
|
||||
if (!IsMethodNameMatch(methodInfo.Name, conventionMethod.Name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var methodParameters = methodInfo.GetParameters();
|
||||
var conventionMethodParameters = conventionMethod.GetParameters();
|
||||
if (conventionMethodParameters.Length != methodParameters.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < conventionMethodParameters.Length; i++)
|
||||
{
|
||||
if (conventionMethodParameters[i].ParameterType.IsGenericParameter)
|
||||
{
|
||||
// Use TModel as wildcard
|
||||
continue;
|
||||
}
|
||||
else if (!IsParameterNameMatch(methodParameters[i].Name, conventionMethodParameters[i].Name) ||
|
||||
!IsParameterTypeMatch(methodParameters[i].ParameterType, conventionMethodParameters[i].ParameterType))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static bool IsMethodNameMatch(string name, string conventionName)
|
||||
{
|
||||
if (!name.StartsWith(conventionName, StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (name.Length == conventionName.Length)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return char.IsUpper(name[conventionName.Length]);
|
||||
}
|
||||
|
||||
internal static bool IsParameterNameMatch(string name, string conventionName)
|
||||
{
|
||||
// Leading underscores could be used to allow multiple parameter names with the same suffix e.g. GetPersonAddress(int personId, int addressId)
|
||||
// A common convention that allows targeting these category of methods would look like Get(int id, int _id)
|
||||
conventionName = conventionName.Trim('_');
|
||||
|
||||
// name = id, conventionName = id
|
||||
if (string.Equals(name, conventionName, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (name.Length <= conventionName.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// name = personId, conventionName = id
|
||||
var index = name.Length - conventionName.Length - 1;
|
||||
if (!char.IsLower(name[index]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
index++;
|
||||
if (name[index] != char.ToUpper(conventionName[0]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
index++;
|
||||
return string.Compare(name, index, conventionName, 1, conventionName.Length - 1, StringComparison.Ordinal) == 0;
|
||||
}
|
||||
|
||||
internal static bool IsParameterTypeMatch(Type parameterType, Type conventionParameterType)
|
||||
{
|
||||
if (conventionParameterType == typeof(object))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return conventionParameterType.IsAssignableFrom(parameterType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
// 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.Core;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
|
||||
public sealed class ApiConventionAttribute : Attribute, IFilterMetadata
|
||||
{
|
||||
public ApiConventionAttribute(Type conventionType)
|
||||
{
|
||||
ConventionType = conventionType ?? throw new ArgumentNullException(nameof(conventionType));
|
||||
|
||||
if (!ConventionType.IsSealed || !ConventionType.IsAbstract)
|
||||
{
|
||||
// Conventions must be static viz abstract + sealed.
|
||||
throw new ArgumentException(Resources.FormatApiConventionMustBeStatic(conventionType), nameof(conventionType));
|
||||
}
|
||||
}
|
||||
|
||||
public Type ConventionType { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
// 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.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.AspNetCore.Mvc.Core;
|
||||
using Microsoft.Extensions.Internal;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc
|
||||
{
|
||||
/// <summary>
|
||||
/// API conventions to be applied to an assembly containing MVC controllers or a single controller.
|
||||
/// <para>
|
||||
/// API conventions are used to influence the output of ApiExplorer.
|
||||
/// Conventions must be static types. Methods in a convention are
|
||||
/// matched to an action method using rules specified by <see cref="ApiConventionNameMatchAttribute" />
|
||||
/// that may be applied to a method name or it's parameters and <see cref="ApiConventionTypeMatchAttribute"/>
|
||||
/// that are applied to parameters.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// When no attributes are found specifying the behavior, MVC matches method names and parameter names are matched
|
||||
/// using <see cref="ApiConventionNameMatchBehavior.Exact"/> and parameter types are matched
|
||||
/// using <see cref="ApiConventionTypeMatchBehavior.AssignableFrom"/>.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
|
||||
public sealed class ApiConventionTypeAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes an <see cref="ApiConventionTypeAttribute"/> instance using <paramref name="conventionType"/>.
|
||||
/// </summary>
|
||||
/// <param name="conventionType">
|
||||
/// The <see cref="Type"/> of the convention.
|
||||
/// <para>
|
||||
/// Conventions must be static types. Methods in a convention are
|
||||
/// matched to an action method using rules specified by <see cref="ApiConventionNameMatchAttribute" />
|
||||
/// that may be applied to a method name or it's parameters and <see cref="ApiConventionTypeMatchAttribute"/>
|
||||
/// that are applied to parameters.
|
||||
/// </para>
|
||||
/// </param>
|
||||
public ApiConventionTypeAttribute(Type conventionType)
|
||||
{
|
||||
ConventionType = conventionType ?? throw new ArgumentNullException(nameof(conventionType));
|
||||
EnsureValid(conventionType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the convention type.
|
||||
/// </summary>
|
||||
public Type ConventionType { get; }
|
||||
|
||||
private static void EnsureValid(Type conventionType)
|
||||
{
|
||||
if (!conventionType.IsSealed || !conventionType.IsAbstract)
|
||||
{
|
||||
// Conventions must be static viz abstract + sealed.
|
||||
throw new ArgumentException(Resources.FormatApiConventionMustBeStatic(conventionType), nameof(conventionType));
|
||||
}
|
||||
|
||||
foreach (var method in conventionType.GetMethods(BindingFlags.Public | BindingFlags.Static))
|
||||
{
|
||||
var unsupportedAttributes = method.GetCustomAttributes(inherit: true)
|
||||
.Where(attribute => !IsAllowedAttribute(attribute))
|
||||
.ToArray();
|
||||
|
||||
if (unsupportedAttributes.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var methodDisplayName = TypeNameHelper.GetTypeDisplayName(method.DeclaringType) + "." + method.Name;
|
||||
var errorMessage = Resources.FormatApiConvention_UnsupportedAttributesOnConvention(
|
||||
methodDisplayName,
|
||||
Environment.NewLine + string.Join(Environment.NewLine, unsupportedAttributes) + Environment.NewLine,
|
||||
$"{nameof(ProducesResponseTypeAttribute)}, {nameof(ApiConventionNameMatchAttribute)}");
|
||||
|
||||
throw new ArgumentException(errorMessage, nameof(conventionType));
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsAllowedAttribute(object attribute)
|
||||
{
|
||||
return attribute is ProducesResponseTypeAttribute ||
|
||||
attribute is ApiConventionNameMatchAttribute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
// 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.ApiExplorer
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines the matching behavior an API convention method or parameter by name.
|
||||
/// <see cref="ApiConventionNameMatchBehavior"/> for supported options.
|
||||
/// <seealso cref="ApiConventionTypeAttribute"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see cref="ApiConventionNameMatchBehavior.Exact"/> is used if no value for this
|
||||
/// attribute is specified on a convention method or parameter.
|
||||
/// </remarks>
|
||||
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
|
||||
public sealed class ApiConventionNameMatchAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="ApiConventionNameMatchAttribute"/>.
|
||||
/// </summary>
|
||||
/// <param name="matchBehavior">The <see cref="ApiConventionNameMatchBehavior"/>.</param>
|
||||
public ApiConventionNameMatchAttribute(ApiConventionNameMatchBehavior matchBehavior)
|
||||
{
|
||||
MatchBehavior = matchBehavior;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="ApiConventionNameMatchBehavior"/>.
|
||||
/// </summary>
|
||||
public ApiConventionNameMatchBehavior MatchBehavior { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||
{
|
||||
/// <summary>
|
||||
/// The behavior for matching the name of a convention parameter or method.
|
||||
/// </summary>
|
||||
public enum ApiConventionNameMatchBehavior
|
||||
{
|
||||
/// <summary>
|
||||
/// Matches any name. Use this if the parameter or method name does not need to be matched.
|
||||
/// </summary>
|
||||
Any,
|
||||
|
||||
/// <summary>
|
||||
/// The parameter or method name must exactly match the convention.
|
||||
/// </summary>
|
||||
Exact,
|
||||
|
||||
/// <summary>
|
||||
/// The parameter or method name in the convention is a proper prefix.
|
||||
/// <para>
|
||||
/// Casing is used to delineate words in a given name. For instance, with this behavior
|
||||
/// the convention name "Get" will match "Get", "GetPerson" or "GetById", but not "getById", "Getaway".
|
||||
/// </para>
|
||||
/// </summary>
|
||||
Prefix,
|
||||
|
||||
/// <summary>
|
||||
/// The parameter or method name in the convention is a proper suffix.
|
||||
/// <para>
|
||||
/// Casing is used to delineate words in a given name. For instance, with this behavior
|
||||
/// the convention name "id" will match "id", or "personId" but not "grid" or "personid".
|
||||
/// </para>
|
||||
/// </summary>
|
||||
Suffix,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,229 @@
|
|||
// 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 System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||
{
|
||||
/// <summary>
|
||||
/// Metadata associated with an action method via API convention.
|
||||
/// </summary>
|
||||
public sealed class ApiConventionResult
|
||||
{
|
||||
public ApiConventionResult(IReadOnlyList<IApiResponseMetadataProvider> responseMetadataProviders)
|
||||
{
|
||||
ResponseMetadataProviders = responseMetadataProviders ??
|
||||
throw new ArgumentNullException(nameof(responseMetadataProviders));
|
||||
}
|
||||
|
||||
public IReadOnlyList<IApiResponseMetadataProvider> ResponseMetadataProviders { get; }
|
||||
|
||||
internal static bool TryGetApiConvention(
|
||||
MethodInfo method,
|
||||
ApiConventionTypeAttribute[] apiConventionAttributes,
|
||||
out ApiConventionResult result)
|
||||
{
|
||||
foreach (var attribute in apiConventionAttributes)
|
||||
{
|
||||
var conventionMethod = GetConventionMethod(method, attribute.ConventionType);
|
||||
if (conventionMethod != null)
|
||||
{
|
||||
var metadataProviders = conventionMethod.GetCustomAttributes(inherit: false)
|
||||
.OfType<IApiResponseMetadataProvider>()
|
||||
.ToArray();
|
||||
|
||||
result = new ApiConventionResult(metadataProviders);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static MethodInfo GetConventionMethod(MethodInfo method, Type conventionType)
|
||||
{
|
||||
foreach (var conventionMethod in conventionType.GetMethods())
|
||||
{
|
||||
if (IsMatch(method, conventionMethod))
|
||||
{
|
||||
return conventionMethod;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
internal static bool IsMatch(MethodInfo methodInfo, MethodInfo conventionMethod)
|
||||
{
|
||||
return MethodMatches() && ParametersMatch();
|
||||
|
||||
bool MethodMatches()
|
||||
{
|
||||
var methodNameMatchBehavior = GetNameMatchBehavior(conventionMethod);
|
||||
if (!IsNameMatch(methodInfo.Name, conventionMethod.Name, methodNameMatchBehavior))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ParametersMatch()
|
||||
{
|
||||
var methodParameters = methodInfo.GetParameters();
|
||||
var conventionMethodParameters = conventionMethod.GetParameters();
|
||||
|
||||
for (var i = 0; i < conventionMethodParameters.Length; i++)
|
||||
{
|
||||
var conventionParameter = conventionMethodParameters[i];
|
||||
if (conventionParameter.IsDefined(typeof(ParamArrayAttribute)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (methodParameters.Length <= i)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var nameMatchBehavior = GetNameMatchBehavior(conventionParameter);
|
||||
var typeMatchBehavior = GetTypeMatchBehavior(conventionParameter);
|
||||
|
||||
if (!IsTypeMatch(methodParameters[i].ParameterType, conventionParameter.ParameterType, typeMatchBehavior) ||
|
||||
!IsNameMatch(methodParameters[i].Name, conventionParameter.Name, nameMatchBehavior))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure convention has at least as many parameters as the method. params convention argument are handled
|
||||
// inside the for loop.
|
||||
return methodParameters.Length == conventionMethodParameters.Length;
|
||||
}
|
||||
}
|
||||
|
||||
internal static ApiConventionNameMatchBehavior GetNameMatchBehavior(ICustomAttributeProvider attributeProvider)
|
||||
{
|
||||
var attribute = GetCustomAttribute<ApiConventionNameMatchAttribute>(attributeProvider);
|
||||
return attribute?.MatchBehavior ?? ApiConventionNameMatchBehavior.Exact;
|
||||
}
|
||||
|
||||
internal static ApiConventionTypeMatchBehavior GetTypeMatchBehavior(ICustomAttributeProvider attributeProvider)
|
||||
{
|
||||
var attribute = GetCustomAttribute<ApiConventionTypeMatchAttribute>(attributeProvider);
|
||||
return attribute?.MatchBehavior ?? ApiConventionTypeMatchBehavior.AssignableFrom;
|
||||
}
|
||||
|
||||
private static TAttribute GetCustomAttribute<TAttribute>(ICustomAttributeProvider attributeProvider)
|
||||
{
|
||||
var attributes = attributeProvider.GetCustomAttributes(inherit: false);
|
||||
for (var i = 0; i < attributes.Length; i++)
|
||||
{
|
||||
if (attributes[i] is TAttribute attribute)
|
||||
{
|
||||
return attribute;
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
internal static bool IsNameMatch(string name, string conventionName, ApiConventionNameMatchBehavior nameMatchBehavior)
|
||||
{
|
||||
switch (nameMatchBehavior)
|
||||
{
|
||||
case ApiConventionNameMatchBehavior.Any:
|
||||
return true;
|
||||
|
||||
case ApiConventionNameMatchBehavior.Exact:
|
||||
return string.Equals(name, conventionName, StringComparison.Ordinal);
|
||||
|
||||
case ApiConventionNameMatchBehavior.Prefix:
|
||||
return IsNameMatchPrefix();
|
||||
|
||||
case ApiConventionNameMatchBehavior.Suffix:
|
||||
return IsNameMatchSuffix();
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
bool IsNameMatchPrefix()
|
||||
{
|
||||
if (name.Length < conventionName.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (name.Length == conventionName.Length)
|
||||
{
|
||||
// name = "Post", conventionName = "Post"
|
||||
return string.Equals(name, conventionName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
if (!name.StartsWith(conventionName, StringComparison.Ordinal))
|
||||
{
|
||||
// name = "GetPerson", conventionName = "Post"
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for name = "PostPerson", conventionName = "Post"
|
||||
// Verify the first letter after the convention name is upper case. In this case 'P' from "Person"
|
||||
return char.IsUpper(name[conventionName.Length]);
|
||||
}
|
||||
|
||||
bool IsNameMatchSuffix()
|
||||
{
|
||||
if (name.Length < conventionName.Length)
|
||||
{
|
||||
// name = "person", conventionName = "personName"
|
||||
return false;
|
||||
}
|
||||
|
||||
if (name.Length == conventionName.Length)
|
||||
{
|
||||
// name = id, conventionName = id
|
||||
return string.Equals(name, conventionName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
// Check for name = personName, conventionName = name
|
||||
var index = name.Length - conventionName.Length - 1;
|
||||
if (!char.IsLower(name[index]))
|
||||
{
|
||||
// Verify letter before "name" is lowercase. In this case the letter 'n' at the end of "person"
|
||||
return false;
|
||||
}
|
||||
|
||||
index++;
|
||||
if (name[index] != char.ToUpper(conventionName[0]))
|
||||
{
|
||||
// Verify the first letter from convention is upper case. In this case 'n' from "name"
|
||||
return false;
|
||||
}
|
||||
|
||||
// Match the remaining letters with exact case. i.e. match "ame" from "personName", "name"
|
||||
index++;
|
||||
return string.Compare(name, index, conventionName, 1, conventionName.Length - 1, StringComparison.Ordinal) == 0;
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool IsTypeMatch(Type type, Type conventionType, ApiConventionTypeMatchBehavior typeMatchBehavior)
|
||||
{
|
||||
switch (typeMatchBehavior)
|
||||
{
|
||||
case ApiConventionTypeMatchBehavior.Any:
|
||||
return true;
|
||||
|
||||
case ApiConventionTypeMatchBehavior.AssignableFrom:
|
||||
return conventionType.IsAssignableFrom(type);
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
// 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.ApiExplorer
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines the matching behavior an API convention parameter by type.
|
||||
/// <see cref="ApiConventionTypeMatchBehavior"/> for supported options.
|
||||
/// <seealso cref="ApiConventionTypeAttribute"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see cref="ApiConventionTypeMatchBehavior.AssignableFrom"/> is used if no value for this
|
||||
/// attribute is specified on a convention parameter.
|
||||
/// </remarks>
|
||||
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
|
||||
public sealed class ApiConventionTypeMatchAttribute : Attribute
|
||||
{
|
||||
public ApiConventionTypeMatchAttribute(ApiConventionTypeMatchBehavior matchBehavior)
|
||||
{
|
||||
MatchBehavior = matchBehavior;
|
||||
}
|
||||
|
||||
public ApiConventionTypeMatchBehavior MatchBehavior { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||
{
|
||||
/// <summary>
|
||||
/// The behavior for matching the name of a convention parameter.
|
||||
/// </summary>
|
||||
public enum ApiConventionTypeMatchBehavior
|
||||
{
|
||||
/// <summary>
|
||||
/// Matches any type. Use this if the parameter does not need to be matched.
|
||||
/// </summary>
|
||||
Any,
|
||||
|
||||
/// <summary>
|
||||
/// The parameter in the convention is the exact type or a subclass of the type
|
||||
/// specified in the convention.
|
||||
/// </summary>
|
||||
AssignableFrom,
|
||||
}
|
||||
}
|
||||
|
|
@ -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.ApiExplorer;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc
|
||||
{
|
||||
|
|
@ -9,23 +10,40 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
{
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public static void Get<TModel>(object id) { }
|
||||
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public static void Get<TModel>() { }
|
||||
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)]
|
||||
public static void Get(
|
||||
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Suffix)]
|
||||
[ApiConventionTypeMatch(ApiConventionTypeMatchBehavior.Any)]
|
||||
object id) { }
|
||||
|
||||
[ProducesResponseType(StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public static void Post<TModel>(TModel model) { }
|
||||
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)]
|
||||
public static void Post(
|
||||
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Any)]
|
||||
[ApiConventionTypeMatch(ApiConventionTypeMatchBehavior.Any)]
|
||||
object model) { }
|
||||
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public static void Put<TModel>(object id, TModel model) { }
|
||||
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)]
|
||||
public static void Put(
|
||||
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Suffix)]
|
||||
[ApiConventionTypeMatch(ApiConventionTypeMatchBehavior.Any)]
|
||||
object id,
|
||||
|
||||
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Any)]
|
||||
[ApiConventionTypeMatch(ApiConventionTypeMatchBehavior.Any)]
|
||||
object model) { }
|
||||
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public static void Delete<TModel>(object id) { }
|
||||
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)]
|
||||
public static void Delete(
|
||||
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Suffix)]
|
||||
[ApiConventionTypeMatch(ApiConventionTypeMatchBehavior.Any)]
|
||||
object id) { }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using System;
|
|||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||
using Microsoft.AspNetCore.Mvc.Core;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
|
|
@ -68,11 +69,15 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
if (isApiController)
|
||||
{
|
||||
InferBoundPropertyModelPrefixes(controllerModel);
|
||||
|
||||
AddGloballyConfiguredApiConventions(controllerModel);
|
||||
}
|
||||
|
||||
var controllerHasSelectorModel = controllerModel.Selectors.Any(s => s.AttributeRouteModel != null);
|
||||
var conventions = controllerModel.Attributes.OfType<ApiConventionTypeAttribute>().ToArray();
|
||||
if (conventions.Length == 0)
|
||||
{
|
||||
var controllerAssembly = controllerModel.ControllerType.Assembly;
|
||||
conventions = controllerAssembly.GetCustomAttributes<ApiConventionTypeAttribute>().ToArray();
|
||||
}
|
||||
|
||||
foreach (var actionModel in controllerModel.Actions)
|
||||
{
|
||||
|
|
@ -90,25 +95,12 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
InferParameterModelPrefixes(actionModel);
|
||||
|
||||
AddMultipartFormDataConsumesAttribute(actionModel);
|
||||
|
||||
DiscoverApiConvention(actionModel, conventions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static void AddGloballyConfiguredApiConventions(ControllerModel controllerModel)
|
||||
{
|
||||
if (controllerModel.Filters.OfType<ApiConventionAttribute>().Any())
|
||||
{
|
||||
// ApiControllerAttribute is already associated with controller. Do not look for conventions configured at assembly.
|
||||
return;
|
||||
}
|
||||
|
||||
var assembly = controllerModel.ControllerType.Assembly;
|
||||
foreach (var attribute in assembly.GetCustomAttributes<ApiConventionAttribute>())
|
||||
{
|
||||
controllerModel.Filters.Add(attribute);
|
||||
}
|
||||
}
|
||||
|
||||
// Internal for unit testing
|
||||
internal void AddMultipartFormDataConsumesAttribute(ActionModel actionModel)
|
||||
{
|
||||
|
|
@ -250,6 +242,20 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
return bindingSource;
|
||||
}
|
||||
|
||||
internal static void DiscoverApiConvention(ActionModel actionModel, ApiConventionTypeAttribute[] apiConventionAttributes)
|
||||
{
|
||||
if (actionModel.Filters.OfType<IApiResponseMetadataProvider>().Any())
|
||||
{
|
||||
// If an action already has providers, don't discover any from conventions.
|
||||
return;
|
||||
}
|
||||
|
||||
if (ApiConventionResult.TryGetApiConvention(actionModel.ActionMethod, apiConventionAttributes, out var result))
|
||||
{
|
||||
actionModel.Properties[typeof(ApiConventionResult)] = result;
|
||||
}
|
||||
}
|
||||
|
||||
private bool ParameterExistsInAnyRoute(ActionModel actionModel, string parameterName)
|
||||
{
|
||||
foreach (var (route, _, _) in ActionAttributeRouteModel.GetAttributeRoutes(actionModel))
|
||||
|
|
|
|||
|
|
@ -1494,6 +1494,20 @@ namespace Microsoft.AspNetCore.Mvc.Core
|
|||
internal static string FormatInvalidTypeTForActionResultOfT(object p0, object p1)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("InvalidTypeTForActionResultOfT"), p0, p1);
|
||||
|
||||
/// <summary>
|
||||
/// Method {0} is decorated with the following attributes that are not allowed on an API convention method:{1}The following attributes are allowed on API convention methods: {2}.
|
||||
/// </summary>
|
||||
internal static string ApiConvention_UnsupportedAttributesOnConvention
|
||||
{
|
||||
get => GetString("ApiConvention_UnsupportedAttributesOnConvention");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Method {0} is decorated with the following attributes that are not allowed on an API convention method:{1}The following attributes are allowed on API convention methods: {2}.
|
||||
/// </summary>
|
||||
internal static string FormatApiConvention_UnsupportedAttributesOnConvention(object p0, object p1, object p2)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("ApiConvention_UnsupportedAttributesOnConvention"), p0, p1, p2);
|
||||
|
||||
private static string GetString(string name, params string[] formatterNames)
|
||||
{
|
||||
var value = _resourceManager.GetString(name);
|
||||
|
|
|
|||
|
|
@ -448,4 +448,7 @@
|
|||
<data name="InvalidTypeTForActionResultOfT" xml:space="preserve">
|
||||
<value>Invalid type parameter '{0}' specified for '{1}'.</value>
|
||||
</data>
|
||||
<data name="ApiConvention_UnsupportedAttributesOnConvention" xml:space="preserve">
|
||||
<value>Method {0} is decorated with the following attributes that are not allowed on an API convention method:{1}The following attributes are allowed on API convention methods: {2}.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -17,304 +17,6 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
|||
{
|
||||
public class ApiResponseTypeProviderTest
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("id", "model")]
|
||||
[InlineData("id", "person")]
|
||||
[InlineData("id", "i")]
|
||||
public void IsParameterNameMatch_ReturnsFalse_IfConventionNameIsNotSuffix(string parameterName, string conventionName)
|
||||
{
|
||||
// Act
|
||||
var result = ApiResponseTypeProvider.IsParameterNameMatch(parameterName, conventionName);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsParameterNameMatch_ReturnsFalse_IfConventionNameIsNotExactCaseSensitiveMatch()
|
||||
{
|
||||
// Arrange
|
||||
var parameterName = "Id";
|
||||
var conventionName = "id";
|
||||
|
||||
// Act
|
||||
var result = ApiResponseTypeProvider.IsParameterNameMatch(parameterName, conventionName);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("rid", "id")]
|
||||
[InlineData("candid", "id")]
|
||||
[InlineData("colocation", "location")]
|
||||
public void IsParamterNameMatch_ReturnsFalse_IfConventionNameIsNotProperSuffix(string parameterName, string conventionName)
|
||||
{
|
||||
// Act
|
||||
var result = ApiResponseTypeProvider.IsParameterNameMatch(parameterName, conventionName);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("id", "id")]
|
||||
[InlineData("model", "model")]
|
||||
public void IsParamterNameMatch_ReturnsTrue_IfConventionNameIsExactMatch(string parameterName, string conventionName)
|
||||
{
|
||||
// Act
|
||||
var result = ApiResponseTypeProvider.IsParameterNameMatch(parameterName, conventionName);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("id", "_id")]
|
||||
[InlineData("model", "_model")]
|
||||
public void IsParamterNameMatch_ReturnsTrue_IfConventionNameIsExactMatchIgnoringLeadingUnderscores(string parameterName, string conventionName)
|
||||
{
|
||||
// Act
|
||||
var result = ApiResponseTypeProvider.IsParameterNameMatch(parameterName, conventionName);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("personId", "id")]
|
||||
[InlineData("userModel", "model")]
|
||||
[InlineData("beaconLocation", "Location")]
|
||||
public void IsParamterNameMatch_ReturnsTrue_IfConventionNameIsProperSuffix(string parameterName, string conventionName)
|
||||
{
|
||||
// Act
|
||||
var result = ApiResponseTypeProvider.IsParameterNameMatch(parameterName, conventionName);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("personId", "_id")]
|
||||
[InlineData("userModel", "_model")]
|
||||
[InlineData("userModel", "__model")]
|
||||
public void IsParamterNameMatch_ReturnsTrue_IfConventionNameIsProperSuffixIgnoringLeadingUnderscores(string parameterName, string conventionName)
|
||||
{
|
||||
// Act
|
||||
var result = ApiResponseTypeProvider.IsParameterNameMatch(parameterName, conventionName);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsParameterTypeMatch_ReturnsFalse_ForUnrelatedTypes()
|
||||
{
|
||||
// Arrange
|
||||
var type = typeof(string);
|
||||
var conventionType = typeof(int);
|
||||
|
||||
// Act
|
||||
var result = ApiResponseTypeProvider.IsParameterTypeMatch(type, conventionType);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsParameterTypeMatch_ReturnsFalse_IfTypeIsBaseClassOfConvention()
|
||||
{
|
||||
// Arrange
|
||||
var type = typeof(BaseModel);
|
||||
var conventionType = typeof(DerivedModel);
|
||||
|
||||
// Act
|
||||
var result = ApiResponseTypeProvider.IsParameterTypeMatch(type, conventionType);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsParameterTypeMatch_ReturnsTrue_IfTypeIsExact()
|
||||
{
|
||||
// Arrange
|
||||
var type = typeof(Uri);
|
||||
var conventionType = typeof(Uri);
|
||||
|
||||
// Act
|
||||
var result = ApiResponseTypeProvider.IsParameterTypeMatch(type, conventionType);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsParameterTypeMatch_ReturnsTrue_IfTypeIsSubtypeOfConvention()
|
||||
{
|
||||
// Arrange
|
||||
var type = typeof(DerivedModel);
|
||||
var conventionType = typeof(BaseModel);
|
||||
|
||||
// Act
|
||||
var result = ApiResponseTypeProvider.IsParameterTypeMatch(type, conventionType);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(typeof(int))]
|
||||
[InlineData(typeof(DerivedModel))]
|
||||
public void IsParameterTypeMatch_ReturnsTrue_IfConventionTypeIsObject(Type type)
|
||||
{
|
||||
// Arrange
|
||||
var conventionType = typeof(object);
|
||||
|
||||
// Act
|
||||
var result = ApiResponseTypeProvider.IsParameterTypeMatch(type, conventionType);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Get", "Post")]
|
||||
[InlineData("Post", "Get")]
|
||||
[InlineData("PostPerson", "Put")]
|
||||
public void IsMethodNameMatch_ReturnsFalse_IfMethodIsNotPrefix(string methodName, string conventionMethodName)
|
||||
{
|
||||
// Act
|
||||
var result = ApiResponseTypeProvider.IsMethodNameMatch(methodName, conventionMethodName);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("PostalService", "Post")]
|
||||
[InlineData("Listings", "List")]
|
||||
[InlineData("Putt", "Put")]
|
||||
public void IsMethodNameMatch_ReturnsFalse_IfMethodIsNotProperPrefix(string methodName, string conventionMethodName)
|
||||
{
|
||||
// Act
|
||||
var result = ApiResponseTypeProvider.IsMethodNameMatch(methodName, conventionMethodName);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsMethodNameMatch_ReturnsTrue_IfMethodNameIsExactMatch()
|
||||
{
|
||||
// Arrange
|
||||
var methodName = "Post";
|
||||
var conventionMethodName = "Post";
|
||||
|
||||
// Act
|
||||
var result = ApiResponseTypeProvider.IsMethodNameMatch(methodName, conventionMethodName);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsMethodNameMatch_ReturnsFalse_IfMethodNameIsExactMatchWithDifferentCasing()
|
||||
{
|
||||
// Arrange
|
||||
var methodName = "post";
|
||||
var conventionMethodName = "Post";
|
||||
|
||||
// Act
|
||||
var result = ApiResponseTypeProvider.IsMethodNameMatch(methodName, conventionMethodName);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("PostPerson", "Post")]
|
||||
[InlineData("GetById", "Get")]
|
||||
[InlineData("SearchList", "Search")]
|
||||
public void IsMethodNameMatch_ReturnsTrue_IfMethodNameIsProperSuffix(string methodName, string conventionMethodName)
|
||||
{
|
||||
// Act
|
||||
var result = ApiResponseTypeProvider.IsMethodNameMatch(methodName, conventionMethodName);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsMethodNameMatch_ReturnsFalse_IfMethodNameIsProperSuffix_WithDifferentCasing()
|
||||
{
|
||||
// Arrange
|
||||
var methodName = "getById";
|
||||
var conventionMethodName = "Get";
|
||||
|
||||
// Act
|
||||
var result = ApiResponseTypeProvider.IsMethodNameMatch(methodName, conventionMethodName);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsMatch_ReturnsFalse_IfMethodNamesAreNotMatches()
|
||||
{
|
||||
// Arrange
|
||||
var conventionMethod = typeof(DefaultApiConventions).GetMethod(nameof(DefaultApiConventions.Post));
|
||||
var method = typeof(TestController).GetMethod(nameof(TestController.GetUser));
|
||||
|
||||
// Act
|
||||
var result = ApiResponseTypeProvider.IsMatch(method, conventionMethod);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsMatch_ReturnsFalse_IfParameterCountsDoNotMatch()
|
||||
{
|
||||
// Arrange
|
||||
var conventionMethod = typeof(DefaultApiConventions).GetMethod(nameof(DefaultApiConventions.Get), new[] { typeof(object) });
|
||||
var method = typeof(TestController).GetMethod(nameof(TestController.GetUserLocation));
|
||||
|
||||
// Act
|
||||
var result = ApiResponseTypeProvider.IsMatch(method, conventionMethod);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsMatch_ReturnsTrue_ForMethodWithObjectParameter()
|
||||
{
|
||||
// Arrange
|
||||
var conventionMethod = typeof(DefaultApiConventions).GetMethod(nameof(DefaultApiConventions.Get), new[] { typeof(object) });
|
||||
var method = typeof(TestController).GetMethod(nameof(TestController.GetUser));
|
||||
|
||||
// Act
|
||||
var result = ApiResponseTypeProvider.IsMatch(method, conventionMethod);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsMatch_ReturnsTrue_ForConventionWithGenericParameter()
|
||||
{
|
||||
// Arrange
|
||||
var conventionMethod = typeof(DefaultApiConventions).GetMethod(nameof(DefaultApiConventions.Put));
|
||||
var method = typeof(TestController).GetMethod(nameof(TestController.PutModel));
|
||||
|
||||
// Act
|
||||
var result = ApiResponseTypeProvider.IsMatch(method, conventionMethod);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetApiResponseTypes_ReturnsResponseTypesFromActionIfPresent()
|
||||
{
|
||||
|
|
@ -322,9 +24,11 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
|||
var actionDescriptor = GetControllerActionDescriptor(
|
||||
typeof(GetApiResponseTypes_ReturnsResponseTypesFromActionIfPresentController),
|
||||
nameof(GetApiResponseTypes_ReturnsResponseTypesFromActionIfPresentController.Get));
|
||||
|
||||
var filter = new FilterDescriptor(new ApiConventionAttribute(typeof(DefaultApiConventions)), FilterScope.Controller);
|
||||
actionDescriptor.FilterDescriptors.Add(filter);
|
||||
actionDescriptor.Properties[typeof(ApiConventionResult)] = new ApiConventionResult(new[]
|
||||
{
|
||||
new ProducesResponseTypeAttribute(201),
|
||||
new ProducesResponseTypeAttribute(404),
|
||||
});
|
||||
|
||||
var provider = GetProvider();
|
||||
|
||||
|
|
@ -359,7 +63,7 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
|||
});
|
||||
}
|
||||
|
||||
[ApiConvention(typeof(DefaultApiConventions))]
|
||||
[ApiConventionType(typeof(DefaultApiConventions))]
|
||||
public class GetApiResponseTypes_ReturnsResponseTypesFromActionIfPresentController : ControllerBase
|
||||
{
|
||||
[Produces(typeof(BaseModel))]
|
||||
|
|
@ -369,14 +73,19 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventions()
|
||||
public void GetApiResponseTypes_ReturnsResponseTypesFromApiConventionItem()
|
||||
{
|
||||
// Arrange
|
||||
var actionDescriptor = GetControllerActionDescriptor(
|
||||
typeof(GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventionsController),
|
||||
nameof(GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventionsController.DeleteBase));
|
||||
var filter = new FilterDescriptor(new ApiConventionAttribute(typeof(DefaultApiConventions)), FilterScope.Controller);
|
||||
actionDescriptor.FilterDescriptors.Add(filter);
|
||||
|
||||
actionDescriptor.Properties[typeof(ApiConventionResult)] = new ApiConventionResult(new[]
|
||||
{
|
||||
new ProducesResponseTypeAttribute(200),
|
||||
new ProducesResponseTypeAttribute(400),
|
||||
new ProducesResponseTypeAttribute(404),
|
||||
});
|
||||
|
||||
var provider = GetProvider();
|
||||
|
||||
|
|
@ -409,133 +118,12 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
|||
});
|
||||
}
|
||||
|
||||
[ApiConvention(typeof(DefaultApiConventions))]
|
||||
[ApiConventionType(typeof(DefaultApiConventions))]
|
||||
public class GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventionsController : ControllerBase
|
||||
{
|
||||
public Task<ActionResult<BaseModel>> DeleteBase(int id) => null;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetApiResponseTypes_ReturnsResponseTypesFromCustomConventions()
|
||||
{
|
||||
// Arrange
|
||||
var actionDescriptor = GetControllerActionDescriptor(
|
||||
typeof(GetApiResponseTypes_ReturnsResponseTypesFromCustomConventionsController),
|
||||
nameof(GetApiResponseTypes_ReturnsResponseTypesFromCustomConventionsController.SearchModel));
|
||||
var filter = new FilterDescriptor(new ApiConventionAttribute(typeof(SearchApiConventions)), FilterScope.Controller);
|
||||
actionDescriptor.FilterDescriptors.Add(filter);
|
||||
|
||||
var provider = GetProvider();
|
||||
|
||||
// Act
|
||||
var result = provider.GetApiResponseTypes(actionDescriptor);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
result.OrderBy(r => r.StatusCode),
|
||||
responseType =>
|
||||
{
|
||||
Assert.Equal(206, responseType.StatusCode);
|
||||
Assert.Equal(typeof(void), responseType.Type);
|
||||
Assert.False(responseType.IsDefaultResponse);
|
||||
Assert.Empty(responseType.ApiResponseFormats);
|
||||
},
|
||||
responseType =>
|
||||
{
|
||||
Assert.Equal(406, responseType.StatusCode);
|
||||
Assert.Equal(typeof(void), responseType.Type);
|
||||
Assert.False(responseType.IsDefaultResponse);
|
||||
Assert.Empty(responseType.ApiResponseFormats);
|
||||
});
|
||||
}
|
||||
|
||||
[ApiConvention(typeof(SearchApiConventions))]
|
||||
public class GetApiResponseTypes_ReturnsResponseTypesFromCustomConventionsController : ControllerBase
|
||||
{
|
||||
public Task<ActionResult<BaseModel>> SearchModel(string searchTerm, int page) => null;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetApiResponseTypes_ReturnsResponseTypesFromFirstMatchingConvention_WhenMultipleConventionsArePresent()
|
||||
{
|
||||
// Arrange
|
||||
var actionDescriptor = GetControllerActionDescriptor(
|
||||
typeof(GetApiResponseTypes_ReturnsResponseTypesFromFirstMatchingConventionController),
|
||||
nameof(GetApiResponseTypes_ReturnsResponseTypesFromFirstMatchingConventionController.SearchModel));
|
||||
var filter = new FilterDescriptor(new ApiConventionAttribute(typeof(DefaultApiConventions)), FilterScope.Controller);
|
||||
actionDescriptor.FilterDescriptors.Add(filter);
|
||||
filter = new FilterDescriptor(new ApiConventionAttribute(typeof(SearchApiConventions)), FilterScope.Controller);
|
||||
actionDescriptor.FilterDescriptors.Add(filter);
|
||||
|
||||
var provider = GetProvider();
|
||||
|
||||
// Act
|
||||
var result = provider.GetApiResponseTypes(actionDescriptor);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
result.OrderBy(r => r.StatusCode),
|
||||
responseType =>
|
||||
{
|
||||
Assert.Equal(206, responseType.StatusCode);
|
||||
Assert.Equal(typeof(void), responseType.Type);
|
||||
Assert.False(responseType.IsDefaultResponse);
|
||||
Assert.Empty(responseType.ApiResponseFormats);
|
||||
},
|
||||
responseType =>
|
||||
{
|
||||
Assert.Equal(406, responseType.StatusCode);
|
||||
Assert.Equal(typeof(void), responseType.Type);
|
||||
Assert.False(responseType.IsDefaultResponse);
|
||||
Assert.Empty(responseType.ApiResponseFormats);
|
||||
});
|
||||
}
|
||||
|
||||
[ApiConvention(typeof(DefaultApiConventions))]
|
||||
[ApiConvention(typeof(SearchApiConventions))]
|
||||
public class GetApiResponseTypes_ReturnsResponseTypesFromFirstMatchingConventionController : ControllerBase
|
||||
{
|
||||
public Task<ActionResult<BaseModel>> Get(int id) => null;
|
||||
|
||||
public Task<ActionResult<BaseModel>> SearchModel(string searchTerm, int page) => null;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetApiResponseTypes_ReturnsResponseTypesFromDefaultConvention_WhenMultipleConventionsArePresent()
|
||||
{
|
||||
// Arrange
|
||||
var actionDescriptor = GetControllerActionDescriptor(
|
||||
typeof(GetApiResponseTypes_ReturnsResponseTypesFromFirstMatchingConventionController),
|
||||
nameof(GetApiResponseTypes_ReturnsResponseTypesFromFirstMatchingConventionController.Get));
|
||||
var filter = new FilterDescriptor(new ApiConventionAttribute(typeof(DefaultApiConventions)), FilterScope.Controller);
|
||||
actionDescriptor.FilterDescriptors.Add(filter);
|
||||
filter = new FilterDescriptor(new ApiConventionAttribute(typeof(SearchApiConventions)), FilterScope.Controller);
|
||||
actionDescriptor.FilterDescriptors.Add(filter);
|
||||
|
||||
var provider = GetProvider();
|
||||
|
||||
// Act
|
||||
var result = provider.GetApiResponseTypes(actionDescriptor);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
result.OrderBy(r => r.StatusCode),
|
||||
responseType =>
|
||||
{
|
||||
Assert.Equal(200, responseType.StatusCode);
|
||||
Assert.Equal(typeof(void), responseType.Type);
|
||||
Assert.False(responseType.IsDefaultResponse);
|
||||
Assert.Empty(responseType.ApiResponseFormats);
|
||||
},
|
||||
responseType =>
|
||||
{
|
||||
Assert.Equal(404, responseType.StatusCode);
|
||||
Assert.Equal(typeof(void), responseType.Type);
|
||||
Assert.False(responseType.IsDefaultResponse);
|
||||
Assert.Empty(responseType.ApiResponseFormats);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetApiResponseTypes_ReturnsDefaultResultsIfNoConventionsMatch()
|
||||
{
|
||||
|
|
@ -543,8 +131,6 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
|||
var actionDescriptor = GetControllerActionDescriptor(
|
||||
typeof(GetApiResponseTypes_ReturnsDefaultResultsIfNoConventionsMatchController),
|
||||
nameof(GetApiResponseTypes_ReturnsDefaultResultsIfNoConventionsMatchController.PostModel));
|
||||
var filter = new FilterDescriptor(new ApiConventionAttribute(typeof(DefaultApiConventions)), FilterScope.Controller);
|
||||
actionDescriptor.FilterDescriptors.Add(filter);
|
||||
|
||||
var provider = GetProvider();
|
||||
|
||||
|
|
@ -565,7 +151,7 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
|||
});
|
||||
}
|
||||
|
||||
[ApiConvention(typeof(DefaultApiConventions))]
|
||||
[ApiConventionType(typeof(DefaultApiConventions))]
|
||||
public class GetApiResponseTypes_ReturnsDefaultResultsIfNoConventionsMatchController : ControllerBase
|
||||
{
|
||||
public Task<ActionResult<BaseModel>> PostModel(int id, BaseModel model) => null;
|
||||
|
|
|
|||
|
|
@ -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.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.AspNetCore.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc
|
||||
{
|
||||
public class ApiConventionTypeAttributeTest
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_ThrowsIfConventionMethodIsAnnotatedWithProducesAttribute()
|
||||
{
|
||||
// Arrange
|
||||
var expected = $"Method {typeof(ConventionWithProducesAttribute).FullName + ".Get"} is decorated with the following attributes that are not allowed on an API convention method:" +
|
||||
Environment.NewLine +
|
||||
typeof(ProducesAttribute).FullName +
|
||||
Environment.NewLine +
|
||||
$"The following attributes are allowed on API convention methods: {nameof(ProducesResponseTypeAttribute)}, {nameof(ApiConventionNameMatchAttribute)}";
|
||||
|
||||
// Act & Assert
|
||||
ExceptionAssert.ThrowsArgument(
|
||||
() => new ApiConventionTypeAttribute(typeof(ConventionWithProducesAttribute)),
|
||||
"conventionType",
|
||||
expected);
|
||||
}
|
||||
|
||||
public static class ConventionWithProducesAttribute
|
||||
{
|
||||
[Produces(typeof(void))]
|
||||
public static void Get() { }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ThrowsIfConventionMethodHasRouteAttribute()
|
||||
{
|
||||
// Arrange
|
||||
var expected = $"Method {typeof(ConventionWithRouteAttribute).FullName + ".Get"} is decorated with the following attributes that are not allowed on an API convention method:" +
|
||||
Environment.NewLine +
|
||||
typeof(HttpGetAttribute).FullName +
|
||||
Environment.NewLine +
|
||||
$"The following attributes are allowed on API convention methods: {nameof(ProducesResponseTypeAttribute)}, {nameof(ApiConventionNameMatchAttribute)}";
|
||||
|
||||
// Act & Assert
|
||||
ExceptionAssert.ThrowsArgument(
|
||||
() => new ApiConventionTypeAttribute(typeof(ConventionWithRouteAttribute)),
|
||||
"conventionType",
|
||||
expected);
|
||||
}
|
||||
|
||||
public static class ConventionWithRouteAttribute
|
||||
{
|
||||
[HttpGet("url")]
|
||||
public static void Get() { }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ThrowsIfMultipleUnsupportedAttributesArePresentOnConvention()
|
||||
{
|
||||
// Arrange
|
||||
var expected = $"Method {typeof(ConventionWitUnsupportedAttributes).FullName + ".Get"} is decorated with the following attributes that are not allowed on an API convention method:" +
|
||||
Environment.NewLine +
|
||||
string.Join(Environment.NewLine, typeof(ProducesAttribute).FullName, typeof(ServiceFilterAttribute).FullName, typeof(AuthorizeAttribute).FullName) +
|
||||
Environment.NewLine +
|
||||
$"The following attributes are allowed on API convention methods: {nameof(ProducesResponseTypeAttribute)}, {nameof(ApiConventionNameMatchAttribute)}";
|
||||
|
||||
// Act & Assert
|
||||
ExceptionAssert.ThrowsArgument(
|
||||
() => new ApiConventionTypeAttribute(typeof(ConventionWitUnsupportedAttributes)),
|
||||
"conventionType",
|
||||
expected);
|
||||
}
|
||||
|
||||
public static class ConventionWitUnsupportedAttributes
|
||||
{
|
||||
[ProducesResponseType(400)]
|
||||
[Produces(typeof(void))]
|
||||
[ServiceFilter(typeof(object))]
|
||||
[Authorize]
|
||||
public static void Get() { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,754 @@
|
|||
// 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.Linq;
|
||||
using System.Reflection;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||
{
|
||||
public class ApiConventionResultTest
|
||||
{
|
||||
[Fact]
|
||||
public void GetApiConvention_ReturnsNull_IfNoConventionMatches()
|
||||
{
|
||||
// Arrange
|
||||
var method = typeof(GetApiConvention_ReturnsNull_IfNoConventionMatchesController).GetMethod(nameof(GetApiConvention_ReturnsNull_IfNoConventionMatchesController.NoMatch));
|
||||
var attribute = new ApiConventionTypeAttribute(typeof(DefaultApiConventions));
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.TryGetApiConvention(method, new[] { attribute }, out var conventionResult);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
Assert.Null(conventionResult);
|
||||
}
|
||||
|
||||
public class GetApiConvention_ReturnsNull_IfNoConventionMatchesController
|
||||
{
|
||||
public IActionResult NoMatch(int id) => null;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetApiConvention_ReturnsResultFromConvention()
|
||||
{
|
||||
// Arrange
|
||||
var method = typeof(GetApiConvention_ReturnsResultFromConventionController)
|
||||
.GetMethod(nameof(GetApiConvention_ReturnsResultFromConventionController.Match));
|
||||
var attribute = new ApiConventionTypeAttribute(typeof(GetApiConvention_ReturnsResultFromConventionType));
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.TryGetApiConvention(method, new[] { attribute }, out var conventionResult);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
Assert.Collection(
|
||||
conventionResult.ResponseMetadataProviders.OrderBy(o => o.StatusCode),
|
||||
r => Assert.Equal(201, r.StatusCode),
|
||||
r => Assert.Equal(403, r.StatusCode));
|
||||
}
|
||||
|
||||
public class GetApiConvention_ReturnsResultFromConventionController
|
||||
{
|
||||
public IActionResult Match(int id) => null;
|
||||
}
|
||||
|
||||
public static class GetApiConvention_ReturnsResultFromConventionType
|
||||
{
|
||||
[ProducesResponseType(200)]
|
||||
[ProducesResponseType(202)]
|
||||
[ProducesResponseType(404)]
|
||||
public static void Get(int id) { }
|
||||
|
||||
[ProducesResponseType(201)]
|
||||
[ProducesResponseType(403)]
|
||||
public static void Match(int id) { }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetApiConvention_ReturnsResultFromFirstMatchingConvention()
|
||||
{
|
||||
// Arrange
|
||||
var method = typeof(GetApiConvention_ReturnsResultFromFirstMatchingConventionController)
|
||||
.GetMethod(nameof(GetApiConvention_ReturnsResultFromFirstMatchingConventionController.Get));
|
||||
var attributes = new[]
|
||||
{
|
||||
new ApiConventionTypeAttribute(typeof(GetApiConvention_ReturnsResultFromConventionType)),
|
||||
new ApiConventionTypeAttribute(typeof(DefaultApiConventions)),
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.TryGetApiConvention(method, attributes, result: out var conventionResult);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
Assert.Collection(
|
||||
conventionResult.ResponseMetadataProviders.OrderBy(o => o.StatusCode),
|
||||
r => Assert.Equal(200, r.StatusCode),
|
||||
r => Assert.Equal(202, r.StatusCode),
|
||||
r => Assert.Equal(404, r.StatusCode));
|
||||
}
|
||||
|
||||
public class GetApiConvention_ReturnsResultFromFirstMatchingConventionController
|
||||
{
|
||||
public IActionResult Get(int id) => null;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetApiConvention_GetAction_MatchesDefaultConvention()
|
||||
{
|
||||
// Arrange
|
||||
var method = typeof(DefaultConventionController)
|
||||
.GetMethod(nameof(DefaultConventionController.GetUser));
|
||||
var attributes = new[] { new ApiConventionTypeAttribute(typeof(DefaultApiConventions)) };
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.TryGetApiConvention(method, attributes, out var conventionResult);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
Assert.Collection(
|
||||
conventionResult.ResponseMetadataProviders.OrderBy(o => o.StatusCode),
|
||||
r => Assert.Equal(200, r.StatusCode),
|
||||
r => Assert.Equal(404, r.StatusCode));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetApiConvention_PostAction_MatchesDefaultConvention()
|
||||
{
|
||||
// Arrange
|
||||
var method = typeof(DefaultConventionController)
|
||||
.GetMethod(nameof(DefaultConventionController.PostUser));
|
||||
var attributes = new[] { new ApiConventionTypeAttribute(typeof(DefaultApiConventions)) };
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.TryGetApiConvention(method, attributes, out var conventionResult);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
Assert.Collection(
|
||||
conventionResult.ResponseMetadataProviders.OrderBy(o => o.StatusCode),
|
||||
r => Assert.Equal(201, r.StatusCode),
|
||||
r => Assert.Equal(400, r.StatusCode));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetApiConvention_PutAction_MatchesDefaultConvention()
|
||||
{
|
||||
// Arrange
|
||||
var method = typeof(DefaultConventionController)
|
||||
.GetMethod(nameof(DefaultConventionController.PutUser));
|
||||
var conventions = new[]
|
||||
{
|
||||
new ApiConventionTypeAttribute(typeof(DefaultApiConventions)),
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.TryGetApiConvention(method, conventions, out var conventionResult);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
Assert.Collection(
|
||||
conventionResult.ResponseMetadataProviders.OrderBy(o => o.StatusCode),
|
||||
r => Assert.Equal(204, r.StatusCode),
|
||||
r => Assert.Equal(400, r.StatusCode),
|
||||
r => Assert.Equal(404, r.StatusCode));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetApiConvention_DeleteAction_MatchesDefaultConvention()
|
||||
{
|
||||
// Arrange
|
||||
var method = typeof(DefaultConventionController)
|
||||
.GetMethod(nameof(DefaultConventionController.Delete));
|
||||
var conventions = new[]
|
||||
{
|
||||
new ApiConventionTypeAttribute(typeof(DefaultApiConventions)),
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.TryGetApiConvention(method, conventions, out var conventionResult);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
Assert.Collection(
|
||||
conventionResult.ResponseMetadataProviders.OrderBy(o => o.StatusCode),
|
||||
r => Assert.Equal(200, r.StatusCode),
|
||||
r => Assert.Equal(400, r.StatusCode),
|
||||
r => Assert.Equal(404, r.StatusCode));
|
||||
}
|
||||
|
||||
public class DefaultConventionController
|
||||
{
|
||||
public IActionResult GetUser(Guid id) => null;
|
||||
|
||||
public IActionResult PostUser(User user) => null;
|
||||
|
||||
public IActionResult PutUser(Guid userId, User user) => null;
|
||||
|
||||
public IActionResult Delete(Guid userId) => null;
|
||||
}
|
||||
|
||||
public class User { }
|
||||
|
||||
[Theory]
|
||||
[InlineData("Method", "method")]
|
||||
[InlineData("Method", "ConventionMethod")]
|
||||
[InlineData("p", "model")]
|
||||
[InlineData("person", "model")]
|
||||
public void IsNameMatch_WithAny_AlwaysReturnsTrue(string name, string conventionName)
|
||||
{
|
||||
// Act
|
||||
var result = ApiConventionResult.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Any);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithExact_ReturnsFalse_IfNamesDifferInCase()
|
||||
{
|
||||
// Arrange
|
||||
var name = "Name";
|
||||
var conventionName = "name";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Exact);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithExact_ReturnsFalse_IfNamesAreDifferent()
|
||||
{
|
||||
// Arrange
|
||||
var name = "Name";
|
||||
var conventionName = "Different";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Exact);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithExact_ReturnsFalse_IfConventionNameIsSubString()
|
||||
{
|
||||
// Arrange
|
||||
var name = "RegularName";
|
||||
var conventionName = "Regular";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Exact);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithExact_ReturnsFalse_IfConventionNameIsSuperString()
|
||||
{
|
||||
// Arrange
|
||||
var name = "Regular";
|
||||
var conventionName = "RegularName";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Exact);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithExact_ReturnsTrue_IfExactMatch()
|
||||
{
|
||||
// Arrange
|
||||
var name = "parameterName";
|
||||
var conventionName = "parameterName";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Exact);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithPrefix_ReturnsTrue_IfNamesAreExact()
|
||||
{
|
||||
// Arrange
|
||||
var name = "PostPerson";
|
||||
var conventionName = "PostPerson";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Prefix);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithPrefix_ReturnsTrue_IfNameIsProperPrefix()
|
||||
{
|
||||
// Arrange
|
||||
var name = "PostPerson";
|
||||
var conventionName = "Post";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Prefix);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithPrefix_ReturnsFalse_IfNamesAreDifferent()
|
||||
{
|
||||
// Arrange
|
||||
var name = "GetPerson";
|
||||
var conventionName = "Post";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Prefix);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithPrefix_ReturnsFalse_IfNamesDifferInCase()
|
||||
{
|
||||
// Arrange
|
||||
var name = "GetPerson";
|
||||
var conventionName = "post";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Prefix);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithPrefix_ReturnsFalse_IfNameIsNotProperPrfix()
|
||||
{
|
||||
// Arrange
|
||||
var name = "Postman";
|
||||
var conventionName = "Post";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Prefix);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithPrefix_ReturnsFalse_IfNameIsSuffix()
|
||||
{
|
||||
// Arrange
|
||||
var name = "GoPost";
|
||||
var conventionName = "Post";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Prefix);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithSuffix_ReturnsFalse_IfNamesAreDifferent()
|
||||
{
|
||||
// Arrange
|
||||
var name = "name";
|
||||
var conventionName = "diff";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Suffix);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithSuffix_ReturnsFalse_IfNameIsNotSuffix()
|
||||
{
|
||||
// Arrange
|
||||
var name = "personId";
|
||||
var conventionName = "idx";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Suffix);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithSuffix_ReturnTrue_IfNameIsExact()
|
||||
{
|
||||
// Arrange
|
||||
var name = "test";
|
||||
var conventionName = "test";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Suffix);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithSuffix_ReturnFalse_IfNameDiffersInCase()
|
||||
{
|
||||
// Arrange
|
||||
var name = "test";
|
||||
var conventionName = "Test";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Suffix);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithSuffix_ReturnTrue_IfNameIsProperSuffix()
|
||||
{
|
||||
// Arrange
|
||||
var name = "personId";
|
||||
var conventionName = "id";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Suffix);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("candid", "id")]
|
||||
[InlineData("canDid", "id")]
|
||||
public void IsNameMatch_WithSuffix_ReturnFalse_IfNameIsNotProperSuffix(string name, string conventionName)
|
||||
{
|
||||
// Act
|
||||
var result = ApiConventionResult.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Suffix);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(typeof(object), typeof(object))]
|
||||
[InlineData(typeof(int), typeof(void))]
|
||||
[InlineData(typeof(string), typeof(DateTime))]
|
||||
public void IsTypeMatch_WithAny_ReturnsTrue(Type type, Type conventionType)
|
||||
{
|
||||
// Act
|
||||
var result = ApiConventionResult.IsTypeMatch(type, conventionType, ApiConventionTypeMatchBehavior.Any);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsTypeMatch_WithAssinableFrom_ReturnsTrueForExact()
|
||||
{
|
||||
// Arrange
|
||||
var type = typeof(Base);
|
||||
var conventionType = typeof(Base);
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsTypeMatch(type, conventionType, ApiConventionTypeMatchBehavior.AssignableFrom);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsTypeMatch_WithAssinableFrom_ReturnsTrueForDerived()
|
||||
{
|
||||
// Arrange
|
||||
var type = typeof(Derived);
|
||||
var conventionType = typeof(Base);
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsTypeMatch(type, conventionType, ApiConventionTypeMatchBehavior.AssignableFrom);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsTypeMatch_WithAssinableFrom_ReturnsFalseForBaseTypes()
|
||||
{
|
||||
// Arrange
|
||||
var type = typeof(Base);
|
||||
var conventionType = typeof(Derived);
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsTypeMatch(type, conventionType, ApiConventionTypeMatchBehavior.AssignableFrom);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsTypeMatch_WithAssinableFrom_ReturnsFalseForUnrelated()
|
||||
{
|
||||
// Arrange
|
||||
var type = typeof(string);
|
||||
var conventionType = typeof(Derived);
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsTypeMatch(type, conventionType, ApiConventionTypeMatchBehavior.AssignableFrom);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsMatch_ReturnsFalse_IfMethodNamesDoNotMatch()
|
||||
{
|
||||
// Arrange
|
||||
var method = typeof(TestController).GetMethod(nameof(TestController.Get));
|
||||
var conventionMethod = typeof(TestConvention).GetMethod(nameof(TestConvention.Post));
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsMatch(method, conventionMethod);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsMatch_ReturnsFalse_IMethodHasMoreParametersThanConvention()
|
||||
{
|
||||
// Arrange
|
||||
var method = typeof(TestController).GetMethod(nameof(TestController.Get));
|
||||
var conventionMethod = typeof(TestConvention).GetMethod(nameof(TestConvention.GetNoArgs));
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsMatch(method, conventionMethod);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsMatch_ReturnsFalse_IfMethodHasFewerParametersThanConvention()
|
||||
{
|
||||
// Arrange
|
||||
var method = typeof(TestController).GetMethod(nameof(TestController.Get));
|
||||
var conventionMethod = typeof(TestConvention).GetMethod(nameof(TestConvention.GetTwoArgs));
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsMatch(method, conventionMethod);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsMatch_ReturnsFalse_IfParametersDoNotMatch()
|
||||
{
|
||||
// Arrange
|
||||
var method = typeof(TestController).GetMethod(nameof(TestController.Get));
|
||||
var conventionMethod = typeof(TestConvention).GetMethod(nameof(TestConvention.GetParameterNotMatching));
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsMatch(method, conventionMethod);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsMatch_ReturnsTrue_IfMethodNameAndParametersMatchs()
|
||||
{
|
||||
// Arrange
|
||||
var method = typeof(TestController).GetMethod(nameof(TestController.Get));
|
||||
var conventionMethod = typeof(TestConvention).GetMethod(nameof(TestConvention.Get));
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsMatch(method, conventionMethod);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsMatch_ReturnsTrue_IfParamsArrayMatchesRemainingArguments()
|
||||
{
|
||||
// Arrange
|
||||
var method = typeof(TestController).GetMethod(nameof(TestController.Search));
|
||||
var conventionMethod = typeof(TestConvention).GetMethod(nameof(TestConvention.Search));
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsMatch(method, conventionMethod);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsMatch_WithEmpty_MatchesMethodWithNoParameters()
|
||||
{
|
||||
// Arrange
|
||||
var method = typeof(TestController).GetMethod(nameof(TestController.SearchEmpty));
|
||||
var conventionMethod = typeof(TestConvention).GetMethod(nameof(TestConvention.SearchWithParams));
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsMatch(method, conventionMethod);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetNameMatchBehavior_ReturnsExact_WhenNoAttributesArePresent()
|
||||
{
|
||||
// Arrange
|
||||
var expected = ApiConventionNameMatchBehavior.Exact;
|
||||
var attributes = new object[0];
|
||||
var provider = Mock.Of<ICustomAttributeProvider>(p => p.GetCustomAttributes(false) == attributes);
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.GetNameMatchBehavior(provider);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetNameMatchBehavior_ReturnsExact_WhenNoNameMatchBehaviorAttributeIsSpecified()
|
||||
{
|
||||
// Arrange
|
||||
var expected = ApiConventionNameMatchBehavior.Exact;
|
||||
var attributes = new object[] { new CLSCompliantAttribute(false), new ProducesResponseTypeAttribute(200) };
|
||||
var provider = Mock.Of<ICustomAttributeProvider>(p => p.GetCustomAttributes(false) == attributes);
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.GetNameMatchBehavior(provider);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetNameMatchBehavior_ReturnsValueFromAttributes()
|
||||
{
|
||||
// Arrange
|
||||
var expected = ApiConventionNameMatchBehavior.Prefix;
|
||||
var attributes = new object[]
|
||||
{
|
||||
new CLSCompliantAttribute(false),
|
||||
new ApiConventionNameMatchAttribute(expected),
|
||||
new ProducesResponseTypeAttribute(200) }
|
||||
;
|
||||
var provider = Mock.Of<ICustomAttributeProvider>(p => p.GetCustomAttributes(false) == attributes);
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.GetNameMatchBehavior(provider);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetTypeMatchBehavior_ReturnsIsAssignableFrom_WhenNoAttributesArePresent()
|
||||
{
|
||||
// Arrange
|
||||
var expected = ApiConventionTypeMatchBehavior.AssignableFrom;
|
||||
var attributes = new object[0];
|
||||
var provider = Mock.Of<ICustomAttributeProvider>(p => p.GetCustomAttributes(false) == attributes);
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.GetTypeMatchBehavior(provider);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetTypeMatchBehavior_ReturnsIsAssignableFrom_WhenNoMatchingAttributesArePresent()
|
||||
{
|
||||
// Arrange
|
||||
var expected = ApiConventionTypeMatchBehavior.AssignableFrom;
|
||||
var attributes = new object[] { new CLSCompliantAttribute(false), new ProducesResponseTypeAttribute(200) };
|
||||
var provider = Mock.Of<ICustomAttributeProvider>(p => p.GetCustomAttributes(false) == attributes);
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.GetTypeMatchBehavior(provider);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetTypeMatchBehavior_ReturnsValueFromAttributes()
|
||||
{
|
||||
// Arrange
|
||||
var expected = ApiConventionTypeMatchBehavior.Any;
|
||||
var attributes = new object[]
|
||||
{
|
||||
new CLSCompliantAttribute(false),
|
||||
new ApiConventionTypeMatchAttribute(expected),
|
||||
new ProducesResponseTypeAttribute(200) }
|
||||
;
|
||||
var provider = Mock.Of<ICustomAttributeProvider>(p => p.GetCustomAttributes(false) == attributes);
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.GetTypeMatchBehavior(provider);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
public class Base { }
|
||||
|
||||
public class Derived : Base { }
|
||||
|
||||
public class TestController
|
||||
{
|
||||
public IActionResult Get(int id) => null;
|
||||
|
||||
public IActionResult Search(string searchTerm, bool sortDescending, int page) => null;
|
||||
|
||||
public IActionResult SearchEmpty() => null;
|
||||
}
|
||||
|
||||
public static class TestConvention
|
||||
{
|
||||
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)]
|
||||
public static void Get(int id) { }
|
||||
|
||||
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Any)]
|
||||
public static void GetNoArgs() { }
|
||||
|
||||
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Any)]
|
||||
public static void GetTwoArgs(int id, string name) { }
|
||||
|
||||
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)]
|
||||
public static void Post(Derived model) { }
|
||||
|
||||
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)]
|
||||
public static void GetParameterNotMatching([ApiConventionTypeMatch(ApiConventionTypeMatchBehavior.AssignableFrom)] Derived model) { }
|
||||
|
||||
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Any)]
|
||||
public static void Search(
|
||||
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Exact)]
|
||||
string searchTerm,
|
||||
params object[] others)
|
||||
{ }
|
||||
|
||||
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Any)]
|
||||
public static void SearchWithParams(params object[] others) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,9 @@ using System.Reflection.Emit;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||
using Microsoft.AspNetCore.Mvc.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
|
@ -875,47 +877,107 @@ Environment.NewLine + "int b";
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void ApiConventionAttributeIsNotAdded_IfModelAlreadyHasAttribute()
|
||||
public void DiscoverApiConvention_DoesNotAddConventionItem_IfActionHasProducesResponseTypeAttribute()
|
||||
{
|
||||
// Arrange
|
||||
var attribute = new ApiConventionAttribute(typeof(DefaultApiConventions));
|
||||
var controllerType = CreateTestControllerType();
|
||||
|
||||
var model = new ControllerModel(controllerType.GetTypeInfo(), new[] { attribute })
|
||||
{
|
||||
Filters = { attribute, },
|
||||
};
|
||||
var actionModel = new ActionModel(
|
||||
typeof(TestApiConventionController).GetMethod(nameof(TestApiConventionController.Delete)),
|
||||
Array.Empty<object>());
|
||||
actionModel.Filters.Add(new ProducesResponseTypeAttribute(200));
|
||||
var attributes = new[] { new ApiConventionTypeAttribute(typeof(DefaultApiConventions)) };
|
||||
|
||||
// Act
|
||||
ApiBehaviorApplicationModelProvider.AddGloballyConfiguredApiConventions(model);
|
||||
ApiBehaviorApplicationModelProvider.DiscoverApiConvention(actionModel, attributes);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
model.Filters,
|
||||
filter => Assert.Same(attribute, filter));
|
||||
Assert.Empty(actionModel.Properties);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApiConventionAttributeIsAdded_IfAttributeExistsInAssembly()
|
||||
public void DiscoverApiConvention_DoesNotAddConventionItem_IfActionHasProducesAttribute()
|
||||
{
|
||||
// Arrange
|
||||
var controllerType = CreateTestControllerType();
|
||||
var model = new ControllerModel(controllerType.GetTypeInfo(), Array.Empty<object>());
|
||||
var actionModel = new ActionModel(
|
||||
typeof(TestApiConventionController).GetMethod(nameof(TestApiConventionController.Delete)),
|
||||
Array.Empty<object>());
|
||||
actionModel.Filters.Add(new ProducesAttribute(typeof(object)));
|
||||
var attributes = new[] { new ApiConventionTypeAttribute(typeof(DefaultApiConventions)) };
|
||||
|
||||
// Act
|
||||
ApiBehaviorApplicationModelProvider.AddGloballyConfiguredApiConventions(model);
|
||||
ApiBehaviorApplicationModelProvider.DiscoverApiConvention(actionModel, attributes);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(actionModel.Properties);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DiscoverApiConvention_DoesNotAddConventionItem_IfNoConventionMatches()
|
||||
{
|
||||
// Arrange
|
||||
var actionModel = new ActionModel(
|
||||
typeof(TestApiConventionController).GetMethod(nameof(TestApiConventionController.NoMatch)),
|
||||
Array.Empty<object>());
|
||||
var attributes = new[] { new ApiConventionTypeAttribute(typeof(DefaultApiConventions)) };
|
||||
|
||||
// Act
|
||||
ApiBehaviorApplicationModelProvider.DiscoverApiConvention(actionModel, attributes);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(actionModel.Properties);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DiscoverApiConvention_AddsConventionItem_IfConventionMatches()
|
||||
{
|
||||
// Arrange
|
||||
var actionModel = new ActionModel(
|
||||
typeof(TestApiConventionController).GetMethod(nameof(TestApiConventionController.Delete)),
|
||||
Array.Empty<object>());
|
||||
var attributes = new[] { new ApiConventionTypeAttribute(typeof(DefaultApiConventions)) };
|
||||
|
||||
// Act
|
||||
ApiBehaviorApplicationModelProvider.DiscoverApiConvention(actionModel, attributes);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
model.Filters,
|
||||
filter => Assert.IsType<ApiConventionAttribute>(filter));
|
||||
actionModel.Properties,
|
||||
kvp =>
|
||||
{
|
||||
Assert.Equal(typeof(ApiConventionResult), kvp.Key);
|
||||
Assert.NotNull(kvp.Value);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DiscoverApiConvention_AddsConventionItem_IfActionHasNonConventionBasedFilters()
|
||||
{
|
||||
// Arrange
|
||||
var actionModel = new ActionModel(
|
||||
typeof(TestApiConventionController).GetMethod(nameof(TestApiConventionController.Delete)),
|
||||
Array.Empty<object>());
|
||||
actionModel.Filters.Add(new AuthorizeFilter());
|
||||
actionModel.Filters.Add(new ServiceFilterAttribute(typeof(object)));
|
||||
actionModel.Filters.Add(new ConsumesAttribute("application/xml"));
|
||||
var attributes = new[] { new ApiConventionTypeAttribute(typeof(DefaultApiConventions)) };
|
||||
|
||||
// Act
|
||||
ApiBehaviorApplicationModelProvider.DiscoverApiConvention(actionModel, attributes);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
actionModel.Properties,
|
||||
kvp =>
|
||||
{
|
||||
Assert.Equal(typeof(ApiConventionResult), kvp.Key);
|
||||
Assert.NotNull(kvp.Value);
|
||||
});
|
||||
}
|
||||
|
||||
// A dynamically generated type in an assembly that has an ApiConventionAttribute.
|
||||
private static TypeBuilder CreateTestControllerType()
|
||||
{
|
||||
var attributeBuilder = new CustomAttributeBuilder(
|
||||
typeof(ApiConventionAttribute).GetConstructor(new[] { typeof(Type) }),
|
||||
typeof(ApiConventionTypeAttribute).GetConstructor(new[] { typeof(Type) }),
|
||||
new[] { typeof(DefaultApiConventions) });
|
||||
|
||||
var assemblyName = new AssemblyName("TestAssembly");
|
||||
|
|
@ -1202,6 +1264,13 @@ Environment.NewLine + "int b";
|
|||
public IActionResult Action([ModelBinder(typeof(object))] Car car) => null;
|
||||
}
|
||||
|
||||
private class TestApiConventionController
|
||||
{
|
||||
public IActionResult NoMatch() => null;
|
||||
|
||||
public IActionResult Delete(int id) => null;
|
||||
}
|
||||
|
||||
private class GpsCoordinates
|
||||
{
|
||||
public long Latitude { get; set; }
|
||||
|
|
|
|||
|
|
@ -1148,6 +1148,189 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
Assert.Equal("multipart/form-data", requestFormat.MediaType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public Task ApiConvention_ForGetMethod_ReturningModel() => ApiConvention_ForGetMethod("GetProduct");
|
||||
|
||||
[Fact]
|
||||
public Task ApiConvention_ForGetMethod_ReturningTaskOfActionResultOfModel() => ApiConvention_ForGetMethod("GetTaskOfActionResultOfProduct");
|
||||
|
||||
private async Task ApiConvention_ForGetMethod(string action)
|
||||
{
|
||||
// Act
|
||||
var response = await Client.GetStringAsync(
|
||||
$"ApiExplorerResponseTypeWithApiConventionController/{action}");
|
||||
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(response);
|
||||
|
||||
// Assert
|
||||
var description = Assert.Single(result);
|
||||
|
||||
Assert.Collection(
|
||||
description.SupportedResponseTypes.OrderBy(r => r.StatusCode),
|
||||
responseType =>
|
||||
{
|
||||
Assert.Equal(typeof(void).FullName, responseType.ResponseType);
|
||||
Assert.Equal(200, responseType.StatusCode);
|
||||
Assert.Empty(responseType.ResponseFormats);
|
||||
},
|
||||
responseType =>
|
||||
{
|
||||
Assert.Equal(typeof(void).FullName, responseType.ResponseType);
|
||||
Assert.Equal(404, responseType.StatusCode);
|
||||
Assert.Empty(responseType.ResponseFormats);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApiConvention_ForGetMethodThatDoesNotMatchConvention()
|
||||
{
|
||||
// Arrange
|
||||
var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" };
|
||||
|
||||
// Act
|
||||
var response = await Client.GetStringAsync(
|
||||
$"ApiExplorerResponseTypeWithApiConventionController/GetProducts");
|
||||
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(response);
|
||||
|
||||
// Assert
|
||||
var description = Assert.Single(result);
|
||||
|
||||
Assert.Collection(
|
||||
description.SupportedResponseTypes.OrderBy(r => r.StatusCode),
|
||||
responseType =>
|
||||
{
|
||||
Assert.Equal(typeof(IEnumerable<ApiExplorerWebSite.Product>).FullName, responseType.ResponseType);
|
||||
Assert.Equal(200, responseType.StatusCode);
|
||||
var actualMediaTypes = responseType.ResponseFormats.Select(r => r.MediaType).OrderBy(r => r);
|
||||
Assert.Equal(expectedMediaTypes, actualMediaTypes);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApiConvention_ForMethodWithResponseTypeAttributes()
|
||||
{
|
||||
// Arrange
|
||||
var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" };
|
||||
|
||||
// Act
|
||||
var response = await Client.PostAsync(
|
||||
$"ApiExplorerResponseTypeWithApiConventionController/PostWithConventions",
|
||||
new StringContent(string.Empty));
|
||||
var responseBody = await response.EnsureSuccessStatusCode().Content.ReadAsStringAsync();
|
||||
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(responseBody);
|
||||
|
||||
// Assert
|
||||
var description = Assert.Single(result);
|
||||
Assert.Collection(
|
||||
description.SupportedResponseTypes.OrderBy(r => r.StatusCode),
|
||||
responseType =>
|
||||
{
|
||||
Assert.Equal(typeof(void).FullName, responseType.ResponseType);
|
||||
Assert.Equal(202, responseType.StatusCode);
|
||||
Assert.Empty(responseType.ResponseFormats);
|
||||
},
|
||||
responseType =>
|
||||
{
|
||||
Assert.Equal(typeof(void).FullName, responseType.ResponseType);
|
||||
Assert.Equal(403, responseType.StatusCode);
|
||||
Assert.Empty(responseType.ResponseFormats);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApiConvention_ForPostMethodThatMatchesConvention()
|
||||
{
|
||||
// Act
|
||||
var response = await Client.PostAsync(
|
||||
$"ApiExplorerResponseTypeWithApiConventionController/PostTaskOfProduct",
|
||||
new StringContent(string.Empty));
|
||||
var responseBody = await response.EnsureSuccessStatusCode().Content.ReadAsStringAsync();
|
||||
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(responseBody);
|
||||
|
||||
// Assert
|
||||
var description = Assert.Single(result);
|
||||
Assert.Collection(
|
||||
description.SupportedResponseTypes.OrderBy(r => r.StatusCode),
|
||||
responseType =>
|
||||
{
|
||||
Assert.Equal(typeof(void).FullName, responseType.ResponseType);
|
||||
Assert.Equal(201, responseType.StatusCode);
|
||||
Assert.Empty(responseType.ResponseFormats);
|
||||
},
|
||||
responseType =>
|
||||
{
|
||||
Assert.Equal(typeof(void).FullName, responseType.ResponseType);
|
||||
Assert.Equal(400, responseType.StatusCode);
|
||||
Assert.Empty(responseType.ResponseFormats);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApiConvention_ForPutActionThatMatchesConvention()
|
||||
{
|
||||
// Act
|
||||
var response = await Client.PutAsync(
|
||||
$"ApiExplorerResponseTypeWithApiConventionController/Put",
|
||||
new StringContent(string.Empty));
|
||||
var responseBody = await response.EnsureSuccessStatusCode().Content.ReadAsStringAsync();
|
||||
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(responseBody);
|
||||
|
||||
// Assert
|
||||
var description = Assert.Single(result);
|
||||
Assert.Collection(
|
||||
description.SupportedResponseTypes.OrderBy(r => r.StatusCode),
|
||||
responseType =>
|
||||
{
|
||||
Assert.Equal(typeof(void).FullName, responseType.ResponseType);
|
||||
Assert.Equal(204, responseType.StatusCode);
|
||||
Assert.Empty(responseType.ResponseFormats);
|
||||
},
|
||||
responseType =>
|
||||
{
|
||||
Assert.Equal(typeof(void).FullName, responseType.ResponseType);
|
||||
Assert.Equal(400, responseType.StatusCode);
|
||||
Assert.Empty(responseType.ResponseFormats);
|
||||
},
|
||||
responseType =>
|
||||
{
|
||||
Assert.Equal(typeof(void).FullName, responseType.ResponseType);
|
||||
Assert.Equal(404, responseType.StatusCode);
|
||||
Assert.Empty(responseType.ResponseFormats);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApiConvention_ForDeleteActionThatMatchesConvention()
|
||||
{
|
||||
// Act
|
||||
var response = await Client.DeleteAsync(
|
||||
$"ApiExplorerResponseTypeWithApiConventionController/DeleteProductAsync");
|
||||
var responseBody = await response.EnsureSuccessStatusCode().Content.ReadAsStringAsync();
|
||||
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(responseBody);
|
||||
|
||||
// Assert
|
||||
var description = Assert.Single(result);
|
||||
Assert.Collection(
|
||||
description.SupportedResponseTypes.OrderBy(r => r.StatusCode),
|
||||
responseType =>
|
||||
{
|
||||
Assert.Equal(typeof(void).FullName, responseType.ResponseType);
|
||||
Assert.Equal(200, responseType.StatusCode);
|
||||
Assert.Empty(responseType.ResponseFormats);
|
||||
},
|
||||
responseType =>
|
||||
{
|
||||
Assert.Equal(typeof(void).FullName, responseType.ResponseType);
|
||||
Assert.Equal(400, responseType.StatusCode);
|
||||
Assert.Empty(responseType.ResponseFormats);
|
||||
},
|
||||
responseType =>
|
||||
{
|
||||
Assert.Equal(typeof(void).FullName, responseType.ResponseType);
|
||||
Assert.Equal(404, responseType.StatusCode);
|
||||
Assert.Empty(responseType.ResponseFormats);
|
||||
});
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetSortedMediaTypes(ApiExplorerResponseType apiResponseType)
|
||||
{
|
||||
return apiResponseType.ResponseFormats
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ApiExplorerWebSite
|
||||
{
|
||||
[ApiController]
|
||||
[Route("ApiExplorerResponseTypeWithApiConventionController/[Action]")]
|
||||
[ApiConventionType(typeof(DefaultApiConventions))]
|
||||
public class ApiExplorerResponseTypeWithApiConventionController : Controller
|
||||
{
|
||||
[HttpGet]
|
||||
public Product GetProduct(int id) => null;
|
||||
|
||||
[HttpGet]
|
||||
public Task<ActionResult<Product>> GetTaskOfActionResultOfProduct(int id) => null;
|
||||
|
||||
[HttpGet]
|
||||
public IEnumerable<Product> GetProducts() => null;
|
||||
|
||||
[HttpPost]
|
||||
[Produces("application/json")]
|
||||
[ProducesResponseType(202)]
|
||||
[ProducesResponseType(403)]
|
||||
public IActionResult PostWithConventions() => null;
|
||||
|
||||
[HttpPost]
|
||||
public Task<IActionResult> PostTaskOfProduct(Product p) => null;
|
||||
|
||||
[HttpPut]
|
||||
public Task<IActionResult> Put(string id, Product product) => null;
|
||||
|
||||
[HttpDelete]
|
||||
public Task<IActionResult> DeleteProductAsync(object id) => null;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue