.netasp.net-core.net-8.0

How to best add the Last-Modified header in asp .net middleware


Is there a way of adding the Last-Modified header to an API response in asp .net? Perhaps a "best practice"?

I'm reading a date with "last modified" from the database, and would like to set that in the header so that resources can cache accordingly. I guess I don't know enough about the life cycle of the response. Is there an event I can attach to where I have access to both the response (headers) and before the result object is serialized to JSON ?

 app.Use(async (context, next) =>
        {                
            context.Response.OnStarting(() =>
            {
                 // context.Response.Body seams to already have been set to a 
                 // stream object, and I would in stead like to find the date in 
                 // the object I'm sending back, which implements an interface 
                 // containing the date
                 context.Response.Headers.Append("Last-Modified","????");
            }
        })

The code in the API controller

[HttpGet]
    [Route("system/resources/{lcid}")]
    [ProducesResponseType(StatusCodes.Status200OK)]        
    public async Task<IResult> Get(int lcid, CancellationToken token)
    {            
        var resources = (await _resourceManager.GetResourcesAsync(lcid, token).ConfigureAwait(false));


        if (resources?.Items?.Any() ?? false)
        {                
            // resources has a .LastModified property
            return Results.Ok(resources );
        }
        else
        {
            return Results.NotFound();
        }

    }

https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified


Solution

  • The easiest way is to just set the header in the action itself :

       if (resources?.Items?.Any() ?? false)
        {                
            Response.Headers.LastModified=resources.LastModified;
            // resources has a .LastModified property
            return Results.Ok(resources );
        }
        else
        {
            return Results.NotFound();
        }
    

    A reusable option could be to create an action filter. Let's assume the result implements this ICacheInfo interface so we don't have to use reflection. It uses a default interface member so it can be applied to objects and modified without breaking the object :

    interface ICacheInfo
    {
        DateTimeOffset LastModified=>DateTimeOffset.Now;
    }
    

    The action filter could look like this:

    public class LastModifiedHeaderAttribute : ActionFilterAttribute
    {
        public override void OnResultExecuting(ResultExecutingContext context)
        {
            if (context.Result is OkObjectResult {Value:ICacheInfo c})
            {
                context.HttpContext.Response.Headers.LastModified=c.LastModified;
            }
    
            base.OnResultExecuting(context);
        }
    }
    

    And applied as an attribute:

    [HttpGet]
    [Route("system/resources/{lcid}")]
    [ProducesResponseType(StatusCodes.Status200OK)]        
    [LastModifiedHeader]
    public async Task<IResult> Get(int lcid, CancellationToken token)
    {  
    ...
    }