OutputCache attribute not working on .NET 7 API endpoint
Asked Answered
M

2

11

I have an API REST .NET 7 and the attribute [OutputCache] is not caching even in a endpoint without authorization

I added this:

services.AddOutputCache(options => 
{    
   options.AddBasePolicy(builder => builder.Cache()); //removing this line doesn't work either            
});

And then in Configure():

app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
   endpoints.MapControllers();
});

app.UseOutputCache(); //putting it higher doesn't work either

My endpoint looks like this:

[AllowAnonymous] 
[OutputCache(Duration = 99999)] 
[HttpGet] 
public async Task<IActionResult> GetAsync([FromQuery] Dto dto) => ReturnDataFromDataBase();

But it doesn't work.

I followed the documentation https://learn.microsoft.com/en-us/aspnet/core/performance/caching/output?view=aspnetcore-7.0

Edit: I add more information, this project was an .net 5 and recently updated to .net 7 ([OutputCache] was introduced in .net 7, that is what I understood). It doesn't work because every time I make a request (with Postman) to this endpoint it enters the ReturnDataFromDataBase method (I put a breakpoint). I can't share my project because it's not mine, but this is the Configure method of startup (feel free to correct me):

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider)
    {
        //app.UseCors(builder => builder.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod());
        app.UseCors();
        app.UseAuthentication();
        app.UseExceptionHandler("/Error");
        app.UseIpRateLimiting();

        app.UseMvc(routes =>
        {
            routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}");
        });

        var pathBase = Configuration["APPL_PATH"];
        if (pathBase == "/")
            pathBase = "";
        if (!string.IsNullOrEmpty(pathBase))
        {
            app.UsePathBase(pathBase);
        }            

        ServiceProviderResolver.ServiceProvider = app.ApplicationServices;

        app.UseHttpsRedirection();
        app.UseRouting();
        app.UseAuthorization();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });

        app.UseOutputCache();
    }

Edit 2: If I move app.UseOutputCache() to the first place in Configure() it works, but the documentation says that it must be placed after UseCors() and UseRouting(), in that case it doesn't work.

Edit 3 (Solution for unauthenticated endpoints): The problem was app.UseMvc(), for some reason all controllers were inheriting from Controller(mvc) and not from ControllerBase, I changed it and then I could remove app.UseMvc() that made it works. I also changed the order like this:

Public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider)
    {
        app.UseIpRateLimiting();

        var pathBase = Configuration["APPL_PATH"];
        if (pathBase == "/")
            pathBase = "";
        if (!string.IsNullOrEmpty(pathBase))
        {
            app.UsePathBase(pathBase);
        }

        ServiceProviderResolver.ServiceProvider = app.ApplicationServices;

        app.UseHttpsRedirection();
        app.UseRouting();
        app.UseCors();
        app.UseAuthentication();
        app.UseAuthorization();
        app.UseOutputCache();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
   }

The next problem is that it doesn't work in endpoints that require authentication (jwt token).

Minier answered 15/12, 2022 at 0:19 Comment(8)
what you mean it doesn't work. the ReturnDataFromDataBase still be executed? or browser still call to back end?Epiboly
shouldn't we use here ResponseCache instead of OutputCache? and also in the configure after app.UseAuthorization() add "app.UseResponseCaching();" and delete the app.UseOutputCache()Telescope
I have tried your code, it work normally. please share your full project code to know more detail info.Epiboly
@Telescope "shouldn't we use here ResponseCache instead of OutputCache" - it depends. Response cache and output cache a different concepts, later one being introduced in .NET 7 AFAIK.Alben
I edited the post with more information. What I can't understand completely is if OutputCache can be used in endpoints that require authentication (jwt token). This is not the case anyway because this one has the AllowAnonimous attributeMinier
@EzequielIrigoyen is it possible, you put your code on github, so I can download and try it. I have used your code, it work normally. so It must be impacted by other part of your code.Epiboly
@灭蒙鸟 I can't share the project because it's not mine. Do you mean it works even in authenticated endpoints? because in endpoints that do not require authentication I already found the solutionMinier
@EzequielIrigoyen I have put sample code in below answer to demonstrate how to cache authorized endpoints. if you still have issue, please raise your issue.Epiboly
E
22

The default OutputCache policy don't cache any method with authorize end point. If you want to cache authorized api, you should customered policy to indicate what you want to cache.

the example of output cache policy

public class OutputCacheWithAuthPolicy : IOutputCachePolicy
    {
        public static readonly OutputCacheWithAuthPolicy Instance = new();
        private OutputCacheWithAuthPolicy() { }

        ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken)
        {
            var attemptOutputCaching = AttemptOutputCaching(context);
            context.EnableOutputCaching = true;
            context.AllowCacheLookup = attemptOutputCaching;
            context.AllowCacheStorage = attemptOutputCaching;
            context.AllowLocking = true;

            // Vary by any query by default
            context.CacheVaryByRules.QueryKeys = "*";
            return ValueTask.CompletedTask;
        }
        private static bool AttemptOutputCaching(OutputCacheContext context)
        {
            // Check if the current request fulfills the requirements to be cached
            var request = context.HttpContext.Request;

            // Verify the method, we only cache get and head verb
            if (!HttpMethods.IsGet(request.Method) && !HttpMethods.IsHead(request.Method))
            {
                return false;
            }
            // we comment out below code to cache authorization response.
            // Verify existence of authorization headers
            //if (!StringValues.IsNullOrEmpty(request.Headers.Authorization) || request.HttpContext.User?.Identity?.IsAuthenticated == true)
            //{
            //    return false;
            //}
            return true;
        }
        public ValueTask ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellation) => ValueTask.CompletedTask;
        public ValueTask ServeResponseAsync(OutputCacheContext context, CancellationToken cancellation) => ValueTask.CompletedTask;

    }

and then register your policy:

builder.Services.AddOutputCache(options =>
            {
                options.AddBasePolicy(builder => builder.Cache());
                options.AddPolicy("OutputCacheWithAuthPolicy", OutputCacheWithAuthPolicy.Instance); 
            });

then when you cached output , you should mark it with this policy:

[Authorize] // the end point is authorized
        [OutputCache(Duration = 600, PolicyName = "OutputCacheWithAuthPolicy")] // incicate policy name
        [HttpGet("GetWeatherForecastWithAuth/{id}/{second}")]
        public IEnumerable<WeatherForecast> GetWithAuth([FromRoute] string id, [FromRoute] string second)
Epiboly answered 18/12, 2022 at 5:50 Comment(0)
A
7

ASP.NET Core middlewares process request in order of registration (see more here), so having app.UseOutputCache(); as last element of processing pipeline does not make sense, you should move it before the middleware which output you want to cache (so app.UseOutputCache(); should be called at least before app.UseEndpoints(...)):

app.UseOutputCache();
app.UseEndpoints(endpoints =>
{
    endpoints.MapControllers();
});
Alben answered 16/12, 2022 at 16:34 Comment(2)
Thank you, I'd add that we have to take into account that app.UseMvc() could cause problems (how I wrote in Edit 3)Minier
This was it for me. Microsoft docs didn't state this in the notes section learn.microsoft.com/en-us/aspnet/core/performance/caching/…. @EzequielIrigoyen mark it as correct answerSweeten

© 2022 - 2024 — McMap. All rights reserved.