diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs index a2f1b1b523..9ff444001b 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.AspNetCore.Mvc.Auditing; +using Volo.Abp.AspNetCore.Mvc.Content; using Volo.Abp.AspNetCore.Mvc.Conventions; using Volo.Abp.AspNetCore.Mvc.ExceptionHandling; using Volo.Abp.AspNetCore.Mvc.Features; @@ -21,6 +22,13 @@ namespace Volo.Abp.AspNetCore.Mvc AddPageFilters(options); AddModelBinders(options); AddMetadataProviders(options, services); + AddFormatters(options); + } + + private static void AddFormatters(MvcOptions options) + { + options.InputFormatters.Insert(0, new RemoteStreamContentInputFormatter()); + options.OutputFormatters.Insert(0, new RemoteStreamContentOutputFormatter()); } private static void AddConventions(MvcOptions options, IServiceCollection services) 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..5cd1333f34 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 @@ -282,9 +282,20 @@ namespace Volo.Abp.AspNetCore.Mvc return; } + var parameterDescriptionNames = apiDescription + .ParameterDescriptions + .Select(p => p.Name) + .ToArray(); + + var methodParameterNames = method + .GetParameters() + .Where(IsNotFromServicesParameter) + .Select(GetMethodParamName) + .ToArray(); + var matchedMethodParamNames = ArrayMatcher.Match( - apiDescription.ParameterDescriptions.Select(p => p.Name).ToArray(), - method.GetParameters().Select(GetMethodParamName).ToArray() + parameterDescriptionNames, + methodParameterNames ); for (var i = 0; i < apiDescription.ParameterDescriptions.Count; i++) @@ -310,6 +321,11 @@ namespace Volo.Abp.AspNetCore.Mvc } } + private static bool IsNotFromServicesParameter(ParameterInfo parameterInfo) + { + return !parameterInfo.IsDefined(typeof(FromServicesAttribute), true); + } + 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..a4f5668b7d --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Content/RemoteStreamContentInputFormatter.cs @@ -0,0 +1,28 @@ +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) + { + return InputFormatterResult.SuccessAsync( + new InternalRemoteStreamContent(context.HttpContext) + ); + } + } +} 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..d3ecfaff27 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Content/RemoteStreamContentOutputFormatter.cs @@ -0,0 +1,30 @@ +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 async override Task WriteResponseBodyAsync(OutputFormatterWriteContext context) + { + var remoteStream = (IRemoteStreamContent)context.Object; + using (var stream = remoteStream.GetStream()) + { + await stream.CopyToAsync(context.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..bca259d422 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Content/IRemoteStreamContent.cs @@ -0,0 +1,13 @@ +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..f217101cea --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Content/RemoteStreamContent.cs @@ -0,0 +1,23 @@ +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..b99cec8a51 --- /dev/null +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/Content/ReferencedRemoteStreamContent.cs @@ -0,0 +1,16 @@ +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..321d538044 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,26 @@ 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(IRemoteStreamContent)) + { + /* 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)); + } + + var stringContent = await responseContent.ReadAsStringAsync(); if (typeof(T) == typeof(string)) { - return (T)Convert.ChangeType(responseAsString, typeof(T)); + return (T)(object)stringContent; } - return JsonSerializer.Deserialize(responseAsString); + return JsonSerializer.Deserialize(await responseContent.ReadAsStringAsync()); } - 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 +151,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..7aacfaad02 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,6 +3,7 @@ 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; @@ -12,24 +13,20 @@ 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 null; + return body; } - 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 +51,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 +93,7 @@ namespace Volo.Abp.Http.Client.DynamicProxying isFirstParam = false; } - return postDataBuilder.ToString(); + return new StringContent(postDataBuilder.ToString(), Encoding.UTF8, MimeTypes.Application.XWwwFormUrlencoded); } } } diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Utils/ArrayMacther_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Utils/ArrayMacther_Tests.cs index a066622748..f30dc082aa 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Utils/ArrayMacther_Tests.cs +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Utils/ArrayMacther_Tests.cs @@ -10,7 +10,10 @@ namespace Volo.Abp.AspNetCore.Mvc.Utils new[] { "p1", "p2", "p3", "p4", "p5" }, new[] { "p1", "p2", "p2", "p2", "p3", "p4", "p5", "p5" }) ] - public void Should_Find_Correct_Items(string[] sourceArray, string[] destinationArray, string[] expectedArray) + public void Should_Find_Correct_Items( + string[] sourceArray, + string[] destinationArray, + string[] expectedArray) { var result = ArrayMatcher.Match(sourceArray, destinationArray); Assert.Equal(expectedArray, result);