Introduce ActionResult<T>
This commit is contained in:
parent
bac68ba3c2
commit
151cf44607
|
|
@ -0,0 +1,61 @@
|
|||
// 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
|
||||
{
|
||||
/// <summary>
|
||||
/// A type that wraps either an <typeparamref name="TValue"/> instance or an <see cref="ActionResult"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TValue">The type of the result.</typeparam>
|
||||
public class ActionResult<TValue> : IConvertToActionResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="ActionResult{TValue}"/> using the specified <paramref name="value"/>.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
public ActionResult(TValue value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Intializes a new instance of <see cref="ActionResult{TValue}"/> using the specified <see cref="ActionResult"/>.
|
||||
/// </summary>
|
||||
/// <param name="result">The <see cref="ActionResult"/>.</param>
|
||||
public ActionResult(ActionResult result)
|
||||
{
|
||||
Result = result ?? throw new ArgumentNullException(nameof(result));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="ActionResult"/>.
|
||||
/// </summary>
|
||||
public ActionResult Result { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value.
|
||||
/// </summary>
|
||||
public TValue Value { get; }
|
||||
|
||||
public static implicit operator ActionResult<TValue>(TValue value)
|
||||
{
|
||||
return new ActionResult<TValue>(value);
|
||||
}
|
||||
|
||||
public static implicit operator ActionResult<TValue>(ActionResult result)
|
||||
{
|
||||
return new ActionResult<TValue>(result);
|
||||
}
|
||||
|
||||
IActionResult IConvertToActionResult.Convert()
|
||||
{
|
||||
return Result ?? new ObjectResult(Value)
|
||||
{
|
||||
DeclaredType = typeof(TValue),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the contract to convert a type to an <see cref="IActionResult"/> during action invocation.
|
||||
/// </summary>
|
||||
public interface IConvertToActionResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts the current instance to an instance of <see cref="IActionResult"/>.
|
||||
/// </summary>
|
||||
/// <returns>The converted <see cref="IActionResult"/>.</returns>
|
||||
IActionResult Convert();
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ using System.Threading.Tasks;
|
|||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.Core;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.Extensions.Internal;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
|
|
@ -311,6 +312,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
|
||||
var diagnosticSource = _diagnosticSource;
|
||||
var logger = _logger;
|
||||
var returnType = executor.MethodReturnType;
|
||||
|
||||
IActionResult result = null;
|
||||
try
|
||||
|
|
@ -321,7 +323,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
controller);
|
||||
logger.ActionMethodExecuting(controllerContext, orderedArguments);
|
||||
|
||||
var returnType = executor.MethodReturnType;
|
||||
if (returnType == typeof(void))
|
||||
{
|
||||
// Sync method returning void
|
||||
|
|
@ -346,6 +347,30 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
Resources.FormatActionResult_ActionReturnValueCannotBeNull(typeof(IActionResult)));
|
||||
}
|
||||
}
|
||||
else if (IsConvertibleToActionResult(executor))
|
||||
{
|
||||
IConvertToActionResult convertToActionResult;
|
||||
if (executor.IsMethodAsync)
|
||||
{
|
||||
// Async method returning awaitable-of-ActionResult<T> (e.g., Task<ActionResult<Person>>)
|
||||
// We have to use ExecuteAsync because we don't know the awaitable's type at compile time.
|
||||
convertToActionResult = (IConvertToActionResult)await executor.ExecuteAsync(controller, orderedArguments);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Sync method returning ActionResult<T>
|
||||
convertToActionResult = (IConvertToActionResult)executor.Execute(controller, orderedArguments);
|
||||
}
|
||||
|
||||
result = convertToActionResult.Convert();
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
Resources.FormatActionResult_ActionReturnValueCannotBeNull(typeof(IConvertToActionResult)));
|
||||
}
|
||||
|
||||
}
|
||||
else if (IsResultIActionResult(executor))
|
||||
{
|
||||
if (executor.IsMethodAsync)
|
||||
|
|
@ -370,10 +395,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
{
|
||||
// Sync method returning arbitrary object
|
||||
var resultAsObject = executor.Execute(controller, orderedArguments);
|
||||
result = resultAsObject as IActionResult ?? new ObjectResult(resultAsObject)
|
||||
{
|
||||
DeclaredType = returnType,
|
||||
};
|
||||
ConvertToActionResult(resultAsObject);
|
||||
|
||||
}
|
||||
else if (executor.AsyncResultType == typeof(void))
|
||||
{
|
||||
|
|
@ -385,10 +408,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
{
|
||||
// Async method returning awaitable-of-nonvoid
|
||||
var resultAsObject = await executor.ExecuteAsync(controller, orderedArguments);
|
||||
result = resultAsObject as IActionResult ?? new ObjectResult(resultAsObject)
|
||||
{
|
||||
DeclaredType = executor.AsyncResultType,
|
||||
};
|
||||
ConvertToActionResult(resultAsObject);
|
||||
}
|
||||
|
||||
_result = result;
|
||||
|
|
@ -402,6 +422,25 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
controllerContext,
|
||||
result);
|
||||
}
|
||||
|
||||
void ConvertToActionResult(object resultAsObject)
|
||||
{
|
||||
if (resultAsObject is IActionResult actionResult)
|
||||
{
|
||||
result = actionResult;
|
||||
}
|
||||
else if (resultAsObject is IConvertToActionResult convertToActionResult)
|
||||
{
|
||||
result = convertToActionResult.Convert();
|
||||
}
|
||||
else
|
||||
{
|
||||
result = new ObjectResult(resultAsObject)
|
||||
{
|
||||
DeclaredType = returnType,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsResultIActionResult(ObjectMethodExecutor executor)
|
||||
|
|
@ -410,6 +449,12 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
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()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
// 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 Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc
|
||||
{
|
||||
public class ActionResultOfTTest
|
||||
{
|
||||
[Fact]
|
||||
public void Convert_ReturnsResultIfSet()
|
||||
{
|
||||
// Arrange
|
||||
var expected = new OkResult();
|
||||
var actionResultOfT = new ActionResult<string>(expected);
|
||||
var convertToActionResult = (IConvertToActionResult)actionResultOfT;
|
||||
|
||||
// Act
|
||||
var result = convertToActionResult.Convert();
|
||||
|
||||
// Assert
|
||||
Assert.Same(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Convert_ReturnsObjectResultWrappingValue()
|
||||
{
|
||||
// Arrange
|
||||
var value = new BaseItem();
|
||||
var actionResultOfT = new ActionResult<BaseItem>(value);
|
||||
var convertToActionResult = (IConvertToActionResult)actionResultOfT;
|
||||
|
||||
// Act
|
||||
var result = convertToActionResult.Convert();
|
||||
|
||||
// Assert
|
||||
var objectResult = Assert.IsType<ObjectResult>(result);
|
||||
Assert.Same(value, objectResult.Value);
|
||||
Assert.Equal(typeof(BaseItem), objectResult.DeclaredType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Convert_InfersDeclaredTypeFromActionResultTypeParameter()
|
||||
{
|
||||
// Arrange
|
||||
var value = new DeriviedItem();
|
||||
var actionResultOfT = new ActionResult<BaseItem>(value);
|
||||
var convertToActionResult = (IConvertToActionResult)actionResultOfT;
|
||||
|
||||
// Act
|
||||
var result = convertToActionResult.Convert();
|
||||
|
||||
// Assert
|
||||
var objectResult = Assert.IsType<ObjectResult>(result);
|
||||
Assert.Same(value, objectResult.Value);
|
||||
Assert.Equal(typeof(BaseItem), objectResult.DeclaredType);
|
||||
}
|
||||
|
||||
private class BaseItem
|
||||
{
|
||||
}
|
||||
|
||||
private class DeriviedItem : BaseItem
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
|
|
@ -15,8 +14,8 @@ using Microsoft.AspNetCore.Mvc.Abstractions;
|
|||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Testing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
|
@ -1392,6 +1391,101 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
Assert.Equal(5, context.Object.Items["Result"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAction_ConvertibleToActionResult()
|
||||
{
|
||||
// Arrange
|
||||
var inputParam = 12;
|
||||
var actionParameters = new Dictionary<string, object> { { "input", inputParam }, };
|
||||
IActionResult result = null;
|
||||
|
||||
var filter = new Mock<IActionFilter>(MockBehavior.Strict);
|
||||
filter.Setup(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>())).Verifiable();
|
||||
filter
|
||||
.Setup(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>()))
|
||||
.Callback<ActionExecutedContext>(c => result = c.Result)
|
||||
.Verifiable();
|
||||
|
||||
var invoker = CreateInvoker(new[] { filter.Object }, nameof(TestController.ActionReturningConvertibleToActionResult), actionParameters);
|
||||
|
||||
// Act
|
||||
await invoker.InvokeAsync();
|
||||
|
||||
// Assert
|
||||
var testActionResult = Assert.IsType<TestActionResult>(result);
|
||||
Assert.Equal(inputParam, testActionResult.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAction_AsyncAction_ConvertibleToActionResult()
|
||||
{
|
||||
// Arrange
|
||||
var inputParam = 13;
|
||||
var actionParameters = new Dictionary<string, object> { { "input", inputParam }, };
|
||||
IActionResult result = null;
|
||||
|
||||
var filter = new Mock<IActionFilter>(MockBehavior.Strict);
|
||||
filter.Setup(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>())).Verifiable();
|
||||
filter
|
||||
.Setup(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>()))
|
||||
.Callback<ActionExecutedContext>(c => result = c.Result)
|
||||
.Verifiable();
|
||||
|
||||
var invoker = CreateInvoker(new[] { filter.Object }, nameof(TestController.ActionReturningConvertibleToActionResultAsync), actionParameters);
|
||||
|
||||
// Act
|
||||
await invoker.InvokeAsync();
|
||||
|
||||
// Assert
|
||||
var testActionResult = Assert.IsType<TestActionResult>(result);
|
||||
Assert.Equal(inputParam, testActionResult.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAction_ConvertibleToActionResult_AsObject()
|
||||
{
|
||||
// Arrange
|
||||
var actionParameters = new Dictionary<string, object>();
|
||||
IActionResult result = null;
|
||||
|
||||
var filter = new Mock<IActionFilter>(MockBehavior.Strict);
|
||||
filter.Setup(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>())).Verifiable();
|
||||
filter
|
||||
.Setup(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>()))
|
||||
.Callback<ActionExecutedContext>(c => result = c.Result)
|
||||
.Verifiable();
|
||||
|
||||
var invoker = CreateInvoker(new[] { filter.Object }, nameof(TestController.ActionReturningConvertibleAsObject), actionParameters);
|
||||
|
||||
// Act
|
||||
await invoker.InvokeAsync();
|
||||
|
||||
// Assert
|
||||
Assert.IsType<TestActionResult>(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAction_ConvertibleToActionResult_ReturningNull_Throws()
|
||||
{
|
||||
// Arrange
|
||||
var expectedMessage = @"Cannot return null from an action method with a return type of 'Microsoft.AspNetCore.Mvc.Infrastructure.IConvertToActionResult'.";
|
||||
var actionParameters = new Dictionary<string, object>();
|
||||
IActionResult result = null;
|
||||
|
||||
var filter = new Mock<IActionFilter>(MockBehavior.Strict);
|
||||
filter.Setup(f => f.OnActionExecuting(It.IsAny<ActionExecutingContext>())).Verifiable();
|
||||
filter
|
||||
.Setup(f => f.OnActionExecuted(It.IsAny<ActionExecutedContext>()))
|
||||
.Callback<ActionExecutedContext>(c => result = c.Result)
|
||||
.Verifiable();
|
||||
|
||||
var invoker = CreateInvoker(new[] { filter.Object }, nameof(TestController.ConvertibleToActionResultReturningNull), actionParameters);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => invoker.InvokeAsync());
|
||||
Assert.Equal(expectedMessage, exception.Message);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
protected override ResourceInvoker CreateInvoker(
|
||||
|
|
@ -1710,6 +1804,22 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
return input;
|
||||
}
|
||||
|
||||
public ConvertibleToActionResult ActionReturningConvertibleToActionResult(int input)
|
||||
=> new ConvertibleToActionResult { Value = input };
|
||||
|
||||
public Task<ConvertibleToActionResult> ActionReturningConvertibleToActionResultAsync(int input)
|
||||
=> Task.FromResult(new ConvertibleToActionResult { Value = input });
|
||||
|
||||
public object ActionReturningConvertibleAsObject() => new ConvertibleToActionResult();
|
||||
|
||||
public IConvertToActionResult ConvertibleToActionResultReturningNull()
|
||||
{
|
||||
var mock = new Mock<IConvertToActionResult>();
|
||||
mock.Setup(m => m.Convert()).Returns((IActionResult)null);
|
||||
|
||||
return mock.Object;
|
||||
}
|
||||
|
||||
public class TaskDerivedType : Task
|
||||
{
|
||||
public TaskDerivedType()
|
||||
|
|
@ -1745,5 +1855,12 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
actionDescriptor.ControllerTypeInfo,
|
||||
ParameterDefaultValues.GetParameterDefaultValues(actionDescriptor.MethodInfo));
|
||||
}
|
||||
|
||||
public class ConvertibleToActionResult : IConvertToActionResult
|
||||
{
|
||||
public int Value { get; set; }
|
||||
|
||||
public IActionResult Convert() => new TestActionResult { Value = Value };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,8 @@ using System.Net.Http;
|
|||
using System.Net.Http.Headers;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
using BasicWebSite.Models;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||
|
|
@ -402,5 +401,46 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
// Assert
|
||||
Assert.Equal(expected, response.Trim());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ActionMethod_ReturningActionMethodOfT_WithBadRequest()
|
||||
{
|
||||
// Arrange
|
||||
var url = "ActionResultOfT/GetProduct";
|
||||
|
||||
// Act
|
||||
var response = await Client.GetAsync(url);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ActionMethod_ReturningActionMethodOfT()
|
||||
{
|
||||
// Arrange
|
||||
var url = "ActionResultOfT/GetProduct?productId=10";
|
||||
|
||||
// Act
|
||||
var response = await Client.GetStringAsync(url);
|
||||
|
||||
// Assert
|
||||
var result = JsonConvert.DeserializeObject<Product>(response);
|
||||
Assert.Equal(10, result.SampleInt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ActionMethod_ReturningSequenceOfObjectsWrappedInActionResultOfT()
|
||||
{
|
||||
// Arrange
|
||||
var url = "ActionResultOfT/GetProductsAsync";
|
||||
|
||||
// Act
|
||||
var response = await Client.GetStringAsync(url);
|
||||
|
||||
// Assert
|
||||
var result = JsonConvert.DeserializeObject<Product[]>(response);
|
||||
Assert.Equal(2, result.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
// 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.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using BasicWebSite.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BasicWebSite.Controllers
|
||||
{
|
||||
public class ActionResultOfTController : Controller
|
||||
{
|
||||
[HttpGet]
|
||||
public ActionResult<Product> GetProduct(int? productId)
|
||||
{
|
||||
if (productId == null)
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
return new Product { SampleInt = productId.Value, };
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IEnumerable<Product>>> GetProductsAsync()
|
||||
{
|
||||
await Task.Delay(0);
|
||||
return new[] { new Product(), new Product() };
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue