Выгрузка и загрузка больших файлов в ASP.NET Core 3.1?

Я работаю над проектом API ASP.NET Core 3.1 с использованием чистой архитектуры, и у меня есть следующие библиотеки классов (уровни):

  • Инфраструктура (средства безопасности, помощники по загрузке и т. Д.)
  • Постоянство (уровень DA)
  • Домен (модели домена)
  • Приложение (варианты использования - Бизнес-логика)
  • API (проект API как мой стартап-проект)

Я хочу иметь возможность загружать большие файлы на сервер (например, размер файла 2 ГБ или даже больше) и загружать их после этого, и хочу сделать это без будущих проблем с переполнением памяти и всем остальным .

Любая помощь будет оценена по достоинству.


person Aspian    schedule 21.06.2020    source источник
comment
Файловые потоки FTW.   -  person spender    schedule 24.06.2020


Ответы (4)


Если у вас есть файлы такого размера, никогда не используйте byte[] или MemoryStream в своем коде. Работайте с потоками только в том случае, если вы загружаете / загружаете файлы.

У вас есть несколько вариантов:

  • Если вы контролируете и клиент, и сервер, подумайте об использовании чего-то вроде tus. Для .NET существуют как клиентские, так и серверные реализации. Это, вероятно, самый простой и надежный вариант.
  • Если вы загружаете большие файлы с помощью HttpClient, просто используйте класс StreamContent для их отправки. Опять же, не используйте MemoryStream в качестве источника, а что-то другое, например FileStream.
  • Если вы загружаете большие файлы с помощью HttpClient, важно указать HttpCompletionOptions, например var response = await httpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead). В противном случае HttpClient будет буферизовать весь ответ в памяти. Затем вы можете обработать файл ответов как поток через var stream = response.Content.ReadAsStreamAsync().

Конкретный совет ASP.NET Core:

  • Если вы хотите получать файлы через HTTP POST, вам необходимо увеличить лимит размера запроса: [RequestSizeLimit(10L * 1024L * 1024L * 1024L)] и [RequestFormLimits(MultipartBodyLengthLimit = 10L * 1024L * 1024L * 1024L)]. Кроме того, вам необходимо отключить привязку значения формы, иначе весь запрос будет буферизирован в память:
   [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
   public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
   {
       public void OnResourceExecuting(ResourceExecutingContext context)
       {
           var factories = context.ValueProviderFactories;
           factories.RemoveType<FormValueProviderFactory>();
           factories.RemoveType<FormFileValueProviderFactory>();
           factories.RemoveType<JQueryFormValueProviderFactory>();
       }

       public void OnResourceExecuted(ResourceExecutedContext context)
       {
       }
   }
  • Чтобы вернуть файл из контроллера, просто верните его с помощью метода File, который принимает поток: return File(stream, mimeType, fileName);

Пример контроллера будет выглядеть так (см. https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?view=aspnetcore-3.1 для отсутствующих вспомогательных классов):

private const MaxFileSize = 10L * 1024L * 1024L * 1024L; // 10GB, adjust to your need

[DisableFormValueModelBinding]
[RequestSizeLimit(MaxFileSize)]
[RequestFormLimits(MultipartBodyLengthLimit = MaxFileSize)]
public async Task ReceiveFile()
{
    if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
        throw new BadRequestException("Not a multipart request");

    var boundary = MultipartRequestHelper.GetBoundary(MediaTypeHeaderValue.Parse(Request.ContentType));
    var reader = new MultipartReader(boundary, Request.Body);

    // note: this is for a single file, you could also process multiple files
    var section = await reader.ReadNextSectionAsync();

    if (section == null)
        throw new BadRequestException("No sections in multipart defined");

    if (!ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var contentDisposition))
        throw new BadRequestException("No content disposition in multipart defined");

    var fileName = contentDisposition.FileNameStar.ToString();
    if (string.IsNullOrEmpty(fileName))
    {
        fileName = contentDisposition.FileName.ToString();
    }

    if (string.IsNullOrEmpty(fileName))
        throw new BadRequestException("No filename defined.");

    using var fileStream = section.Body;
    await SendFileSomewhere(fileStream);
}

// This should probably not be inside the controller class
private async Task SendFileSomewhere(Stream stream)
{
    using var request = new HttpRequestMessage()
    {
        Method = HttpMethod.Post,
        RequestUri = new Uri("YOUR_DESTINATION_URI"),
        Content = new StreamContent(stream),
    };
    using var response = await _httpClient.SendAsync(request);
    // TODO check response status etc.
}

