Responscache i ASP.NET Core

Det här inlägget beskriver hur du lägger till responscache till controllers (klasser och metoder) i ASP.NET Core. Responscachning minskar belastningen på en webbserver, cachning innebär färre förfrågningar till en server och mindre arbete som behöver utföras av en server. Svarscache implementeras med huvudtaggar (headers) som anger hur klienter, ombud och mellanprogram tillfälligt skall spara den data som erhålls.

Responscache kan användas när den mottagna informationen väntas vara oförändrad under en tidsperiod, eller när det inte är viktigt att erhålla den senaste informationen vid varje begäran. Jag kommer att implementera svarscache för en API-metod som returnerar en tabell för ishockey, hockeytabeller förändras inte så ofta och jag vill kunna hantera så många förfrågningar från klienter och ombud som möjligt.

Cache-Control är den primära huvudtaggen som används för cache, den används för att ange cache-direktiven: public, private, max-age, no-cache och no-store. Andra cache-rubriker är: Age, Expires, Pragma and Vary.

Konfiguration

Vår webb-API-metod måste kunna hantera förfrågningar från JavaScript, vi lägger till en CORS-policy och indikerar att vi använder CORS i StartUp-klassen för vårt projekt.

using System;
using System.Globalization;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Rewrite;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Annytab.Middleware;
using Annytab.Repositories;
using Annytab.Options;
using Annytab.Rules;

namespace Hockeytabeller
{
    /// <summary>
    /// This class handles application startup
    /// </summary>
    public class Startup
    {
        /// <summary>
        /// Variables
        /// </summary>
        public IConfiguration configuration { get; set; }

        /// <summary>
        /// Create a new startup object
        /// </summary>
        /// <param name="configuration">A reference to configuration</param>
        public Startup(IConfiguration configuration)
        {
            this.configuration = configuration;

        } // End of the constructor method

        /// <summary>
        /// This method is used to add services to the container.
        /// </summary>
        /// <param name="services"></param>
        public void ConfigureServices(IServiceCollection services)
        {
            // Add the mvc framework
            services.AddRazorPages();

            // Add memory cache
            services.AddDistributedMemoryCache();

            // Add redis distributed cache
            if (configuration.GetSection("AppSettings")["RedisConnectionString"] != "")
            {
                services.AddDistributedRedisCache(options =>
                {
                    options.Configuration = configuration.GetSection("AppSettings")["RedisConnectionString"];
                    options.InstanceName = "Hockeytabeller:";
                });
            }

            // Add cors
            services.AddCors(options =>
            {
                options.AddPolicy("AnyOrigin", builder => builder.AllowAnyOrigin());
            });

            // Add the session service
            services.AddSession(options =>
            {
                // Set session options
                options.IdleTimeout = TimeSpan.FromMinutes(20d);
                options.Cookie.Name = ".Hockeytabeller";
                options.Cookie.Path = "/";
                options.Cookie.HttpOnly = true;
                options.Cookie.SameSite = SameSiteMode.Lax;
                options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
            });

            // Create database options
            services.Configure<DatabaseOptions>(options => 
            {
                options.connection_string = configuration.GetSection("AppSettings")["ConnectionString"];
                options.sql_retry_count = 1;
            });

            // Create cache options
            services.Configure<CacheOptions>(options => 
            {
                options.expiration_in_minutes = 240d;
            });

            // Add Authentication
            services.AddAuthentication()
                .AddCookie("Administrator", options =>
                {
                    options.ExpireTimeSpan = TimeSpan.FromDays(1);
                    options.Cookie.MaxAge = TimeSpan.FromDays(1);
                    options.Cookie.HttpOnly = true;
                    options.Cookie.SameSite = SameSiteMode.Lax;
                    options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
                    options.Events.OnRedirectToLogin = (context) =>
                    {
                        context.Response.StatusCode = StatusCodes.Status401Unauthorized;
                        context.Response.Redirect("/admin_login");
                        return Task.CompletedTask;
                    };
                })
                .AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("ApiAuthentication", null);


            // Add clients
            services.AddHttpClient();

            // Add repositories
            services.AddSingleton<IDatabaseRepository, MsSqlRepository>();
            services.AddSingleton<IWebsiteSettingRepository, WebsiteSettingRepository>();
            services.AddSingleton<IAdministratorRepository, AdministratorRepository>();
            services.AddSingleton<IFinalRepository, FinalRepository>();
            services.AddSingleton<IGroupRepository, GroupRepository>();
            services.AddSingleton<IStaticPageRepository, StaticPageRepository>();
            services.AddSingleton<IXslTemplateRepository, XslTemplateRepository>();
            services.AddSingleton<ISitemapRepository, SitemapRepository>();
            services.AddSingleton<IXslProcessorRepository, XslProcessorRepository>();
            services.AddSingleton<ICommonServices, CommonServices>();

        } // End of the ConfigureServices method

        /// <summary>
        /// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        /// </summary>
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IWebsiteSettingRepository website_settings_repository)
        {
            // Use error handling
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseStatusCodePagesWithReExecute("/home/error/{0}");
            }

            // Get website settings
            KeyStringList settings = website_settings_repository.GetAllFromCache();
            bool redirect_https = settings.Get("REDIRECT-HTTPS") == "true" ? true : false;
            bool redirect_www = settings.Get("REDIRECT-WWW") == "true" ? true : false;
            bool redirect_non_www = settings.Get("REDIRECT-NON-WWW") == "true" ? true : false;

            // Add redirection and use a rewriter
            RedirectHttpsWwwNonWwwRule rule = new RedirectHttpsWwwNonWwwRule
            {
                status_code = 301,
                redirect_to_https = redirect_https,
                redirect_to_www = redirect_www,
                redirect_to_non_www = redirect_non_www,
                hosts_to_ignore = new string[] { "localhost", "hockeytabeller001.azurewebsites.net" }
            };
            RewriteOptions options = new RewriteOptions();
            options.Rules.Add(rule);
            app.UseRewriter(options);

            // Use static files
            app.UseStaticFiles(new StaticFileOptions
            {
                OnPrepareResponse = ctx =>
                {
                    // Cache static files for 30 days
                    ctx.Context.Response.Headers.Append("Cache-Control", "public,max-age=25920000");
                    ctx.Context.Response.Headers.Append("Expires", DateTime.UtcNow.AddDays(300).ToString("R", CultureInfo.InvariantCulture));
                }
            });

            // Use sessions
            app.UseSession();

            // For most apps, calls to UseAuthentication, UseAuthorization, and UseCors must 
            // appear between the calls to UseRouting and UseEndpoints to be effective.
            app.UseRouting();

            // Use CORS
            app.UseCors();

            // Use authentication and authorization middlewares
            app.UseAuthentication();
            app.UseAuthorization();

            // Routing endpoints
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    "default",
                    "{controller=home}/{action=index}/{id?}");
            });

        } // End of the Configure method

    } // End of the class

} // End of the namespace

Mall för svar

Vår webb-API-metod returnerar html och mallen för svaret visas nedan.

@using Annytab.Repositories
@using Annytab.Models
@inject IGroupRepository group_repository
@{
    // Get a group
    Group group = ViewBag.Group;
    IList<TeamInGroupStanding> teams = this.group_repository.GetTeamsFromXml(group.data_content);
    Int32 rowCounter = 0;
}

@*Get teams in the group*@
<a href="@("https://www.hockeytabeller.se/home/group/" + group.page_name)" rel="nofollow" class="annytab-ht-not-hover">
    <div class="annytab-ht-table">
        <div class="annytab-ht-row">
            <div class="annytab-ht-col-th-normal">RK</div>
            <div class="annytab-ht-col-th-wide">Lag</div>
            <div class="annytab-ht-col-th-normal">GP</div>
            <div class="annytab-ht-col-th-normal">TP</div>
        </div>

@for (int j = 0; j < teams.Count; j++)
{
    @if (teams[j].name == "")
    {
        <div class="annytab-ht-row">
            <div class="annytab-ht-col-line"></div>
            <div class="annytab-ht-col-line"></div>
            <div class="annytab-ht-col-line"></div>
            <div class="annytab-ht-col-line"></div>
        </div>
    }
    else
    {
        rowCounter++;
        <div class="@(rowCounter % 2 != 0 ? "annytab-ht-row-main" : "annytab-ht-row-alt")">
            <div class="annytab-ht-col-normal">@((rowCounter).ToString())</div>
            <div class="annytab-ht-col-wide">@teams[j].name</div>
            <div class="annytab-ht-col-normal">@teams[j].games</div>
            <div class="annytab-ht-col-normal">@teams[j].points</div>
        </div>
    }
}
    </div>
</a>

API-controller

Innehållet i vår API-controller visas nedan. Ett ResponseCache-attribut kan anges för en klass eller för enskilda metoder i klassen. Vår API-metod kommer att returnera ett svar med en Cache-Control-tagg som har ett public-direktiv (ResponseCacheLocation.Any) och ett max-age-direktiv (Duration) på 3600 sekunder.

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Logging;
using Annytab.Repositories;
using Annytab.Models;

