Search code examples
c#asp.net-corecachingasp.net-core-webapibrowser-cache

ASP.NET Core Web API checking if-modified-since header not working because browser does not even submit to server


I have a controller that returns a FileStreamResult. Part of the processing looks to see if the 'if-modified-since' header is less that current date in the database to return either NotModified() or the actual FileStreamResult. The problem is, the client (in this case Swagger) doesn't even submit to my server so I can check the database date against the passed in header. I haven't set any 'caching' information in Startup. Given the code below, any ideas how to make sure it submits back to my web api so I can do the database compare?

Here is screenshot of Chrome network tab. You can see the first one (displayed as test) submitted to server (I had disable cache turned on), then submitted again and it was served by (disk cache) - never hitting the server.

enter image description here

Note, I followed these links as my template:

  1. https://rehansaeed.com/asp-net-core-caching-in-practice/#last-modified--if-modified-since
  2. https://www.geekytidbits.com/efficient-caching-dynamic-resources-asp-net-304-not-modified/
[ApiController]
public class DownloadSpecificVersion : ControllerBase
{
    private readonly IDbConnectionFactory dbConnectionFactory;

    public DownloadSpecificVersion( IDbConnectionFactory dbConnectionFactory ) => this.dbConnectionFactory = dbConnectionFactory;

    public record Parameters
    {
        private string _folder;
        [FromRoute]
        public string Folder
        {
            get => _folder.KatAppFolder();
            set => _folder = value;
        }

        [FromRoute]
        public string Name { get; init; }

        [FromRoute]
        public string Version { get; init; }
    }

    [HttpGet( "/api/kat-apps/{folder}/{name}/version/{version}" )]
    [SwaggerOperation(
        Summary = "Download specific version of KatApp kaml file",
        Description = "Download specific version of KatApp kaml file",
        OperationId = "KatApps.DownloadSpecificVersion",
        Tags = new[] { "KatApps" } )]
    [ProducesResponseType( typeof( FileStreamResult ), StatusCodes.Status200OK )]
    [ProducesResponseType( StatusCodes.Status304NotModified )]
    [ProducesResponseType( typeof( ValidationProblemDetails ), StatusCodes.Status401Unauthorized )]
    [ProducesResponseType( typeof( ValidationProblemDetails ), StatusCodes.Status404NotFound )]
    public async Task<IActionResult> HandleAsync( [FromQuery] Parameters parameters )
    {
        using ( var cn = await dbConnectionFactory.CreateDataLockerConnectionAsync() )
        {
            var keyInfo = await cn.QueryBuilder( $@"omitted, query to find item in db" ).QueryFirstOrDefaultAsync<CacheDownloadInfo>();

            return keyInfo != null
                ? await CachedOrModifiedAsync( keyInfo, dbConnectionFactory )
                : NotFound();
        }
    }
}

protected async Task<IActionResult> CachedOrModifiedAsync( CacheDownloadInfo cacheDownloadInfo, IDbConnectionFactory dbConnectionFactory )
{
    var lastModifiedDate = cacheDownloadInfo.LastModifiedDate.Value.ToUniversalTime();

    // https://rehansaeed.com/asp-net-core-caching-in-practice/#last-modified--if-modified-since
    // https://www.geekytidbits.com/efficient-caching-dynamic-resources-asp-net-304-not-modified/
    var requestHeaders = HttpContext.Request.GetTypedHeaders();

    // HTTP does not provide milliseconds, so remove it from the comparison
    if ( requestHeaders.IfModifiedSince.HasValue && lastModifiedDate.AddMilliseconds( -lastModifiedDate.Millisecond ) <= requestHeaders.IfModifiedSince.Value )
    {
        return NotModified();
    }

    var responseHeaders = HttpContext.Response.GetTypedHeaders();

    responseHeaders.LastModified = lastModifiedDate;

    var fs = File.OpenRead( "c:\test.txt" ); // This is really code that gets a 'stream' from the database
    return new FileStreamResult( fs, "text/plain" );
}

Solution

  • My issue was I was testing this in browser console with simple $.ajax() call. I needed to set the ifModified: true in the configuration for it call the server with the if-modified-since header.

    I discovered another issue when using Swagger. It only seemed to work if I set the following:

    responseHeaders.CacheControl = new Microsoft.Net.Http.Headers.CacheControlHeaderValue
    {
        Public = true,
        MustRevalidate = true,
        MaxAge = new TimeSpan( 0, 0, 0 ),
    };
    responseHeaders.Expires = DateTime.UtcNow;
    

    This will result in:

    Cache-Control:public, must-revalidate, max-age=0 Last-Modified:Sun, 10 Jun 2012 20:19:21 GMT