Browse Source

Add ContentTypes and IsRemoteStream to ReturnValueApiDescriptionModel

Refs abpframework/abp#23732
pull/25639/head
maliming 4 months ago
parent
commit
89772d2433
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 4
      framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ActionApiDescriptionModel.cs
  2. 38
      framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ReturnValueApiDescriptionModel.cs
  3. 285
      framework/test/Volo.Abp.Http.Tests/Volo/Abp/Http/Modeling/ReturnValueApiDescriptionModel_Tests.cs

4
framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ActionApiDescriptionModel.cs

@ -45,7 +45,7 @@ public class ActionApiDescriptionModel
}
public static ActionApiDescriptionModel Create([NotNull] string uniqueName, [NotNull] MethodInfo method, [NotNull] string url, string? httpMethod, [NotNull] IList<string> supportedVersions, bool? allowAnonymous = null, IList<AuthorizeDataApiDescriptionModel>? authorizeDatas = null, string? implementFrom = null)
public static ActionApiDescriptionModel Create([NotNull] string uniqueName, [NotNull] MethodInfo method, [NotNull] string url, string? httpMethod, [NotNull] IList<string> supportedVersions, bool? allowAnonymous = null, IList<AuthorizeDataApiDescriptionModel>? authorizeDatas = null, string? implementFrom = null, IList<string>? returnValueContentTypes = null)
{
Check.NotNull(uniqueName, nameof(uniqueName));
Check.NotNull(method, nameof(method));
@ -58,7 +58,7 @@ public class ActionApiDescriptionModel
Name = method.Name,
Url = url,
HttpMethod = httpMethod,
ReturnValue = ReturnValueApiDescriptionModel.Create(method.ReturnType),
ReturnValue = ReturnValueApiDescriptionModel.Create(method.ReturnType, returnValueContentTypes),
Parameters = new List<ParameterApiDescriptionModel>(),
ParametersOnMethod = method
.GetParameters()

38
framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ReturnValueApiDescriptionModel.cs

@ -1,4 +1,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Volo.Abp.Content;
using Volo.Abp.Reflection;
using Volo.Abp.Threading;
@ -13,19 +16,50 @@ public class ReturnValueApiDescriptionModel
public string? Summary { get; set; }
public IList<string>? ContentTypes { get; set; }
public bool IsRemoteStream { get; set; }
public ReturnValueApiDescriptionModel()
{
}
public static ReturnValueApiDescriptionModel Create(Type type)
public static ReturnValueApiDescriptionModel Create(Type type, IList<string>? contentTypes = null)
{
var unwrappedType = AsyncHelper.UnwrapTask(type);
return new ReturnValueApiDescriptionModel
{
Type = TypeHelper.GetFullNameHandlingNullableAndGenerics(unwrappedType),
TypeSimple = ApiTypeNameHelper.GetSimpleTypeName(unwrappedType)
TypeSimple = ApiTypeNameHelper.GetSimpleTypeName(unwrappedType),
ContentTypes = contentTypes,
IsRemoteStream = IsRemoteStreamType(unwrappedType)
};
}
private static bool IsRemoteStreamType(Type type)
{
if (typeof(IRemoteStreamContent).IsAssignableFrom(type))
{
return true;
}
if (type.IsArray && type.GetElementType() is { } elementType &&
typeof(IRemoteStreamContent).IsAssignableFrom(elementType))
{
return true;
}
if (typeof(IEnumerable).IsAssignableFrom(type) && type.IsGenericType)
{
var genericArg = type.GetGenericArguments()[0];
if (typeof(IRemoteStreamContent).IsAssignableFrom(genericArg))
{
return true;
}
}
return false;
}
}

285
framework/test/Volo.Abp.Http.Tests/Volo/Abp/Http/Modeling/ReturnValueApiDescriptionModel_Tests.cs