В этом примере мы передаем весь файл в другую службу. В некоторых случаях было бы лучше временно сохранить файл на диск.

person Manuel Allenspach    schedule 24.06.2020
comment
Спасибо за решение, сейчас работаю над этим. Я просто хотел знать, используя классы HttpClient и StreamContent, нужно ли нам по-прежнему отправлять большие файлы по частям, или загрузка файла с помощью рекомендованного вами решения просто выполнит работу без каких-либо проблем? - person Aspian; 24.06.2020
comment
Не могли бы вы отредактировать ответ, написать метод действия для получения большого файла в классе контроллера из html-формы и загрузки его на сервер с помощью HttpClient и класса StreamContent? Я просто хочу убедиться, что все делаю правильно и знаю, как вы это сделаете. Заранее спасибо. - person Aspian; 24.06.2020
comment
В ручном разбиении файла нет необходимости. Однако имейте в виду, что если вы отправляете большие файлы и возникает сетевая ошибка, вам необходимо повторно отправить весь файл (это то, что tus пытается решить). Если вы разбиваете их на части, вам нужно отправить только тот фрагмент, который не удался, но в этот момент я бы просто использовал tus, а не изобретал колесо. - person Manuel Allenspach; 24.06.2020
comment
@Aspian добавил пример. - person Manuel Allenspach; 24.06.2020
comment
Большое спасибо за ваше время и помощь мне. - person Aspian; 25.06.2020
comment
Можно ли использовать это в клиентском приложении .Net Core? идеи ?? вроде у меня будет только путь к файлу - person A Coder; 06.11.2020
comment
@ACoder, чтобы загрузить файл? Используйте что-то вроде new StreamContent(File.OpenRead(pathToFile)) и используйте его в качестве тела запроса. - person Manuel Allenspach; 10.11.2020
comment
Библиотека tus обрабатывает только загрузку файлов. - person Bronek; 20.11.2020
comment
@ManuelAllenspach большое спасибо за этот ответ, не могли бы вы помочь с функцией загрузки для больших файлов? Я использую return File (stream, mimeType, fileName), а поток - memoryStream, но я хочу, чтобы файл передавался на клиентскую сторону. - person Moneer Kamal; 03.12.2020
comment
@Bronek также обрабатывает загрузку файлов github.com/tusdotnet/tusdotnet/blob/master/Source/TestSites/ - person Anton Shakalo; 19.03.2021

Я нашел эту статью полезной - https://www.tugberkugurlu.com/archive/efficiently-streaming-large-http-responses-with-httpclient

Вот версия кода для загрузки большого файла:

static public async Task HttpDownloadFileAsync(HttpClient httpClient, string url, string fileToWriteTo) {
  using HttpResponseMessage response = await httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
  using Stream streamToReadFrom = await response.Content.ReadAsStreamAsync(); 
  using Stream streamToWriteTo = File.Open(fileToWriteTo, FileMode.Create); 
  await streamToReadFrom.CopyToAsync(streamToWriteTo);
}
person kofifus    schedule 22.04.2021
comment
Это способ загрузки большого файла на Клиент. Это не решение для сервера. - person Mr. Squirrel.Downy; 21.05.2021

Иногда проблема заключается в том, что мы использовали Nginx в качестве прокси-сервера для нашего приложения asp.net Core, развернутого в докере в среде ubuntu / Linux. Это именно то, что в моем случае, когда я пытался отлаживать на стороне ядра докера или .net, но фактическое решение приходит путем настройки конфигурации Nginx как

client_max_body_size 50M;

эту строку можно добавить в часть настройки Nginx для местоположения или сервера для вашего сайта, на котором вы столкнулись с проблемами.

Может быть кому-то полезно.

person Rahman Bokhari    schedule 24.05.2021

Проблема в том, что вам приходится иметь дело с длинными файлами, где бы вы ни потребляли их, вам нужны большие ресурсы для чтения. Одно из возможных решений - разделить файл на основе информации на разные части или обработать его в отдельном задании или потоке или использовать параллелизм в .net для его обработки. вы можете указать размер файла, а также прочитайте следующий очень полезный для вас блог.

Загрузить большие файлы

person ISK    schedule 25.05.2021