Add mapping service for action results

This allows the use of custom 'envelope' types like ActionResult<> with
a corresponding API Explorer implementation.

Basically this PR services to decouple a bunch of infrastructure from
ActionResult<>.
This commit is contained in:
Ryan Nowak 2018-03-21 22:22:58 -07:00
parent 927af3125e
commit c93c168df3
17 changed files with 317 additions and 78 deletions

View File

@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Routing;
@ -25,6 +26,7 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
public class DefaultApiDescriptionProvider : IApiDescriptionProvider
{
private readonly MvcOptions _mvcOptions;
private readonly IActionResultTypeMapper _mapper;
private readonly IInlineConstraintResolver _constraintResolver;
private readonly IModelMetadataProvider _modelMetadataProvider;
@ -35,6 +37,7 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
/// <param name="constraintResolver">The <see cref="IInlineConstraintResolver"/> used for resolving inline
/// constraints.</param>
/// <param name="modelMetadataProvider">The <see cref="IModelMetadataProvider"/>.</param>
[Obsolete("This constructor is obsolete and will be removed in a future release.")]
public DefaultApiDescriptionProvider(
IOptions<MvcOptions> optionsAccessor,
IInlineConstraintResolver constraintResolver,
@ -45,6 +48,26 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
_modelMetadataProvider = modelMetadataProvider;
}
/// <summary>
/// Creates a new instance of <see cref="DefaultApiDescriptionProvider"/>.
/// </summary>
/// <param name="optionsAccessor">The accessor for <see cref="MvcOptions"/>.</param>
/// <param name="constraintResolver">The <see cref="IInlineConstraintResolver"/> used for resolving inline
/// constraints.</param>
/// <param name="modelMetadataProvider">The <see cref="IModelMetadataProvider"/>.</param>
/// <param name="mapper"> The <see cref="IActionResultTypeMapper"/>.</param>
public DefaultApiDescriptionProvider(
IOptions<MvcOptions> optionsAccessor,
IInlineConstraintResolver constraintResolver,
IModelMetadataProvider modelMetadataProvider,
IActionResultTypeMapper mapper)
{
_mvcOptions = optionsAccessor.Value;
_constraintResolver = constraintResolver;
_modelMetadataProvider = modelMetadataProvider;
_mapper = mapper;
}
/// <inheritdoc />
public int Order => -1000;
@ -481,12 +504,14 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
{
return typeof(void);
}
// Unwrap the type if it's a Task<T>. The Task (non-generic) case was already handled.
var unwrappedType = UnwrapGenericType(declaredReturnType, typeof(Task<>));
// Unwrap the type if it's ActionResult<T> or Task<ActionResult<T>>.
unwrappedType = UnwrapGenericType(unwrappedType, typeof(ActionResult<>));
Type unwrappedType = declaredReturnType;
if (declaredReturnType.IsGenericType &&
declaredReturnType.GetGenericTypeDefinition() == typeof(Task<>))
{
unwrappedType = declaredReturnType.GetGenericArguments()[0];
}
// If the method is declared to return IActionResult or a derived class, that information
// isn't valuable to the formatter.
@ -494,16 +519,11 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
{
return null;
}
else
{
return unwrappedType;
}
Type UnwrapGenericType(Type type, Type queryType)
{
var genericType = ClosedGenericMatcher.ExtractGenericInterface(type, queryType);
return genericType?.GenericTypeArguments[0] ?? type;
}
// If we get here, the type should be a user-defined data type or an envelope type
// like ActionResult<T>. The mapper service will unwrap envelopes.
unwrappedType = _mapper.GetResultDataType(unwrappedType);
return unwrappedType;
}
private Type GetRuntimeReturnType(Type declaredReturnType)

View File

@ -202,6 +202,7 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddSingleton<ControllerActionInvokerCache>();
services.TryAddEnumerable(
ServiceDescriptor.Singleton<IFilterProvider, DefaultFilterProvider>());
services.TryAddSingleton<IActionResultTypeMapper, ActionResultTypeMapper>();
//
// Request body limit filters

View File

