From 7b9966fa4ddfc4033e1108022bb8a47ea1cafadd Mon Sep 17 00:00:00 2001 From: Alexandru Bagu Date: Wed, 7 Oct 2020 18:14:30 +0300 Subject: [PATCH] added streaming support --- .../AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs | 3 ++ .../AspNetCoreApiDescriptionModelProvider.cs | 26 ++++++++++++++--- .../Content/InternalRemoteStreamContent.cs | 25 ++++++++++++++++ .../RemoteStreamContentInputFormatter.cs | 27 +++++++++++++++++ .../RemoteStreamContentOutputFormatter.cs | 29 +++++++++++++++++++ .../Volo/Abp/Content/IRemoteStreamContent.cs | 12 ++++++++ .../Volo/Abp/Content/RemoteStreamContent.cs | 24 +++++++++++++++ .../Content/ReferencedRemoteStreamContent.cs | 15 ++++++++++ .../DynamicHttpProxyInterceptor.cs | 28 +++++++++++++----- .../DynamicProxying/RequestPayloadBuilder.cs | 26 ++++++++++++----- 10 files changed, 197 insertions(+), 18 deletions(-) create mode 100644 framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Content/InternalRemoteStreamContent.cs create mode 100644 framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Content/RemoteStreamContentInputFormatter.cs create mode 100644 framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Content/RemoteStreamContentOutputFormatter.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Content/IRemoteStreamContent.cs create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Content/RemoteStreamContent.cs create mode 100644 framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/Content/ReferencedRemoteStreamContent.cs diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs index 88a81fa1ec..1ceb0bc837 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcModule.cs @@ -37,6 +37,7 @@ using Volo.Abp.Http.Modeling; using Volo.Abp.Localization; using Volo.Abp.Modularity; using Volo.Abp.UI; +using Volo.Abp.AspNetCore.Mvc.Content; namespace Volo.Abp.AspNetCore.Mvc { @@ -172,6 +173,8 @@ namespace Volo.Abp.AspNetCore.Mvc Configure(mvcOptions => { mvcOptions.AddAbp(context.Services); + mvcOptions.InputFormatters.Insert(0, new RemoteStreamContentInputFormatter()); + mvcOptions.OutputFormatters.Insert(0, new RemoteStreamContentOutputFormatter()); }); Configure(options => diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs index 617650d0e2..9d9ab9075d 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Reflection; using System.Text; @@ -282,10 +283,21 @@ namespace Volo.Abp.AspNetCore.Mvc return; } - var matchedMethodParamNames = ArrayMatcher.Match( - apiDescription.ParameterDescriptions.Select(p => p.Name).ToArray(), - method.GetParameters().Select(GetMethodParamName).ToArray() - ); + /* + * --- bug report --- + ArrayMatcher.Match has the following bug: + sourceArray: [ "test1", "test2", "test3" ], + destinationArray: [ "test2", "test3" ], + expectedResult: [ "test2", "test3" ], + result: [ "test1", "test1" ] + */ + /*Because of ArrayMatcher.Match bug parameters that are + * service provided by FromServicesAttribute must be excluded + * because Microsoft's API Explorer does not include them + */ + var parameterDescriptionNames = apiDescription.ParameterDescriptions.Select(p => p.Name).ToArray(); + var methodParameterNames = method.GetParameters().Where(NotServiceProvidedParam).Select(GetMethodParamName).ToArray(); + var matchedMethodParamNames = ArrayMatcher.Match(parameterDescriptionNames, methodParameterNames); for (var i = 0; i < apiDescription.ParameterDescriptions.Count; i++) { @@ -310,6 +322,12 @@ namespace Volo.Abp.AspNetCore.Mvc } } + private bool NotServiceProvidedParam(ParameterInfo parameterInfo) + { + var fromServicesAttribute = parameterInfo.GetCustomAttribute(); + return fromServicesAttribute == null; + } + public string GetMethodParamName(ParameterInfo parameterInfo) { var modelNameProvider = parameterInfo.GetCustomAttributes() diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Content/InternalRemoteStreamContent.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Content/InternalRemoteStreamContent.cs new file mode 100644 index 0000000000..aaaa849f4f --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Content/InternalRemoteStreamContent.cs @@ -0,0 +1,25 @@ +using System.IO; +using Microsoft.AspNetCore.Http; +using Volo.Abp.Content; + +namespace Volo.Abp.AspNetCore.Mvc.Content +{ + internal class InternalRemoteStreamContent : IRemoteStreamContent + { + private readonly HttpContext _httpContext; + + public InternalRemoteStreamContent(HttpContext httpContext) + { + _httpContext = httpContext; + } + + public string ContentType => _httpContext.Request.ContentType; + + public long? ContentLength => _httpContext.Request.ContentLength; + + public Stream GetStream() + { + return _httpContext.Request.Body; + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Content/RemoteStreamContentInputFormatter.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Content/RemoteStreamContentInputFormatter.cs new file mode 100644 index 0000000000..c135864bf8 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Content/RemoteStreamContentInputFormatter.cs @@ -0,0 +1,27 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Net.Http.Headers; +using Microsoft.AspNetCore.Mvc.Formatters; +using Volo.Abp.Content; + +namespace Volo.Abp.AspNetCore.Mvc.Content +{ + public class RemoteStreamContentInputFormatter : InputFormatter + { + public RemoteStreamContentInputFormatter() + { + SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("*/*")); + } + + protected override bool CanReadType(Type type) + { + return typeof(IRemoteStreamContent) == type; + } + + public override Task ReadRequestBodyAsync(InputFormatterContext context) + { + var stream = new InternalRemoteStreamContent(context.HttpContext); + return InputFormatterResult.SuccessAsync(stream); + } + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Content/RemoteStreamContentOutputFormatter.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Content/RemoteStreamContentOutputFormatter.cs new file mode 100644 index 0000000000..b3baee3bc9 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Content/RemoteStreamContentOutputFormatter.cs @@ -0,0 +1,29 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.Net.Http.Headers; +using Volo.Abp.Content; + +namespace Volo.Abp.AspNetCore.Mvc.Content +{ + public class RemoteStreamContentOutputFormatter : OutputFormatter + { + public RemoteStreamContentOutputFormatter() + { + SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("*/*")); + } + + protected override bool CanWriteType(Type type) + { + return typeof(IRemoteStreamContent).IsAssignableFrom(type); + } + + public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context) + { + var httpContext = context.HttpContext; + var remoteStream = context.Object as IRemoteStreamContent; + using (var stream = remoteStream.GetStream()) + await stream.CopyToAsync(httpContext.Response.Body); + } + } +} diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Content/IRemoteStreamContent.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Content/IRemoteStreamContent.cs new file mode 100644 index 0000000000..d8ecd8b9bf --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Content/IRemoteStreamContent.cs @@ -0,0 +1,12 @@ +using System; +using System.IO; + +namespace Volo.Abp.Content +{ + public interface IRemoteStreamContent + { + string ContentType { get; } + long? ContentLength { get; } + Stream GetStream(); + } +} diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Content/RemoteStreamContent.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Content/RemoteStreamContent.cs new file mode 100644 index 0000000000..67a4f8eb2e --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Content/RemoteStreamContent.cs @@ -0,0 +1,24 @@ +using System; +using System.IO; + +namespace Volo.Abp.Content +{ + public class RemoteStreamContent : IRemoteStreamContent + { + private readonly Stream _stream; + + public RemoteStreamContent(Stream stream) + { + _stream = stream; + } + + public virtual string ContentType { get; set; } + + public virtual long? ContentLength => _stream.Length; + + public virtual Stream GetStream() + { + return _stream; + } + } +} diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/Content/ReferencedRemoteStreamContent.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/Content/ReferencedRemoteStreamContent.cs new file mode 100644 index 0000000000..80e4176e74 --- /dev/null +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/Content/ReferencedRemoteStreamContent.cs @@ -0,0 +1,15 @@ +using System.IO; +using Volo.Abp.Content; + +namespace Volo.Abp.Http.Client.Content +{ + internal class ReferencedRemoteStreamContent : RemoteStreamContent + { + private readonly object[] references; + + public ReferencedRemoteStreamContent(Stream stream, params object[] references) : base(stream) + { + this.references = references; + } + } +} diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptor.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptor.cs index 5313457ba7..c0dde47603 100644 --- a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptor.cs +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptor.cs @@ -10,9 +10,11 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; +using Volo.Abp.Content; using Volo.Abp.DependencyInjection; using Volo.Abp.DynamicProxy; using Volo.Abp.Http.Client.Authentication; +using Volo.Abp.Http.Client.Content; using Volo.Abp.Http.Modeling; using Volo.Abp.Http.ProxyScripting.Generators; using Volo.Abp.Json; @@ -103,17 +105,27 @@ namespace Volo.Abp.Http.Client.DynamicProxying private async Task MakeRequestAndGetResultAsync(IAbpMethodInvocation invocation) { - var responseAsString = await MakeRequestAsync(invocation); + var responseContent = await MakeRequestAsync(invocation); - if (typeof(T) == typeof(string)) + if (typeof(T) == typeof(IRemoteStreamContent)) { - return (T)Convert.ChangeType(responseAsString, typeof(T)); + /*returning a class that holds a reference to response + * content just to be sure that GC does not dispose of + * it before we finish doing our work with the stream*/ + return (T)((object)new ReferencedRemoteStreamContent(await responseContent.ReadAsStreamAsync(), responseContent)); + } + else + { + var stringContent = await responseContent.ReadAsStringAsync(); + + if (typeof(T) == typeof(string)) + return (T)((object)stringContent); + return JsonSerializer.Deserialize(await responseContent.ReadAsStringAsync()); } - return JsonSerializer.Deserialize(responseAsString); } - private async Task MakeRequestAsync(IAbpMethodInvocation invocation) + private async Task MakeRequestAsync(IAbpMethodInvocation invocation) { var clientConfig = ClientOptions.HttpClientProxies.GetOrDefault(typeof(TService)) ?? throw new AbpException($"Could not get DynamicHttpClientProxyConfig for {typeof(TService).FullName}."); var remoteServiceConfig = AbpRemoteServiceOptions.RemoteServices.GetConfigurationOrDefault(clientConfig.RemoteServiceName); @@ -140,14 +152,16 @@ namespace Volo.Abp.Http.Client.DynamicProxying ) ); - var response = await client.SendAsync(requestMessage, GetCancellationToken()); + var response = await client.SendAsync(requestMessage, + HttpCompletionOption.ResponseHeadersRead /*this will buffer only the headers, the content will be used as a stream*/, + GetCancellationToken()); if (!response.IsSuccessStatusCode) { await ThrowExceptionForResponseAsync(response); } - return await response.Content.ReadAsStringAsync(); + return response.Content; } private ApiVersionInfo GetApiVersionInfo(ActionApiDescriptionModel action) diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/RequestPayloadBuilder.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/RequestPayloadBuilder.cs index afc5f9e4f0..372d6f2921 100644 --- a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/RequestPayloadBuilder.cs +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/RequestPayloadBuilder.cs @@ -3,33 +3,35 @@ using System.Linq; using System.Net.Http; using System.Text; using JetBrains.Annotations; +using Volo.Abp.Content; using Volo.Abp.Http.Modeling; using Volo.Abp.Http.ProxyScripting.Generators; using Volo.Abp.Json; +using Volo.Abp.Reflection; namespace Volo.Abp.Http.Client.DynamicProxying { public static class RequestPayloadBuilder { [CanBeNull] - public static HttpContent BuildContent(ActionApiDescriptionModel action,IReadOnlyDictionary methodArguments, IJsonSerializer jsonSerializer, ApiVersionInfo apiVersion) + public static HttpContent BuildContent(ActionApiDescriptionModel action, IReadOnlyDictionary methodArguments, IJsonSerializer jsonSerializer, ApiVersionInfo apiVersion) { var body = GenerateBody(action, methodArguments, jsonSerializer); if (body != null) { - return new StringContent(body, Encoding.UTF8, MimeTypes.Application.Json); + return body; } body = GenerateFormPostData(action, methodArguments); if (body != null) { - return new StringContent(body, Encoding.UTF8, MimeTypes.Application.XWwwFormUrlencoded); + return body; } return null; } - private static string GenerateBody(ActionApiDescriptionModel action, IReadOnlyDictionary methodArguments, IJsonSerializer jsonSerializer) + private static HttpContent GenerateBody(ActionApiDescriptionModel action, IReadOnlyDictionary methodArguments, IJsonSerializer jsonSerializer) { var parameters = action .Parameters @@ -54,10 +56,20 @@ namespace Volo.Abp.Http.Client.DynamicProxying return null; } - return jsonSerializer.Serialize(value); + if (value is IRemoteStreamContent remoteStreamContent) + { + var content = new StreamContent(remoteStreamContent.GetStream()); + content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(remoteStreamContent.ContentType); + content.Headers.ContentLength = remoteStreamContent.ContentLength; + return content; + } + else + { + return new StringContent(jsonSerializer.Serialize(value), Encoding.UTF8, MimeTypes.Application.Json); + } } - private static string GenerateFormPostData(ActionApiDescriptionModel action, IReadOnlyDictionary methodArguments) + private static HttpContent GenerateFormPostData(ActionApiDescriptionModel action, IReadOnlyDictionary methodArguments) { var parameters = action .Parameters @@ -86,7 +98,7 @@ namespace Volo.Abp.Http.Client.DynamicProxying isFirstParam = false; } - return postDataBuilder.ToString(); + return new StringContent(postDataBuilder.ToString(), Encoding.UTF8, MimeTypes.Application.XWwwFormUrlencoded); } } }