Trim Async suffix on action names (#7420)
Fixes https://github.com/aspnet/AspNetCore/issues/4849
This commit is contained in:
parent
6827bb7443
commit
173b2f91fb
|
|
@ -296,7 +296,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
actionModel.ActionName = methodInfo.Name;
|
actionModel.ActionName = CanonicalizeActionName(methodInfo.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
var apiVisibility = attributes.OfType<IApiDescriptionVisibilityProvider>().FirstOrDefault();
|
var apiVisibility = attributes.OfType<IApiDescriptionVisibilityProvider>().FirstOrDefault();
|
||||||
|
|
@ -371,6 +371,19 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||||
return actionModel;
|
return actionModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string CanonicalizeActionName(string actionName)
|
||||||
|
{
|
||||||
|
const string Suffix = "Async";
|
||||||
|
|
||||||
|
if (_mvcOptions.SuppressAsyncSuffixInActionNames &&
|
||||||
|
actionName.EndsWith(Suffix, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
actionName = actionName.Substring(0, actionName.Length - Suffix.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return actionName;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns <c>true</c> if the <paramref name="methodInfo"/> is an action. Otherwise <c>false</c>.
|
/// Returns <c>true</c> if the <paramref name="methodInfo"/> is an action. Otherwise <c>false</c>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||||
using Microsoft.AspNetCore.Mvc.Filters;
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||||
|
|
@ -209,6 +211,26 @@ namespace Microsoft.AspNetCore.Mvc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value that determines if MVC will remove the suffix "Async" applied to
|
||||||
|
/// controller action names.
|
||||||
|
/// <para>
|
||||||
|
/// <see cref="ControllerActionDescriptor.ActionName"/> is used to construct the route to the action as
|
||||||
|
/// well as in view lookup. When <see langword="true"/>, MVC will trim the suffix "Async" applied
|
||||||
|
/// to action method names.
|
||||||
|
/// For example, the action name for <c>ProductsController.ListProductsAsync</c> will be
|
||||||
|
/// canonicalized as <c>ListProducts.</c>. Consequently, it will be routeable at
|
||||||
|
/// <c>/Products/ListProducts</c> with views looked up at <c>/Views/Products/ListProducts.cshtml</c>.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// This option does not affect values specified using using <see cref="ActionNameAttribute"/>.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
/// <value>
|
||||||
|
/// The default value is <see langword="true"/>.
|
||||||
|
/// </value>
|
||||||
|
public bool SuppressAsyncSuffixInActionNames { get; set; } = true;
|
||||||
|
|
||||||
IEnumerator<ICompatibilitySwitch> IEnumerable<ICompatibilitySwitch>.GetEnumerator() => _switches.GetEnumerator();
|
IEnumerator<ICompatibilitySwitch> IEnumerable<ICompatibilitySwitch>.GetEnumerator() => _switches.GetEnumerator();
|
||||||
|
|
||||||
IEnumerator IEnumerable.GetEnumerator() => _switches.GetEnumerator();
|
IEnumerator IEnumerable.GetEnumerator() => _switches.GetEnumerator();
|
||||||
|
|
|
||||||
|
|
@ -293,6 +293,66 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OnProvidersExecuting_RemovesAsyncSuffix_WhenOptionIsSet()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var options = new MvcOptions();
|
||||||
|
var provider = new TestApplicationModelProvider(options, new EmptyModelMetadataProvider());
|
||||||
|
var typeInfo = typeof(AsyncActionController).GetTypeInfo();
|
||||||
|
var methodInfo = typeInfo.GetMethod(nameof(AsyncActionController.GetPersonAsync));
|
||||||
|
|
||||||
|
var context = new ApplicationModelProviderContext(new[] { typeInfo });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
provider.OnProvidersExecuting(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var controllerModel = Assert.Single(context.Result.Controllers);
|
||||||
|
var action = Assert.Single(controllerModel.Actions, a => a.ActionMethod == methodInfo);
|
||||||
|
Assert.Equal("GetPerson", action.ActionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OnProvidersExecuting_DoesNotRemoveAsyncSuffix_WhenOptionIsDisabled()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var options = new MvcOptions { SuppressAsyncSuffixInActionNames = false };
|
||||||
|
var provider = new TestApplicationModelProvider(options, new EmptyModelMetadataProvider());
|
||||||
|
var typeInfo = typeof(AsyncActionController).GetTypeInfo();
|
||||||
|
var methodInfo = typeInfo.GetMethod(nameof(AsyncActionController.GetPersonAsync));
|
||||||
|
|
||||||
|
var context = new ApplicationModelProviderContext(new[] { typeInfo });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
provider.OnProvidersExecuting(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var controllerModel = Assert.Single(context.Result.Controllers);
|
||||||
|
var action = Assert.Single(controllerModel.Actions, a => a.ActionMethod == methodInfo);
|
||||||
|
Assert.Equal(nameof(AsyncActionController.GetPersonAsync), action.ActionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OnProvidersExecuting_DoesNotRemoveAsyncSuffix_WhenActionNameIsSpecifiedUsingActionNameAttribute()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var options = new MvcOptions();
|
||||||
|
var provider = new TestApplicationModelProvider(options, new EmptyModelMetadataProvider());
|
||||||
|
var typeInfo = typeof(AsyncActionController).GetTypeInfo();
|
||||||
|
var methodInfo = typeInfo.GetMethod(nameof(AsyncActionController.GetAddressAsync));
|
||||||
|
|
||||||
|
var context = new ApplicationModelProviderContext(new[] { typeInfo });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
provider.OnProvidersExecuting(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var controllerModel = Assert.Single(context.Result.Controllers);
|
||||||
|
var action = Assert.Single(controllerModel.Actions, a => a.ActionMethod == methodInfo);
|
||||||
|
Assert.Equal("GetRealAddressAsync", action.ActionName);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CreateControllerModel_DerivedFromControllerClass_HasFilter()
|
public void CreateControllerModel_DerivedFromControllerClass_HasFilter()
|
||||||
{
|
{
|
||||||
|
|
@ -1774,6 +1834,14 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||||
public void Edit() { }
|
public void Edit() { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class AsyncActionController : Controller
|
||||||
|
{
|
||||||
|
public Task<IActionResult> GetPersonAsync() => null;
|
||||||
|
|
||||||
|
[ActionName("GetRealAddressAsync")]
|
||||||
|
public Task<IActionResult> GetAddressAsync() => null;
|
||||||
|
}
|
||||||
|
|
||||||
private class TestApplicationModelProvider : DefaultApplicationModelProvider
|
private class TestApplicationModelProvider : DefaultApplicationModelProvider
|
||||||
{
|
{
|
||||||
public TestApplicationModelProvider()
|
public TestApplicationModelProvider()
|
||||||
|
|
|
||||||
|
|
@ -1398,7 +1398,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var response = await Client.DeleteAsync(
|
var response = await Client.DeleteAsync(
|
||||||
$"ApiExplorerResponseTypeWithApiConventionController/DeleteProductAsync");
|
$"ApiExplorerResponseTypeWithApiConventionController/DeleteProduct");
|
||||||
var responseBody = await response.EnsureSuccessStatusCode().Content.ReadAsStringAsync();
|
var responseBody = await response.EnsureSuccessStatusCode().Content.ReadAsStringAsync();
|
||||||
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(responseBody);
|
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(responseBody);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -278,5 +278,37 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal("Action exception message: This is a custom exception.", responseBody);
|
Assert.Equal("Action exception message: This is a custom exception.", responseBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AsyncSuffixIsIgnored()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var response = await Client.GetAsync("AsyncActions/ActionWithSuffix");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ActionIsNotRoutedWithAsyncSuffix()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var response = await Client.GetAsync("AsyncActions/ActionWithSuffixAsync");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await response.AssertStatusCodeAsync(HttpStatusCode.NotFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ViewLookupWithAsyncSuffix()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var response = await Client.GetAsync("AsyncActions/ActionReturningView");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
|
||||||
|
var content = await response.Content.ReadAsStringAsync();
|
||||||
|
Assert.Equal("Hello world!", content.Trim());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -452,7 +452,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
public async Task ActionMethod_ReturningSequenceOfObjectsWrappedInActionResultOfT()
|
public async Task ActionMethod_ReturningSequenceOfObjectsWrappedInActionResultOfT()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var url = "ActionResultOfT/GetProductsAsync";
|
var url = "ActionResultOfT/GetProducts";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var response = await Client.GetStringAsync(url);
|
var response = await Client.GetStringAsync(url);
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
|
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Testing.xunit;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
|
|
@ -58,7 +57,7 @@ After flush inside partial<form action=""/FlushPoint/PageWithoutLayout"" method=
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("PageWithPartialsAndViewComponents", "FlushAsync invoked inside RenderSection")]
|
[InlineData("PageWithPartialsAndViewComponents", "FlushAsync invoked inside RenderSection")]
|
||||||
[InlineData("PageWithRenderSectionAsync", "FlushAsync invoked inside RenderSectionAsync")]
|
[InlineData("PageWithRenderSection", "FlushAsync invoked inside RenderSectionAsync")]
|
||||||
public async Task FlushPointsAreExecutedForPagesWithComponentsPartialsAndSections(string action, string title)
|
public async Task FlushPointsAreExecutedForPagesWithComponentsPartialsAndSections(string action, string title)
|
||||||
{
|
{
|
||||||
var expected = $@"<title>{ title }</title>
|
var expected = $@"<title>{ title }</title>
|
||||||
|
|
|
||||||
|
|
@ -417,7 +417,10 @@ Partial that does not specify Layout
|
||||||
</layout-for-viewstart-with-layout>";
|
</layout-for-viewstart-with-layout>";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var body = await Client.GetStringAsync("http://localhost/PartialsWithLayout/PartialsRenderedViaPartialAsync");
|
var response = await Client.GetAsync("http://localhost/PartialsWithLayout/PartialsRenderedViaPartial");
|
||||||
|
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
|
||||||
|
|
||||||
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal(expected, body.Trim(), ignoreLineEndingDifferences: true);
|
Assert.Equal(expected, body.Trim(), ignoreLineEndingDifferences: true);
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,17 @@ namespace BasicWebSite.Controllers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> ActionWithSuffixAsync()
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<IActionResult> ActionReturningViewAsync()
|
||||||
|
{
|
||||||
|
return Task.FromResult<IActionResult>(View());
|
||||||
|
}
|
||||||
|
|
||||||
public async void AsyncVoidAction()
|
public async void AsyncVoidAction()
|
||||||
{
|
{
|
||||||
await Task.Delay(SimulateDelayMilliseconds);
|
await Task.Delay(SimulateDelayMilliseconds);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
Hello world!
|
||||||
|
|
@ -25,7 +25,7 @@ namespace RazorWebSite.Controllers
|
||||||
// (b) Partials rendered via PartialAsync can execute Layout.
|
// (b) Partials rendered via PartialAsync can execute Layout.
|
||||||
public IActionResult PartialsRenderedViaPartialAsync()
|
public IActionResult PartialsRenderedViaPartialAsync()
|
||||||
{
|
{
|
||||||
return View();
|
return View(nameof(PartialsRenderedViaPartialAsync));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue