Skip to content
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
5 changes: 3 additions & 2 deletions src/Mvc/Mvc.ApiExplorer/src/ApiResponseTypeProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -284,9 +284,10 @@ internal static void CalculateResponseFormatForType(ApiResponseType apiResponse,
unwrappedType = declaredReturnType.GetGenericArguments()[0];
}

// If the method is declared to return IActionResult or a derived class, that information
// If the method is declared to return IActionResult, IResult or a derived class, that information
// isn't valuable to the formatter.
if (typeof(IActionResult).IsAssignableFrom(unwrappedType))
if (typeof(IActionResult).IsAssignableFrom(unwrappedType) ||
typeof(IResult).IsAssignableFrom(unwrappedType))
{
return null;
}
Expand Down
19 changes: 18 additions & 1 deletion src/Mvc/Mvc.ApiExplorer/test/ApiResponseTypeProviderTest.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Reflection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Formatters;
Expand Down Expand Up @@ -749,6 +750,20 @@ public void GetApiResponseTypes_HandlesActionWithMultipleContentTypesAndProduces
});
}

[Fact]
public void GetApiResponseTypes_ReturnNoResponseTypes_IfActionWithIResultReturnType()
{
// Arrange
var actionDescriptor = GetControllerActionDescriptor(typeof(TestController), nameof(TestController.GetIResult));
var provider = new ApiResponseTypeProvider(new EmptyModelMetadataProvider(), new ActionResultTypeMapper(), new MvcOptions());

// Act
var result = provider.GetApiResponseTypes(actionDescriptor);

// Assert
Assert.False(result.Any());
}

private static ApiResponseTypeProvider GetProvider()
{
var mvcOptions = new MvcOptions
Expand Down Expand Up @@ -794,6 +809,8 @@ public class TestController
public ActionResult<DerivedModel> GetUserLocation(int a, int b) => null;

public ActionResult<DerivedModel> PutModel(string userId, DerivedModel model) => null;

public IResult GetIResult(int id) => null;
Comment thread
brunolins16 marked this conversation as resolved.
}

