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)