Merge branch 'release/2.2'
This commit is contained in:
commit
5b9ef5972c
|
|
@ -48,8 +48,8 @@
|
||||||
<MicrosoftAspNetCoreRazorTagHelpersTestingSourcesPackageVersion>3.0.0-alpha1-10454</MicrosoftAspNetCoreRazorTagHelpersTestingSourcesPackageVersion>
|
<MicrosoftAspNetCoreRazorTagHelpersTestingSourcesPackageVersion>3.0.0-alpha1-10454</MicrosoftAspNetCoreRazorTagHelpersTestingSourcesPackageVersion>
|
||||||
<MicrosoftAspNetCoreResponseCachingAbstractionsPackageVersion>3.0.0-alpha1-10454</MicrosoftAspNetCoreResponseCachingAbstractionsPackageVersion>
|
<MicrosoftAspNetCoreResponseCachingAbstractionsPackageVersion>3.0.0-alpha1-10454</MicrosoftAspNetCoreResponseCachingAbstractionsPackageVersion>
|
||||||
<MicrosoftAspNetCoreResponseCachingPackageVersion>3.0.0-alpha1-10454</MicrosoftAspNetCoreResponseCachingPackageVersion>
|
<MicrosoftAspNetCoreResponseCachingPackageVersion>3.0.0-alpha1-10454</MicrosoftAspNetCoreResponseCachingPackageVersion>
|
||||||
<MicrosoftAspNetCoreRoutingAbstractionsPackageVersion>3.0.0-alpha1-10454</MicrosoftAspNetCoreRoutingAbstractionsPackageVersion>
|
<MicrosoftAspNetCoreRoutingAbstractionsPackageVersion>3.0.0-a-alpha1-outbound-parameter-transformer-master-16998</MicrosoftAspNetCoreRoutingAbstractionsPackageVersion>
|
||||||
<MicrosoftAspNetCoreRoutingPackageVersion>3.0.0-alpha1-10454</MicrosoftAspNetCoreRoutingPackageVersion>
|
<MicrosoftAspNetCoreRoutingPackageVersion>3.0.0-a-alpha1-outbound-parameter-transformer-master-16998</MicrosoftAspNetCoreRoutingPackageVersion>
|
||||||
<MicrosoftAspNetCoreServerIISIntegrationPackageVersion>3.0.0-alpha1-10454</MicrosoftAspNetCoreServerIISIntegrationPackageVersion>
|
<MicrosoftAspNetCoreServerIISIntegrationPackageVersion>3.0.0-alpha1-10454</MicrosoftAspNetCoreServerIISIntegrationPackageVersion>
|
||||||
<MicrosoftAspNetCoreServerKestrelPackageVersion>3.0.0-alpha1-10454</MicrosoftAspNetCoreServerKestrelPackageVersion>
|
<MicrosoftAspNetCoreServerKestrelPackageVersion>3.0.0-alpha1-10454</MicrosoftAspNetCoreServerKestrelPackageVersion>
|
||||||
<MicrosoftAspNetCoreSessionPackageVersion>3.0.0-alpha1-10454</MicrosoftAspNetCoreSessionPackageVersion>
|
<MicrosoftAspNetCoreSessionPackageVersion>3.0.0-alpha1-10454</MicrosoftAspNetCoreSessionPackageVersion>
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
||||||
internal readonly struct DeclaredApiResponseMetadata
|
internal readonly struct DeclaredApiResponseMetadata
|
||||||
{
|
{
|
||||||
public static DeclaredApiResponseMetadata ImplicitResponse { get; } =
|
public static DeclaredApiResponseMetadata ImplicitResponse { get; } =
|
||||||
new DeclaredApiResponseMetadata(statusCode: 0, attributeData: null, attributeSource: null, @implicit: true, @default: false);
|
new DeclaredApiResponseMetadata(statusCode: 200, attributeData: null, attributeSource: null, @implicit: true, @default: false);
|
||||||
|
|
||||||
public static DeclaredApiResponseMetadata ForProducesResponseType(int statusCode, AttributeData attributeData, IMethodSymbol attributeSource)
|
public static DeclaredApiResponseMetadata ForProducesResponseType(int statusCode, AttributeData attributeData, IMethodSymbol attributeSource)
|
||||||
{
|
{
|
||||||
|
|
@ -41,8 +41,16 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
||||||
|
|
||||||
public IMethodSymbol AttributeSource { get; }
|
public IMethodSymbol AttributeSource { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <c>True</c> if this <see cref="DeclaredApiResponseMetadata" /> is the implicit 200 associated with an
|
||||||
|
/// action specifying no metadata.
|
||||||
|
/// </summary>
|
||||||
public bool IsImplicit { get; }
|
public bool IsImplicit { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <c>True</c> if this <see cref="DeclaredApiResponseMetadata" /> is from a <c>ProducesDefaultResponseTypeAttribute</c>.
|
||||||
|
/// Matches all failure (400 and above) status codes.
|
||||||
|
/// </summary>
|
||||||
public bool IsDefault { get; }
|
public bool IsDefault { get; }
|
||||||
|
|
||||||
internal static bool Contains(IList<DeclaredApiResponseMetadata> declaredApiResponseMetadata, ActualApiResponseMetadata actualMetadata)
|
internal static bool Contains(IList<DeclaredApiResponseMetadata> declaredApiResponseMetadata, ActualApiResponseMetadata actualMetadata)
|
||||||
|
|
|
||||||
|
|
@ -135,12 +135,33 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||||
|
|
||||||
if (contentTypes.Count == 0)
|
if (contentTypes.Count == 0)
|
||||||
{
|
{
|
||||||
|
// None of the IApiResponseMetadataProvider specified a content type. This is common for actions that
|
||||||
|
// specify one or more ProducesResponseType but no ProducesAttribute. In this case, formatters will participate in conneg
|
||||||
|
// and respond to the incoming request.
|
||||||
|
// Querying IApiResponseTypeMetadataProvider.GetSupportedContentTypes with "null" should retrieve all supported
|
||||||
|
// content types that each formatter may respond in.
|
||||||
contentTypes.Add((string)null);
|
contentTypes.Add((string)null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var responseTypes = results.Values;
|
||||||
|
CalculateResponseFormats(responseTypes, contentTypes);
|
||||||
|
return responseTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CalculateResponseFormats(ICollection<ApiResponseType> responseTypes, MediaTypeCollection declaredContentTypes)
|
||||||
|
{
|
||||||
var responseTypeMetadataProviders = _mvcOptions.OutputFormatters.OfType<IApiResponseTypeMetadataProvider>();
|
var responseTypeMetadataProviders = _mvcOptions.OutputFormatters.OfType<IApiResponseTypeMetadataProvider>();
|
||||||
|
|
||||||
foreach (var apiResponse in results.Values)
|
// Given the content-types that were declared for this action, determine the formatters that support the content-type for the given
|
||||||
|
// response type.
|
||||||
|
// 1. Responses that do not specify an type do not have any associated content-type. This usually is meant for status-code only responses such
|
||||||
|
// as return NotFound();
|
||||||
|
// 2. When a type is specified, use GetSupportedContentTypes to expand wildcards and get the range of content-types formatters support.
|
||||||
|
// 3. When no formatter supports the specified content-type, use the user specified value as is. This is useful in actions where the user
|
||||||
|
// dictates the content-type.
|
||||||
|
// e.g. [Produces("application/pdf")] Action() => FileStream("somefile.pdf", "applicaiton/pdf");
|
||||||
|
|
||||||
|
foreach (var apiResponse in responseTypes)
|
||||||
{
|
{
|
||||||
var responseType = apiResponse.Type;
|
var responseType = apiResponse.Type;
|
||||||
if (responseType == null || responseType == typeof(void))
|
if (responseType == null || responseType == typeof(void))
|
||||||
|
|
@ -150,8 +171,10 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||||
|
|
||||||
apiResponse.ModelMetadata = _modelMetadataProvider.GetMetadataForType(responseType);
|
apiResponse.ModelMetadata = _modelMetadataProvider.GetMetadataForType(responseType);
|
||||||
|
|
||||||
foreach (var contentType in contentTypes)
|
foreach (var contentType in declaredContentTypes)
|
||||||
{
|
{
|
||||||
|
var isSupportedContentType = false;
|
||||||
|
|
||||||
foreach (var responseTypeMetadataProvider in responseTypeMetadataProviders)
|
foreach (var responseTypeMetadataProvider in responseTypeMetadataProviders)
|
||||||
{
|
{
|
||||||
var formatterSupportedContentTypes = responseTypeMetadataProvider.GetSupportedContentTypes(
|
var formatterSupportedContentTypes = responseTypeMetadataProvider.GetSupportedContentTypes(
|
||||||
|
|
@ -163,6 +186,8 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isSupportedContentType = true;
|
||||||
|
|
||||||
foreach (var formatterSupportedContentType in formatterSupportedContentTypes)
|
foreach (var formatterSupportedContentType in formatterSupportedContentTypes)
|
||||||
{
|
{
|
||||||
apiResponse.ApiResponseFormats.Add(new ApiResponseFormat
|
apiResponse.ApiResponseFormats.Add(new ApiResponseFormat
|
||||||
|
|
@ -172,10 +197,17 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isSupportedContentType && contentType != null)
|
||||||
|
{
|
||||||
|
// No output formatter was found that supports this content type. Add the user specified content type as-is to the result.
|
||||||
|
apiResponse.ApiResponseFormats.Add(new ApiResponseFormat
|
||||||
|
{
|
||||||
|
MediaType = contentType,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return results.Values;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Type GetDeclaredReturnType(ControllerActionDescriptor action)
|
private Type GetDeclaredReturnType(ControllerActionDescriptor action)
|
||||||
|
|
|
||||||
|
|
@ -225,7 +225,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||||
return ReplaceTokens(template, values, routeTokenTransformer: null);
|
return ReplaceTokens(template, values, routeTokenTransformer: null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string ReplaceTokens(string template, IDictionary<string, string> values, IParameterTransformer routeTokenTransformer)
|
public static string ReplaceTokens(string template, IDictionary<string, string> values, IOutboundParameterTransformer routeTokenTransformer)
|
||||||
{
|
{
|
||||||
var builder = new StringBuilder();
|
var builder = new StringBuilder();
|
||||||
var state = TemplateParserState.Plaintext;
|
var state = TemplateParserState.Plaintext;
|
||||||
|
|
@ -379,7 +379,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||||
|
|
||||||
if (routeTokenTransformer != null)
|
if (routeTokenTransformer != null)
|
||||||
{
|
{
|
||||||
value = routeTokenTransformer.Transform(value);
|
value = routeTokenTransformer.TransformOutbound(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.Append(value);
|
builder.Append(value);
|
||||||
|
|
|
||||||
|
|
@ -8,17 +8,17 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// An <see cref="IActionModelConvention"/> that sets attribute routing token replacement
|
/// An <see cref="IActionModelConvention"/> that sets attribute routing token replacement
|
||||||
/// to use the specified <see cref="IParameterTransformer"/> on <see cref="ActionModel"/> selectors.
|
/// to use the specified <see cref="IOutboundParameterTransformer"/> on <see cref="ActionModel"/> selectors.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class RouteTokenTransformerConvention : IActionModelConvention
|
public class RouteTokenTransformerConvention : IActionModelConvention
|
||||||
{
|
{
|
||||||
private readonly IParameterTransformer _parameterTransformer;
|
private readonly IOutboundParameterTransformer _parameterTransformer;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new instance of <see cref="RouteTokenTransformerConvention"/> with the specified <see cref="IParameterTransformer"/>.
|
/// Creates a new instance of <see cref="RouteTokenTransformerConvention"/> with the specified <see cref="IOutboundParameterTransformer"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="parameterTransformer">The <see cref="IParameterTransformer"/> to use with attribute routing token replacement.</param>
|
/// <param name="parameterTransformer">The <see cref="IOutboundParameterTransformer"/> to use with attribute routing token replacement.</param>
|
||||||
public RouteTokenTransformerConvention(IParameterTransformer parameterTransformer)
|
public RouteTokenTransformerConvention(IOutboundParameterTransformer parameterTransformer)
|
||||||
{
|
{
|
||||||
if (parameterTransformer == null)
|
if (parameterTransformer == null)
|
||||||
{
|
{
|
||||||
|
|
@ -32,7 +32,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||||
{
|
{
|
||||||
if (ShouldApply(action))
|
if (ShouldApply(action))
|
||||||
{
|
{
|
||||||
action.Properties[typeof(IParameterTransformer)] = _parameterTransformer;
|
action.Properties[typeof(IOutboundParameterTransformer)] = _parameterTransformer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -390,8 +390,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
actionDescriptor.Properties.TryGetValue(typeof(IParameterTransformer), out var transformer);
|
actionDescriptor.Properties.TryGetValue(typeof(IOutboundParameterTransformer), out var transformer);
|
||||||
var routeTokenTransformer = transformer as IParameterTransformer;
|
var routeTokenTransformer = transformer as IOutboundParameterTransformer;
|
||||||
|
|
||||||
actionDescriptor.AttributeRouteInfo.Template = AttributeRouteModel.ReplaceTokens(
|
actionDescriptor.AttributeRouteInfo.Template = AttributeRouteModel.ReplaceTokens(
|
||||||
actionDescriptor.AttributeRouteInfo.Template,
|
actionDescriptor.AttributeRouteInfo.Template,
|
||||||
|
|
|
||||||
|
|
@ -277,9 +277,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
// Use the first transformer policy
|
// Use the first transformer policy
|
||||||
for (var k = 0; k < parameterPolicies.Count; k++)
|
for (var k = 0; k < parameterPolicies.Count; k++)
|
||||||
{
|
{
|
||||||
if (parameterPolicies[k] is IParameterTransformer parameterTransformer)
|
if (parameterPolicies[k] is IOutboundParameterTransformer parameterTransformer)
|
||||||
{
|
{
|
||||||
parameterRouteValue = parameterTransformer.Transform(parameterRouteValue);
|
parameterRouteValue = parameterTransformer.TransformOutbound(parameterRouteValue);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,11 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||||
Assert.False(responseType.IsDefaultResponse);
|
Assert.False(responseType.IsDefaultResponse);
|
||||||
Assert.Collection(
|
Assert.Collection(
|
||||||
responseType.ApiResponseFormats,
|
responseType.ApiResponseFormats,
|
||||||
format => Assert.Equal("application/json", format.MediaType));
|
format =>
|
||||||
|
{
|
||||||
|
Assert.Equal("application/json", format.MediaType);
|
||||||
|
Assert.IsType<TestOutputFormatter>(format.Formatter);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
responseType =>
|
responseType =>
|
||||||
{
|
{
|
||||||
|
|
@ -106,7 +110,11 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||||
Assert.False(responseType.IsDefaultResponse);
|
Assert.False(responseType.IsDefaultResponse);
|
||||||
Assert.Collection(
|
Assert.Collection(
|
||||||
responseType.ApiResponseFormats,
|
responseType.ApiResponseFormats,
|
||||||
format => Assert.Equal("application/json", format.MediaType));
|
format =>
|
||||||
|
{
|
||||||
|
Assert.Equal("application/json", format.MediaType);
|
||||||
|
Assert.IsType<TestOutputFormatter>(format.Formatter);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
responseType =>
|
responseType =>
|
||||||
{
|
{
|
||||||
|
|
@ -115,7 +123,11 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||||
Assert.False(responseType.IsDefaultResponse);
|
Assert.False(responseType.IsDefaultResponse);
|
||||||
Assert.Collection(
|
Assert.Collection(
|
||||||
responseType.ApiResponseFormats,
|
responseType.ApiResponseFormats,
|
||||||
format => Assert.Equal("application/json", format.MediaType));
|
format =>
|
||||||
|
{
|
||||||
|
Assert.Equal("application/json", format.MediaType);
|
||||||
|
Assert.IsType<TestOutputFormatter>(format.Formatter);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
responseType =>
|
responseType =>
|
||||||
{
|
{
|
||||||
|
|
@ -156,7 +168,11 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||||
Assert.False(responseType.IsDefaultResponse);
|
Assert.False(responseType.IsDefaultResponse);
|
||||||
Assert.Collection(
|
Assert.Collection(
|
||||||
responseType.ApiResponseFormats,
|
responseType.ApiResponseFormats,
|
||||||
format => Assert.Equal("application/json", format.MediaType));
|
format =>
|
||||||
|
{
|
||||||
|
Assert.Equal("application/json", format.MediaType);
|
||||||
|
Assert.IsType<TestOutputFormatter>(format.Formatter);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
responseType =>
|
responseType =>
|
||||||
{
|
{
|
||||||
|
|
@ -663,6 +679,36 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetApiResponseTypes_UsesContentTypeWithoutWildCard_WhenNoFormatterSupportsIt()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var actionDescriptor = GetControllerActionDescriptor(typeof(TestController), nameof(TestController.GetUser));
|
||||||
|
actionDescriptor.FilterDescriptors.Add(new FilterDescriptor(new ProducesAttribute("application/pdf"), FilterScope.Action));
|
||||||
|
|
||||||
|
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(DerivedModel), responseType.Type);
|
||||||
|
Assert.False(responseType.IsDefaultResponse);
|
||||||
|
Assert.Collection(
|
||||||
|
responseType.ApiResponseFormats,
|
||||||
|
format =>
|
||||||
|
{
|
||||||
|
Assert.Equal("application/pdf", format.MediaType);
|
||||||
|
Assert.Null(format.Formatter);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private static ApiResponseTypeProvider GetProvider()
|
private static ApiResponseTypeProvider GetProvider()
|
||||||
{
|
{
|
||||||
var mvcOptions = new MvcOptions
|
var mvcOptions = new MvcOptions
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ namespace Microsoft.AspNetCore.Mvc.Test.ApplicationModels
|
||||||
convention.Apply(model);
|
convention.Apply(model);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.True(model.Properties.TryGetValue(typeof(IParameterTransformer), out var routeTokenTransformer));
|
Assert.True(model.Properties.TryGetValue(typeof(IOutboundParameterTransformer), out var routeTokenTransformer));
|
||||||
Assert.Equal(transformer, routeTokenTransformer);
|
Assert.Equal(transformer, routeTokenTransformer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -68,7 +68,7 @@ namespace Microsoft.AspNetCore.Mvc.Test.ApplicationModels
|
||||||
convention.Apply(model);
|
convention.Apply(model);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.False(model.Properties.TryGetValue(typeof(IParameterTransformer), out _));
|
Assert.False(model.Properties.TryGetValue(typeof(IOutboundParameterTransformer), out _));
|
||||||
}
|
}
|
||||||
|
|
||||||
private MethodInfo GetMethodInfo()
|
private MethodInfo GetMethodInfo()
|
||||||
|
|
@ -76,17 +76,17 @@ namespace Microsoft.AspNetCore.Mvc.Test.ApplicationModels
|
||||||
return typeof(RouteTokenTransformerConventionTest).GetMethod(nameof(GetMethodInfo), BindingFlags.NonPublic | BindingFlags.Instance);
|
return typeof(RouteTokenTransformerConventionTest).GetMethod(nameof(GetMethodInfo), BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TestParameterTransformer : IParameterTransformer
|
private class TestParameterTransformer : IOutboundParameterTransformer
|
||||||
{
|
{
|
||||||
public string Transform(string value)
|
public string TransformOutbound(object value)
|
||||||
{
|
{
|
||||||
return value;
|
return value?.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class CustomRouteTokenTransformerConvention : RouteTokenTransformerConvention
|
private class CustomRouteTokenTransformerConvention : RouteTokenTransformerConvention
|
||||||
{
|
{
|
||||||
public CustomRouteTokenTransformerConvention(IParameterTransformer parameterTransformer) : base(parameterTransformer)
|
public CustomRouteTokenTransformerConvention(IOutboundParameterTransformer parameterTransformer) : base(parameterTransformer)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -771,11 +771,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
||||||
return dataSource;
|
return dataSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class UpperCaseParameterTransform : IParameterTransformer
|
private class UpperCaseParameterTransform : IOutboundParameterTransformer
|
||||||
{
|
{
|
||||||
public string Transform(string value)
|
public string TransformOutbound(object value)
|
||||||
{
|
{
|
||||||
return value?.ToUpperInvariant();
|
return value?.ToString().ToUpperInvariant();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
|
@ -829,17 +830,27 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
// Assert
|
// Assert
|
||||||
var description = Assert.Single(result);
|
var description = Assert.Single(result);
|
||||||
var responseType = Assert.Single(description.SupportedResponseTypes);
|
var responseType = Assert.Single(description.SupportedResponseTypes);
|
||||||
Assert.Equal(1, responseType.ResponseFormats.Count);
|
Assert.Collection(
|
||||||
|
responseType.ResponseFormats,
|
||||||
var responseFormat = responseType.ResponseFormats[0];
|
responseFormat =>
|
||||||
Assert.Equal("application/hal+json", responseFormat.MediaType);
|
{
|
||||||
Assert.Equal(typeof(JsonOutputFormatter).FullName, responseFormat.FormatterType);
|
Assert.Equal("application/hal+custom", responseFormat.MediaType);
|
||||||
|
Assert.Null(responseFormat.FormatterType);
|
||||||
|
},
|
||||||
|
responseFormat =>
|
||||||
|
{
|
||||||
|
Assert.Equal("application/hal+json", responseFormat.MediaType);
|
||||||
|
Assert.Equal(typeof(JsonOutputFormatter).FullName, responseFormat.FormatterType);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ApiExplorer_ResponseContentType_NoMatch()
|
public async Task ApiExplorer_ResponseContentType_NoMatch()
|
||||||
{
|
{
|
||||||
// Arrange & Act
|
// Arrange
|
||||||
|
var expectedMediaTypes = new[] { "application/custom", "text/hal+bson" };
|
||||||
|
|
||||||
|
// Act
|
||||||
var response = await Client.GetAsync("http://localhost/ApiExplorerResponseContentType/NoMatch");
|
var response = await Client.GetAsync("http://localhost/ApiExplorerResponseContentType/NoMatch");
|
||||||
|
|
||||||
var body = await response.Content.ReadAsStringAsync();
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
|
|
@ -848,7 +859,11 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
// Assert
|
// Assert
|
||||||
var description = Assert.Single(result);
|
var description = Assert.Single(result);
|
||||||
var responseType = Assert.Single(description.SupportedResponseTypes);
|
var responseType = Assert.Single(description.SupportedResponseTypes);
|
||||||
Assert.Empty(responseType.ResponseFormats);
|
|
||||||
|
|
||||||
|
Assert.Equal(typeof(Product).FullName, responseType.ResponseType);
|
||||||
|
Assert.Equal(200, responseType.StatusCode);
|
||||||
|
Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType));
|
||||||
}
|
}
|
||||||
|
|
||||||
[ConditionalTheory]
|
[ConditionalTheory]
|
||||||
|
|
@ -1147,6 +1162,28 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
Assert.Equal("multipart/form-data", requestFormat.MediaType);
|
Assert.Equal("multipart/form-data", requestFormat.MediaType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ApiBehavior_UsesContentTypeFromProducesAttribute_WhenNoFormatterSupportsIt()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedMediaTypes = new[] { "application/pdf" };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var body = await Client.GetStringAsync("ApiExplorerApiController/ProducesWithUnsupportedContentType");
|
||||||
|
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var description = Assert.Single(result);
|
||||||
|
Assert.Collection(
|
||||||
|
description.SupportedResponseTypes.OrderBy(r => r.StatusCode),
|
||||||
|
responseType =>
|
||||||
|
{
|
||||||
|
Assert.Equal(typeof(Stream).FullName, responseType.ResponseType);
|
||||||
|
Assert.Equal(200, responseType.StatusCode);
|
||||||
|
Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public Task ApiConvention_ForGetMethod_ReturningModel() => ApiConvention_ForGetMethod("GetProduct");
|
public Task ApiConvention_ForGetMethod_ReturningModel() => ApiConvention_ForGetMethod("GetProduct");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,10 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
||||||
public Task NoDiagnosticsAreReturned_ForApiController_WithAllDocumentedStatusCodes()
|
public Task NoDiagnosticsAreReturned_ForApiController_WithAllDocumentedStatusCodes()
|
||||||
=> RunNoDiagnosticsAreReturned();
|
=> RunNoDiagnosticsAreReturned();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public Task NoDiagnosticsAreReturned_ForOkResultReturningAction()
|
||||||
|
=> RunNoDiagnosticsAreReturned();
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public Task NoDiagnosticsAreReturned_ForApiController_IfStatusCodesCannotBeInferred()
|
public Task NoDiagnosticsAreReturned_ForApiController_IfStatusCodesCannotBeInferred()
|
||||||
=> RunNoDiagnosticsAreReturned();
|
=> RunNoDiagnosticsAreReturned();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,169 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
||||||
|
{
|
||||||
|
public class DeclaredApiResponseMetadataTest
|
||||||
|
{
|
||||||
|
private readonly ReturnStatementSyntax ReturnStatement = SyntaxFactory.ReturnStatement();
|
||||||
|
private readonly AttributeData AttributeData = new TestAttributeData();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Matches_ReturnsTrue_IfDeclaredMetadataIsImplicit_AndActualMetadataIsDefaultResponse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var declaredMetadata = DeclaredApiResponseMetadata.ImplicitResponse;
|
||||||
|
var actualMetadata = new ActualApiResponseMetadata(ReturnStatement);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var matches = declaredMetadata.Matches(actualMetadata);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Matches_ReturnsTrue_IfDeclaredMetadataIsImplicit_AndActualMetadataReturns200()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var declaredMetadata = DeclaredApiResponseMetadata.ImplicitResponse;
|
||||||
|
var actualMetadata = new ActualApiResponseMetadata(ReturnStatement, 200);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var matches = declaredMetadata.Matches(actualMetadata);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Matches_ReturnsTrue_IfDeclaredMetadataIs200_AndActualMetadataIsDefaultResponse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var declaredMetadata = DeclaredApiResponseMetadata.ForProducesResponseType(200, AttributeData, Mock.Of<IMethodSymbol>());
|
||||||
|
var actualMetadata = new ActualApiResponseMetadata(ReturnStatement);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var matches = declaredMetadata.Matches(actualMetadata);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <example>
|
||||||
|
/// [ProducesResponseType(201)]
|
||||||
|
/// public IActionResult SomeAction => new Model();
|
||||||
|
/// </example>
|
||||||
|
[Fact]
|
||||||
|
public void Matches_ReturnsTrue_IfDeclaredMetadataIs201_AndActualMetadataIsDefault()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var declaredMetadata = DeclaredApiResponseMetadata.ForProducesResponseType(201, AttributeData, Mock.Of<IMethodSymbol>());
|
||||||
|
var actualMetadata = new ActualApiResponseMetadata(ReturnStatement);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var matches = declaredMetadata.Matches(actualMetadata);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <example>
|
||||||
|
/// [ProducesResponseType(201)]
|
||||||
|
/// public IActionResult SomeAction => Ok(new Model());
|
||||||
|
/// </example>
|
||||||
|
[Fact]
|
||||||
|
public void Matches_ReturnsFalse_IfDeclaredMetadataIs201_AndActualMetadataIs200()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var declaredMetadata = DeclaredApiResponseMetadata.ForProducesResponseType(201, AttributeData, Mock.Of<IMethodSymbol>());
|
||||||
|
var actualMetadata = new ActualApiResponseMetadata(ReturnStatement, 200);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var matches = declaredMetadata.Matches(actualMetadata);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Matches_ReturnsTrue_IfDeclaredMetadataAndActualMetadataHaveSameStatusCode()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var declaredMetadata = DeclaredApiResponseMetadata.ForProducesResponseType(302, AttributeData, Mock.Of<IMethodSymbol>());
|
||||||
|
var actualMetadata = new ActualApiResponseMetadata(ReturnStatement, 302);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var matches = declaredMetadata.Matches(actualMetadata);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(400)]
|
||||||
|
[InlineData(409)]
|
||||||
|
[InlineData(500)]
|
||||||
|
public void Matches_ReturnsTrue_IfDeclaredMetadataIsDefault_AndActualMetadataIsErrorStatusCode(int actualStatusCode)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var declaredMetadata = DeclaredApiResponseMetadata.ForProducesDefaultResponse(AttributeData, Mock.Of<IMethodSymbol>());
|
||||||
|
var actualMetadata = new ActualApiResponseMetadata(ReturnStatement, actualStatusCode);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var matches = declaredMetadata.Matches(actualMetadata);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Matches_ReturnsFalse_IfDeclaredMetadataIsDefault_AndActualMetadataIsNotErrorStatusCode()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var declaredMetadata = DeclaredApiResponseMetadata.ForProducesDefaultResponse(AttributeData, Mock.Of<IMethodSymbol>());
|
||||||
|
var actualMetadata = new ActualApiResponseMetadata(ReturnStatement, 204);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var matches = declaredMetadata.Matches(actualMetadata);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Matches_ReturnsFalse_IfDeclaredMetadataIsDefault_AndActualMetadataIsDefaultResponse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var declaredMetadata = DeclaredApiResponseMetadata.ForProducesDefaultResponse(AttributeData, Mock.Of<IMethodSymbol>());
|
||||||
|
var actualMetadata = new ActualApiResponseMetadata(ReturnStatement);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var matches = declaredMetadata.Matches(actualMetadata);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestAttributeData : AttributeData
|
||||||
|
{
|
||||||
|
protected override INamedTypeSymbol CommonAttributeClass => throw new System.NotImplementedException();
|
||||||
|
|
||||||
|
protected override IMethodSymbol CommonAttributeConstructor => throw new System.NotImplementedException();
|
||||||
|
|
||||||
|
protected override SyntaxReference CommonApplicationSyntaxReference => throw new System.NotImplementedException();
|
||||||
|
|
||||||
|
protected override ImmutableArray<TypedConstant> CommonConstructorArguments => throw new System.NotImplementedException();
|
||||||
|
|
||||||
|
protected override ImmutableArray<KeyValuePair<string, TypedConstant>> CommonNamedArguments => throw new System.NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Analyzer.Testing;
|
using Microsoft.AspNetCore.Analyzer.Testing;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Api.Analyzers.TestFiles.SymbolApiResponseMetadataProviderTest;
|
||||||
using Microsoft.CodeAnalysis;
|
using Microsoft.CodeAnalysis;
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
@ -433,6 +435,95 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
||||||
Assert.True(actualResponseMetadata.Value.IsDefaultResponse);
|
Assert.True(actualResponseMetadata.Value.IsDefaultResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task TryGetActualResponseMetadata_ActionWithActionResultOfTReturningOkResult()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var typeName = typeof(TryGetActualResponseMetadataController).FullName;
|
||||||
|
var methodName = nameof(TryGetActualResponseMetadataController.ActionWithActionResultOfTReturningOkResult);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var (success, responseMetadatas, _) = await TryGetActualResponseMetadata(typeName, methodName);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(success);
|
||||||
|
Assert.Collection(
|
||||||
|
responseMetadatas,
|
||||||
|
metadata =>
|
||||||
|
{
|
||||||
|
Assert.False(metadata.IsDefaultResponse);
|
||||||
|
Assert.Equal(200, metadata.StatusCode);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task TryGetActualResponseMetadata_ActionWithActionResultOfTReturningModel()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var typeName = typeof(TryGetActualResponseMetadataController).FullName;
|
||||||
|
var methodName = nameof(TryGetActualResponseMetadataController.ActionWithActionResultOfTReturningModel);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var (success, responseMetadatas, _) = await TryGetActualResponseMetadata(typeName, methodName);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(success);
|
||||||
|
Assert.Collection(
|
||||||
|
responseMetadatas,
|
||||||
|
metadata =>
|
||||||
|
{
|
||||||
|
Assert.True(metadata.IsDefaultResponse);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task TryGetActualResponseMetadata_ActionReturningNotFoundAndModel()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var typeName = typeof(TryGetActualResponseMetadataController).FullName;
|
||||||
|
var methodName = nameof(TryGetActualResponseMetadataController.ActionReturningNotFoundAndModel);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var (success, responseMetadatas, testSource) = await TryGetActualResponseMetadata(typeName, methodName);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(success);
|
||||||
|
Assert.Collection(
|
||||||
|
responseMetadatas,
|
||||||
|
metadata =>
|
||||||
|
{
|
||||||
|
Assert.False(metadata.IsDefaultResponse);
|
||||||
|
Assert.Equal(204, metadata.StatusCode);
|
||||||
|
AnalyzerAssert.DiagnosticLocation(testSource.MarkerLocations["MM1"], metadata.ReturnStatement.GetLocation());
|
||||||
|
|
||||||
|
},
|
||||||
|
metadata =>
|
||||||
|
{
|
||||||
|
Assert.True(metadata.IsDefaultResponse);
|
||||||
|
AnalyzerAssert.DiagnosticLocation(testSource.MarkerLocations["MM2"], metadata.ReturnStatement.GetLocation());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<(bool result, IList<ActualApiResponseMetadata> responseMetadatas, TestSource testSource)> TryGetActualResponseMetadata(string typeName, string methodName)
|
||||||
|
{
|
||||||
|
var testSource = MvcTestSource.Read(GetType().Name, "TryGetActualResponseMetadataTests");
|
||||||
|
var project = DiagnosticProject.Create(GetType().Assembly, new[] { testSource.Source });
|
||||||
|
|
||||||
|
var compilation = await GetCompilation("TryGetActualResponseMetadataTests");
|
||||||
|
|
||||||
|
var type = compilation.GetTypeByMetadataName(typeName);
|
||||||
|
var method = (IMethodSymbol)type.GetMembers(methodName).First();
|
||||||
|
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||||
|
|
||||||
|
var syntaxTree = method.DeclaringSyntaxReferences[0].SyntaxTree;
|
||||||
|
var methodSyntax = (MethodDeclarationSyntax)syntaxTree.GetRoot().FindNode(method.Locations[0].SourceSpan);
|
||||||
|
var semanticModel = compilation.GetSemanticModel(syntaxTree);
|
||||||
|
|
||||||
|
var result = SymbolApiResponseMetadataProvider.TryGetActualResponseMetadata(symbolCache, semanticModel, methodSyntax, CancellationToken.None, out var responseMetadatas);
|
||||||
|
|
||||||
|
return (result, responseMetadatas, testSource);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<ActualApiResponseMetadata?> RunInspectReturnStatementSyntax([CallerMemberName]string test = null)
|
private async Task<ActualApiResponseMetadata?> RunInspectReturnStatementSyntax([CallerMemberName]string test = null)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
||||||
|
{
|
||||||
|
[ApiController]
|
||||||
|
public class NoDiagnosticsAreReturned_ForOkResultReturningAction : ControllerBase
|
||||||
|
{
|
||||||
|
public async Task<ActionResult<IEnumerable<NoDiagnosticsAreReturned_ForOkResultReturningAction>>> Action()
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
var models = new List<NoDiagnosticsAreReturned_ForOkResultReturningActionModel>();
|
||||||
|
|
||||||
|
return Ok(models);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NoDiagnosticsAreReturned_ForOkResultReturningActionModel { }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers.TestFiles.SymbolApiResponseMetadataProviderTest
|
||||||
|
{
|
||||||
|
public class TryGetActualResponseMetadataController : ControllerBase
|
||||||
|
{
|
||||||
|
public async Task<ActionResult<IEnumerable<TryGetActualResponseMetadataModel>>> ActionWithActionResultOfTReturningOkResult()
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
var models = new List<TryGetActualResponseMetadataModel>();
|
||||||
|
|
||||||
|
return Ok(models);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ActionResult<IEnumerable<TryGetActualResponseMetadataModel>>> ActionWithActionResultOfTReturningModel()
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
var models = new List<TryGetActualResponseMetadataModel>();
|
||||||
|
|
||||||
|
return models;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ActionResult<TryGetActualResponseMetadataModel>> ActionReturningNotFoundAndModel(int id)
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
|
||||||
|
if (id == 0)
|
||||||
|
{
|
||||||
|
/*MM1*/return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*MM2*/return new TryGetActualResponseMetadataModel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TryGetActualResponseMetadataModel { }
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using System.IO;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace ApiExplorerWebSite
|
namespace ApiExplorerWebSite
|
||||||
{
|
{
|
||||||
|
|
@ -27,5 +28,8 @@ namespace ApiExplorerWebSite
|
||||||
public void ActionWithFormFileCollectionParameter(IFormFileCollection formFile)
|
public void ActionWithFormFileCollectionParameter(IFormFileCollection formFile)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Produces("application/pdf", Type = typeof(Stream))]
|
||||||
|
public IActionResult ProducesWithUnsupportedContentType() => null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -11,9 +11,9 @@ namespace RoutingWebSite
|
||||||
public class ControllerRouteTokenTransformerConvention : IApplicationModelConvention
|
public class ControllerRouteTokenTransformerConvention : IApplicationModelConvention
|
||||||
{
|
{
|
||||||
private readonly Type _controllerType;
|
private readonly Type _controllerType;
|
||||||
private readonly IParameterTransformer _parameterTransformer;
|
private readonly IOutboundParameterTransformer _parameterTransformer;
|
||||||
|
|
||||||
public ControllerRouteTokenTransformerConvention(Type controllerType, IParameterTransformer parameterTransformer)
|
public ControllerRouteTokenTransformerConvention(Type controllerType, IOutboundParameterTransformer parameterTransformer)
|
||||||
{
|
{
|
||||||
if (parameterTransformer == null)
|
if (parameterTransformer == null)
|
||||||
{
|
{
|
||||||
|
|
@ -30,7 +30,7 @@ namespace RoutingWebSite
|
||||||
{
|
{
|
||||||
foreach (var action in controller.Actions)
|
foreach (var action in controller.Actions)
|
||||||
{
|
{
|
||||||
action.Properties[typeof(IParameterTransformer)] = _parameterTransformer;
|
action.Properties[typeof(IOutboundParameterTransformer)] = _parameterTransformer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,12 @@ using Microsoft.AspNetCore.Routing;
|
||||||
|
|
||||||
namespace RoutingWebSite
|
namespace RoutingWebSite
|
||||||
{
|
{
|
||||||
public class SlugifyParameterTransformer : IParameterTransformer
|
public class SlugifyParameterTransformer : IOutboundParameterTransformer
|
||||||
{
|
{
|
||||||
public string Transform(string value)
|
public string TransformOutbound(object value)
|
||||||
{
|
{
|
||||||
// Slugify value
|
// Slugify value
|
||||||
return Regex.Replace(value, "([a-z])([A-Z])", "$1-$2", RegexOptions.None, TimeSpan.FromMilliseconds(100)).ToLower();
|
return value == null ? null : Regex.Replace(value.ToString(), "([a-z])([A-Z])", "$1-$2", RegexOptions.None, TimeSpan.FromMilliseconds(100)).ToLower();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue