dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.19k stars 9.93k forks source link

[ResponseCache] for Blazor static server rendering #49130

Open danroth27 opened 1 year ago

danroth27 commented 1 year ago

In MVC & Razor Pages we have attributes, like ResponseCacheAttribute and OutputCacheAttribute, for configuring response & output caching on a page or action. We should consider having similar support for Blazor server-side rendering.

ghost commented 1 year ago

We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.

marinasundstrom commented 1 year ago

This useful especially in the context of using server-side rendering (SSR) to gain performance.

Adding support for caching components would server those who are building apps relying on parts if a web page being cached due to high-load. E-commerce sites and sites selling tickets to concerts etc.

Also worth mentioning:

In MVC Razor and Razor Pages, you can cache certain portions of a view using a tag helper. So apart from attributes you can have a special Cache component as well.

<Cache ExpiresAfter="@TimeSpan.FromSeconds(120)">
    @DateTime.Now
</Cache>

Modeled after: https://learn.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/built-in/cache-tag-helper?view=aspnetcore-7.0

ghost commented 8 months ago

We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.

DamianEdwards commented 8 months ago

I think this already works, we use the OutputCache attribute on a Razor Component page rendered using SSR in the Aspire Starter App template.

JeepNL commented 8 months ago

@DamianEdwards Do you maybe have a link? I'm interested to try this!

DamianEdwards commented 7 months ago

https://github.com/dotnet/aspire/blob/main/src/Aspire.ProjectTemplates/templates/aspire-starter/AspireStarterApplication.1.Web/Components/Pages/Weather.razor#L3

JeepNL commented 7 months ago

@DamianEdwards Thanks, I'm gonna check it out!

VahidN commented 6 months ago

This is a simplified version of CacheTagHelper for Blazor SSR which can be used to cache the content of a given component:

CacheComponent.razor

@using Microsoft.Extensions.Caching.Memory
@typeparam TComponent where TComponent : IComponent

@if (_cachedContent != null)
{
    @((MarkupString)_cachedContent)
}

@code{

    private const string CacheKeyPrefix = $"__{nameof(CacheComponent<TComponent>)}__";
    private readonly TimeSpan _defaultExpiration = TimeSpan.FromSeconds(30);
    private string? _cachedContent;

    [Inject] internal HtmlRenderer HtmlRenderer { set; get; } = null!;

    [Inject] internal IMemoryCache MemoryCache { get; set; } = null!;

    /// <summary>
    ///     Parameters for the component.
    /// </summary>
    [Parameter]
    public IDictionary<string, object?>? Parameters { set; get; }

    /// <summary>
    ///     Gets or sets the exact <see cref="DateTimeOffset" /> the cache entry should be evicted.
    /// </summary>
    [Parameter]
    public DateTimeOffset? ExpiresOn { get; set; }

    /// <summary>
    ///     Gets or sets the duration, from the time the cache entry was added, when it should be evicted.
    /// </summary>
    [Parameter]
    public TimeSpan? ExpiresAfter { get; set; }

    /// <summary>
    ///     Gets or sets the duration from last access that the cache entry should be evicted.
    /// </summary>
    [Parameter]
    public TimeSpan? ExpiresSliding { get; set; }

    /// <summary>
    ///     Gets or sets the <see cref="CacheItemPriority" /> policy for the cache entry.
    /// </summary>
    [Parameter]
    public CacheItemPriority? Priority { get; set; }

    /// <summary>
    ///     Gets or sets the key of the cache entry.
    /// </summary>
    [Parameter, EditorRequired]
    public required string CacheKey { get; set; }

    private string CacheEntryKey => $"{CacheKeyPrefix}{CacheKey}";

    protected override Task OnInitializedAsync()
        => ProcessAsync();

    public void InvalidateCache()
        => MemoryCache.Remove(CacheEntryKey);

    private async Task ProcessAsync()
    {
        if (!MemoryCache.TryGetValue(CacheEntryKey, out _cachedContent))
        {
            _cachedContent = await HtmlRenderer.Dispatcher.InvokeAsync(async () =>
            {
                var output = await HtmlRenderer.RenderComponentAsync<TComponent>(Parameters is null ? ParameterView.Empty : ParameterView.FromDictionary(Parameters));

                return output.ToHtmlString();
            });

            _ = MemoryCache.Set(CacheEntryKey, _cachedContent, GetMemoryCacheEntryOptions());
        }
    }

    private MemoryCacheEntryOptions GetMemoryCacheEntryOptions()
    {
        var hasEvictionCriteria = false;
        var options = new MemoryCacheEntryOptions();
        options.SetSize(1);

        if (ExpiresOn != null)
        {
            hasEvictionCriteria = true;
            options.SetAbsoluteExpiration(ExpiresOn.Value);
        }

        if (ExpiresAfter != null)
        {
            hasEvictionCriteria = true;
            options.SetAbsoluteExpiration(ExpiresAfter.Value);
        }

        if (ExpiresSliding != null)
        {
            hasEvictionCriteria = true;
            options.SetSlidingExpiration(ExpiresSliding.Value);
        }

        if (Priority != null)
        {
            options.SetPriority(Priority.Value);
        }

        if (!hasEvictionCriteria)
        {
            options.SetSlidingExpiration(_defaultExpiration);
        }

        return options;
    }

}

