Я столкнулся с проблемой, что у меня есть SQL, созданный пользователем, этот SQL я теперь запускаю через XPO (DevExpress, скоро без оболочки напрямую через NPGSQL). Там я уже загружаю все данные в свою память и трансформирую результат в собственный класс-обертку.
Теперь я беру свой собственный объект и использую CSVHelper для создания из него CSV-файла. Затем я возвращаю этот файл пользователю через WebAPI.
Поскольку SQL-запрос может иметь размер 1-2 ГБ, потребление памяти значительно возрастает.
Каков наилучший способ предотвратить это?
Раньше я мало работал с потоками и сейчас читаю о них. Если я правильно понял, MemoryStream мне мало что приносит, потому что там данные тоже загружаются напрямую в память.
[HttpGet("export/data")]
public async Task<IActionResult> ExportData(Guid sqlId)
{
this.OpenConn(); //opens the connection
string sql = this.GetSql(sqlId);
using (NpgsqlCommand command = new NpgsqlCommand(sql, conn))
{
int val;
NpgsqlDataReader reader = command.ExecuteReader();
while (reader.Read())
{
// Logic to create the csv file
}
this.CloseConn(); //close the current connection
}
}
Теперь я мог создать свой CSV-файл, используя CSVHelper с MemoryStream, а затем вернуть MemoryStream для загрузки.
var ms = new MemoryStream();
var streamWriter = new StreamWriter(ms, Encoding.UTF8);
var csvWriter = new CsvWriter(streamWriter, CultureInfo.InvariantCulture);
// Logic to create the csv file, with the reader from npgsql
// [...]
return File(ms, "text/csv", "export.csv");
Если я правильно понял, это не дает мне много преимуществ, так как я сохранил данные в MemoryStream и, таким образом, память все равно заполняется. Каков наилучший способ справиться с этим? Записать файл CSV во временный каталог на диске, а затем вернуть его? Например, я мог бы получить доступ к S3Bucket здесь или использовать свой собственный каталог внутри Kubernetes. Если я возвращаю поток через файл, я не могу просто удалить файл после успешной передачи, не так ли? Я не могу выполнить последнее действие? Я действительно хочу передать файл только для запроса, а затем не хранить его где-то.
Спасибо!
К счастью, это тоже скорее исключение и редко используется. Но я не хочу оставлять много памяти на тот редкий случай, когда служба не запускает OutOfMemory. Чаще всего это будет несколько сотен МБ.
ИМХО, вам следует подумать о записи данных непосредственно на диск, а когда они будут полностью записаны, предложить их загрузить.
Я сделал то же самое для другого варианта использования и сохранил данные в S3Bucket. В этом случае данные должны храниться непосредственно в PowerBI и предоставляться через единый URL-адрес. Я также обдумывал, нужно ли пересохранять данные каждый день, а затем делать сохраненные файлы доступными по ссылке. Тем не менее, я хотел сначала попробовать, чтобы увидеть, смогу ли я передать данные напрямую.
@joey rather the exception and is rarely used. тогда отключи его полностью. API-интерфейсы явно предотвращают такие случаи, вынуждая клиентов использовать пейджинг или потоковую передачу. Ни GraphQL, ни OData не допускают произвольного размера результатов. Оба обеспечивают размер страницы и предоставляют клиентам несколько вариантов пейджинга.
@joey the data should be stored directly in PowerBI PowerBI уже может считывать постраничные результаты для конечных точек OData или GraphQL. Хотя гораздо лучше настроить локальный шлюз данных вместо того, чтобы вручную загружать в него свои данные.
@joey другой вариант - просто загрузить CSV, хранящийся на S3 или на локальном диске. Power BI скопирует его внутри, а не запросит данные с диска. CSV-коннектор может загружать CSV-файлы как из локального, так и из веб-хранилища.
В настоящее время я не могу каким-либо значимым образом просмотреть результат, потому что SQL пишется пользователем самостоятельно (у каждого клиента есть своя собственная база данных, поэтому они могут свободно к ней обращаться. Эта база данных также отделена от базы данных приложения.). Я все еще хотел посмотреть в будущем, что я могу разобрать и подготовить SQL-запрос, чтобы я мог предоставить LIMITS для подкачки. Но я пока не уверен, как я буду делать это правильно.
@PanagiotisKanavos Я не профессионал PowerBI - это требование было у клиента. Я также видел локальный шлюз данных. Но поскольку мы являемся решением SaaS, я не хотел ставить шлюз в нашу инфраструктуру, потому что тогда мне понадобится один шлюз на каждого клиента, если они тоже захотят это сделать, верно?
Почему вы сериализуете его в SQL-запросы? Вы должны заставить пользователей загружать в лучшем формате и загружать его, используя что-то вроде BeginBinaryImportnpgsql.org/doc/copy.html





Самый простой способ — сохранить файл локально и передать его в потоковом режиме.
В качестве альтернативы вы можете попробовать написать напрямую в Response.Body (не забудьте сбросить) или использовать подход FileCallbackResult Стивена Клири:
public class FileCallbackResult : FileResult
{
private Func<Stream, ActionContext, Task> _callback;
public FileCallbackResult(string contentType, Func<Stream, ActionContext, Task> callback)
: base(contentType)
{
if (callback == null)
throw new ArgumentNullException(nameof(callback));
_callback = callback;
}
public override Task ExecuteResultAsync(ActionContext context)
{
if (context == null)
throw new ArgumentNullException(nameof(context));
var executor = new FileCallbackResultExecutor(context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>());
return executor.ExecuteAsync(context, this);
}
private sealed class FileCallbackResultExecutor : FileResultExecutorBase
{
public FileCallbackResultExecutor(ILoggerFactory loggerFactory)
: base(CreateLogger<FileCallbackResultExecutor>(loggerFactory))
{
}
public Task ExecuteAsync(ActionContext context, FileCallbackResult result)
{
SetHeadersAndLog(context, result, null, false);
return result._callback(context.HttpContext.Response.Body, context);
}
}
}
И пример использования:
[HttpGet("data")]
public async Task<IActionResult> ExportData()
{
return new FileCallbackResult("text/csv", async (outStream, context) =>
{
await using var sw = new StreamWriter(outStream, leaveOpen: true);
await using var csvWriter = new CsvWriter(sw, CultureInfo.InvariantCulture, true);
for (int i = 0; i < 100; i++)
{
await Task.Delay(10);
csvWriter.WriteRecord(new {Int = i, Text = "Qww" + i});
await csvWriter.NextRecordAsync();
}
})
{
FileDownloadName = "qwerty.csv"
};
}
Но в целом я бы рекомендовал пересмотреть этот подход, если это возможно. Если это экспорт для какого-то конвейера ETL, например, вы можете переключиться на просто загрузку файлов в какое-то общее место.
О, очень хорошо. Это выглядит как хороший вариант, который я могу использовать быстро. На следующем шаге я, вероятно, должен ограничить размер файла и предоставить слишком большие файлы по-другому.
TBH, обслуживающий файлы размером 1-2 ГБ через веб-API в виде одного фрагмента, в целом не является хорошей идеей.