question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Why is /GetScripts not cachable and/or cached?

See original GitHub issue

Hi,

  • ASP.NET 4.6.1
  • ABP 3.7.2
  • MVC without ZERO (ie permissions, menu etc). Basically we GetScripts() for generating service proxies

Now while I realise it is generated at runtime, in my case it is 100% the same all of the time. And as a result it is returned for every single page request.

So why can it not be cached internally with a circuit breaker as needed?

We use the following circuit breaker code to deal with physical files on disk where we hash the last changed file time:

    /// <summary>
    ///     <para>
    ///         Use this class as follows to create cache busing URL's:
    ///     </para>
    ///     <para>
    ///         <link rel="stylesheet" href="@OzCpCacheBusterHelper.FingerPrint('/css/mainStyle.css')" />
    ///     </para>
    /// </summary>
    /// <remarks>
    ///     <para>
    ///         By adding this snippet of XML to the web.config’s
    ///         <system.webServer>
    ///             section, we instruct IIS 7+ to intercept all URLs with a
    ///             folder name containing “v=[md5Hash]” and rewrite the URL to the original file path.
    ///     </para>
    ///     <para>
    ///         <!-- Rule to support OzCpCacheBusterHelper -->
    ///         <rule name="fingerprint" stopProcessing="true">
    ///             <match url="(.*)(v-[a-zA-Z0-9]+/)([\S]+)" />
    ///             <action type="Rewrite" url="{R:1}/{R:3}" />
    ///         </rule>
    ///     </para>
    ///     <para>
    ///         You need to run the AppPool in Integrated Pipeline mode for the <system.webServer> section to have any effect.
    ///     </para>
    /// </remarks>
    public static class OzCpCacheBusterHelper
    {
        /// <summary>
        ///     Constructs a cache busting path in the form of /original/path/to/file.css/v-xxx where
        ///     xxx is the MD5 hash of the last write timestamp of the physical file located at
        ///     \WebRoot\original\path\to\file.css
        /// </summary>
        /// <param name="aRootRelativePath">Relative path to the file.</param>
        /// <param name="aCdnPath">Path to the CDN version of the file.</param>
        /// <returns>Returns a valid cache busting path to the original resource.</returns>
        public static string FingerPrint(string aRootRelativePath, string aCdnPath = "")
        {
            //Return the CDN path if we have one assigned and we are not debugging
            if (!string.IsNullOrEmpty(aCdnPath) && !HttpContext.Current.IsDebuggingEnabled)
            {
                return aCdnPath;
            }

            //Should the relative path not already exist in the cache then lets add it
            if (HttpRuntime.Cache[aRootRelativePath] == null)
            {
                //Determine both absolute and relative paths
                string relative = VirtualPathUtility.ToAbsolute("~" + aRootRelativePath);
                string absolute = HostingEnvironment.MapPath(relative);

                if (!File.Exists(absolute))
                {
                    throw new FileNotFoundException("File not found", absolute);
                }

                //Construct a file path in the form {relative}/v-{Md5 Of Last Write Time}/ to ensure we have unique path 
                DateTime fileLastWriteTime = File.GetLastWriteTime(absolute);
                int index = relative.LastIndexOf('/');
                string result = relative.Insert(index, "/v-" + Md5($"{fileLastWriteTime:yyy-MM-dd_hh:mm:ss.fff}"));
                HttpRuntime.Cache.Insert(aRootRelativePath, result, new CacheDependency(absolute));
            }
            return HttpRuntime.Cache[aRootRelativePath] as string;
        }

        /// <summary>
        ///     Computes an MD5 hash of the input value
        /// </summary>
        /// <param name="aInput">String value to hash.</param>
        /// <returns>Returns an MD5 hash of the input as a string.</returns>
        private static string Md5(string aInput)
        {
            //Compute the hash
            MD5 md5 = MD5.Create();
            byte[] inputBytes = Encoding.ASCII.GetBytes(aInput);
            byte[] hash = md5.ComputeHash(inputBytes);

            //Convert byte array to string
            StringBuilder sb = new StringBuilder();
            foreach (byte hashCharacter in hash)
            {
                sb.Append(hashCharacter.ToString("X2"));
            }
            return sb.ToString();
        }
    }

That way clients receive a cached copy until the file is updated. I would think something similair could be applied to the generated contents of GetScripts() ie. An MD5 of its contents and if unchanged then the content is returned from a HttpRuntime.Cache[] entry.

