【问题标题】:Send large file from WebAPI.Content Length is 0从 WebAPI 发送大文件。内容长度为 0
【发布时间】:2018-02-07 11:09:48
【问题描述】:

我正在尝试将大文件 (GB) 从一个 WebAPI (.NET Core) 发送到另一个 WebApi (.Net Core)。

我已经设法发送较小的文件作为多部分请求的一部分,就像在上一篇文章中一样:link

要发送更大的文件,我需要(我认为)将此文件作为 StreamContent 发送,但是我在接收请求的 API 中得到 Content length = 0。

即使我发送(用于测试)较小的文件 (10 Mb),也会出现问题。

客户端代码:

    [HttpPost("UploadFiles")]
    public async Task<IActionResult> Post(IFormFile file)
    {
        var filePath = Path.GetTempFileName();

        using (var stream = new FileStream(filePath, FileMode.Create, FileAccess.ReadWrite))
        {
            await file.CopyToAsync(stream);
            using (var formDataContent = new MultipartFormDataContent())
            {
                using (var httpClient = new HttpClient())
                {
                    formDataContent.Add(CreateFileContent(stream, "myfile.test", "application/octet-stream"));

                    var response = await httpClient.PostAsync(
                        "http://localhost:56595/home/upload",
                        formDataContent);

                    return Json(response);
                }
            }
        }
    }

    internal static StreamContent CreateFileContent(Stream stream, string fileName, string contentType)
    {
        var fileContent = new StreamContent(stream);
        fileContent.Headers.ContentDisposition = new System.Net.Http.Headers.ContentDispositionHeaderValue("form-data")
        {
            Name = "\"file\"",
            FileName = "\"" + fileName + "\"",
        };
        fileContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(contentType);
        return fileContent;
    }

服务器端代码:

    [HttpPost]
    public ActionResult Upload()
    {
        IFormFile fileFromRequest = Request.Form.Files.First();

        string myFileName = fileFromRequest.Name;

        // some code

        return Ok();
    }

问题出在哪里?

为了创建多部分请求,我使用了以下建议:

HttpClient StreamContent append filename twice

POST StreamContent with Multiple Files

