Introduce ActionResult<T>

This commit is contained in:
Pranav K 2017-08-10 19:06:44 -07:00
parent bac68ba3c2
commit 151cf44607
7 changed files with 393 additions and 14 deletions

View File

@ -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),
};
}
}
}

View File

@ -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();
}
}

View File

@ -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()

View File

@ -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
{
}
}
}

View File

@ -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 };
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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() };
}
}
}