namespace Hockeytabeller.Api
{
    /// <summary>
    /// This class handles groups
    /// </summary>
    [Route("api/groups/[action]")]
    public class GroupsController : Controller
    {
        #region Variables

        private readonly ILogger logger;
        private readonly IGroupRepository group_repository;

        #endregion

        #region Constructors

        /// <summary>
        /// Create a new controller
        /// </summary>
        public GroupsController(ILogger<GroupsController> logger, IGroupRepository group_repository)
        {
            // Set values for instance variables
            this.logger = logger;
            this.group_repository = group_repository;

        } // End of the constructor

        #endregion

        #region Get methods

        // Get html by page name
        // GET api/groups/get_overview_as_html/shl
        [HttpGet("{id}")]
        [Microsoft.AspNetCore.Cors.EnableCors("AnyOrigin")]
        [ResponseCache(Duration = 3600, Location = ResponseCacheLocation.Any)]
        public IActionResult get_overview_as_html(string id = "")
        {
            // Get a group
            Group group = this.group_repository.GetOneByPageName(id);

            // Create view data
            ViewDataDictionary view_data = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary());
            view_data.Add("Group", group);

            // Return a partial view
            return new PartialViewResult { ViewName = "Views/home/_group_table.cshtml", ViewData=view_data, ContentType="text/html" };

        } // End of the get_overview_as_html method

        #endregion

    } // End of the class

} // End of the namespace

Testa responscache

Du kan köra tester genom att göra förfrågningar till API-metoden från Postman. Du kan också använda koden nedan för att köra tester, använd Chrome DevTools för att kontrollera att CORS fungerar och för att kontrollera att svar har cachats.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>Test</title>
    <style>
        .annytab-ht-not-hover {
            text-decoration: none;

        }

        .annytab-ht-table {
            display: table;
            width: 100%;
            padding: 0px;
            margin: 0px;
            font-family: Arial, Helvetica, sans-serif;
            background-color: #ffffff;
            color: #000000;
            overflow: hidden;
        }

        .annytab-ht-row {
            display: table-row;
        }

        .annytab-ht-row-main {
            display: table-row;
            background-color: #ffffff;
        }

        .annytab-ht-row-alt {
            display: table-row;
            background-color: #f0f0f0;
        }

        .annytab-ht-col-th-normal {
            display: table-cell;
            padding: 4px;
            color: #3d3d3d;
            border-bottom: 1px solid #9e9e9e;
            border-top: 1px solid #9e9e9e;
            font-size: 14px;
            line-height: 18px;
            vertical-align: middle;
            text-align: center;
            width: 20%;
        }

        .annytab-ht-col-th-wide {
            display: table-cell;
            padding: 4px;
            color: #3d3d3d;
            border-bottom: 1px solid #9e9e9e;
            border-top: 1px solid #9e9e9e;
            font-size: 14px;
            line-height: 18px;
            vertical-align: middle;
            text-align: left;
            width: 40%;
        }

        .annytab-ht-col-line {
            display: table-cell;
            height: 1px;
            background-color: #000000;
            padding: 0px;
        }

        .annytab-ht-col-normal {
            display: table-cell;
            padding: 4px;
            font-size: 12px;
            line-height: 12px;
            word-break: break-word;
            vertical-align: middle;
            text-align: center;
        }

        .annytab-ht-col-wide {
            display: table-cell;
            padding: 4px;
            font-size: 12px;
            line-height: 12px;
            word-break: break-word;
            vertical-align: middle;
            text-align: left;
        }
    </style>

</head>

<body style="width:100%;font-family:Arial, Helvetica, sans-serif;padding:20px;">

    <div class="hockeytabeller.se" data-group="shl"></div>

</body>

</html>

<script>
    // Initialize when DOM content has been loaded
    document.addEventListener('DOMContentLoaded', function () {
        var elements = document.getElementsByClassName('hockeytabeller.se');
        for (var i = 0; i < elements.length; i++) {
            get_group(elements[i]);
            }
        }, false);

    // Get a group
    function get_group(element)
    {
        var xhr = new XMLHttpRequest();
        xhr.open('GET', 'https://www.hockeytabeller.se/api/groups/get_overview_as_html/' + element.getAttribute('data-group'), true);
        xhr.onload = function () {
            if (xhr.status === 200) {
                element.insertAdjacentHTML('beforeend', xhr.response);
            }
        };
        xhr.send();

    } // End of the get_group method
</script>

Lämna ett svar

E-postadressen publiceras inte. Obligatoriska fält är märkta *