The time to perform the above MD5 and cache inspection would be well worth it as I am guessing that would be less than the time it takes to deliver GetScripts() on the wire as well as the saved bytes on the wire.

Thoughts?

Issue Analytics

  • State:closed
  • Created 5 years ago
  • Comments:28 (25 by maintainers)

github_iconTop GitHub Comments

2reactions
bbakermmccommented, Dec 10, 2018

@ismcagdas You guys already cache it at the server level, so how is it any different if you were to cache the requests lol. Just invalidate it when someone changes a permission or localization in the UI. You already do it now for the local cache. The other option is to break out things that can be cached and what can not. I assume the app service js stuff can be.

https://stackoverflow.com/questions/1167890/how-to-programmatically-clear-outputcache-for-controller-action-method

2reactions
natikicommented, Sep 21, 2018

As we don’t have permissions issues we did end up caching it in our MVC applications by changing our templates to call:

Our Layout controller then has /layout/getscripts:

/// <summary>
///     Allows us to cache the ABP GetScripts() call which is slow and unchanging.
/// </summary>
/// <returns>Returns the result of /AbpScripts/GetScripts</returns>
[OutputCache(Duration = 300)]
public virtual ActionResult GetScripts()
{
    //Return they JS we got from the ABP end point via Flurl
    return new JavaScriptResult
           {
               Script = OzCrGetScriptsHelper.GetScripts(Request.RequestContext.HttpContext, ApplicationSettings.Optomizations.BundlingAndMinification.Value)
           };
}

And we have a common method to minify it after retrieving it from the original end point /abscripts/getscripts:

namespace OzCruisingPresentation.Mvc.Helpers.GetScripts
{
    /// <summary>
    ///     Class to manage the retrieval of the scripts returned from ABP\GetScripts so that we can minify it etc.
    /// </summary>
    public static class OzCrGetScriptsHelper
    {
        /// <summary>
        ///     Gets the content of ABP\GetScripts and optionally minifies it.
        /// </summary>
        /// <param name="aHttpContext">Context the request is running in.</param>
        /// <param name="aBundlingAndMinificationEnabled">Based on whether we are optomising or not.</param>
        /// <returns>Returns the javascript optionally minified.</returns>
        public static string GetScripts(HttpContextBase aHttpContext, bool aBundlingAndMinificationEnabled)
        {
            //#TODO (DE) 2018-08-21: Improve this once the authors have fixed:
            //#TODO (DE) 2018-08-21: https://github.com/aspnetboilerplate/aspnetboilerplate/issues/3673
            //#TODO (DE) 2018-08-21: https://github.com/tmenier/Flurl/issues/365   //Don't like the proposed solution. This will be sorted when we move to the developer localised domains

            //Get the contents of what ABP needs to return
            string result;
            if (OzCpSiteLocationHelper.IsDeveloperMachine() || aHttpContext.Request.Url.Host.Equals("localhost", StringComparison.OrdinalIgnoreCase))
                result = AsyncHelper.RunSync(() => $"http://{aHttpContext.Request.Url.Host}/abpscripts/getscripts".GetStringAsync());
            else
                result = AsyncHelper.RunSync(() => $"https://{aHttpContext.Request.Url.Host}/abpscripts/getscripts".GetStringAsync());

            if (aBundlingAndMinificationEnabled)
            {
                UglifyResult resultMinified = Uglify.Js(result);

                return resultMinified.HasErrors ? result : resultMinified.Code;
            }

            //Return what we have constructed
            return result;
        }
    }
}

Note: The SSL complication can be avoided ultimately by either not using SSL, not using Flurl, Using Flurl and implementing a custom certificate handler.

@hikalkan Hopefully you can add a module configuration that allows minification of any dynamically generated scripts as well.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Is it possible to use jquery getScript on a file that is cached ...
1 Answer 1 · 1. Hmm... doesn't seem to be honoring the cache: true for me as it is still appending the query...
Read more >
DXR.axd is not being cached
Hi, Just found that DXR.axd isn't being cached for some unknown reason.. It's re-downloading that same resource file on every page load, ...
Read more >
jQuery .getScript() refactor to prevent caching
getScript() function is that it includes a unique id (timestamp or such) to each ajax script call.
Read more >
Why doesn't FireFox cache my JavaScript file?
1 Answer. It is not recommended to cache resources with a query string. A query string usually represents a dynamic resource. However IE...
Read more >
Opting Out of Caching
Turbo Drive maintains a cache of recently visited pages. ... To specify that a page should not be cached at all, use the...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found