Skip to content

Commit

Permalink
Unwrap ValueTask<T> return types (RicoSuter#4374)
Browse files Browse the repository at this point in the history
* Unwrap ValueTask<T> return types, same as Task<T>, unwrap (Value)Task<ActionResult<T>> and unify unwrapping for consistency RicoSuter#4373

* lahma suggestion for performance
  • Loading branch information
alasdaircs authored and lahma committed Jan 20, 2024
1 parent 5fcc690 commit 830e055
Show file tree
Hide file tree
Showing 6 changed files with 216 additions and 24 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System;
using System.Threading.Tasks;

using Microsoft.AspNetCore.Mvc;

using NSwag.Annotations;

namespace NSwag.Generation.AspNetCore.Tests.Web.Controllers.Responses
{
[ApiController]
[Route( "api/wrappedresponse" )]
public class WrappedResponseController : Controller
{

[HttpGet( "task" )]
public async Task Task()
{
throw new NotImplementedException();
}

[HttpGet( "int" )]
public int Int()
{
throw new NotImplementedException();
}

[HttpGet( "taskofint" )]
public async Task<int> TaskOfInt()
{
throw new NotImplementedException();
}

[HttpGet( "valuetaskofint" )]
public async ValueTask<int> ValueTaskOfInt()
{
throw new NotImplementedException();
}

[HttpGet( "actionresultofint" )]
public ActionResult<int> ActionResultOfInt()
{
throw new NotImplementedException();
}

[HttpGet( "taskofactionresultofint" )]
public async Task<ActionResult<int>> TaskOfActionResultOfInt()
{
throw new NotImplementedException();
}

[HttpGet( "valuetaskofactionresultofint" )]
public async ValueTask<ActionResult<int>> ValueTaskOfActionResultOfInt()
{
throw new NotImplementedException();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System;
using System.Linq;
using System.Threading.Tasks;

using Xunit;

using NJsonSchema;

using NSwag.Generation.AspNetCore.Tests.Web.Controllers.Responses;

namespace NSwag.Generation.AspNetCore.Tests.Responses
{
public class WrappedResponseTests : AspNetCoreTestsBase
{
[Fact]
public async Task When_response_is_wrapped_in_certain_generic_result_types_then_discard_the_wrapper_type()
{
// Arrange
var settings = new AspNetCoreOpenApiDocumentGeneratorSettings();

// Act
var document = await GenerateDocumentAsync(settings, typeof(WrappedResponseController));

// Assert
OpenApiResponse GetOperationResponse(String ActionName)
=> document.Operations.Where(op => op.Operation.OperationId == $"{nameof(WrappedResponseController).Substring(0, nameof(WrappedResponseController).Length - "Controller".Length )}_{ActionName}").Single().Operation.ActualResponses.Single().Value;
JsonObjectType GetOperationResponseSchemaType( String ActionName )
=> GetOperationResponse( ActionName ).Schema.Type;
var IntType = JsonSchema.FromType<int>().Type;

Assert.Null(GetOperationResponse(nameof(WrappedResponseController.Task)).Schema);
Assert.Equal(IntType, GetOperationResponseSchemaType(nameof( WrappedResponseController.Int)));
Assert.Equal(IntType, GetOperationResponseSchemaType(nameof( WrappedResponseController.TaskOfInt)));
Assert.Equal(IntType, GetOperationResponseSchemaType(nameof( WrappedResponseController.ValueTaskOfInt)));
Assert.Equal(IntType, GetOperationResponseSchemaType(nameof( WrappedResponseController.ActionResultOfInt)));
Assert.Equal(IntType, GetOperationResponseSchemaType(nameof( WrappedResponseController.TaskOfActionResultOfInt)));
Assert.Equal(IntType, GetOperationResponseSchemaType(nameof( WrappedResponseController.ValueTaskOfActionResultOfInt)));
}
}
}
84 changes: 84 additions & 0 deletions src/NSwag.Generation.WebApi.Tests/WrappedResponseTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Results;

using CoreMvc = Microsoft.AspNetCore.Mvc;
using Microsoft.VisualStudio.TestTools.UnitTesting;

using NJsonSchema;


namespace NSwag.Generation.WebApi.Tests
{
[TestClass]
public class WrappedResponseTests
{
public class WrappedResponseController : ApiController
{
[HttpGet, Route( "task" )]
public Task Task()
{
throw new NotImplementedException();
}

[HttpGet, Route( "int" )]
public int Int()
{
throw new NotImplementedException();
}

[HttpGet, Route( "taskofint" )]
public Task<int> TaskOfInt()
{
throw new NotImplementedException();
}

[HttpGet, Route( "valuetaskofint" )]
public ValueTask<int> ValueTaskOfInt()
{
throw new NotImplementedException();
}

[HttpGet, Route( "jsonresultofint" )]
public JsonResult<int> JsonResultOfInt()
{
throw new NotImplementedException();
}

[HttpGet, Route( "actionresultofint" )]
public CoreMvc.ActionResult<int> ActionResultOfInt()
{
throw new NotImplementedException();
}

}

[TestMethod]
public async Task When_response_is_wrapped_in_certain_generic_result_types_then_discard_the_wrapper_type()
{
// Arrange
var generator = new WebApiOpenApiDocumentGenerator( new WebApiOpenApiDocumentGeneratorSettings() );

// Act
var document = await generator.GenerateForControllerAsync<WrappedResponseController>();

// Assert
OpenApiResponse GetOperationResponse( String ActionName )
=> document.Operations.Where( op => op.Operation.OperationId == $"{nameof(WrappedResponseController).Substring(0,nameof(WrappedResponseController).Length - "Controller".Length )}_{ActionName}" ).Single().Operation.ActualResponses.Single().Value;
JsonObjectType GetOperationResponseSchemaType( String ActionName )
=> GetOperationResponse( ActionName ).Schema.Type;
var IntType = JsonSchema.FromType<int>().Type;

Assert.IsNull( GetOperationResponse( nameof( WrappedResponseController.Task ) ).Schema );
Assert.AreEqual( IntType, GetOperationResponseSchemaType( nameof( WrappedResponseController.Int ) ) );
Assert.AreEqual( IntType, GetOperationResponseSchemaType( nameof( WrappedResponseController.TaskOfInt ) ) );
Assert.AreEqual( IntType, GetOperationResponseSchemaType( nameof( WrappedResponseController.ValueTaskOfInt ) ) );
Assert.AreEqual( IntType, GetOperationResponseSchemaType( nameof( WrappedResponseController.JsonResultOfInt ) ) );
Assert.AreEqual( IntType, GetOperationResponseSchemaType( nameof( WrappedResponseController.ActionResultOfInt ) ) );

}
}
}
25 changes: 25 additions & 0 deletions src/NSwag.Generation/GenericResultWrapperTypes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;
using System.Linq;

namespace NSwag.Generation
{
internal static class GenericResultWrapperTypes
{
internal static bool IsGenericWrapperType( string typeName )
=>
typeName == "Task`1" ||
typeName == "ValueTask`1" ||
typeName == "JsonResult`1" ||
typeName == "ActionResult`1"
;

internal static void RemoveGenericWrapperTypes<T>(ref T o, Func<T,string> getName, Func<T,T> unwrap)
{
// We iterate because a common signature is public async Task<ActionResult<T>> Action()
while (IsGenericWrapperType(getName(o)))
{
o = unwrap(o);
}
}
}
}
29 changes: 9 additions & 20 deletions src/NSwag.Generation/OpenApiSchemaGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,29 +51,18 @@ protected override void GenerateObject(JsonSchema schema, JsonTypeDescription ty
}
}

/// <summary>Generetes a schema directly or referenced for the requested schema type; also adds nullability if required.</summary>
/// <typeparam name="TSchemaType">The resulted schema type which may reference the actual schema.</typeparam>
/// <param name="contextualType">The type of the schema to generate.</param>
/// <param name="isNullable">Specifies whether the property, parameter or requested schema type is nullable.</param>
/// <param name="schemaResolver">The schema resolver.</param>
/// <param name="transformation">An action to transform the resulting schema (e.g. property or parameter) before the type of reference is determined (with $ref or allOf/oneOf).</param>
/// <returns>The requested schema object.</returns>
public override TSchemaType GenerateWithReferenceAndNullability<TSchemaType>(
/// <summary>Generetes a schema directly or referenced for the requested schema type; also adds nullability if required.</summary>
/// <typeparam name="TSchemaType">The resulted schema type which may reference the actual schema.</typeparam>
/// <param name="contextualType">The type of the schema to generate.</param>
/// <param name="isNullable">Specifies whether the property, parameter or requested schema type is nullable.</param>
/// <param name="schemaResolver">The schema resolver.</param>
/// <param name="transformation">An action to transform the resulting schema (e.g. property or parameter) before the type of reference is determined (with $ref or allOf/oneOf).</param>
/// <returns>The requested schema object.</returns>
public override TSchemaType GenerateWithReferenceAndNullability<TSchemaType>(
ContextualType contextualType, bool isNullable,
JsonSchemaResolver schemaResolver, Action<TSchemaType, JsonSchema> transformation = null)
{
if (contextualType.TypeName == "Task`1")
{
contextualType = contextualType.OriginalGenericArguments[0];
}
else if (contextualType.TypeName == "JsonResult`1")
{
contextualType = contextualType.OriginalGenericArguments[0];
}
else if (contextualType.TypeName == "ActionResult`1")
{
contextualType = contextualType.OriginalGenericArguments[0];
}
GenericResultWrapperTypes.RemoveGenericWrapperTypes (ref contextualType,t=>t.TypeName,t=>t.OriginalGenericArguments[0]);

if (IsFileResponse(contextualType))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,10 +268,7 @@ private void LoadDefaultSuccessResponse(ParameterInfo returnParameter, string su
returnType = typeof(void);
}

while (returnType.Name == "Task`1" || returnType.Name == "ActionResult`1")
{
returnType = returnType.GenericTypeArguments[0];
}
GenericResultWrapperTypes.RemoveGenericWrapperTypes (ref returnType,t=>t.Name,t=>t.GenericTypeArguments[0]);

if (IsVoidResponse(returnType))
{
Expand Down

0 comments on commit 830e055

Please sign in to comment.