Skip to content

Commit

Permalink
Trim Async suffix on action names
Browse files Browse the repository at this point in the history
Fixes #4849
  • Loading branch information
pranavkm committed Feb 12, 2019
1 parent 6827bb7 commit 607b8a7
Show file tree
Hide file tree
Showing 11 changed files with 156 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ internal PropertyModel CreatePropertyModel(PropertyInfo propertyInfo)
}
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 PropertyModel CreatePropertyModel(PropertyInfo propertyInfo)
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 MaxModelValidationErrors
}
}

/// <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 @@ public async Task PartialsRenderedViaPartialAsync_CanRenderLayouts()
</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));
}
}
}

0 comments on commit 607b8a7

Please sign in to comment.