从WebAPI发送大文件。内容长度为0

时间:2017-08-29 21:20:07

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

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

我已经设法发送较小的文件作为Multipart Request的一部分,如上一篇文章所述:link

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

enter image description here 即使我发送(测试)较小的文件(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;
    }

Serverside代码:

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

        string myFileName = fileFromRequest.Name;

        // some code

        return Ok();
    }

问题出在哪里?

要创建Multipart请求,我使用了以下建议:

HttpClient StreamContent append filename twice

POST StreamContent with Multiple Files

1 个答案:

答案 0 :(得分:7)

最后我明白了:

有两个问题:

<强> 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文件,我们需要粘贴在答案中的代码。