0

I have an MVC 5 website that uses Entity Framework for the database interactions.

I would like to use an IEnumerable as a private variable in a controller so that other custom ActionResults in the same controller can use the same information without having to re-query each time. I don't mean the other CRUD ActionResults, but rather other custom methods that do things with the data seen on the Index page, which is usually a subset of the full database table. It would be helpful to query once, then re-use the same data.

In this example, I have private IEnumerable<CourseList> _data; as a class-level variable and IEnumerable<CourseList> data as an Index()-level variable. I use Debug.WriteLine to determine if each variable is empty or not.

As I expect, within the scope of the Index() ActionResult both variables data and _data are not null. Within the scope of ClickedFromIndexPageLink(), _data -- the class-level variable is null.

My theory is that while i have sequentially loaded Index first, the controller doesn't know that. And as far as the controller is concerned, when I request _data contents in the other ActionResult, it hasn't been filled yet. However, in real time, I have clicked Index first and so should expect to see _data filled with the Index query.

To see what I do as a workaround, scroll down to see "Method B (does work, but is repetitive)".

Ts there any simple way to have an IEnumerable used as a private class variable in this manner, or is my workaround the only possible approach?

Method A (doesn't work):

Debug.WriteLine() results:

Begin Index() test *****
Is data Null? Not Null
Is _data Null? Not Null
End Index test *****

Begin ClickedFromIndexPageLink() test*****
Is _data Null? Null
End ClickedFromIndexPageLink test*****

The code:

using IenumerableAsClassVariable.Models;
using System.Collections.Generic;
using System.Data.Entity;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Web.Mvc;

namespace IenumerableAsClassVariable.Controllers
{
    // This is the helper class & function used to determine if the IEnumerable is null or empty
    public static class CustomHelpers
    {
        public static string IsNullOrEmpty<T>(this IEnumerable<T> enumerable)
        {
            if (enumerable == null)
                return "Null";
            else
                return "Not Null";
        }
    }

    public class CourseListsController : Controller
    {
        private CreditSlipLogContext db = new CreditSlipLogContext();
        private IEnumerable<CourseList> _data;

        // If IEnumerable is null or empty return true; else false.       

        // GET: CourseLists
        public ActionResult Index()
        {
            IEnumerable<CourseList> data = db.CourseLists.AsEnumerable();
            Debug.WriteLine("-----");
            Debug.WriteLine("Begin Index test *****");
            Debug.WriteLine("Is data Null? " + CustomHelpers.IsNullOrEmpty(data));
            _data = data;
            Debug.WriteLine("Is _data Null? " + CustomHelpers.IsNullOrEmpty(_data));
            Debug.WriteLine("End Index test *****");           

            return View(db.CourseLists.ToList());
        }

        public ActionResult ClickedFromIndexPageLink()
        {

            Debug.WriteLine("Begin ClickedFromIndexPageLink test*****");
            Debug.WriteLine("Is _data Null? " + CustomHelpers.IsNullOrEmpty(_data));
            Debug.WriteLine("End ClickedFromIndexPageLink test*****");

            ViewBag.IsDataNull = CustomHelpers.IsNullOrEmpty(_data);

            return View();
        }

        #region OtherCrudActionResultsAreHidden
        #endregion

    }
}

Method B (does work, but is repetitive):

As I expect, my results aren't null:

Begin ClickedFromIndexPageLink test*****
Is data Null? Not Null
Is _data Null? Not Null
End ClickedFromIndexPageLink test*****

This is because I re-query in the ActionResult, just as I do in the Index() ACtionResult:

public ActionResult ClickedFromIndexPageLink()
        {
            IEnumerable<CourseList> data = db.CourseLists.AsEnumerable();

            Debug.WriteLine("Begin ClickedFromIndexPageLink test*****");
            Debug.WriteLine("Is data Null? " + CustomHelpers.IsNullOrEmpty(data));
            _data = data;
            Debug.WriteLine("Is _data Null? " + CustomHelpers.IsNullOrEmpty(_data));
            Debug.WriteLine("End ClickedFromIndexPageLink test*****");

            ViewBag.IsDataNull = CustomHelpers.IsNullOrEmpty(_data);

            return View();
        }

3 Answers3

5

Everytime you call an action method is a separate Http request. Remember, Http is stateless and one request has no idea what the previous request did. so you won't get the private variable value you did set in your previous action method call.

You may consider caching the data which will be available to multiple requests until the cache expires. You may use the MemoryCache class available in dot net.

Quick sample

const string  CacheKey = "courses";
public ActionResult Index()
{
    var courseList = GetCourses();
    // do something with courseList 
    return View(courseList );
}
public ActionResult List()
{
    var course = GetCourses();
   // do something with courseList 
    return View(course);
}

private List<Category> GetCourses()
{
    var db = new YourDbContext();
    var cache = MemoryCache.Default;
    if (!cache.Contains(CacheKey))  // checks to see it exists in cache
    {
        var policy = new CacheItemPolicy();
        policy.AbsoluteExpiration = DateTime.Now.AddDays(1);

        var courses = db.CourseLists.ToList();
        cache.Set(CacheKey , courses, policy);
    }
    return (List<Category>) cache.Get(CacheKey);
}

Of course you may move this away from your controller code to a new class/layer to keep separation of concern.

If you prefer to convert your entity object to simply POCO/ViewModel collection before storing in cache,

 var courseVms = db.CourseLists.Select(s=>new CourseViewModel {
                          Id =s.Id, Name=s.Name }).ToList();

 cache.Set(cacheKey, courseVms , policy);

And your GetCourses method will be returning a List<CourseViewModel>

Remember, caching will keep the data until the cache expires. So it is a good idea to keep data which won't usually change that often (Ex: Look up data etc). If you are caching your transactional data, you need to update the cache every time a change is made to the data (Ex : A new course is added, one course is deleted etc..)

MemoryCache class resides in System.Runtime.Caching namespace which resides in System.Runtime.Caching.dll. So you need to add a reference to this assembly.

If you want to do the same kind of caching within your ASP.NET5/ MVC6 application, You may use the IMemoryCache implementation as explained in this answer.

Community
  • 1
  • 1
Shyju
  • 214,206
  • 104
  • 411
  • 497
  • I notice in your example, you use a "single column" list. Will this example work for a model-typed IEnumerable with multiple columns? –  Dec 30 '15 at 15:59
  • Yes. I will update the answer with your sample data. – Shyju Dec 30 '15 at 15:59
  • This answer tackles the heart of the problem (you need caching) but I'd strongly recommend the System.Web.Caching `Cache` and `CacheDependency` types for caching so you don't have to manage a cache and also so your code is more maintainable. – moarboilerplate Dec 30 '15 at 16:19
  • This is where I'm trying to replicate the code. I found the `System.Web.Caching` namespace but can't find `MemoryCache.Default`. –  Dec 30 '15 at 16:22
  • @Rubix_Revenge you'd choose one or the other. MemoryCache is in a separate assembly (`System.Runtime.Caching`). To store something in the cache you're talking about, just call the static method `Cache.Add`. – moarboilerplate Dec 30 '15 at 16:26
  • @moarboilerplate Is correct. I updated my answer to include the Assembly and namespace information. – Shyju Dec 30 '15 at 16:30
  • I've never written any caching code before, so I'm going to have to give myself a crash course. You'd addressed the big question: ActionResults are independent of one another because they are stateless. What a user does in real time does not equate with what the controller's ActionResult sees. –  Dec 30 '15 at 16:37
0

Shyju's answer is right in that you need caching but I'd recommend the following options over managing a MemoryCache:

1) Use the ASP.NET cache in System.Web.Caching. Add to cache via Cache.Add, retrieve using the indexer (Cache["key"]) or Get. Note that this is a static reference so if you need to get at this in a business logic library you'll need to set up your data as a dependency of your business objects (you probably are going to want to inject this IEnumerable into the constructor of your controller at the very least).