Requirements:

builder.Services.AddMemoryCache();
builder.Services.AddScoped<HtmlRenderer>();

Usage:

<CacheComponent TComponent="MySidebarComponent"
                ExpiresAfter="TimeSpan.FromMinutes(1)"
                CacheKey="side-bar-menu-1"/>
zubairkhakwani commented 4 months ago

@DamianEdwards Thanks, I'm gonna check it out!

Did you manage to cache Blazor SSR page with or without .Net Aspire? Thank you in advance for your response.

zubairkhakwani commented 4 months ago

@javiercn , I was wondering if there are any plans to add support for Blazor SSR pages anytime soon. I appreciate your time and efforts.

danroth27 commented 4 months ago

@javiercn , I was wondering if there are any plans to add support for Blazor SSR pages anytime soon. I appreciate your time and efforts.

@zubairkhakwani Unfortunately, this work isn't planned for Blazor in .NET 9 due to competing priorities. We are open to community contributions for designing and implementing this feature.

danroth27 commented 4 months ago

Did you manage to cache Blazor SSR page with or without .Net Aspire? Thank you in advance for your response.

@zubairkhakwani Output caching should already work with Blazor static SSR pages. You can set up that output caching middleware and add @attribute [OutputCache] to static SSR pages that you want to cache. This functionality doesn't require .NET Aspire. Please let us know if you're seeing any issues with this functionality.

danroth27 commented 4 months ago

I've opened https://github.com/dotnet/aspnetcore/issues/55520 to separately track adding a Blazor Cache component as an analog to the existing cache tag helper used in MVC & Razor Pages apps.

zubairkhakwani commented 4 months ago

Did you manage to cache Blazor SSR page with or without .Net Aspire? Thank you in advance for your response.

@zubairkhakwani Output caching should already work with Blazor static SSR pages. You can set up that output caching middleware and add @attribute [OutputCache] to static SSR pages that you want to cache. This functionality doesn't require .NET Aspire. Please let us know if you're seeing any issues with this functionality.

I have registered the middleware just like I do it in MVC app

builder.Services.AddOutputCache(); 
app.UseOutputCache();

and on my SSR component page I used it like @attribute [OutputCache(Duration = 1000)]

to check if cache is working I am using @DateTime.Now.ToString("F") in the same component/Page but on every refresh the time is changed by one second.

I really appreciate your comment & please let me know if I am missing something or it is a bug. I am using dotnet 8.0.204

I am adding a GIF below to showcase the issue.

bandicam 2024-05-04 09-46-40-377

VahidN commented 4 months ago

@zubairkhakwani This order of defining middlewares works for me:

app.UseStaticFiles();
app.UseSession();
app.UseRouting();
app.UseAntiforgery();

app.UseOutputCache();

app.MapRazorComponents<App>();
app.MapControllers();
app.Run();
zubairkhakwani commented 4 months ago

@zubairkhakwani This order of defining middlewares works for me:

app.UseStaticFiles();
app.UseSession();
app.UseRouting();
app.UseAntiforgery();

app.UseOutputCache();

app.MapRazorComponents<App>();
app.MapControllers();
app.Run();

Thank you @VahidN putting app.UseOutputCache(); right after

app.UseStaticFiles();
app.UseAntiforgery();

worked like a charm. I had tried putting it before app.MapRazorComponents<App>() but it was not working because I was putting it after

app.UseAuthentication();
app.UseAuthorization();

Thank you again, and have a good day.

halter73 commented 6 days ago

I renamed this because, as noted in the previous comments, [OutputCache] works. [ResponseCache] does not. Unlike [OutputCache], [ResponseCache] was not designed to work outside of MVC and Razor Pages, but it does do things [OutputCache] does not like make it easy to just set client caching rules as noted by #56769, so there's still a gap.