Skip to content

Trim Async suffix on action names #7420

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 12, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ internal ActionModel CreateActionModel(
}
else
{
actionModel.ActionName = methodInfo.Name;
actionModel.ActionName = CanonicalizeActionName(methodInfo.Name);
}

var apiVisibility = attributes.OfType<IApiDescriptionVisibilityProvider>().FirstOrDefault();
Expand Down Expand Up @@ -371,6 +371,19 @@ internal ActionModel CreateActionModel(
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>
Expand Down
22 changes: 22 additions & 0 deletions src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/MvcOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -209,6 +211,26 @@ public int? MaxValidationDepth
}
}

/// <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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,66 @@ public void OnProvidersExecuting_UsesBindingSourceSpecifiedOnParameter()
});
}

[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()
{
Expand Down Expand Up @@ -1774,6 +1834,14 @@ private class MultipleRouteProviderOnActionController
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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1398,7 +1398,7 @@ public async Task ApiConvention_ForDeleteActionThatMatchesConvention()

// Act
var response = await Client.DeleteAsync(
$"ApiExplorerResponseTypeWithApiConventionController/DeleteProductAsync");
$"ApiExplorerResponseTypeWithApiConventionController/DeleteProduct");
var responseBody = await response.EnsureSuccessStatusCode().Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(responseBody);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -278,5 +278,37 @@ public async Task CustomAwaitableOfContentResultExceptionAction_ReturnsCorrectEr
// 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());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,7 @@ public async Task ActionMethod_ReturningActionMethodOfT()
public async Task ActionMethod_ReturningSequenceOfObjectsWrappedInActionResultOfT()
{
// Arrange
var url = "ActionResultOfT/GetProductsAsync";
var url = "ActionResultOfT/GetProducts";

// Act
var response = await Client.GetStringAsync(url);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Testing.xunit;
using Xunit;

namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Expand Down Expand Up @@ -58,7 +57,7 @@ Inside partial

[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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ public override void OnActionExecuted(ActionExecutedContext context)
}
}

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);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hello world!
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public IActionResult PartialsRenderedViaRenderPartial()
// (b) Partials rendered via PartialAsync can execute Layout.
public IActionResult PartialsRenderedViaPartialAsync()
{
return View();
return View(nameof(PartialsRenderedViaPartialAsync));
}
}
}