Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Asp.Net MVC Outputcache in base controller not working

I am using output cache attribute in asp.net mvc base controller but it is being called every time as it is in OnActionExecuting. Is there any option to call the method only onetime to load all default values?

protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
    GetDefaults();
    base.OnActionExecuting(filterContext);
}

[OutputCache(Duration = 60000)]
private ActionResult GetDefaults()
{
    //code to load all my default values
    // Viewdata values used in all pages in my aplication
    // Viewdata values used in all pages in my aplication
    // Viewdata values used in all pages in my aplication
    return null;
}

Any other best practice to load all default values for all pages and cache them?

like image 638
Kurkula Avatar asked Jan 29 '17 06:01

Kurkula


3 Answers

If all you want is to cache data, then OutputCache is not the right mechanism. OutputCache can be used to cache the HTML that is generated by an action method so it doesn't have to be regenerated. If you want to cache data, you can do so readily by using HttpContextBase.Cache.

Also, I recommend against using a base controller class. This is sure to mean you will be mixing logic for feature a and feature b and feature c in the same base controller - you are creating a god object. MVC has a better way - filters which can be registered to run for all actions and can be used on specific actions if you use them in conjunction with filters.

Although, there might not be any point at all to using a global filter for your cache, because normally data is cached at the point it is requested, I have created a demo of how it could be done. Note that caching is a very broad subject that can be done numerous ways, but since you have provided NO INFORMATION about what is being cached, what the source of the data is, or how it is being used, I am only showing this as one possible way.

MyCacheFilter

Here we have an action filter that does the caching. Since action filters are guaranteed to run before the view, this is one way it could be done.

MyCacheFilter does 3 things:

  1. Reads the data from the cache
  2. Checks whether the data exists, and if not, reloads the cache
  3. Adds a reference to the data object to the request cache, where it can be accessed from anywhere else in the application, including the view.

public class MyCacheFilter : IActionFilter
{
    /// <summary>
    /// The cache key that is used to store/retrieve your default values.
    /// </summary>
    private static string MY_DEFAULTS_CACHE_KEY = "MY_DEFAULTS";

    public void OnActionExecuted(ActionExecutedContext filterContext)
    {
        // Do nothing
    }

    public void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var cache = filterContext.HttpContext.Cache;

        // This method is called for each request. We check to ensure the cache
        // is initialized, and if not, load the values into it.
        IDictionary<string, string> defaults = 
            cache[MY_DEFAULTS_CACHE_KEY] as IDictionary<string, string>;
        if (defaults == null)
        {
            // The value doesn't exist in the cache, load it
            defaults = GetDefaults();

            // Store the defaults in the cache
            cache.Insert(
                MY_DEFAULTS_CACHE_KEY,
                defaults,
                null,
                DateTime.Now.AddHours(1), // Cache for exactly 1 hour from now
                System.Web.Caching.Cache.NoSlidingExpiration);
        }

        // Caching work is done, now return the result to the view. We can
        // do that by storing it in the request cache.
        filterContext.HttpContext.SetMyDefaults(defaults);
    }

    private IDictionary<string, string> GetDefaults()
    {
        // You weren't specific about where your defaults data is coming from
        // or even what data type it is, but you can load it from anywhere in this method
        // and return any data type. The type returned should either by readonly or thread safe.
        var defaults = new Dictionary<string, string>
        {
            { "value1", "testing" },
            { "value2", "hello world" },
            { "value3", "this works" }
        };


        // IMPORTANT: Cached data is shared throughout the application. You should make
        // sure the data structure that holds is readonly so it cannot be updated.
        // Alternatively, you could make it a thread-safe dictionary (such as ConcurrentDictionary),
        // so it can be updated and the updates will be shared between all users.
        // I am showing a readonly dictionary because it is the safest and simplest way.
        return new System.Collections.ObjectModel.ReadOnlyDictionary<string, string>(defaults);
    }
}

MyCacheFilter Usage

To use our cache filter and ensure the cache is populated before any view is show, we register it as a global filter. Global filters are great for keeping separate pieces of functionality in different classes, where they can be easily maintained (unlike a base controller).

