Потоковая передача данных из базы данных - ASP.NET Core & SqlDataReader.GetStream () - PullRequest
3 голосов
/ 25 октября 2019

Я пытаюсь свести к минимуму загрузку больших объектов в память из базы данных при отправке их с сайта ASP.NET Core, потому что иногда я нажимаю OutOfMemoryException.

Я рассчитывалПоток это. Теперь из моего исследования SQL Server поддерживает это, если вы указали CommandBehavior.SequentialAccess в своей команде. Я подумал, что если я собираюсь транслировать его, я мог бы также транслировать его настолько напрямую, насколько это возможно, поэтому я в значительной степени транслирую его прямо с DataReader на ASP.NET MVC ActionResult.

Но как только FileStreamResult (скрытый под вызовом File()) завершил выполнение, как мне очистить мой читатель / команду? Соединение было предоставлено DI, так что это не проблема, но я создаю читателя / команду в вызове GetDocumentStream().

У меня есть подкласс ActionFilterAttribute, зарегистрированный в MVC, так что это дает мнеточку входа, которую я могу вызвать для ActionFilterAttribute.OnResultExecuted(), но я абсолютно не знаю, что туда поместить, кроме моей текущей логики, которая связана с очисткой моей транзакции базы данных и фиксацией / откатом вещей (не включены, так как это не очень актуально).

Есть ли способ убрать после моего DataReader / Command и при этом предоставить Stream до File()?

public class DocumentsController : Controller
{
    private DocumentService documentService;

    public FilesController(DocumentService documentService)
    {
        this.documentService = documentService;
    }

    public IActionResult Stream(Guid id, string contentType = "application/octet-stream") // Defaults to octet-stream when unspecified
    {
        // Simple lookup by Id so that I can use it for the Name and ContentType below
        if(!(documentService.GetDocument(id)) is Document document) 
            return NotFound();

        var cd = new System.Net.Http.Headers.ContentDispositionHeaderValue("inline") {FileNameStar = document.DocumentName};
        Response.Headers.Add(Microsoft.Net.Http.Headers.HeaderNames.ContentDisposition, cd.ToString());

        return File(documentService.GetDocumentStream(id), document.ContentType ?? contentType);
    }

    /*
    public class Document
    {
        public Guid Id { get; set; }
        public string DocumentName { get; set; }
        public string ContentType { get; set; }
    }
    */
}

public class DocumentService
{
    private readonly DbConnection connection;

    public DocumentService(DbConnection connection)
    {
        this.connection = connection;
    }

    /* Other content omitted for brevity */

    public Stream GetDocumentStream(Guid documentId)
    {
        //Source table definition
        /*
            CREATE TABLE [dbo].[tblDocuments]
            (
                [DocumentId] [uniqueidentifier] NOT NULL,
                [ContentType] [varchar](100) NULL,
                [DocumentName] [varchar](100) NULL,
                [DocumentData] [varbinary](max) NULL
            CONSTRAINT [PK_DocumentId] PRIMARY KEY NONCLUSTERED ([DocumentID] ASC)
            )
            GO
         */

        const string query = "SELECT DocumentData FROM tblDocuments WHERE DocumentId = @documentId";

        //build up the command
        var command = connection.CreateCommand();
        command.CommandText = query;
        var parameter = command.CreateParameter();
        parameter.DbType = System.Data.DbType.Guid;
        parameter.ParameterName = "@documentId";
        parameter.Value = documentId;
        command.Parameters.Add(parameter);

        //Execute commmand with SequentialAccess to support streaming the data
        var reader = command.ExecuteReader(System.Data.CommandBehavior.SequentialAccess);

        if(reader.Read())
            return reader.GetStream(0);
        else
            return Stream.Null;
    }
}

Ответы [ 2 ]

2 голосов
/ 25 октября 2019

После публикации комментария я также вспомнил, что есть редко используемый API.

Вы можете зарегистрировать любые одноразовые классы, которые будут удалены в конце запроса (когда ответ будет написан):

HttpContext.Response.RegisterForDispose(reader);

В качестве альтернативы подключите ответный вызов OnCompleted и сделайтеваши очистки там (т.е. если объекты не являются одноразовыми или если вам нужно вызывать специальные методы как часть очистки)SqlSequencialStreamingResult класс может быть вашим лучшим выбором

0 голосов
/ 29 октября 2019

Так что я работал над всеми предложениями @ Tseng, сначала простыми, заканчивая более сложным. В конечном итоге я не смог использовать метод регистрации объекта, который должен быть удален для меня, потому что это происходит слишком поздно, и я очищаю свою транзакцию базы данных в методе OnResultExecuted моего ActionFilter. Поэтому я выбрал собственный класс ActionResult.

* StreamableDisposable (и его интерфейс) просто для того, чтобы упростить тот факт, что мне нужно вернуть две вещи, поток и устройство чтения данных, и не сделалхочу раскрыть основополагающий метод GetStream(0) для читателя, только вернув его.

public interface IStreamableDisposible : IDisposable
{
    public System.IO.Stream Stream { get; }
}
public class StreamableDisposible : IStreamableDisposible
{
    private readonly IDisposable toDisposeOf;

    public StreamableDisposible(System.IO.Stream stream, System.Data.Common.DbDataReader toDisposeOf)
    {
        Stream = stream ?? throw new ArgumentNullException(nameof(stream));
        this.toDisposeOf = toDisposeOf;
    }

    public System.IO.Stream Stream { get; set; }

    public void Dispose()
    {
        toDisposeOf?.Dispose();
    }
}

Это новый класс ActionResult, так что я могу гарантировать, что мой одноразовый предмет будет очищен сразу послепоток завершен и используется для выполнения результата.

public class DisposingFileStreamResult : FileStreamResult
{
    private readonly IStreamableDisposible streamableDisposible;

    public DisposingFileStreamResult(IStreamableDisposible streamableDisposible, string contentType)
        : base(streamableDisposible.Stream, contentType)
    {
        this.streamableDisposible = streamableDisposible ?? throw new ArgumentNullException(nameof(streamableDisposible));
    }

    public override void ExecuteResult(ActionContext context)
    {
        base.ExecuteResult(context);
        streamableDisposible.Dispose();
    }

    public override Task ExecuteResultAsync(ActionContext context)
    {
        return base.ExecuteResultAsync(context).ContinueWith(x => streamableDisposible.Dispose());
    }
}

Это позволяет мне обновить мой метод GetDocumentStream() следующим образом:

public StreamableDisposible GetDatabaseStream(Guid documentId)
{
    const string query = "SELECT DocumentData FROM tblDocuments WHERE DocumentId = @documentId AND DocumentData IS NOT NULL AND DATALENGTH(DocumentData) > 0";
    using var command = ((NHibernateData)Data).ManualCommand();

    command.CommandText = query;
    var parameter = command.CreateParameter();
    parameter.DbType = System.Data.DbType.Guid;
    parameter.ParameterName = "@documentId";
    parameter.Value = documentId;
    command.Parameters.Add(parameter);

    //Execute commmand with SequentialAccess to support streaming the data
    var reader = command.ExecuteReader(System.Data.CommandBehavior.SequentialAccess);
    if(reader.Read())
        return new StreamableDisposible(reader.GetStream(0), reader);
    else
    {
        reader.Dispose();
        return null;
    }
}

И теперь мое действие выглядит так

public IActionResult Stream(Guid id, string contentType = "application/octet-stream") // Defaults to octet-stream when unspecified
{
    // Simple lookup by Id so that I can use it for the Name and ContentType below
    if(!(documentService.GetDocument(id)) is Document document) 
        return NotFound();

    var cd = new System.Net.Http.Headers.ContentDispositionHeaderValue("inline") {FileNameStar = document.DocumentName};
    Response.Headers.Add(Microsoft.Net.Http.Headers.HeaderNames.ContentDisposition, cd.ToString());

    var sd = var sd = documentService.GetDocumentStream(id);
    return new DisposingFileStreamResult(sd, document.ContentType ?? contentType);
}

Я добавил проверки в оператор SELECT для учета пустых столбцов данных или просто нулевой длины данных, чтобы исключить необходимость иметь проверки как для StreamableDisposable, так и для самого Stream, равного нулю, или простовозможно никаких данных и т. д.

Это почти весь код, который я использовал в итоге.

...