@ -0,0 +1,48 @@
// 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;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Mvc.Infrastructure
{
/// <summary>
/// Provides a mapping from the return value of an action to an <see cref="IActionResult"/>
/// for request processing.
/// </summary>
/// <remarks>
/// The default implementation of this service handles the conversion of
/// <see cref="ActionResult{TValue}"/> to an <see cref="IActionResult"/> during request
/// processing as well as the mapping of <see cref="ActionResult{TValue}"/> to <c>TValue</c>
/// during API Explorer processing.
/// </remarks>
public interface IActionResultTypeMapper
{
/// <summary>
/// Gets the result data type that corresponds to <paramref name="returnType"/>. This
/// method will not be called for actions that return <c>void</c> or an <see cref="IActionResult"/>
/// type.
/// </summary>
/// <param name="returnType">The declared return type of an action.</param>
/// <returns>A <see cref="Type"/> that represents the response data.</returns>
/// <remarks>
/// Prior to calling this method, the infrastructure will unwrap <see cref="Task{TResult}"/> or
/// other task-like types.
/// </remarks>
Type GetResultDataType(Type returnType);
/// <summary>
/// Converts the result of an action to an <see cref="IActionResult"/> for response processing.
/// This method will be not be called when a method returns <c>void</c> or an
/// <see cref="IActionResult"/> value.
/// </summary>
/// <param name="value">The action return value. May be <c>null</c>.</param>
/// <param name="returnType">The declared return type.</param>
/// <returns>An <see cref="IActionResult"/> for response processing.</returns>
/// <remarks>
/// Prior to calling this method, the infrastructure will unwrap <see cref="Task{TResult}"/> or
/// other task-like types.
/// </remarks>
IActionResult Convert(object value, Type returnType);
}
}

View File

