Skip to content

Commit fb37aa2

Browse files
authored
Unwrap ValueTask<T> return types (#4374)
* Unwrap ValueTask<T> return types, same as Task<T>, unwrap (Value)Task<ActionResult<T>> and unify unwrapping for consistency #4373 * lahma suggestion for performance
1 parent 1868e99 commit fb37aa2

File tree

6 files changed

+216
-24
lines changed

6 files changed

+216
-24
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
4+
using Microsoft.AspNetCore.Mvc;
5+
6+
using NSwag.Annotations;
7+
8+
namespace NSwag.Generation.AspNetCore.Tests.Web.Controllers.Responses
9+
{
10+
[ApiController]
11+
[Route( "api/wrappedresponse" )]
12+
public class WrappedResponseController : Controller
13+
{
14+
15+
[HttpGet( "task" )]
16+
public async Task Task()
17+
{
18+
throw new NotImplementedException();
19+
}
20+
21+
[HttpGet( "int" )]
22+
public int Int()
23+
{
24+
throw new NotImplementedException();
25+
}
26+
27+
[HttpGet( "taskofint" )]
28+
public async Task<int> TaskOfInt()
29+
{
30+
throw new NotImplementedException();
31+
}
32+
33+
[HttpGet( "valuetaskofint" )]
34+
public async ValueTask<int> ValueTaskOfInt()
35+
{
36+
throw new NotImplementedException();
37+
}
38+
39+
[HttpGet( "actionresultofint" )]
40+
public ActionResult<int> ActionResultOfInt()
41+
{
42+
throw new NotImplementedException();
43+
}
44+
45+
[HttpGet( "taskofactionresultofint" )]
46+
public async Task<ActionResult<int>> TaskOfActionResultOfInt()
47+
{
48+
throw new NotImplementedException();
49+
}
50+
51+
[HttpGet( "valuetaskofactionresultofint" )]
52+
public async ValueTask<ActionResult<int>> ValueTaskOfActionResultOfInt()
53+
{
54+
throw new NotImplementedException();
55+
}
56+
}
57+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using System;
2+
using System.Linq;
3+
using System.Threading.Tasks;
4+
5+
using Xunit;
6+
7+
using NJsonSchema;
8+
9+
using NSwag.Generation.AspNetCore.Tests.Web.Controllers.Responses;
10+
11+
namespace NSwag.Generation.AspNetCore.Tests.Responses
12+
{
13+
public class WrappedResponseTests : AspNetCoreTestsBase
14+
{
15+
[Fact]
16+
public async Task When_response_is_wrapped_in_certain_generic_result_types_then_discard_the_wrapper_type()
17+
{
18+
// Arrange
19+
var settings = new AspNetCoreOpenApiDocumentGeneratorSettings();
20+
21+
// Act
22+
var document = await GenerateDocumentAsync(settings, typeof(WrappedResponseController));
23+
24+
// Assert
25+
OpenApiResponse GetOperationResponse(String ActionName)
26+
=> document.Operations.Where(op => op.Operation.OperationId == $"{nameof(WrappedResponseController).Substring(0, nameof(WrappedResponseController).Length - "Controller".Length )}_{ActionName}").Single().Operation.ActualResponses.Single().Value;
27+
JsonObjectType GetOperationResponseSchemaType( String ActionName )
28+
=> GetOperationResponse( ActionName ).Schema.Type;
29+
var IntType = JsonSchema.FromType<int>().Type;
30+
31+
Assert.Null(GetOperationResponse(nameof(WrappedResponseController.Task)).Schema);
32+
Assert.Equal(IntType, GetOperationResponseSchemaType(nameof( WrappedResponseController.Int)));
33+
Assert.Equal(IntType, GetOperationResponseSchemaType(nameof( WrappedResponseController.TaskOfInt)));
34+
Assert.Equal(IntType, GetOperationResponseSchemaType(nameof( WrappedResponseController.ValueTaskOfInt)));
35+
Assert.Equal(IntType, GetOperationResponseSchemaType(nameof( WrappedResponseController.ActionResultOfInt)));
36+
Assert.Equal(IntType, GetOperationResponseSchemaType(nameof( WrappedResponseController.TaskOfActionResultOfInt)));
37+
Assert.Equal(IntType, GetOperationResponseSchemaType(nameof( WrappedResponseController.ValueTaskOfActionResultOfInt)));
38+
}
39+
}
40+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
using System.Web.Http;
6+
using System.Web.Http.Results;
7+
8+
using CoreMvc = Microsoft.AspNetCore.Mvc;
9+
using Microsoft.VisualStudio.TestTools.UnitTesting;
10+
11+
using NJsonSchema;
12+
13+
14+
namespace NSwag.Generation.WebApi.Tests
15+
{
16+
[TestClass]
17+
public class WrappedResponseTests
18+
{
19+
public class WrappedResponseController : ApiController
20+
{
21+
[HttpGet, Route( "task" )]
22+
public Task Task()
23+
{
24+
throw new NotImplementedException();
25+
}
26+
27+
[HttpGet, Route( "int" )]
28+
public int Int()
29+
{
30+
throw new NotImplementedException();
31+
}
32+
33+
[HttpGet, Route( "taskofint" )]
34+
public Task<int> TaskOfInt()
35+
{
36+
throw new NotImplementedException();
37+
}
38+
39+
[HttpGet, Route( "valuetaskofint" )]
40+
public ValueTask<int> ValueTaskOfInt()
41+
{
42+
throw new NotImplementedException();
43+
}
44+
45+
[HttpGet, Route( "jsonresultofint" )]
46+
public JsonResult<int> JsonResultOfInt()
47+
{
48+
throw new NotImplementedException();
49+
}
50+
51+
[HttpGet, Route( "actionresultofint" )]
52+
public CoreMvc.ActionResult<int> ActionResultOfInt()
53+
{
54+
throw new NotImplementedException();
55+
}
56+
57+
}
58+
59+
[TestMethod]
60+
public async Task When_response_is_wrapped_in_certain_generic_result_types_then_discard_the_wrapper_type()
61+
{
62+
// Arrange
63+
var generator = new WebApiOpenApiDocumentGenerator( new WebApiOpenApiDocumentGeneratorSettings() );
64+
65+
// Act
66+
var document = await generator.GenerateForControllerAsync<WrappedResponseController>();
67+
68+
// Assert
69+
OpenApiResponse GetOperationResponse( String ActionName )
70+
=> document.Operations.Where( op => op.Operation.OperationId == $"{nameof(WrappedResponseController).Substring(0,nameof(WrappedResponseController).Length - "Controller".Length )}_{ActionName}" ).Single().Operation.ActualResponses.Single().Value;
71+
JsonObjectType GetOperationResponseSchemaType( String ActionName )
72+
=> GetOperationResponse( ActionName ).Schema.Type;
73+
var IntType = JsonSchema.FromType<int>().Type;
74+
75+
Assert.IsNull( GetOperationResponse( nameof( WrappedResponseController.Task ) ).Schema );
76+
Assert.AreEqual( IntType, GetOperationResponseSchemaType( nameof( WrappedResponseController.Int ) ) );
77+
Assert.AreEqual( IntType, GetOperationResponseSchemaType( nameof( WrappedResponseController.TaskOfInt ) ) );
78+
Assert.AreEqual( IntType, GetOperationResponseSchemaType( nameof( WrappedResponseController.ValueTaskOfInt ) ) );
79+
Assert.AreEqual( IntType, GetOperationResponseSchemaType( nameof( WrappedResponseController.JsonResultOfInt ) ) );
80+
Assert.AreEqual( IntType, GetOperationResponseSchemaType( nameof( WrappedResponseController.ActionResultOfInt ) ) );
81+
82+
}
83+
}
84+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System;
2+
using System.Linq;
3+
4+
namespace NSwag.Generation
5+
{
6+
internal static class GenericResultWrapperTypes
7+
{
8+
internal static bool IsGenericWrapperType( string typeName )
9+
=>
10+
typeName == "Task`1" ||
11+
typeName == "ValueTask`1" ||
12+
typeName == "JsonResult`1" ||
13+
typeName == "ActionResult`1"
14+
;
15+
16+
internal static void RemoveGenericWrapperTypes<T>(ref T o, Func<T,string> getName, Func<T,T> unwrap)
17+
{
18+
// We iterate because a common signature is public async Task<ActionResult<T>> Action()
19+
while (IsGenericWrapperType(getName(o)))
20+
{
21+
o = unwrap(o);
22+
}
23+
}
24+
}
25+
}

src/NSwag.Generation/OpenApiSchemaGenerator.cs

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -51,29 +51,18 @@ protected override void GenerateObject(JsonSchema schema, JsonTypeDescription ty
5151
}
5252
}
5353

54-
/// <summary>Generetes a schema directly or referenced for the requested schema type; also adds nullability if required.</summary>
55-
/// <typeparam name="TSchemaType">The resulted schema type which may reference the actual schema.</typeparam>
56-
/// <param name="contextualType">The type of the schema to generate.</param>
57-
/// <param name="isNullable">Specifies whether the property, parameter or requested schema type is nullable.</param>
58-
/// <param name="schemaResolver">The schema resolver.</param>
59-
/// <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>
60-
/// <returns>The requested schema object.</returns>
61-
public override TSchemaType GenerateWithReferenceAndNullability<TSchemaType>(
54+
/// <summary>Generetes a schema directly or referenced for the requested schema type; also adds nullability if required.</summary>
55+
/// <typeparam name="TSchemaType">The resulted schema type which may reference the actual schema.</typeparam>
56+
/// <param name="contextualType">The type of the schema to generate.</param>
57+
/// <param name="isNullable">Specifies whether the property, parameter or requested schema type is nullable.</param>
58+
/// <param name="schemaResolver">The schema resolver.</param>
59+
/// <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>
60+
/// <returns>The requested schema object.</returns>
61+
public override TSchemaType GenerateWithReferenceAndNullability<TSchemaType>(
6262
ContextualType contextualType, bool isNullable,
6363
JsonSchemaResolver schemaResolver, Action<TSchemaType, JsonSchema> transformation = null)
6464
{
65-
if (contextualType.TypeName == "Task`1")
66-
{
67-
contextualType = contextualType.OriginalGenericArguments[0];
68-
}
69-
else if (contextualType.TypeName == "JsonResult`1")
70-
{
71-
contextualType = contextualType.OriginalGenericArguments[0];
72-
}
73-
else if (contextualType.TypeName == "ActionResult`1")
74-
{
75-
contextualType = contextualType.OriginalGenericArguments[0];
76-
}
65+
GenericResultWrapperTypes.RemoveGenericWrapperTypes (ref contextualType,t=>t.TypeName,t=>t.OriginalGenericArguments[0]);
7766

7867
if (IsFileResponse(contextualType))
7968
{

src/NSwag.Generation/Processors/OperationResponseProcessorBase.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -268,10 +268,7 @@ private void LoadDefaultSuccessResponse(ParameterInfo returnParameter, string su
268268
returnType = typeof(void);
269269
}
270270

271-
while (returnType.Name == "Task`1" || returnType.Name == "ActionResult`1")
272-
{
273-
returnType = returnType.GenericTypeArguments[0];
274-
}
271+
GenericResultWrapperTypes.RemoveGenericWrapperTypes (ref returnType,t=>t.Name,t=>t.GenericTypeArguments[0]);
275272

276273
if (IsVoidResponse(returnType))
277274
{

0 commit comments

Comments
 (0)