@ -0,0 +1,285 @@
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Shouldly;
using Volo.Abp.Content;
using Volo.Abp.Http.Modeling;
using Xunit;
namespace Volo.Abp.Http.Modeling;
public class ReturnValueApiDescriptionModel_Tests
{
[Fact]
public void Create_Without_ContentTypes_Should_Leave_Property_Null()
{
var model = ReturnValueApiDescriptionModel.Create(typeof(string));
model.ShouldNotBeNull();
model.TypeSimple.ShouldBe("string");
model.ContentTypes.ShouldBeNull();
}
[Fact]
public void Create_With_ContentTypes_Should_Populate_The_Property()
{
var model = ReturnValueApiDescriptionModel.Create(
typeof(string),
new[] { "application/json", "text/plain" });
model.ContentTypes.ShouldNotBeNull();
model.ContentTypes!.ShouldBe(new[] { "application/json", "text/plain" });
}
}
public class ReturnValueApiDescriptionModel_IsRemoteStream_Tests
{
[Fact]
public void Direct_IRemoteStreamContent_Should_Be_True()
{
var model = ReturnValueApiDescriptionModel.Create(typeof(IRemoteStreamContent));
model.IsRemoteStream.ShouldBeTrue();
}
[Fact]
public void Concrete_RemoteStreamContent_Should_Be_True()
{
var model = ReturnValueApiDescriptionModel.Create(typeof(RemoteStreamContent));
model.IsRemoteStream.ShouldBeTrue();
}
[Fact]
public void Custom_Subclass_Of_IRemoteStreamContent_Should_Be_True()
{
var model = ReturnValueApiDescriptionModel.Create(typeof(MyCustomStreamContent));
model.IsRemoteStream.ShouldBeTrue();
}
[Fact]
public void Task_Of_IRemoteStreamContent_Should_Be_True_After_UnwrapTask()
{
var model = ReturnValueApiDescriptionModel.Create(typeof(Task<IRemoteStreamContent>));
model.IsRemoteStream.ShouldBeTrue();
}
[Fact]
public void Task_Of_Custom_Stream_Subclass_Should_Be_True_After_UnwrapTask()
{
var model = ReturnValueApiDescriptionModel.Create(typeof(Task<MyCustomStreamContent>));
model.IsRemoteStream.ShouldBeTrue();
}
[Fact]
public void IRemoteStreamContent_Array_Should_Be_True()
{
var model = ReturnValueApiDescriptionModel.Create(typeof(IRemoteStreamContent[]));
model.IsRemoteStream.ShouldBeTrue();
}
[Fact]
public void Concrete_RemoteStreamContent_Array_Should_Be_True()
{
var model = ReturnValueApiDescriptionModel.Create(typeof(RemoteStreamContent[]));
model.IsRemoteStream.ShouldBeTrue();
}
[Fact]
public void List_Of_IRemoteStreamContent_Should_Be_True()
{
var model = ReturnValueApiDescriptionModel.Create(typeof(List<IRemoteStreamContent>));
model.IsRemoteStream.ShouldBeTrue();
}
[Fact]
public void IEnumerable_Of_IRemoteStreamContent_Should_Be_True()
{
var model = ReturnValueApiDescriptionModel.Create(typeof(IEnumerable<IRemoteStreamContent>));
model.IsRemoteStream.ShouldBeTrue();
}
[Fact]
public void IReadOnlyCollection_Of_IRemoteStreamContent_Should_Be_True()
{
var model = ReturnValueApiDescriptionModel.Create(typeof(IReadOnlyCollection<IRemoteStreamContent>));
model.IsRemoteStream.ShouldBeTrue();
}
[Fact]
public void Plain_String_Should_Be_False()
{
var model = ReturnValueApiDescriptionModel.Create(typeof(string));
model.IsRemoteStream.ShouldBeFalse();
}
[Fact]
public void Int_Should_Be_False()
{
var model = ReturnValueApiDescriptionModel.Create(typeof(int));
model.IsRemoteStream.ShouldBeFalse();
}
[Fact]
public void Plain_Dto_Should_Be_False()
{
var model = ReturnValueApiDescriptionModel.Create(typeof(PlainDto));
model.IsRemoteStream.ShouldBeFalse();
}
[Fact]
public void Dto_Containing_IRemoteStreamContent_Property_Should_Be_False()
{
// ABP design constraint: IRemoteStreamContent only works as the DIRECT endpoint return type.
// When nested inside a DTO, the server JSON-serializes the DTO and the stream metadata only —
// the binary payload is lost. So the proxy should NOT use blob mode for this case.
var model = ReturnValueApiDescriptionModel.Create(typeof(DtoWithStream));
model.IsRemoteStream.ShouldBeFalse();
}
[Fact]
public void Dto_Inheriting_From_Type_With_Stream_Should_Be_False()
{
var model = ReturnValueApiDescriptionModel.Create(typeof(DtoInheritingStream));
model.IsRemoteStream.ShouldBeFalse();
}
[Fact]
public void Byte_Array_Should_Be_False()
{
// byte[] is serialized as base64 JSON by ABP, not as binary stream.
var model = ReturnValueApiDescriptionModel.Create(typeof(byte[]));
model.IsRemoteStream.ShouldBeFalse();
}
[Fact]
public void Dictionary_Should_Be_False()
{
var model = ReturnValueApiDescriptionModel.Create(typeof(Dictionary<string, int>));
model.IsRemoteStream.ShouldBeFalse();
}
private class MyCustomStreamContent : IRemoteStreamContent
{
public string? FileName => null;
public string? ContentType => null;
public long? ContentLength => null;
public Stream GetStream() => Stream.Null;
public void Dispose() { }
}
private class PlainDto
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
}
private class DtoWithStream
{
public string FileName { get; set; } = string.Empty;
public IRemoteStreamContent? File { get; set; }
}
private class DtoInheritingStream : PlainDto
{
public IRemoteStreamContent? Stream { get; set; }
}
}
public class ReturnValueApiDescriptionModel_BackwardsCompat_Tests
{
[Fact]
public void Deserializing_Json_Without_ContentTypes_Field_Should_Leave_It_Null()
{
// Old backend JSON (no contentTypes field) read by new client.
var json = """
{
"type": "System.String",
"typeSimple": "string"
}
""";
var model = System.Text.Json.JsonSerializer.Deserialize<ReturnValueApiDescriptionModel>(
json,
new System.Text.Json.JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
model.ShouldNotBeNull();
model!.Type.ShouldBe("System.String");
model.TypeSimple.ShouldBe("string");
model.ContentTypes.ShouldBeNull();
}
[Fact]
public void Deserializing_Json_With_ContentTypes_Field_Should_Populate_It()
{
var json = """
{
"type": "System.String",
"typeSimple": "string",
"contentTypes": ["application/json", "text/plain"]
}
""";
var model = System.Text.Json.JsonSerializer.Deserialize<ReturnValueApiDescriptionModel>(
json,
new System.Text.Json.JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
model!.ContentTypes.ShouldNotBeNull();
model.ContentTypes!.ShouldBe(new[] { "application/json", "text/plain" });
}
[Fact]
public void Serializing_With_Null_ContentTypes_Should_Emit_Null_Or_Omit()
{
var model = ReturnValueApiDescriptionModel.Create(typeof(string));
var json = System.Text.Json.JsonSerializer.Serialize(model);
// Either "contentTypes":null or omitted — both are acceptable for old clients.
var deserialized = System.Text.Json.JsonSerializer.Deserialize<ReturnValueApiDescriptionModel>(
json,
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
deserialized!.ContentTypes.ShouldBeNull();
}
}
public class ActionApiDescriptionModel_Tests
{
[Fact]
public void Create_Should_Propagate_ReturnValueContentTypes()
{
var method = typeof(ActionApiDescriptionModel_Tests).GetMethod(nameof(SampleMethod))!;
var model = ActionApiDescriptionModel.Create(
uniqueName: "SampleMethod",
method: method,
url: "api/test/sample",
httpMethod: "GET",
supportedVersions: new[] { "1.0" },
allowAnonymous: true,
authorizeDatas: null,
implementFrom: null,
returnValueContentTypes: new[] { "application/octet-stream" });
model.ReturnValue.ContentTypes.ShouldNotBeNull();
model.ReturnValue.ContentTypes!.ShouldBe(new[] { "application/octet-stream" });
}
[Fact]
public void Create_Without_ReturnValueContentTypes_Should_Leave_Null()
{
var method = typeof(ActionApiDescriptionModel_Tests).GetMethod(nameof(SampleMethod))!;
var model = ActionApiDescriptionModel.Create(
uniqueName: "SampleMethod",
method: method,
url: "api/test/sample",
httpMethod: "GET",
supportedVersions: new[] { "1.0" });
model.ReturnValue.ContentTypes.ShouldBeNull();
}
public string SampleMethod() => string.Empty;
}
Loading…
Cancel
Save