2) Use a singleton. If you're not going to change this data, you can simply create a static IList or IReadOnlyList and set it once at application start (making it static, not an instance property is the key to making this persist across requests). You can wrap it via the more traditional singleton pattern if you want. You could also use an IoC container and register it as a singleton with an initializer method and let the container inject it where it is needed. *Note that a static property like this is inherently not thread-safe. If you need to change this data, use one of the thread-safe (concurrent) collections.

To sum up, here is the sequence of events you want:

(Design time) -define static thing

(Application start) -Set static thing to data/initialize static thing

(Application runtime) -Access static thing

moarboilerplate
  • 1,633
  • 9
  • 23
  • I'm using my code as a concrete example of what i am trying to do. The records in some models (and view) change several times an hour; some others won't change in a year. In any case, I'm thinking my "Method B" that I posted in my question isn't looking so bad after all. –  Dec 30 '15 at 16:39
  • @Rubix_Revenge if your data needs to be refreshed more frequently than when the app recycles, you need a more robust caching solution that can configure expirations. Or, like you said, you can reassess the cost/benefit of querying every time. FWIW, you can configure the ASP.NET cache entries with custom expirations. – moarboilerplate Dec 30 '15 at 16:48
0

@Shyju answered my question:

Everytime you call an action method is a separate Http request. Remember, Http is stateless and one request has no idea what the previous request did. so you won't get the private variable value you did set in your previous action method call.

I have not yet delved into caching, but part of his answer inspired me to adjust my code, so that I write my index query only once, but can call it from whatever method in the same controller that I want. In the end, it's still using Method B (presented in my question), but it enables me to type my index query only once and reduce typo or other simple coding errors that come from reduplicating code.

using IenumerableAsClassVariable.Models;
using System.Collections.Generic;
using System.Data.Entity;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Web.Mvc;
using System.Web.Caching;

namespace IenumerableAsClassVariable.Controllers
{
    // This is the helper class & function used to determine if the IEnumerable is null or empty
    public static class CustomHelpers
    {
        public static string IsNullOrEmpty<T>(this IEnumerable<T> enumerable)
        {
            if (enumerable == null)
                return "Null";
            else
                return "Not Null";
        }
    }

    public class CourseListsController : Controller
    {
        private CreditSlipLogContext db = new CreditSlipLogContext();   

        // This this the "index" query that is called by the Index 
        // and can be called by any other methods in this controller that I choose.
        private IEnumerable<CourseList> GetIndexQuery()
        {   
            using (var dbc = new CreditSlipLogContext())
            {
                return  db.CourseLists.AsEnumerable();
            }

        }

        // GET: CourseLists
        public ActionResult Index()
        {
            var data = GetIndexQuery();
            Debug.WriteLine("-----");
            Debug.WriteLine("Begin Index test *****");
            Debug.WriteLine("Is data Null? " + CustomHelpers.IsNullOrEmpty(data));                        
            Debug.WriteLine("End Index test *****");           

            return View(db.CourseLists.ToList());
        }

        public ActionResult ClickedFromIndexPageLink()
        {
            var data = GetIndexQuery();
            Debug.WriteLine("-----");
            Debug.WriteLine("Begin Index test *****");
            Debug.WriteLine("Is data Null? " + CustomHelpers.IsNullOrEmpty(data));
            Debug.WriteLine("End Index test *****");  

            ViewBag.IsDataNull = CustomHelpers.IsNullOrEmpty(data);

            return View();
        }

        #region OtherCrudActionResultsAreHidden
        #endregion

    }
}