private class TestOutputFormatter : OutputFormatter
Expand Down
6 changes: 4 additions & 2 deletions src/Mvc/Mvc.Core/src/ActionResultOfT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ public sealed class ActionResult<TValue> : IConvertToActionResult
/// <param name="value">The value.</param>
public ActionResult(TValue value)
{
if (typeof(IActionResult).IsAssignableFrom(typeof(TValue)))
if (typeof(IActionResult).IsAssignableFrom(typeof(TValue)) ||
typeof(IResult).IsAssignableFrom(typeof(TValue)))
{
var error = Resources.FormatInvalidTypeTForActionResultOfT(typeof(TValue), "ActionResult<T>");
throw new ArgumentException(error);
Expand All @@ -36,7 +37,8 @@ public ActionResult(TValue value)
/// <param name="result">The <see cref="ActionResult"/>.</param>
public ActionResult(ActionResult result)
{
if (typeof(IActionResult).IsAssignableFrom(typeof(TValue)))
if (typeof(IActionResult).IsAssignableFrom(typeof(TValue)) ||
typeof(IResult).IsAssignableFrom(typeof(TValue)))
{
var error = Resources.FormatInvalidTypeTForActionResultOfT(typeof(TValue), "ActionResult<T>");
throw new ArgumentException(error);
Expand Down
31 changes: 31 additions & 0 deletions src/Mvc/Mvc.Core/src/HttpActionResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Http;

namespace Microsoft.AspNetCore.Mvc;

/// <summary>
/// An <see cref="ActionResult"/> that when executed will produce a response based on the <see cref="IResult"/> provided.
/// </summary>
internal sealed class HttpActionResult : ActionResult
{
/// <summary>
/// Gets the instance of the current <see cref="IResult"/>.
/// </summary>
public IResult Result { get; }

/// <summary>
/// Initializes a new instance of the <see cref="HttpActionResult"/> class with the
/// <see cref="IResult"/> provided.
/// </summary>
/// <param name="result">The <see cref="IResult"/> instance to be used during the <see cref="ExecuteResultAsync"/> invocation.</param>
public HttpActionResult(IResult result)
{
Result = result;
}

/// <inheritdoc/>
public override Task ExecuteResultAsync(ActionContext context)
=> Result.ExecuteAsync(context.HttpContext);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

#nullable enable

using Microsoft.AspNetCore.Http;

namespace Microsoft.AspNetCore.Mvc.Infrastructure;

internal class ActionResultTypeMapper : IActionResultTypeMapper
Expand Down Expand Up @@ -35,6 +37,11 @@ public IActionResult Convert(object? value, Type returnType)
return converter.Convert();
}

if (value is IResult httpResult)
{
return new HttpActionResult(httpResult);
}

return new ObjectResult(value)
{
DeclaredType = returnType,
Expand Down
17 changes: 17 additions & 0 deletions src/Mvc/Mvc.Core/test/ActionResultOfTTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@ public void Constructor_WithActionResult_ThrowsForInvalidType()
Assert.Equal($"Invalid type parameter '{typeof(FileStreamResult)}' specified for 'ActionResult<T>'.", ex.Message);
}

[Fact]
public void Constructor_WithIResult_ThrowsForInvalidType()
{
// Arrange
var result = new TestResult();

// Act & Assert
var ex = Assert.Throws<ArgumentException>(() => new ActionResult<TestResult>(value: result));
Assert.Equal($"Invalid type parameter '{typeof(TestResult)}' specified for 'ActionResult<T>'.", ex.Message);
}

[Fact]
public void Convert_ReturnsResultIfSet()
{
Expand Down Expand Up @@ -106,4 +117,10 @@ private class BaseItem
private class DerivedItem : BaseItem
{
}

private class TestResult : IResult
{
public Task ExecuteAsync(HttpContext httpContext)
=> Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Reflection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Logging.Abstractions;
Expand Down Expand Up @@ -77,6 +79,46 @@ public void OnProvidersExecuting_AppliesConventions()
Assert.Equal(BindingSource.Body, parameterModel.BindingInfo.BindingSource);
}

[Fact]
public void OnProvidersExecuting_AppliesConventionsForIResult()
{
// Arrange
var controllerModel = new ControllerModel(typeof(TestApiController).GetTypeInfo(), new[] { new ApiControllerAttribute() })
{
Selectors = { new SelectorModel { AttributeRouteModel = new AttributeRouteModel() } },
};

var method = typeof(TestApiController).GetMethod(nameof(TestApiController.TestActionWithIResult));

var actionModel = new ActionModel(method, Array.Empty<object>())
{
Controller = controllerModel,
};
controllerModel.Actions.Add(actionModel);

var parameter = method.GetParameters()[0];
var parameterModel = new ParameterModel(parameter, Array.Empty<object>())
{
Action = actionModel,
};
actionModel.Parameters.Add(parameterModel);

var context = new ApplicationModelProviderContext(new[] { controllerModel.ControllerType });
context.Result.Controllers.Add(controllerModel);

var provider = GetProvider();

// Act
provider.OnProvidersExecuting(context);

// Assert
// Verify some of the side-effects of executing API behavior conventions.
Assert.True(actionModel.ApiExplorer.IsVisible);
Assert.NotEmpty(actionModel.Filters.OfType<ModelStateInvalidFilterFactory>());
Assert.NotEmpty(actionModel.Filters.OfType<ClientErrorResultFilterFactory>());
Assert.Equal(BindingSource.Body, parameterModel.BindingInfo.BindingSource);
}

[Fact]
public void Constructor_SetsUpConventions()
{
Expand Down Expand Up @@ -178,5 +220,6 @@ private static ApiBehaviorApplicationModelProvider GetProvider(
private class TestApiController : ControllerBase
{
public IActionResult TestAction(object value) => null;
public IResult TestActionWithIResult(object value) => null;
}
}
57 changes: 57 additions & 0 deletions src/Mvc/Mvc.Core/test/HttpActionResultTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;

namespace Microsoft.AspNetCore.Mvc;

public class HttpActionResultTests
{
[Fact]
public void HttpActionResult_InitializesWithResultsStaticMethods()
{
// Arrange & Act
var httpResult = Mock.Of<IResult>();
var result = new HttpActionResult(httpResult);

// Assert
Assert.Equal(httpResult, result.Result);
}

[Fact]
public async Task HttpActionResult_InvokesInternalHttpResult()
{
// Arrange
var httpContext = new DefaultHttpContext
{
RequestServices = CreateServices().BuildServiceProvider()
};

var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());

var httpResult = new Mock<IResult>();
httpResult.Setup(s => s.ExecuteAsync(httpContext))
.Returns(() => Task.CompletedTask)
.Verifiable();
var result = new HttpActionResult(httpResult.Object);

// Act
await result.ExecuteResultAsync(context);

// Assert
httpResult.Verify();
}

private static IServiceCollection CreateServices()
{
var services = new ServiceCollection();
services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
return services;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Http;
using Moq;

namespace Microsoft.AspNetCore.Mvc.Infrastructure;
Expand All @@ -23,6 +24,37 @@ public void Convert_WithIConvertToActionResult_DelegatesToInterface()
Assert.Same(expected, result);
}

[Fact]
public void Convert_WithIResult_DelegatesToInterface()
Comment thread
brunolins16 marked this conversation as resolved.
{
// Arrange
var mapper = new ActionResultTypeMapper();

var returnValue = Mock.Of<IResult>();

// Act
var result = mapper.Convert(returnValue, returnValue.GetType());

// Assert
var httpResult = Assert.IsType<HttpActionResult>(result);
Assert.Same(returnValue, httpResult.Result);
}

[Fact]
public void Convert_WithIConvertToActionResultAndIResult_DelegatesToInterface()
{
// Arrange
var mapper = new ActionResultTypeMapper();

var returnValue = new CustomConvertibleIResult();

// Act
var result = mapper.Convert(returnValue, returnValue.GetType());

// Assert
Assert.IsType<EmptyResult>(result);
}

[Fact]
public void Convert_WithRegularType_CreatesObjectResult()
{
Expand Down Expand Up @@ -69,4 +101,11 @@ public void GetResultDataType_WithRegularType_ReturnsType()
// Assert
Assert.Equal(typeof(string), result);
}

private class CustomConvertibleIResult : IConvertToActionResult, IResult
{
public IActionResult Convert() => new EmptyResult();

public Task ExecuteAsync(HttpContext httpContext) => throw new NotImplementedException();
}
}
45 changes: 45 additions & 0 deletions src/Mvc/test/Mvc.FunctionalTests/HttpActionResultTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using BasicWebSite.Models;

namespace Microsoft.AspNetCore.Mvc.FunctionalTests;

public class HttpActionResultTests : IClassFixture<MvcTestFixture<BasicWebSite.StartupWithSystemTextJson>>
{
public HttpActionResultTests(MvcTestFixture<BasicWebSite.StartupWithSystemTextJson> fixture)
{
Client = fixture.CreateDefaultClient();
}

public HttpClient Client { get; }

[Fact]
public async Task ActionCanReturnIResultWithContent()
{
// Arrange
var id = 1;
var url = $"/contact/{nameof(BasicWebSite.ContactApiController.ActionReturningObjectIResult)}/{id}";
var response = await Client.GetAsync(url);

// Assert
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
var result = await response.Content.ReadFromJsonAsync<Contact>();
Assert.NotNull(result);
Assert.Equal(id, result.ContactId);
}

[Fact]
public async Task ActionCanReturnIResultWithStatusCodeOnly()
{
// Arrange
var url = $"/contact/{nameof(BasicWebSite.ContactApiController.ActionReturningStatusCodeIResult)}";
var response = await Client.GetAsync(url);

// Assert
await response.AssertStatusCodeAsync(HttpStatusCode.NoContent);
}
}
Loading