Так что я работал над всеми предложениями @ 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
, равного нулю, или простовозможно никаких данных и т. д.
Это почти весь код, который я использовал в итоге.