【问题讨论】:

    标签: c# asp.net asp.net-web-api asp.net-core


    【解决方案1】:

    终于想通了:

    有两个问题:

    1.流指针位置

    在客户端代码中,改变这个:

    await file.CopyToAsync(stream);
    

    到那个:

    await file.CopyToAsync(stream);
    stream.Position = 0;
    

    问题是请求中的文件被复制到流中并且指针的左侧位置位于流的末尾。这就是为什么从客户端发送的请求具有适当长度的流,但实际上当它开始读取它时,它不能(读取 0 个字节)。

    2。服务器上处理请求的方式错误。

    我使用来自dotnetcoretutorials.com的代码


    下面的工作代码:

    客户端:

        [HttpPost("UploadFiles")]
        public async Task<IActionResult> Post(IFormFile file)
        {
            var filePath = Path.GetTempFileName();
            using (var stream = new FileStream(filePath, FileMode.Create, FileAccess.ReadWrite))
            {
                await file.CopyToAsync(stream);
                stream.Position = 0;
                using (var formDataContent = new MultipartFormDataContent())
                {
                    using (var httpClient = new HttpClient())
                    {
                        formDataContent.Add(CreateFileContent(stream, "myfile.test", "application/octet-stream"));
    
                        var response = await httpClient.PostAsync(
                            "http://localhost:56595/home/upload",
                            formDataContent);
                        return Json(response);
                    }
                }
            }
        }
    
        internal static StreamContent CreateFileContent(Stream stream, string fileName, string contentType)
        {
            var fileContent = new StreamContent(stream);
            fileContent.Headers.ContentDisposition = new System.Net.Http.Headers.ContentDispositionHeaderValue("form-data")
            {
                Name = "\"file\"",
                FileName = "\"" + fileName + "\"",
            };
            fileContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(contentType);
            return fileContent;
        }
    

    服务器端:

    控制器:

                [HttpPost]
                [DisableFormValueModelBinding]
                public async Task<IActionResult> Upload()
                {
                    var viewModel = new MyViewModel();
                    try
                    {
                        FormValueProvider formModel;
                        using (var stream = System.IO.File.Create("c:\\temp\\myfile.temp"))
                        {
                            formModel = await Request.StreamFile(stream);
                        }
    
                        var bindingSuccessful = await TryUpdateModelAsync(viewModel, prefix: "",
                            valueProvider: formModel);
    
                        if (!bindingSuccessful)
                        {
                            if (!ModelState.IsValid)
                            {
                                return BadRequest(ModelState);
                            }
                        }
                    }
                    catch(Exception exception)
                    {
                        throw;
                    }
                    return Ok(viewModel);
                }
    

    来自控制器的方法的帮助类:

        public static class MultipartRequestHelper
    {
        // Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq"
        // The spec says 70 characters is a reasonable limit.
        public static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit)
        {
            var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary);
            if (string.IsNullOrWhiteSpace(boundary.ToString()))
            {
                throw new InvalidDataException("Missing content-type boundary.");
            }
    
            if (boundary.Length > lengthLimit)
            {
                throw new InvalidDataException(
                    $"Multipart boundary length limit {lengthLimit} exceeded.");
            }
    
            return boundary.ToString();
        }
    
        public static bool IsMultipartContentType(string contentType)
        {
            return !string.IsNullOrEmpty(contentType)
                   && contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0;
        }
    
        public static bool HasFormDataContentDisposition(ContentDispositionHeaderValue contentDisposition)
        {
            // Content-Disposition: form-data; name="key";
            return contentDisposition != null
                   && contentDisposition.DispositionType.Equals("form-data")
                   && string.IsNullOrEmpty(contentDisposition.FileName.ToString())
                   && string.IsNullOrEmpty(contentDisposition.FileNameStar.ToString());
        }
    
        public static bool HasFileContentDisposition(ContentDispositionHeaderValue contentDisposition)
        {
            // Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg"
            return contentDisposition != null
                   && contentDisposition.DispositionType.Equals("form-data")
                   && (!string.IsNullOrEmpty(contentDisposition.FileName.ToString())
                       || !string.IsNullOrEmpty(contentDisposition.FileNameStar.ToString()));
        }
    }
    
    public static class FileStreamingHelper
    {
        private static readonly FormOptions _defaultFormOptions = new FormOptions();
    
        public static async Task<FormValueProvider> StreamFile(this HttpRequest request, Stream targetStream)
        {
            if (!MultipartRequestHelper.IsMultipartContentType(request.ContentType))
            {
                throw new Exception($"Expected a multipart request, but got {request.ContentType}");
            }
    
            // Used to accumulate all the form url encoded key value pairs in the 
            // request.
            var formAccumulator = new KeyValueAccumulator();
            string targetFilePath = null;
    
            var boundary = MultipartRequestHelper.GetBoundary(
                MediaTypeHeaderValue.Parse(request.ContentType),
                _defaultFormOptions.MultipartBoundaryLengthLimit);
            var reader = new MultipartReader(boundary, request.Body);
    
            var section = await reader.ReadNextSectionAsync();
            while (section != null)
            {
                ContentDispositionHeaderValue contentDisposition;
                var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out contentDisposition);
    
                if (hasContentDispositionHeader)
                {
                    if (MultipartRequestHelper.HasFileContentDisposition(contentDisposition))
                    {
                        await section.Body.CopyToAsync(targetStream);
                    }
                    else if (MultipartRequestHelper.HasFormDataContentDisposition(contentDisposition))
                    {
                        // Content-Disposition: form-data; name="key"
                        //
                        // value
    
                        // Do not limit the key name length here because the 
                        // multipart headers length limit is already in effect.
                        var key = HeaderUtilities.RemoveQuotes(contentDisposition.Name);
                        var encoding = GetEncoding(section);
                        using (var streamReader = new StreamReader(
                            section.Body,
                            encoding,
                            detectEncodingFromByteOrderMarks: true,
                            bufferSize: 1024,
                            leaveOpen: true))
                        {
                            // The value length limit is enforced by MultipartBodyLengthLimit
                            var value = await streamReader.ReadToEndAsync();
                            if (String.Equals(value, "undefined", StringComparison.OrdinalIgnoreCase))
                            {
                                value = String.Empty;
                            }
                            formAccumulator.Append(key.ToString(), value);
    
                            if (formAccumulator.ValueCount > _defaultFormOptions.ValueCountLimit)
                            {
                                throw new InvalidDataException($"Form key count limit {_defaultFormOptions.ValueCountLimit} exceeded.");
                            }
                        }
                    }
                }
    
                // Drains any remaining section body that has not been consumed and
                // reads the headers for the next section.
                section = await reader.ReadNextSectionAsync();
            }
    
            // Bind form data to a model
            var formValueProvider = new FormValueProvider(
                BindingSource.Form,
                new FormCollection(formAccumulator.GetResults()),
                CultureInfo.CurrentCulture);
    
            return formValueProvider;
        }
    
        private static Encoding GetEncoding(MultipartSection section)
        {
            MediaTypeHeaderValue mediaType;
            var hasMediaTypeHeader = MediaTypeHeaderValue.TryParse(section.ContentType, out mediaType);
            // UTF-7 is insecure and should not be honored. UTF-8 will succeed in 
            // most cases.
            if (!hasMediaTypeHeader || Encoding.UTF7.Equals(mediaType.Encoding))
            {
                return Encoding.UTF8;
            }
            return mediaType.Encoding;
        }
    }
    
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
    public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
    {
        public void OnResourceExecuting(ResourceExecutingContext context)
        {
            var formValueProviderFactory = context.ValueProviderFactories
                .OfType<FormValueProviderFactory>()
                .FirstOrDefault();
            if (formValueProviderFactory != null)
            {
                context.ValueProviderFactories.Remove(formValueProviderFactory);
            }
    
            var jqueryFormValueProviderFactory = context.ValueProviderFactories
                .OfType<JQueryFormValueProviderFactory>()
                .FirstOrDefault();
            if (jqueryFormValueProviderFactory != null)
            {
                context.ValueProviderFactories.Remove(jqueryFormValueProviderFactory);
            }
        }
    
        public void OnResourceExecuted(ResourceExecutedContext context)
        {
        }
    }
    

    其他想法:

    • (在客户端)行:

      fileContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(contentType);

    不需要发送文件。

    • (在客户端)当 MediaTypeHeaderValue 是以下之一时发送文件:

      应用程序/x-msdownload

      应用程序/json

      应用程序/八位字节流

    • (在服务器端)要在服务器端使用带有contentDisposition.FileNameStar 的行,您需要将它们更改为contentDisposition.FileNameStar.ToString()

    • (在服务器端)用于服务器端的代码将适用于较小的文件(Mb),但要发送 GB 文件,我们需要粘贴在答案中的代码。

    • 部分代码取自aspnet core docs

    【讨论】:

    • 复制/粘贴来自 Microsoft 示例的代码,无需参考。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-08-23
    • 1970-01-01
    • 1970-01-01
    • 2012-03-01
    • 2017-12-27
    相关资源
    最近更新 更多