I have an ASP.NET Core 3.1 based project written in C#. I have the following struct
object that I am trying to call using a middleware.
internal struct StaticAsyncFileContext
{
private const int StreamCopyBufferSize = 64 * 1024;
private readonly HttpContext _context;
private readonly StaticAsyncFileOptions _options;
private readonly HttpRequest _request;
private readonly HttpResponse _response;
private readonly ILogger _logger;
private readonly IAsyncFileProvider _fileProvider;
private readonly IContentTypeProvider _contentTypeProvider;
private string _method;
private bool _isGet;
private PathString _subPath;
private string _contentType;
private IFileInfo _fileInfo;
private long _length;
private DateTimeOffset _lastModified;
private EntityTagHeaderValue _etag;
private RequestHeaders _requestHeaders;
private ResponseHeaders _responseHeaders;
private PreconditionState _ifMatchState;
private PreconditionState _ifNoneMatchState;
private PreconditionState _ifModifiedSinceState;
private PreconditionState _ifUnmodifiedSinceState;
private RangeItemHeaderValue _range;
public IFileInfo GetFileInfo()
{
return _fileInfo;
}
public StaticAsyncFileContext(HttpContext context, StaticAsyncFileOptions options, PathString matchUrl, IAsyncFileProvider fileProvider, IContentTypeProvider contentTypeProvider)
{
_context = context;
_options = options;
_request = context.Request;
_response = context.Response;
_requestHeaders = _request.GetTypedHeaders();
_responseHeaders = _response.GetTypedHeaders();
_fileProvider = fileProvider ?? throw new ArgumentNullException($"{nameof(fileProvider)} cannot be null.");
_contentTypeProvider = contentTypeProvider ?? throw new ArgumentNullException($"{nameof(contentTypeProvider)} cannot be null.");
_subPath = PathString.Empty;
_method = null;
_contentType = null;
_fileInfo = null;
_length = 0;
_etag = null;
_range = null;
_isGet = false;
IsHeadMethod = false;
IsRangeRequest = false;
_lastModified = new DateTimeOffset();
_ifMatchState = PreconditionState.Unspecified;
_ifNoneMatchState = PreconditionState.Unspecified;
_ifModifiedSinceState = PreconditionState.Unspecified;
_ifUnmodifiedSinceState = PreconditionState.Unspecified;
}
internal enum PreconditionState
{
Unspecified,
NotModified,
ShouldProcess,
PreconditionFailed
}
public bool IsHeadMethod { get; private set; }
public bool IsRangeRequest { get; private set; }
public string SubPath
{
get { return _subPath.Value; }
}
public async Task<bool> LookupFileInfoAsync()
{
_fileInfo = await _fileProvider.GetFileInfoAsync(_subPath.Value);
if (_fileInfo.Exists)
{
_length = _fileInfo.Length;
DateTimeOffset last = _fileInfo.LastModified;
// Truncate to the second.
_lastModified = new DateTimeOffset(last.Year, last.Month, last.Day, last.Hour, last.Minute, last.Second, last.Offset).ToUniversalTime();
long etagHash = _lastModified.ToFileTime() ^ _length;
_etag = new EntityTagHeaderValue('\"' + Convert.ToString(etagHash, 16) + '\"');
}
return _fileInfo.Exists;
}
public void ApplyResponseHeaders(int statusCode)
{
// Here _fileInfo should not be null since it was set by LookupFileInfoAsync()
_response.StatusCode = statusCode;
if (statusCode < 400)
{
// these headers are returned for 200, 206, and 304
// they are not returned for 412 and 416
if (!string.IsNullOrEmpty(_contentType))
{
_response.ContentType = _contentType;
}
_responseHeaders.LastModified = _lastModified;
_responseHeaders.ETag = _etag;
_responseHeaders.Headers[HeaderNames.AcceptRanges] = "bytes";
}
if (statusCode == Constants.Status200Ok)
{
// this header is only returned here for 200
// it already set to the returned range for 206
// it is not returned for 304, 412, and 416
_response.ContentLength = _length;
}
_options.OnPrepareResponse(new StaticFileResponseContext(_context, _fileInfo));
}
public Task SendStatusAsync(int statusCode)
{
ApplyResponseHeaders(statusCode);
_logger.LogHandled(statusCode, SubPath);
return Task.CompletedTask;
}
public PreconditionState GetPreconditionState()
{
return GetMaxPreconditionState(_ifMatchState, _ifNoneMatchState, _ifModifiedSinceState, _ifUnmodifiedSinceState);
}
public async Task SendAsync()
{
ApplyResponseHeaders(Constants.Status200Ok);
string physicalPath = _fileInfo.PhysicalPath;
var sendFile = _context.Features.Get<IHttpResponseBodyFeature>();
if (sendFile != null && !string.IsNullOrEmpty(physicalPath))
{
// We don't need to directly cancel this, if the client disconnects it will fail silently.
await sendFile.SendFileAsync(physicalPath, 0, _length, CancellationToken.None);
return;
}
try
{
using (var readStream = _fileInfo.CreateReadStream())
{
// Larger StreamCopyBufferSize is required because in case of FileStream readStream isn't going to be buffering
await StreamCopyOperation.CopyToAsync(readStream, _response.Body, _length, StreamCopyBufferSize, _context.RequestAborted);
}
}
catch (OperationCanceledException ex)
{
_logger.LogWriteCancelled(ex);
// Don't throw this exception, it's most likely caused by the client disconnecting.
// However, if it was cancelled for any other reason we need to prevent empty responses.
_context.Abort();
}
}
}
In the above struct
when the method LookupFileInfoAsync()
is invoked, it suppose to set the value of _fileInfo
, _length
and _lastModified
properties. If the method GetPreconditionState()
or GetFileInfo()
are called after LookupFileInfoAsync()
, the value of _fileInfo
should NOT be null if _fileInfo.Exists
returns true.
Here is how my middleware invokes this struct
public async Task Invoke(HttpContext context)
{
var fileContext = new StaticAsyncFileContext(context, _options, _matchUrl, _logger, _fileProvider, _contentTypeProvider);
if (!await fileContext.LookupFileInfoAsync())
{
_logger.LogFileNotFound(fileContext.SubPath);
} else {
// This is an unesassary line, it is only added to ensure that FileInfo was set
IFileInfo fileInfo = fileContext.GetFileInfo();
switch (fileContext.GetPreconditionState())
{
case StaticAsyncFileContext.PreconditionState.Unspecified:
case StaticAsyncFileContext.PreconditionState.ShouldProcess:
if (fileContext.IsHeadMethod)
{
await fileContext.SendStatusAsync(Constants.Status200Ok);
return;
}
try
{
await fileContext.SendAsync();
_logger.LogFileServed(fileContext.SubPath, fileContext.PhysicalPath);
return;
}
catch (FileNotFoundException)
{
context.Response.Clear();
}
break;
//... stripped out for simplicity
default:
var exception = new NotImplementedException(fileContext.GetPreconditionState().ToString());
Debug.Fail(exception.ToString());
throw exception;
}
}
await _next(context);
}
As you can see that LookupFileInfoAsync()
is called first and that ensures that _infoFile
property is NOT null since it returned true. But in my case, calling fileContext.GetFileInfo()
after calling LookupFileInfoAsync()
return null. When stepping through my code I can differently see that _fileInfo
is being set correctly. But on the next method call, the value of _fileInfo
will be reset to null somehow. If you look at Microsoft's StaticFileContext.cs file, you'll notice that the code is very similar except that LookupFileInfo()
called is being called asynchronously in my case.
Question
What could be causing the _fileInfo
property to reset to null after LookupFileInfoAsync()
is called?