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
|
||||
{
|
||||
actionModel.ActionName = methodInfo.Name;
|
||||
actionModel.ActionName = CanonicalizeActionName(methodInfo.Name);
|
||||
}
|
||||
|
||||
var apiVisibility = attributes.OfType<IApiDescriptionVisibilityProvider>().FirstOrDefault();
|
||||
|
|
@ -371,6 +371,19 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
|||
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>
|
||||
/// Returns <c>true</c> if the <paramref name="methodInfo"/> is an action. Otherwise <c>false</c>.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
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 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]
|
||||
public void CreateControllerModel_DerivedFromControllerClass_HasFilter()
|
||||
{
|
||||
|
|
@ -1774,6 +1834,14 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
|||
public void Edit() { }
|
||||
}
|
||||
|
||||
private class AsyncActionController : Controller
|
||||
{
|
||||
public Task<IActionResult> GetPersonAsync() => null;
|
||||
|
||||
[ActionName("GetRealAddressAsync")]
|
||||
public Task<IActionResult> GetAddressAsync() => null;
|
||||
}
|
||||
|
||||
private class TestApplicationModelProvider : DefaultApplicationModelProvider
|
||||
{
|
||||
public TestApplicationModelProvider()
|
||||
|
|
|
|||
|
|
@ -1398,7 +1398,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
|
||||
// Act
|
||||
var response = await Client.DeleteAsync(
|
||||
$"ApiExplorerResponseTypeWithApiConventionController/DeleteProductAsync");
|
||||
$"ApiExplorerResponseTypeWithApiConventionController/DeleteProduct");
|
||||
var responseBody = await response.EnsureSuccessStatusCode().Content.ReadAsStringAsync();
|
||||
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(responseBody);
|
||||
|
||||
|
|
|
|||
|
|
@ -278,5 +278,37 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
// Assert
|
||||
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()
|
||||
{
|
||||
// Arrange
|
||||
var url = "ActionResultOfT/GetProductsAsync";
|
||||
var url = "ActionResultOfT/GetProducts";
|
||||
|
||||
// Act
|
||||
var response = await Client.GetStringAsync(url);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Testing.xunit;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||
|
|
@ -58,7 +57,7 @@ After flush inside partial<form action=""/FlushPoint/PageWithoutLayout"" method=
|
|||
|
||||
[Theory]
|
||||
[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)
|
||||
{
|
||||
var expected = $@"<title>{ title }</title>
|
||||
|
|
|
|||
|
|
@ -417,7 +417,10 @@ Partial that does not specify Layout
|
|||
</layout-for-viewstart-with-layout>";
|
||||
|
||||
// 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.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()
|
||||
{
|
||||
await Task.Delay(SimulateDelayMilliseconds);
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
Hello world!
|
||||
|
|
@ -25,7 +25,7 @@ namespace RazorWebSite.Controllers
|
|||
// (b) Partials rendered via PartialAsync can execute Layout.
|
||||
public IActionResult PartialsRenderedViaPartialAsync()
|
||||
{
|
||||
return View();
|
||||
return View(nameof(PartialsRenderedViaPartialAsync));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue