从数据库流式传输数据 - ASP.NET Core & SqlDataReader.GetStream()
Streaming data from the database - ASP.NET Core & SqlDataReader.GetStream()
在从 ASP.NET 核心站点发送大型对象时,我尽量减少将它们从数据库加载到内存中,因为我偶尔会遇到 OutOfMemoryException
。
我想我会流式传输它。现在根据我的研究 SQL 只要您在命令中指定 CommandBehavior.SequentialAccess
服务器就支持它。我想如果我要流式传输它,我最好尽可能直接流式传输它,所以我几乎是直接从 DataReader
流式传输它到 ASP.NET MVC ActionResult
.
但是一旦 FileStreamResult
(隐藏在对 File()
的调用下)完成执行,我该如何清理我的 reader/command?连接由 DI 提供,所以这不是问题,但我在 GetDocumentStream()
.
的调用中创建了 reader/command
我在 MVC 中注册了一个 ActionFilterAttribute
的子类,所以这给了我一个可以调用 ActionFilterAttribute.OnResultExecuted()
的入口点,但我完全不知道该放什么处理清理我的数据库事务和 commit/rollback 东西的当前逻辑(不包括在内,因为它不是真正相关的)。
有没有办法在我的 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;
}
}
发表评论后我也想起了一个很少用的API。
您可以注册任何一次性 classes 以在请求结束时处理(当响应已写入时):
HttpContext.Response.RegisterForDispose(reader);
或者挂接到 OnCompleted
响应回调并在那里进行清理(即,如果对象不是一次性的,或者如果您需要调用特殊方法作为清理的一部分)
HttpContext.Response.OnCompleted(() =>
{
reader.Dispose();
return Task.CompletedTask;
});
最后但并非最不重要的一点,如果用很多方法完成,拥有自己的 SqlSequencialStreamingResult
class 可能是您最好的选择
所以我按照@Tseng 的所有建议,先是简单的建议,然后是更复杂的建议。最终我无法使用为我注册要处理的对象的方法,因为在这个过程中发生得太晚了,我正在 ActionFilter
的 OnResultExecuted
方法中清理我的数据库事务。所以我选择了自定义 ActionResult
class.
StreamableDisposable
(及其接口)只是为了简化我需要 return 两件事的事实,流和数据 reader,而不想要仅通过 returning reader.
来公开 reader 的底层 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
class,这样我可以确保在流完成用于执行结果后立即清理我的一次性用品。
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
本身为空,或者可能没有数据等
这几乎是我最终使用的所有代码。
在从 ASP.NET 核心站点发送大型对象时,我尽量减少将它们从数据库加载到内存中,因为我偶尔会遇到 OutOfMemoryException
。
我想我会流式传输它。现在根据我的研究 SQL 只要您在命令中指定 CommandBehavior.SequentialAccess
服务器就支持它。我想如果我要流式传输它,我最好尽可能直接流式传输它,所以我几乎是直接从 DataReader
流式传输它到 ASP.NET MVC ActionResult
.
但是一旦 FileStreamResult
(隐藏在对 File()
的调用下)完成执行,我该如何清理我的 reader/command?连接由 DI 提供,所以这不是问题,但我在 GetDocumentStream()
.
我在 MVC 中注册了一个 ActionFilterAttribute
的子类,所以这给了我一个可以调用 ActionFilterAttribute.OnResultExecuted()
的入口点,但我完全不知道该放什么处理清理我的数据库事务和 commit/rollback 东西的当前逻辑(不包括在内,因为它不是真正相关的)。
有没有办法在我的 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;
}
}
发表评论后我也想起了一个很少用的API。
您可以注册任何一次性 classes 以在请求结束时处理(当响应已写入时):
HttpContext.Response.RegisterForDispose(reader);
或者挂接到 OnCompleted
响应回调并在那里进行清理(即,如果对象不是一次性的,或者如果您需要调用特殊方法作为清理的一部分)
HttpContext.Response.OnCompleted(() =>
{
reader.Dispose();
return Task.CompletedTask;
});
最后但并非最不重要的一点,如果用很多方法完成,拥有自己的 SqlSequencialStreamingResult
class 可能是您最好的选择
所以我按照@Tseng 的所有建议,先是简单的建议,然后是更复杂的建议。最终我无法使用为我注册要处理的对象的方法,因为在这个过程中发生得太晚了,我正在 ActionFilter
的 OnResultExecuted
方法中清理我的数据库事务。所以我选择了自定义 ActionResult
class.
StreamableDisposable
(及其接口)只是为了简化我需要 return 两件事的事实,流和数据 reader,而不想要仅通过 returning reader.
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
class,这样我可以确保在流完成用于执行结果后立即清理我的一次性用品。
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
本身为空,或者可能没有数据等
这几乎是我最终使用的所有代码。