I would like to store an integer value in the application cache.
The GetOrSet
method should always return an int, and never be null. If there is no cached value, then the getDataCallback
function will provide the integer.
But I need the cache check to return a nullable int in order to distinguish no value
from a 0
integer value.
I am looking for any faults in the code below, or possible improvements.
It probably looks very simple to others, but I find working with nullable ints quite confusing.
public int GetOrSet(string cacheKey, Func<int> getDataCallback)
{
int? data = (int?)HttpContext.Current.Application[cacheKey]; // this should return null if there is no value stored in the cache
if (! data.HasValue)
{
int intData = getDataCallback();
HttpContext.Current.Application[cacheKey] = intData;
return intData;
}
else
{
return data.Value;
}
}
Update
Big thanks to both @Nkosi and @Flater for their help.
I marked @Flater's answer as correct because I had to chose one only. But I really hated having to do this as both answers were equally helpful and I used both their suggestions in the end. I would encourage anybody reading this to check them both out.
3 Answers 3
Your current method is slightly violating SRP, it's both checking is a cache exists, and creating one if needed. I'd prefer to separate the two:
public int GetData(string cacheKey, Func<int> getDataCallback)
{
int? cachedData = (int?)HttpContext.Current.Application[cacheKey];
return cachedData.HasValue
? cachedData.Value
: CreateCache(cacheKey, getDataCallback);
}
private int CreateCache(string cacheKey, Func<int> getDataCallback)
{
int intData = getDataCallback();
HttpContext.Current.Application[cacheKey] = intData;
return intData;
}
The first method only checks the existing cache and, if missing, calls the second method, which will create a new cache.
- The code could be refactored to slightly shorten it, but that would be at the cost of readability.
- I renamed
GetOrSet
toGetData
for clarity's sake. The method exists to give you the data, regardless of whether it's cached or created on the fly. Pedantically, you'd need to name itGet_or_SetAndThenGet
for it to be an accurate description. I preferGetData
, it sticks to the important facts and remains concise. - Nkosi is also correct in the comments that you should ideally abstract this snippet away from using
HttpContext
, but that's not necessary for the question at hand.
Note that you could also stop using int?
if you find it confusing:
public int GetData(string cacheKey, Func<int> getDataCallback)
{
object cachedData = HttpContext.Current.Application[cacheKey];
return cachedData != null
? (int) cachedData
: CreateCache(cacheKey, getDataCallback);
}
-
\$\begingroup\$ This: return cachedData.HasValue ? cachedData.Value : CreateCache(cacheKey, getDataCallback); can be more easily expressed as: "return cachedData ?? CreateCache(cacheKey, getDataCallback);" \$\endgroup\$JanErikGunnar– JanErikGunnar2018年03月05日 11:44:30 +00:00Commented Mar 5, 2018 at 11:44
-
\$\begingroup\$ @JanErikGunnar: IIRC, that would try to return a value of type
int?
(i.e.cachedData
), notint
(i.e.cachedData.Value
). I initially used??
and switched to using the ternary specifically because of that consideration. \$\endgroup\$Flater– Flater2018年03月05日 11:52:37 +00:00Commented Mar 5, 2018 at 11:52 -
\$\begingroup\$ No, the ?? operator "de-Nullable" the return value: docs.microsoft.com/en-us/dotnet/csharp/programming-guide/… \$\endgroup\$JanErikGunnar– JanErikGunnar2018年03月05日 12:02:01 +00:00Commented Mar 5, 2018 at 12:02
-
\$\begingroup\$ @Flater: you can use
??
if you cast it the right way:return (int)(cachedData ?? CreateCache(cacheKey, getDataCallback));
\$\endgroup\$user73941– user739412018年03月05日 12:03:43 +00:00Commented Mar 5, 2018 at 12:03 -
\$\begingroup\$ @HenrikHansen: Sure, but that's not particularly readable. \$\endgroup\$Flater– Flater2018年03月05日 12:06:27 +00:00Commented Mar 5, 2018 at 12:06
Assuming the above code is not itself an abstraction, tightly coupling your code to HttpContext
would make it not unit testable as HttpContext.Current
is null
outside of IIS.
Consider encapsulating that code behind an abstraction.
public interface IApplicationCache {
object this[string cacheKey] { get; set}
}
public class ApplicationCacheImplementation {
public object this[string cacheKey] {
get {
return HttpContext.Current.Application[cacheKey];
}
set {
HttpContext.Current.Application[cacheKey] = value;
}
}
}
whose implementation will be injected explicitly into the dependent class.
Other than that you logic is sound but can also benefit from some defensive coding (null checks on the provided arguments) and separation of concerns.
public class MyClass {
private readonly IApplicationCache applicationCache;
public MyClass(IApplicationCache applicationCache) {
this.applicationCache = applicationCache;
}
public int GetOrSet(string cacheKey, Func<int> valueFactory) {
if (string.IsNullOrWhiteSpace(cacheKey)) throw new ArgumentException("cacheKey");
if (valueFactory == null) throw new ArgumentException("valueFactory");
return (int)(get(cacheKey) ?? set(cacheKey, valueFactory));
}
private object get(string cacheKey) {
return applicationCache[cacheKey];
}
private int set(string cacheKey, Func<int> valueFactory) {
int value = valueFactory();
applicationCache[cacheKey] = value;
return value;
}
}
Note the naming changes to help make the code a little more readable.
-
\$\begingroup\$ Very useful examples, it made things so much clearer. Thank you very much. I even cheated and copied the caching abstraction wholesale. \$\endgroup\$Carmax– Carmax2018年03月13日 23:49:55 +00:00Commented Mar 13, 2018 at 23:49
More code than needed
int? data = (int?)HttpContext.Current.Application[cacheKey];
if (! data.HasValue)
{
data = getDataCallback();
HttpContext.Current.Application[cacheKey] = data;
}
return data.Value;
-
\$\begingroup\$ Oh happy day 2 down votes without a comment. Come on what is the problem? \$\endgroup\$paparazzo– paparazzo2018年03月13日 22:36:51 +00:00Commented Mar 13, 2018 at 22:36
-
\$\begingroup\$ Although I found the other answers very helpful, yours is actually the most succinct answer to my question, so I upvoted it \$\endgroup\$Carmax– Carmax2018年03月13日 23:42:50 +00:00Commented Mar 13, 2018 at 23:42
-
\$\begingroup\$ @paparazzo Same here, upvote. However, you could have addressed the subtle change you made to the original posted flow. I first thought you copy pasted the OP. \$\endgroup\$dfhwze– dfhwze2019年05月25日 20:13:28 +00:00Commented May 25, 2019 at 20:13
HttpContext
would make it not unit testable asHttpContext.Current
isnull
outside of IIS. consider encapsulating that code behind an abstraction. other than that you logic is sound but can also benefit from some defensive coding (null check on the callback) \$\endgroup\$HttpContext.Current
or could I just do it for theApplication
bit? \$\endgroup\$