0

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?

  • 5
    The [Microsoft guidelines](https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/choosing-between-class-and-struct) say that structs should not exceed 16 bytes, and should be immutable. Yours is *massive*, and mutable. Are you sure you don't want a class there? – canton7 Feb 12 '20 at 22:21
  • 2
    You're hitting this: [Struct's private field value is not updated using an async method](https://stackoverflow.com/questions/31642535/structs-private-field-value-is-not-updated-using-an-async-method/31646624#31646624). Turn your struct into a class. – canton7 Feb 12 '20 at 22:24

1 Answers1

1

Firstly, it's ideal if structs are immutable. This single rule would most likely fix your problem.

Secondly structs are value types, which means you are copying them a lot of the times (not the reference to them).

Just like any other value types, if you make copies of them, the changes are independent of the other copies

Structs (C# Programming Guide)

Structs are copied on assignment. When a struct is assigned to a new variable, all the data is copied, and any modification to the new copy does not change the data for the original copy. This is important to remember when working with collections of value types such as Dictionary<string, myStruct>.

halfer
  • 19,824
  • 17
  • 99
  • 186
TheGeneral
  • 79,002
  • 9
  • 103
  • 141
  • `fileContext` is only being assigned once though, I don't see anywhere that it would get copied. – yaakov Feb 12 '20 at 22:23
  • @yaakov async and await does does all sorts of magical things – TheGeneral Feb 12 '20 at 22:31
  • I was going to say the same thing as @yaakov, So are you saying here I should not use `struct` rather a class since `async` operation copies the values instead of referencing them? –  Feb 12 '20 at 22:33
  • @John yeah basically if you stick by the immutable struct paradigm you don't get this problem by default. Change it to a class and you are on your way – TheGeneral Feb 12 '20 at 22:35