From e4077d55690f69188b9c43df2aaaaf6ec121f8d4 Mon Sep 17 00:00:00 2001 From: Alexandru Bagu Date: Fri, 28 May 2021 14:44:01 +0300 Subject: [PATCH] 1. allow the usage of IRemoteContentStream or RemoteContentStream when the incoming stream is not a form content format 2. remove all rewinds related to streams; the user is the one who must ensure his streams are at the correct position when calling the api; stream rewind would exclude the ability of sending partial streams (at least skippable streams, because the tail cannot be limitted in .net core) 3. RemoteStreamContent now must get the content type associated with the stream in its constructor; it may also receive the length of the stream in a read-only form (this is because some stream classes cannot provide the length and it is provided via http headers) 4. IRemoteStreamContent should implement IDisposable to allow the auto clean up of streams (example: FileStream) --- .../AbpRemoteStreamContentModelBinder.cs | 9 +-- .../RemoteStreamContentOutputFormatter.cs | 10 +-- .../Volo/Abp/Content/IRemoteStreamContent.cs | 5 +- .../Volo/Abp/Content/RemoteStreamContent.cs | 23 ++++-- .../Volo/Abp/Extensions/StreamExtensions.cs | 29 ++++++++ .../DynamicHttpProxyInterceptor.cs | 5 +- .../DynamicProxying/RequestPayloadBuilder.cs | 14 ++-- .../RemoteStreamContentTestController.cs | 17 +++-- ...RemoteStreamContentTestController_Tests.cs | 26 ++++++- .../PersonAppServiceClientProxy_Tests.cs | 72 ++++++++----------- .../TestApp/Application/PeopleAppService.cs | 6 +- 11 files changed, 130 insertions(+), 86 deletions(-) create mode 100644 framework/src/Volo.Abp.Core/Volo/Abp/Extensions/StreamExtensions.cs diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ContentFormatters/AbpRemoteStreamContentModelBinder.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ContentFormatters/AbpRemoteStreamContentModelBinder.cs index da58641d7e..e4f3e46a77 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ContentFormatters/AbpRemoteStreamContentModelBinder.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ContentFormatters/AbpRemoteStreamContentModelBinder.cs @@ -108,13 +108,14 @@ namespace Volo.Abp.AspNetCore.Mvc.ContentFormatters if (file.Name.Equals(modelName, StringComparison.OrdinalIgnoreCase)) { - postedFiles.Add(new RemoteStreamContent(file.OpenReadStream()) - { - ContentType = file.ContentType - }); + postedFiles.Add(new RemoteStreamContent(file.OpenReadStream(), file.ContentType, file.Length)); } } } + else if (bindingContext.IsTopLevelObject) + { + postedFiles.Add(new RemoteStreamContent(request.Body, request.ContentType, request.ContentLength)); + } } } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ContentFormatters/RemoteStreamContentOutputFormatter.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ContentFormatters/RemoteStreamContentOutputFormatter.cs index 188306227a..0edc91d30a 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ContentFormatters/RemoteStreamContentOutputFormatter.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ContentFormatters/RemoteStreamContentOutputFormatter.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Net.Http.Headers; @@ -27,13 +28,8 @@ namespace Volo.Abp.AspNetCore.Mvc.ContentFormatters context.HttpContext.Response.ContentType = remoteStream.ContentType; using (var stream = remoteStream.GetStream()) - { - if (stream.CanSeek) - { - stream.Position = 0; - } - - await stream.CopyToAsync(context.HttpContext.Response.Body); + { + 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 index bca259d422..4805983287 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Content/IRemoteStreamContent.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Content/IRemoteStreamContent.cs @@ -1,8 +1,9 @@ -using System.IO; +using System; +using System.IO; namespace Volo.Abp.Content { - public interface IRemoteStreamContent + public interface IRemoteStreamContent : IDisposable { string ContentType { get; } diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Content/RemoteStreamContent.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Content/RemoteStreamContent.cs index f217101cea..fc30b93f7d 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Content/RemoteStreamContent.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Content/RemoteStreamContent.cs @@ -4,20 +4,31 @@ namespace Volo.Abp.Content { public class RemoteStreamContent : IRemoteStreamContent { - private readonly Stream _stream; - - public RemoteStreamContent(Stream stream) + private readonly Stream _stream; + private readonly string _contentType; + private readonly long? _length; + private readonly bool _leaveOpen; + + public RemoteStreamContent(Stream stream, string contentType, long? readOnlylength = null, bool leaveOpen = false) { _stream = stream; + _contentType = contentType; + _length = readOnlylength ?? (stream.GetNullableLength() - stream.GetNullablePosition()); + _leaveOpen = leaveOpen; } - public virtual string ContentType { get; set; } - - public virtual long? ContentLength => _stream.Length; + public virtual string ContentType => _contentType; + public virtual long? ContentLength => _length; public virtual Stream GetStream() { return _stream; } + + public virtual void Dispose() + { + if (!_leaveOpen) + _stream?.Dispose(); + } } } diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Extensions/StreamExtensions.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Extensions/StreamExtensions.cs new file mode 100644 index 0000000000..0501e92d56 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Extensions/StreamExtensions.cs @@ -0,0 +1,29 @@ +using System.IO; + +public static class StreamExtensions +{ + public static long? GetNullableLength(this Stream stream) + { + try + { + return stream?.Length; + } + catch + { + /*some stream classes throw exceptions when accessing Length because they do not have access to such information */ + return null; + } + } + public static long? GetNullablePosition(this Stream stream) + { + try + { + return stream?.Position; + } + catch + { + /*some stream classes throw exceptions when accessing Position because they do not have access to such information */ + return null; + } + } +} 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 dd96ad98a2..78773cc364 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 @@ -112,10 +112,7 @@ namespace Volo.Abp.Http.Client.DynamicProxying /* 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 RemoteStreamContent(await responseContent.ReadAsStreamAsync()) - { - ContentType = responseContent.Headers.ContentType?.ToString() - }; + return (T)(object)new RemoteStreamContent(await responseContent.ReadAsStreamAsync(), responseContent.Headers.ContentType?.ToString(), responseContent.Headers.ContentLength); } var stringContent = await responseContent.ReadAsStringAsync(); 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 1096fa971e..47b2558f4c 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 @@ -82,15 +82,12 @@ namespace Volo.Abp.Http.Client.DynamicProxying if (value is IRemoteStreamContent remoteStreamContent) { var stream = remoteStreamContent.GetStream(); - if (stream.CanSeek) - { - stream.Position = 0; - } var streamContent = new StreamContent(stream); if (!remoteStreamContent.ContentType.IsNullOrWhiteSpace()) { streamContent.Headers.ContentType = new MediaTypeHeaderValue(remoteStreamContent.ContentType); - } + } + streamContent.Headers.ContentLength = stream.GetNullableLength() - stream.GetNullablePosition(); formData.Add(streamContent, parameter.Name, parameter.Name); } else if (value is IEnumerable remoteStreamContents) @@ -98,15 +95,12 @@ namespace Volo.Abp.Http.Client.DynamicProxying foreach (var content in remoteStreamContents) { var stream = content.GetStream(); - if (stream.CanSeek) - { - stream.Position = 0; - } var streamContent = new StreamContent(stream); if (!content.ContentType.IsNullOrWhiteSpace()) { streamContent.Headers.ContentType = new MediaTypeHeaderValue(content.ContentType); - } + } + streamContent.Headers.ContentLength = stream.GetNullableLength() - stream.GetNullablePosition(); formData.Add(streamContent, parameter.Name, parameter.Name); } } diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ContentFormatters/RemoteStreamContentTestController.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ContentFormatters/RemoteStreamContentTestController.cs index e74c9d52e8..de7cc8d5cc 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ContentFormatters/RemoteStreamContentTestController.cs +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ContentFormatters/RemoteStreamContentTestController.cs @@ -16,11 +16,8 @@ namespace Volo.Abp.AspNetCore.Mvc.ContentFormatters { var memoryStream = new MemoryStream(); await memoryStream.WriteAsync(Encoding.UTF8.GetBytes("DownloadAsync")); - - return new RemoteStreamContent(memoryStream) - { - ContentType = "application/rtf" - }; + memoryStream.Position = 0; + return new RemoteStreamContent(memoryStream, "application/rtf"); } [HttpPost] @@ -32,5 +29,15 @@ namespace Volo.Abp.AspNetCore.Mvc.ContentFormatters return await reader.ReadToEndAsync() + ":" + file.ContentType; } } + + [HttpPost] + [Route("Upload-Raw")] + public async Task UploadRawAsync(IRemoteStreamContent file) + { + using (var reader = new StreamReader(file.GetStream())) + { + return await reader.ReadToEndAsync() + ":" + file.ContentType; + } + } } } diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ContentFormatters/RemoteStreamContentTestController_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ContentFormatters/RemoteStreamContentTestController_Tests.cs index 12745da732..a901c131a2 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ContentFormatters/RemoteStreamContentTestController_Tests.cs +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ContentFormatters/RemoteStreamContentTestController_Tests.cs @@ -16,8 +16,8 @@ namespace Volo.Abp.AspNetCore.Mvc.ContentFormatters var result = await GetResponseAsync("/api/remote-stream-content-test/download"); result.Content.Headers.ContentType?.ToString().ShouldBe("application/rtf"); (await result.Content.ReadAsStringAsync()).ShouldBe("DownloadAsync"); - } - + } + [Fact] public async Task UploadAsync() { @@ -30,12 +30,32 @@ namespace Volo.Abp.AspNetCore.Mvc.ContentFormatters var streamContent = new StreamContent(memoryStream); streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/rtf"); - requestMessage.Content = new MultipartFormDataContent {{streamContent, "file", "file"}}; + requestMessage.Content = new MultipartFormDataContent { { streamContent, "file", "file" } }; var response = await Client.SendAsync(requestMessage); (await response.Content.ReadAsStringAsync()).ShouldBe("UploadAsync:application/rtf"); } } + + [Fact] + public async Task UploadRawAsync() + { + using (var requestMessage = new HttpRequestMessage(HttpMethod.Post, "/api/remote-stream-content-test/upload-raw")) + { + var memoryStream = new MemoryStream(); + var text = @"{ ""hello"": ""world"" }"; + await memoryStream.WriteAsync(Encoding.UTF8.GetBytes(text)); + memoryStream.Position = 0; + + var streamContent = new StreamContent(memoryStream); + streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + + requestMessage.Content = streamContent; + + var response = await Client.SendAsync(requestMessage); + (await response.Content.ReadAsStringAsync()).ShouldBe($"{text}:application/json"); + } + } } } diff --git a/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/PersonAppServiceClientProxy_Tests.cs b/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/PersonAppServiceClientProxy_Tests.cs index 026373d568..9f172fcfd1 100644 --- a/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/PersonAppServiceClientProxy_Tests.cs +++ b/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/PersonAppServiceClientProxy_Tests.cs @@ -49,7 +49,7 @@ namespace Volo.Abp.Http.DynamicProxying { var people = await _peopleAppService.GetListAsync(new PagedAndSortedResultRequestDto()); people.TotalCount.ShouldBeGreaterThan(0); - people.Items.Count.ShouldBe((int) people.TotalCount); + people.Items.Count.ShouldBe((int)people.TotalCount); } [Fact] @@ -62,7 +62,7 @@ namespace Volo.Abp.Http.DynamicProxying { id1, id2 - }, new[] {"name1", "name2"}); + }, new[] { "name1", "name2" }); @params.ShouldContain(id1.ToString("N")); @params.ShouldContain(id2.ToString("N")); @@ -86,11 +86,11 @@ namespace Volo.Abp.Http.DynamicProxying { var uniquePersonName = Guid.NewGuid().ToString(); - var person = await _peopleAppService.CreateAsync(new PersonDto - { - Name = uniquePersonName, - Age = 42 - } + var person = await _peopleAppService.CreateAsync(new PersonDto + { + Name = uniquePersonName, + Age = 42 + } ); person.ShouldNotBeNull(); @@ -107,10 +107,10 @@ namespace Volo.Abp.Http.DynamicProxying { await Assert.ThrowsAsync(async () => { - var person = await _peopleAppService.CreateAsync(new PersonDto - { - Age = 42 - } + var person = await _peopleAppService.CreateAsync(new PersonDto + { + Age = 42 + } ); }); } @@ -194,10 +194,20 @@ namespace Volo.Abp.Http.DynamicProxying var memoryStream = new MemoryStream(); await memoryStream.WriteAsync(Encoding.UTF8.GetBytes("UploadAsync")); memoryStream.Position = 0; - var result = await _peopleAppService.UploadAsync(new RemoteStreamContent(memoryStream) - { - ContentType = "application/rtf" - }); + var result = await _peopleAppService.UploadAsync(new RemoteStreamContent(memoryStream, "application/rtf")); + result.ShouldBe("UploadAsync:application/rtf"); + } + + [Fact] + public async Task UploadPartialAsync() + { + var memoryStream = new MemoryStream(); + var rawData = new byte[16]; + var text = Encoding.UTF8.GetBytes("UploadAsync"); + await memoryStream.WriteAsync(rawData); + await memoryStream.WriteAsync(text); + memoryStream.Position = rawData.Length; + var result = await _peopleAppService.UploadAsync(new RemoteStreamContent(memoryStream, "application/rtf")); result.ShouldBe("UploadAsync:application/rtf"); } @@ -214,15 +224,8 @@ namespace Volo.Abp.Http.DynamicProxying var result = await _peopleAppService.UploadMultipleAsync(new List() { - new RemoteStreamContent(memoryStream) - { - ContentType = "application/rtf" - }, - - new RemoteStreamContent(memoryStream2) - { - ContentType = "application/rtf2" - } + new RemoteStreamContent(memoryStream, "application/rtf"), + new RemoteStreamContent(memoryStream2, "application/rtf2") }); result.ShouldBe("File1:application/rtfFile2:application/rtf2"); } @@ -236,10 +239,7 @@ namespace Volo.Abp.Http.DynamicProxying var result = await _peopleAppService.CreateFileAsync(new CreateFileInput() { Name = "123.rtf", - Content = new RemoteStreamContent(memoryStream) - { - ContentType = "application/rtf" - } + Content = new RemoteStreamContent(memoryStream, "application/rtf") }); result.ShouldBe("123.rtf:CreateFileAsync:application/rtf"); } @@ -264,23 +264,13 @@ namespace Volo.Abp.Http.DynamicProxying Name = "123.rtf", Contents = new List() { - new RemoteStreamContent(memoryStream) - { - ContentType = "application/rtf" - }, - - new RemoteStreamContent(memoryStream2) - { - ContentType = "application/rtf2" - } + new RemoteStreamContent(memoryStream, "application/rtf"), + new RemoteStreamContent(memoryStream2, "application/rtf2"), }, Inner = new CreateFileInput() { Name = "789.rtf", - Content = new RemoteStreamContent(memoryStream3) - { - ContentType = "application/rtf3" - } + Content = new RemoteStreamContent(memoryStream3, "application/rtf3") } }); result.ShouldBe("123.rtf:File1:application/rtf123.rtf:File2:application/rtf2789.rtf:File3:application/rtf3"); diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/PeopleAppService.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/PeopleAppService.cs index edd6a3b93f..6841882ce6 100644 --- a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/PeopleAppService.cs +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/PeopleAppService.cs @@ -72,11 +72,9 @@ namespace Volo.Abp.TestApp.Application { var memoryStream = new MemoryStream(); await memoryStream.WriteAsync(Encoding.UTF8.GetBytes("DownloadAsync")); + memoryStream.Position = 0; - return new RemoteStreamContent(memoryStream) - { - ContentType = "application/rtf" - }; + return new RemoteStreamContent(memoryStream, "application/rtf"); } public async Task UploadAsync(IRemoteStreamContent streamContent)