I'm trying to minimize loading large objects into memory from the database when sending them out from an ASP.NET Core site because I'm hitting OutOfMemoryException on occasion.
I figured I'd stream it. Now from my research SQL Server supports this so long as you've specified CommandBehavior.SequentialAccess in your command. I figured if I'm going to stream it, I might as well stream it as directly as possible, so I'm pretty much streaming it directly from the DataReader to the ASP.NET MVC ActionResult.
But once the FileStreamResult (hidden under the call to File()) has finished executing, how do I clean up my reader/command? The connection was provided by DI, so that's not a problem, but I create the reader/command in the call to GetDocumentStream().
I have a subclass of ActionFilterAttribute registered in MVC, so that gives me an entry point I can call for ActionFilterAttribute.OnResultExecuted(), but I have absolutely no idea on what to put there other than my current logic that deals with cleaning up my database transaction and commit/rollback stuff (not included since it's not really relevant).
Is there a way to cleanup after my DataReader/Command and still provide a Stream to 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;
}
}
usingpattern for resources you allocated by yourselfResponse.RegisterForDisposeAsync(...)> "Registers an object for asynchronous disposal by the host once the request has finished processing."