public class FilterConfig
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        // Add our MyCacheFilter globally so it runs before every request
        filters.Add(new MyCacheFilter());
        filters.Add(new HandleErrorAttribute());
    }
}

HttpContextBaseExtensions

To make the cached data type-safe for the rest of the application, here are a couple of handy extension methods.

/// <summary>
/// Extensions for convenience of using the request cache in views and filters.
/// Note this is placed in the global namespace so you don't have to import it in your views.
/// </summary>
public static class HttpContextBaseExtensions
{
    /// <summary>
    /// The key that is used to store your context values in the current request cache.
    /// The request cache is simply used here to transfer the cached data to the view.
    /// The difference between the request cache (HttpContext.Items) and HttpContext.Cache is that HttpContext.Items
    /// is immediately released at the end of the request. HttpContext.Cache is stored (in RAM) for the length of
    /// the timeout (or alternatively, using a sliding expiration that keeps it alive for X time after 
    /// the most recent request for it).
    /// 
    /// Note that by using a reference type
    /// this is very efficient. We aren't storing a copy of the data in the request cache, we
    /// are simply storing a pointer to the same object that exists in the cache.
    /// </summary>
    internal static string MY_DEFAULTS_KEY = "MY_DEFAULTS";


    /// <summary>
    /// This is a convenience method so we don't need to scatter the reference to the request cache key
    /// all over the application. It also makes our cache type safe.
    /// </summary>
    public static string GetMyDefault(this HttpContextBase context, string defaultKey)
    {
        // Get the defaults from the request cache.
        IDictionary<string, string> defaults = context.Items[MY_DEFAULTS_KEY] as IDictionary<string, string>;

        // Get the specific value out of the cache that was requested.
        // TryGetValue() is used to prevent an exception from being thrown if the key doesn't
        // exist. In that case, the result will be null
        string result = null;
        defaults.TryGetValue(defaultKey, out result);

        return result ?? String.Empty;
    }

    /// <summary>
    /// This is a convenience method so we don't need to scatter the reference to the request cache key
    /// all over the application. It also makes our cache type safe.
    /// </summary>
    internal static void SetMyDefaults(this HttpContextBase context, IDictionary<string, string> defaults)
    {
        context.Items[MY_DEFAULTS_KEY] = defaults;
    }
}

Usage

Finally, we get to using the data from a view. Since we have extension methods on the HttpContextBase object, all we need to do is access it through the view and call our extension method.

<p>
    value1: @this.Context.GetMyDefault("value1")<br />
    value2: @this.Context.GetMyDefault("value2")<br />
    value3: @this.Context.GetMyDefault("value3")<br />
</p>

I have created a working demo of this solution in this GitHub repository.

Again, this isn't the only way. You may wish to alter this a bit for your application. For example, you could use ViewData to return the data to the view instead of HttpContextBase.Items. Or you may wish to get rid of the global filter and move the caching pattern into a single extension method that loads/returns the data from the cache. The exact solution depends on what you haven't provided in your question - requirements.

like image 134
NightOwl888 Avatar answered Oct 20 '22 13:10

NightOwl888


You can use either Application_Start() or Session_Start() in the Global.asax.cs for storing data only once depending on whether you want the data to be refreshed on application start or per session start. That would be up to what your application needs to do.

Having a base controller like you did is good if you need something to be done for every action. The most common being the [Authorize] filter that you need to do for every action to see if the user is authorized for security reasons. Another way to do this is also writing your own ActionFilterAttribute and doing the caching part you need to do. Then all you need to do is to add this new Action filter to any of the actions that you need to do this caching. Pleas see here for how to do this : https://msdn.microsoft.com/en-us/library/dd410056(v=vs.98).aspx But since you want the data to load only once I think an action filter may NOT be the right place to do caching.

like image 43
Arathy T N Avatar answered Oct 20 '22 14:10

Arathy T N


There is no way to leverage the the cache engine here. The reason is obvious if you consider why it (caching) actually works, when an action is called it is never the user code that's calling it, it is always the mvc framework that does. That way it has a chance to apply caching. In your example the user code is calling the method directly, there is no indirection here, just a plain old call - no caching is involved.

like image 21
Matan Shahar Avatar answered Oct 20 '22 14:10

Matan Shahar