@ -27,7 +27,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
new AwaitableObjectResultExecutor(),
};
public abstract ValueTask<IActionResult> Execute(ObjectMethodExecutor executor, object controller, object[] arguments);
public abstract ValueTask<IActionResult> Execute(
IActionResultTypeMapper mapper,
ObjectMethodExecutor executor,
object controller,
object[] arguments);
protected abstract bool CanExecute(ObjectMethodExecutor executor);
@ -48,7 +52,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// void LogMessage(..)
private class VoidResultExecutor : ActionMethodExecutor
{
public override ValueTask<IActionResult> Execute(ObjectMethodExecutor executor, object controller, object[] arguments)
public override ValueTask<IActionResult> Execute(
IActionResultTypeMapper mapper,
ObjectMethodExecutor executor,
object controller,
object[] arguments)
{
executor.Execute(controller, arguments);
return new ValueTask<IActionResult>(new EmptyResult());
@ -62,7 +70,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// CreatedAtResult Put(..)
private class SyncActionResultExecutor : ActionMethodExecutor
{
public override ValueTask<IActionResult> Execute(ObjectMethodExecutor executor, object controller, object[] arguments)
public override ValueTask<IActionResult> Execute(
IActionResultTypeMapper mapper,
ObjectMethodExecutor executor,
object controller,
object[] arguments)
{
var actionResult = (IActionResult)executor.Execute(controller, arguments);
EnsureActionResultNotNull(executor, actionResult);
@ -78,11 +90,15 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// object Index(..)
private class SyncObjectResultExecutor : ActionMethodExecutor
{
public override ValueTask<IActionResult> Execute(ObjectMethodExecutor executor, object controller, object[] arguments)
public override ValueTask<IActionResult> Execute(
IActionResultTypeMapper mapper,
ObjectMethodExecutor executor,
object controller,
object[] arguments)
{
// Sync method returning arbitrary object
var returnValue = executor.Execute(controller, arguments);
var actionResult = ConvertToActionResult(returnValue, executor.MethodReturnType);
var actionResult = ConvertToActionResult(mapper, returnValue, executor.MethodReturnType);
return new ValueTask<IActionResult>(actionResult);
}
@ -93,7 +109,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// Task SaveState(..)
private class TaskResultExecutor : ActionMethodExecutor
{
public override async ValueTask<IActionResult> Execute(ObjectMethodExecutor executor, object controller, object[] arguments)
public override async ValueTask<IActionResult> Execute(
IActionResultTypeMapper mapper,
ObjectMethodExecutor executor,
object controller,
object[] arguments)
{
await (Task)executor.Execute(controller, arguments);
return new EmptyResult();
@ -106,7 +126,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// Custom task-like type with no return value.
private class AwaitableResultExecutor : ActionMethodExecutor
{
public override async ValueTask<IActionResult> Execute(ObjectMethodExecutor executor, object controller, object[] arguments)
public override async ValueTask<IActionResult> Execute(
IActionResultTypeMapper mapper,
ObjectMethodExecutor executor,
object controller,
object[] arguments)
{
await executor.ExecuteAsync(controller, arguments);
return new EmptyResult();
@ -122,7 +146,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// Task<IActionResult> Post(..)
private class TaskOfIActionResultExecutor : ActionMethodExecutor
{
public override async ValueTask<IActionResult> Execute(ObjectMethodExecutor executor, object controller, object[] arguments)
public override async ValueTask<IActionResult> Execute(
IActionResultTypeMapper mapper,
ObjectMethodExecutor executor,
object controller,
object[] arguments)
{
// Async method returning Task<IActionResult>
// Avoid extra allocations by calling Execute rather than ExecuteAsync and casting to Task<IActionResult>.
@ -141,7 +169,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// ValueTask<ViewResult> GetViewsAsync(..)
private class TaskOfActionResultExecutor : ActionMethodExecutor
{
public override async ValueTask<IActionResult> Execute(ObjectMethodExecutor executor, object controller, object[] arguments)
public override async ValueTask<IActionResult> Execute(
IActionResultTypeMapper mapper,
ObjectMethodExecutor executor,
object controller,
object[] arguments)
{
// Async method returning awaitable-of-IActionResult (e.g., Task<ViewResult>)
// We have to use ExecuteAsync because we don't know the awaitable's type at compile time.
@ -161,11 +193,15 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// Task<Customer> GetCustomerAsync(..)
private class AwaitableObjectResultExecutor : ActionMethodExecutor
{
public override async ValueTask<IActionResult> Execute(ObjectMethodExecutor executor, object controller, object[] arguments)
public override async ValueTask<IActionResult> Execute(
IActionResultTypeMapper mapper,
ObjectMethodExecutor executor,
object controller,
object[] arguments)
{
// Async method returning awaitable-of-nonvoid
var returnValue = await executor.ExecuteAsync(controller, arguments);
var actionResult = ConvertToActionResult(returnValue, executor.MethodReturnType);
var actionResult = ConvertToActionResult(mapper, returnValue, executor.MethodReturnType);
return actionResult;
}
@ -176,30 +212,14 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{
if (actionResult == null)
{
throw new InvalidOperationException(
Resources.FormatActionResult_ActionReturnValueCannotBeNull(executor.AsyncResultType ?? executor.MethodReturnType));
var type = executor.AsyncResultType ?? executor.MethodReturnType;
throw new InvalidOperationException(Resources.FormatActionResult_ActionReturnValueCannotBeNull(type));
}
}
private static IActionResult ConvertToActionResult(object returnValue, Type declaredType)
private IActionResult ConvertToActionResult(IActionResultTypeMapper mapper, object returnValue, Type declaredType)
{
IActionResult result;
switch (returnValue)
{
case IActionResult actionResult:
result = actionResult;
break;
case IConvertToActionResult convertToActionResult:
result = convertToActionResult.Convert();
break;
default:
result = new ObjectResult(returnValue)
{
DeclaredType = declaredType,
};
break;
}
var result = (returnValue as IActionResult) ?? mapper.Convert(returnValue, declaredType);
if (result == null)
{
throw new InvalidOperationException(Resources.FormatActionResult_ActionReturnValueCannotBeNull(declaredType));

View File

@ -0,0 +1,45 @@
// 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;
using Microsoft.AspNetCore.Mvc.Infrastructure;
namespace Microsoft.AspNetCore.Mvc.Internal
{
public class ActionResultTypeMapper : IActionResultTypeMapper
{
public Type GetResultDataType(Type returnType)
{
if (returnType == null)
{
throw new ArgumentNullException(nameof(returnType));
}
if (returnType.IsGenericType &&
returnType.GetGenericTypeDefinition() == typeof(ActionResult<>))
{
return returnType.GetGenericArguments()[0];
}
return returnType;
}
public IActionResult Convert(object value, Type returnType)
{
if (returnType == null)
{
throw new ArgumentNullException(nameof(returnType));
}
if (value is IConvertToActionResult converter)
{
return converter.Convert();
}
return new ObjectResult(value)
{
DeclaredType = returnType,
};
}
}
}

View File

@ -28,10 +28,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
internal ControllerActionInvoker(
ILogger logger,
DiagnosticSource diagnosticSource,
IActionResultTypeMapper mapper,
ControllerContext controllerContext,
ControllerActionInvokerCacheEntry cacheEntry,
IFilterMetadata[] filters)
: base(diagnosticSource, logger, controllerContext, filters, controllerContext.ValueProviderFactories)
: base(diagnosticSource, logger, mapper, controllerContext, filters, controllerContext.ValueProviderFactories)
{
if (cacheEntry == null)
{
@ -347,7 +348,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
controller);
logger.ActionMethodExecuting(controllerContext, orderedArguments);
var stopwatch = ValueStopwatch.StartNew();
var actionResultValueTask = actionMethodExecutor.Execute(objectMethodExecutor, controller, orderedArguments);
var actionResultValueTask = actionMethodExecutor.Execute(_mapper, objectMethodExecutor, controller, orderedArguments);
if (actionResultValueTask.IsCompletedSuccessfully)
{
result = actionResultValueTask.Result;
@ -370,18 +371,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
}
}
private static bool IsResultIActionResult(ObjectMethodExecutor executor)
{
var resultType = executor.AsyncResultType ?? executor.MethodReturnType;
return typeof(IActionResult).IsAssignableFrom(resultType);
}
private bool IsConvertibleToActionResult(ObjectMethodExecutor executor)
{
var resultType = executor.AsyncResultType ?? executor.MethodReturnType;
return typeof(IConvertToActionResult).IsAssignableFrom(resultType);
}
/// <remarks><see cref="ResourceInvoker.InvokeFilterPipelineAsync"/> for details on what the
/// variables in this method represent.</remarks>
protected override async Task InvokeInnerFilterAsync()

View File

@ -7,6 +7,7 @@ using System.Diagnostics;
using System.Linq;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@ -20,18 +21,21 @@ namespace Microsoft.AspNetCore.Mvc.Internal
private readonly int _maxModelValidationErrors;
private readonly ILogger _logger;
private readonly DiagnosticSource _diagnosticSource;
private readonly IActionResultTypeMapper _mapper;
public ControllerActionInvokerProvider(
ControllerActionInvokerCache controllerActionInvokerCache,
IOptions<MvcOptions> optionsAccessor,
ILoggerFactory loggerFactory,
DiagnosticSource diagnosticSource)
DiagnosticSource diagnosticSource,
IActionResultTypeMapper mapper)
{
_controllerActionInvokerCache = controllerActionInvokerCache;
_valueProviderFactories = optionsAccessor.Value.ValueProviderFactories.ToArray();
_maxModelValidationErrors = optionsAccessor.Value.MaxModelValidationErrors;
_logger = loggerFactory.CreateLogger<ControllerActionInvoker>();
_diagnosticSource = diagnosticSource;
_mapper = mapper;
}
public int Order => -1000;
@ -56,6 +60,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var invoker = new ControllerActionInvoker(
_logger,
_diagnosticSource,
_mapper,
controllerContext,
cacheResult.cacheEntry,
cacheResult.filters);

View File

@ -8,6 +8,7 @@ using System.Runtime.ExceptionServices;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging;
@ -18,6 +19,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{
protected readonly DiagnosticSource _diagnosticSource;
protected readonly ILogger _logger;
protected readonly IActionResultTypeMapper _mapper;
protected readonly ActionContext _actionContext;
protected readonly IFilterMetadata[] _filters;
protected readonly IList<IValueProviderFactory> _valueProviderFactories;
@ -38,12 +40,14 @@ namespace Microsoft.AspNetCore.Mvc.Internal
public ResourceInvoker(
DiagnosticSource diagnosticSource,
ILogger logger,
IActionResultTypeMapper mapper,
ActionContext actionContext,
IFilterMetadata[] filters,
IList<IValueProviderFactory> valueProviderFactories)
{
_diagnosticSource = diagnosticSource ?? throw new ArgumentNullException(nameof(diagnosticSource));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
_actionContext = actionContext ?? throw new ArgumentNullException(nameof(actionContext));
_filters = filters ?? throw new ArgumentNullException(nameof(filters));

View File

@ -9,6 +9,7 @@ using System.Runtime.ExceptionServices;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
@ -43,6 +44,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
IPageHandlerMethodSelector handlerMethodSelector,
DiagnosticSource diagnosticSource,
ILogger logger,
IActionResultTypeMapper mapper,
PageContext pageContext,
IFilterMetadata[] filterMetadata,
PageActionInvokerCacheEntry cacheEntry,
@ -52,6 +54,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
: base(
diagnosticSource,
logger,
mapper,
pageContext,
filterMetadata,
pageContext.ValueProviderFactories)

View File

@ -42,6 +42,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
private readonly RazorProjectFileSystem _razorFileSystem;
private readonly DiagnosticSource _diagnosticSource;
private readonly ILogger<PageActionInvoker> _logger;
private readonly IActionResultTypeMapper _mapper;
private volatile InnerCache _currentCache;
public PageActionInvokerProvider(
@ -60,7 +61,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
IPageHandlerMethodSelector selector,
RazorProjectFileSystem razorFileSystem,
DiagnosticSource diagnosticSource,
ILoggerFactory loggerFactory)
ILoggerFactory loggerFactory,
IActionResultTypeMapper mapper)
{
_loader = loader;
_pageFactoryProvider = pageFactoryProvider;
@ -79,6 +81,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
_razorFileSystem = razorFileSystem;
_diagnosticSource = diagnosticSource;
_logger = loggerFactory.CreateLogger<PageActionInvoker>();
_mapper = mapper;
}
public int Order { get; } = -1000;
@ -158,6 +161,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
_selector,
_diagnosticSource,
_logger,
_mapper,
pageContext,
filters,
cacheEntry,

View File

@ -1449,7 +1449,8 @@ namespace Microsoft.AspNetCore.Mvc.Description
var provider = new DefaultApiDescriptionProvider(
optionsAccessor,
constraintResolver.Object,
modelMetadataProvider);
modelMetadataProvider,
new ActionResultTypeMapper());
provider.OnProvidersExecuting(context);
provider.OnProvidersExecuted(context);

View File

@ -17,12 +17,13 @@ namespace Microsoft.AspNetCore.Mvc.Core.Internal
public void ActionMethodExecutor_ExecutesVoidActions()
{
// Arrange
var mapper = new ActionResultTypeMapper();
var controller = new TestController();
var objectMethodExecutor = GetExecutor(nameof(TestController.VoidAction));
var actionMethodExecutor = ActionMethodExecutor.GetExecutor(objectMethodExecutor);
// Act
var valueTask = actionMethodExecutor.Execute(objectMethodExecutor, controller, Array.Empty<object>());
var valueTask = actionMethodExecutor.Execute(mapper, objectMethodExecutor, controller, Array.Empty<object>());
// Assert
Assert.True(controller.Executed);
@ -33,12 +34,13 @@ namespace Microsoft.AspNetCore.Mvc.Core.Internal
public void ActionMethodExecutor_ExecutesActionsReturningIActionResult()
{
// Arrange
var mapper = new ActionResultTypeMapper();
var controller = new TestController();
var objectMethodExecutor = GetExecutor(nameof(TestController.ReturnIActionResult));
var actionMethodExecutor = ActionMethodExecutor.GetExecutor(objectMethodExecutor);
// Act
var valueTask = actionMethodExecutor.Execute(objectMethodExecutor, controller, Array.Empty<object>());
var valueTask = actionMethodExecutor.Execute(mapper, objectMethodExecutor, controller, Array.Empty<object>());
// Assert
Assert.True(valueTask.IsCompleted);
@ -49,12 +51,13 @@ namespace Microsoft.AspNetCore.Mvc.Core.Internal
public void ActionMethodExecutor_ExecutesActionsReturningSubTypeOfActionResult()
{
// Arrange
var mapper = new ActionResultTypeMapper();
var controller = new TestController();
var objectMethodExecutor = GetExecutor(nameof(TestController.ReturnsIActionResultSubType));
var actionMethodExecutor = ActionMethodExecutor.GetExecutor(objectMethodExecutor);
// Act
var valueTask = actionMethodExecutor.Execute(objectMethodExecutor, controller, Array.Empty<object>());
var valueTask = actionMethodExecutor.Execute(mapper, objectMethodExecutor, controller, Array.Empty<object>());
// Assert
Assert.IsType<PartialViewResult>(valueTask.Result);
@ -64,12 +67,13 @@ namespace Microsoft.AspNetCore.Mvc.Core.Internal
public void ActionMethodExecutor_ExecutesActionsReturningActionResultOfT()
{
// Arrange
var mapper = new ActionResultTypeMapper();
var controller = new TestController();
var objectMethodExecutor = GetExecutor(nameof(TestController.ReturnsActionResultOfT));
var actionMethodExecutor = ActionMethodExecutor.GetExecutor(objectMethodExecutor);
// Act
var valueTask = actionMethodExecutor.Execute(objectMethodExecutor, controller, Array.Empty<object>());
var valueTask = actionMethodExecutor.Execute(mapper, objectMethodExecutor, controller, Array.Empty<object>());
// Assert
var result = Assert.IsType<ObjectResult>(valueTask.Result);
@ -81,12 +85,13 @@ namespace Microsoft.AspNetCore.Mvc.Core.Internal
public void ActionMethodExecutor_ExecutesActionsReturningModelAsModel()
{
// Arrange
var mapper = new ActionResultTypeMapper();
var controller = new TestController();
var objectMethodExecutor = GetExecutor(nameof(TestController.ReturnsModelAsModel));
var actionMethodExecutor = ActionMethodExecutor.GetExecutor(objectMethodExecutor);
// Act
var valueTask = actionMethodExecutor.Execute(objectMethodExecutor, controller, Array.Empty<object>());
var valueTask = actionMethodExecutor.Execute(mapper, objectMethodExecutor, controller, Array.Empty<object>());
// Assert
var result = Assert.IsType<ObjectResult>(valueTask.Result);
@ -98,12 +103,13 @@ namespace Microsoft.AspNetCore.Mvc.Core.Internal
public void ActionMethodExecutor_ExecutesActionsReturningModelAsObject()
{
// Arrange
var mapper = new ActionResultTypeMapper();
var controller = new TestController();
var objectMethodExecutor = GetExecutor(nameof(TestController.ReturnModelAsObject));
var actionMethodExecutor = ActionMethodExecutor.GetExecutor(objectMethodExecutor);
// Act
var valueTask = actionMethodExecutor.Execute(objectMethodExecutor, controller, Array.Empty<object>());
var valueTask = actionMethodExecutor.Execute(mapper, objectMethodExecutor, controller, Array.Empty<object>());
// Assert
var result = Assert.IsType<ObjectResult>(valueTask.Result);
@ -115,12 +121,13 @@ namespace Microsoft.AspNetCore.Mvc.Core.Internal
public void ActionMethodExecutor_ExecutesActionsReturningActionResultAsObject()
{
// Arrange
var mapper = new ActionResultTypeMapper();
var controller = new TestController();
var objectMethodExecutor = GetExecutor(nameof(TestController.ReturnsIActionResultSubType));
var actionMethodExecutor = ActionMethodExecutor.GetExecutor(objectMethodExecutor);
// Act
var valueTask = actionMethodExecutor.Execute(objectMethodExecutor, controller, Array.Empty<object>());
var valueTask = actionMethodExecutor.Execute(mapper, objectMethodExecutor, controller, Array.Empty<object>());
// Assert
Assert.IsType<PartialViewResult>(valueTask.Result);
@ -130,12 +137,13 @@ namespace Microsoft.AspNetCore.Mvc.Core.Internal
public void ActionMethodExecutor_ExecutesActionsReturnTask()
{
// Arrange
var mapper = new ActionResultTypeMapper();
var controller = new TestController();
var objectMethodExecutor = GetExecutor(nameof(TestController.ReturnsTask));
var actionMethodExecutor = ActionMethodExecutor.GetExecutor(objectMethodExecutor);
// Act
var valueTask = actionMethodExecutor.Execute(objectMethodExecutor, controller, Array.Empty<object>());
var valueTask = actionMethodExecutor.Execute(mapper, objectMethodExecutor, controller, Array.Empty<object>());
// Assert
Assert.True(controller.Executed);
@ -146,12 +154,13 @@ namespace Microsoft.AspNetCore.Mvc.Core.Internal
public void ActionMethodExecutorExecutesActionsAsynchronouslyReturningIActionResult()
{
// Arrange
var mapper = new ActionResultTypeMapper();
var controller = new TestController();
var objectMethodExecutor = GetExecutor(nameof(TestController.ReturnIActionResultAsync));
var actionMethodExecutor = ActionMethodExecutor.GetExecutor(objectMethodExecutor);
// Act
var valueTask = actionMethodExecutor.Execute(objectMethodExecutor, controller, Array.Empty<object>());
var valueTask = actionMethodExecutor.Execute(mapper, objectMethodExecutor, controller, Array.Empty<object>());
// Assert
Assert.IsType<ViewResult>(valueTask.Result);
@ -161,12 +170,13 @@ namespace Microsoft.AspNetCore.Mvc.Core.Internal
public async Task ActionMethodExecutor_ExecutesActionsAsynchronouslyReturningActionResultSubType()
{
// Arrange
var mapper = new ActionResultTypeMapper();
var controller = new TestController();
var objectMethodExecutor = GetExecutor(nameof(TestController.ReturnIActionResultAsync));
var actionMethodExecutor = ActionMethodExecutor.GetExecutor(objectMethodExecutor);
// Act
var valueTask = actionMethodExecutor.Execute(objectMethodExecutor, controller, Array.Empty<object>());
var valueTask = actionMethodExecutor.Execute(mapper, objectMethodExecutor, controller, Array.Empty<object>());
// Assert
await valueTask;
@ -177,12 +187,13 @@ namespace Microsoft.AspNetCore.Mvc.Core.Internal
public void ActionMethodExecutor_ExecutesActionsAsynchronouslyReturningModel()
{
// Arrange
var mapper = new ActionResultTypeMapper();
var controller = new TestController();
var objectMethodExecutor = GetExecutor(nameof(TestController.ReturnsModelAsModelAsync));
var actionMethodExecutor = ActionMethodExecutor.GetExecutor(objectMethodExecutor);
// Act
var valueTask = actionMethodExecutor.Execute(objectMethodExecutor, controller, Array.Empty<object>());
var valueTask = actionMethodExecutor.Execute(mapper, objectMethodExecutor, controller, Array.Empty<object>());
// Assert
var result = Assert.IsType<ObjectResult>(valueTask.Result);
@ -194,12 +205,13 @@ namespace Microsoft.AspNetCore.Mvc.Core.Internal
public void ActionMethodExecutor_ExecutesActionsAsynchronouslyReturningModelAsObject()
{
// Arrange
var mapper = new ActionResultTypeMapper();
var controller = new TestController();
var objectMethodExecutor = GetExecutor(nameof(TestController.ReturnsModelAsObjectAsync));
var actionMethodExecutor = ActionMethodExecutor.GetExecutor(objectMethodExecutor);
// Act
var valueTask = actionMethodExecutor.Execute(objectMethodExecutor, controller, Array.Empty<object>());
var valueTask = actionMethodExecutor.Execute(mapper, objectMethodExecutor, controller, Array.Empty<object>());
// Assert
var result = Assert.IsType<ObjectResult>(valueTask.Result);
@ -211,12 +223,13 @@ namespace Microsoft.AspNetCore.Mvc.Core.Internal
public void ActionMethodExecutor_ExecutesActionsAsynchronouslyReturningIActionResultAsObject()
{
// Arrange
var mapper = new ActionResultTypeMapper();
var controller = new TestController();
var objectMethodExecutor = GetExecutor(nameof(TestController.ReturnIActionResultAsObjectAsync));
var actionMethodExecutor = ActionMethodExecutor.GetExecutor(objectMethodExecutor);
// Act
var valueTask = actionMethodExecutor.Execute(objectMethodExecutor, controller, Array.Empty<object>());
var valueTask = actionMethodExecutor.Execute(mapper, objectMethodExecutor, controller, Array.Empty<object>());
// Assert
Assert.IsType<OkResult>(valueTask.Result);
@ -226,12 +239,13 @@ namespace Microsoft.AspNetCore.Mvc.Core.Internal
public void ActionMethodExecutor_ExecutesActionsAsynchronouslyReturningActionResultOfT()
{
// Arrange
var mapper = new ActionResultTypeMapper();
var controller = new TestController();
var objectMethodExecutor = GetExecutor(nameof(TestController.ReturnActionResultOFTAsync));
var actionMethodExecutor = ActionMethodExecutor.GetExecutor(objectMethodExecutor);
// Act
var valueTask = actionMethodExecutor.Execute(objectMethodExecutor, controller, Array.Empty<object>());
var valueTask = actionMethodExecutor.Execute(mapper, objectMethodExecutor, controller, Array.Empty<object>());
// Assert
var result = Assert.IsType<ObjectResult>(valueTask.Result);
@ -243,13 +257,14 @@ namespace Microsoft.AspNetCore.Mvc.Core.Internal
public void ActionMethodExecutor_ThrowsIfIConvertFromIActionResult_ReturnsNull()
{
// Arrange
var mapper = new ActionResultTypeMapper();
var controller = new TestController();
var objectMethodExecutor = GetExecutor(nameof(TestController.ReturnsCustomConvertibleFromIActionResult));
var actionMethodExecutor = ActionMethodExecutor.GetExecutor(objectMethodExecutor);
// Act & Assert
var ex = Assert.Throws<InvalidOperationException>(
() => actionMethodExecutor.Execute(objectMethodExecutor, controller, Array.Empty<object>()));
() => actionMethodExecutor.Execute(mapper, objectMethodExecutor, controller, Array.Empty<object>()));
Assert.Equal($"Cannot return null from an action method with a return type of '{typeof(CustomConvertibleFromAction)}'.", ex.Message);
}

View File

@ -0,0 +1,75 @@
// 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 Microsoft.AspNetCore.Mvc.Infrastructure;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Internal
{
public class ActionResultTypeMapperTest
{
[Fact]
public void Convert_WithIConvertToActionResult_DelegatesToInterface()
{
// Arrange
var mapper = new ActionResultTypeMapper();
var expected = new EmptyResult();
var returnValue = Mock.Of<IConvertToActionResult>(r => r.Convert() == expected);
// Act
var result = mapper.Convert(returnValue, typeof(string));
// Assert
Assert.Same(expected, result);
}
[Fact]
public void Convert_WithRegularType_CreatesObjectResult()
{
// Arrange
var mapper = new ActionResultTypeMapper();
var returnValue = "hello";
// Act
var result = mapper.Convert(returnValue, typeof(string));
// Assert
var objectResult = Assert.IsType<ObjectResult>(result);
Assert.Same(returnValue, objectResult.Value);
Assert.Equal(typeof(string), objectResult.DeclaredType);
}
[Fact]
public void GetResultDataType_WithActionResultOfT_UnwrapsType()
{
// Arrange
var mapper = new ActionResultTypeMapper();
var returnType = typeof(ActionResult<string>);
// Act
var result = mapper.GetResultDataType(returnType);
// Assert
Assert.Equal(typeof(string), result);
}
[Fact]
public void GetResultDataType_WithRegularType_ReturnsType()
{
// Arrange
var mapper = new ActionResultTypeMapper();
var returnType = typeof(string);
// Act
var result = mapper.GetResultDataType(returnType);
// Assert
Assert.Equal(typeof(string), result);
}
}
}

View File

@ -1345,6 +1345,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var invoker = new ControllerActionInvoker(
new NullLoggerFactory().CreateLogger<ControllerActionInvoker>(),
new DiagnosticListener("Microsoft.AspNetCore"),
new ActionResultTypeMapper(),
controllerContext,
cacheEntry,
new IFilterMetadata[0]);
@ -1624,6 +1625,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var invoker = new ControllerActionInvoker(
logger,
diagnosticSource,
new ActionResultTypeMapper(),
controllerContext,
cacheEntry,
filters);

View File

@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Routing;
@ -285,6 +286,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
new MockControllerFactory(controller ?? this),
new NullLoggerFactory().CreateLogger<ControllerActionInvoker>(),
diagnosticSource,
new ActionResultTypeMapper(),
actionContext,
new List<IValueProviderFactory>(),
maxAllowedErrorsInModelState: 200);
@ -389,12 +391,14 @@ namespace Microsoft.AspNetCore.Mvc.Internal
MockControllerFactory controllerFactory,
ILogger logger,
DiagnosticSource diagnosticSource,
IActionResultTypeMapper mapper,
ActionContext actionContext,
IReadOnlyList<IValueProviderFactory> valueProviderFactories,
int maxAllowedErrorsInModelState)
: base(
logger,
diagnosticSource,
mapper,
CreatControllerContext(actionContext, valueProviderFactories, maxAllowedErrorsInModelState),
CreateCacheEntry((ControllerActionDescriptor)actionContext.ActionDescriptor, controllerFactory),
filters)

View File

@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Mvc.Razor;
@ -515,7 +516,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
Mock.Of<IPageHandlerMethodSelector>(),
fileSystem,
new DiagnosticListener("Microsoft.AspNetCore"),
NullLoggerFactory.Instance);
NullLoggerFactory.Instance,
new ActionResultTypeMapper());
}
private IActionDescriptorCollectionProvider CreateActionDescriptorCollection(PageActionDescriptor descriptor)

View File

@ -1217,6 +1217,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal
selector.Object,
diagnosticListener ?? new DiagnosticListener("Microsoft.AspNetCore"),
logger ?? NullLogger.Instance,
new ActionResultTypeMapper(),
pageContext,
filters ?? Array.Empty<IFilterMetadata>(),
cacheEntry,