I have struggled with this for a bit, and I have a feeling I am very close. In a .NET MVC web application I have used to have assemblyinfo information displayed in the front end without issue. In a bit of optimization I wanted to move that code out to a general purpose helper class.
For ease of use I have made it a static class, but I have hit several snags in the process. But now it throws a System.StackOverflowException when I try to use it, sadly. Here is the code:
public static class VersionInformationHelper
{
public static string GetVersionNumber
{
get
{
if (!string.IsNullOrWhiteSpace(GetVersionNumber.GetType().Assembly.GetName().Version.ToString()))
{
return "v" + GetVersionNumber.GetType().Assembly.GetName().Version.ToString();
}
else
{
return string.Empty;
}
}
}
/// <remark>
/// This doesnt exactly return the commit hash so to speak. Well it does, but Teamcity is set to enter the corresponding commit hash information when building,
/// into productversion in "AssemblyInfo.cs". It could be any string really. But we assume that a commit hash will always be in that location.
/// It's "Assembly informational version" in the assemblyinfo patcher build feature in teamcity.
/// </remark>
public static string GetCommitHash
{
get
{
if (!string.IsNullOrWhiteSpace(System.Diagnostics.FileVersionInfo.GetVersionInfo(GetVersionNumber.GetType().Assembly.Location).ProductVersion))
{
return System.Diagnostics.FileVersionInfo.GetVersionInfo(GetVersionNumber.GetType().Assembly.Location).ProductVersion;
}
else
{
return string.Empty;
}
}
}
public static string GetBuildDate
{
get
{
return string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:dd/MM/yy HH:mm:ss}", System.IO.File.GetLastWriteTime(GetVersionNumber.GetType().Assembly.Location));
}
}
}
EDIT
Fixed code based on feedback (GetVersionNumber and GetCommitHash" has been changed):
public static class VersionInformationHelper
{
public static string GetVersionNumber
{
get
{
if (!string.IsNullOrWhiteSpace(System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString()))
{
return "v" + System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString();
}
else
{
return string.Empty;
}
}
}
/// <remark>
/// This doesnt exactly return the commit hash so to speak. Well it does, but Teamcity is set to enter the corresponding commit hash information when building,
/// into productversion in "AssemblyInfo.cs". It could be any string really. But we assume that a commit hash will always be in that location.
/// It's "Assembly informational version" in the assemblyinfo patcher build feature in teamcity.
/// </remark>
public static string GetCommitHash
{
get
{
if (!string.IsNullOrWhiteSpace(System.Diagnostics.FileVersionInfo.GetVersionInfo(System.Reflection.Assembly.GetExecutingAssembly().Location).ProductVersion))
{
return System.Diagnostics.FileVersionInfo.GetVersionInfo(System.Reflection.Assembly.GetExecutingAssembly().Location).ProductVersion;
}
else
{
return string.Empty;
}
}
}
public static string GetBuildDate
{
get
{
return string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:dd/MM/yy HH:mm:ss}", System.IO.File.GetLastWriteTime(GetVersionNumber.GetType().Assembly.Location));
}
}
}
You are reading GetVersionNumber from the getter of same GetVersionNumber (twice). This will loop infinitely (or until a stack overflow occurs).
You probably need to change the two occurences with Assembly.GetExecutingAssembly().GetName().Version.ToString() or another method to get the version.
When I look at a method in the .NET Framework, it can throw several possible exceptions.
From a planning perspective, what do I need to ask to plan and handle these exceptions? I understand that depending on the impact of an exception, that would influence showing it in the UI layer. For example, if a process at the back end which is opaque to the user experience throws an exception, you would not want to show that to the user as s/he would have no idea what that is.
Thanks
I wanted to write it as a comment but it is to long for the comment textbox...
There are exceptions that are important to show the users because they can handle it by themselves, for example: file name is illegal.
There are exceptions that i find important to show the users because they (the users) will function as my QA team, for example: some method will not update the GUI and they probably will get to the conclusion that they should report that further on, they will show me the exception and i will discover that my array is out of bounds...
there are exceptions that promoting them to the user will just confuse them and like you said
"s/he would have no idea what that is"
for example, For example:
"if a process at the back end which is opaque throws an exception".
those exception i am saving for myself (in database..)..
I am always reminding myself that the end user has a different psychology compare to a developer (-:
I hope i understood your question in the right manner.
You'd normally have a business logic layer in your application, and you'd wrap the methods in that library in safe calls. Those methods may even say what exceptions they can throw, and you may want to handle those exceptions in a unique way if you so wish.
Here's an example of a couple of classes that can handle both unexpected and custom exceptions:
class Program
{
static void Main(string[] args)
{
MethodInvokationResult result = SafeActionInvokator.HandleSafely(() =>
{
MyFakeBusinessEngine.DivideTwoNumbers(5, 0);
});
if (result.Exception is System.DivideByZeroException)
{
Debug.WriteLine($"A divide by zerp exception was caught");
}
else if (!result.Success)
{
Debug.WriteLine($"An unknown error occured.");
}
}
}
^ You can see that you can wrap the calls and handle them in a sensible way.
public class MethodInvokationResult
{
public bool Success { get; set; }
public Exception Exception { get; set; }
}
^ A simple result class
public static class MyFakeBusinessEngine
{
/// <summary>
/// Divides two numbers
/// </summary>
/// <param name="a">numerator</param>
/// <param name="b">denominator</param>
/// <returns>the result</returns>
/// <exception cref="System.DivideByZeroException">When b is zero, divide by zero exception is thrown.</exception>
public static int DivideTwoNumbers(int a, int b)
{
return a / b;
}
}
^ A well documented method, that even tells other developers what exceptions it expects to throw
public static class SafeActionInvokator
{
/// <summary>
/// Executes a method, and if any exception is thrown, will log the error and swallow the exception.
/// </summary>
/// <param name="methodToExecute"></param>
public static MethodInvokationResult HandleSafely(Action methodToExecute)
{
try
{
methodToExecute.Invoke();
return new MethodInvokationResult()
{
Exception = null,
Success = true
};
}
catch (Exception ex)
{
Debug.WriteLine($"{ex}");
return new MethodInvokationResult()
{
Exception = ex,
Success = false
};
}
}
}
^ The wrapper for the method you call, wrapping this in a try catch allows you to handle all exceptions in the same way, or put any custom logic you'd like in there.
This can be expanded to be a call that can return any actual result value too. You could make a generic result class instead of the simple one I've shown.
FINALLY! As to your actual question, I'd create some of these layers throughout your application, and re-throw any exceptions with a severity rating included. These would propagate to your UI layer and you can chose what to do based on Severity.
Maybe even something like this!
public class MajorException : Exception { }
public class MediumException : Exception { }
public class MinorException : Exception { }
public class UserError : Exception { }
Hope this helps!
On my C# Widows form, I want to make use of the Process object information even after its exit, but I am getting the exception “System process has exited, so the requested information is not available”.
What I tried thus far is to save it as a var and tag it with my ListView item, but it still throws the same exception.
//ListView Parameters (omitted redundant ones)
Process currentProcess = Process.GetProcessById(Convert.ToInt32(processStringID);
newListViewItem.Tag = currentProcess;
listView.Items.Add(newListViewItem);
I have an event of selected index changed, so when the user clicks on the ListView Item, it should show information about the process that was tagged with the item even if it has already exited.
private void listView_SelectedIndexChanged(object sender, EventArgs e)
{
try
{
Process processItem = (Process)listView.SelectedItems[0].Tag;
//Sample of getting process information (Error happens here)
MessageBox.Show(processItem.Name + processItem.VersionInfo);
}
catch (Exception ex)
{
throw ex;
}
}
Tldr; I need a way to save the entire Process object so I can get it's information later even if the process has exited. I am open to ideas as to how this can be implemented. Please assist me, as I am unable to think of any solution with my current understanding on programming.
Store it in a Session,
To store data:
Session["process"] = processItem;
To pull data from session:
var process = (Process)Session["process"]; // Don't forget to cast back to it's original type
Data is available even if you navigate to other pages unless you manually remove it.
Update:
Since question is not clear at first.
Create a global variable using static classes
public static class GlobalVar
{
/// <summary>
/// Global variable that is constant.
/// </summary>
public const string GlobalString = "Important Text";
/// <summary>
/// Static value protected by access routine.
/// </summary>
static int _globalValue;
/// <summary>
/// Access routine for global variable.
/// </summary>
public static int GlobalValue
{
get
{
return _globalValue;
}
set
{
_globalValue = value;
}
}
/// <summary>
/// Global static field.
/// </summary>
public static bool GlobalBoolean;
}
See this post : http://www.dotnetperls.com/global-variable
I understand that you can unit test a void method by checking for its effects. However, looking at the loadConfigFile method in this code:
internal XmlDocument configData;
public ConfigFile()
{
configData = new XmlDocument();
}
/// <summary>
/// Load config file into memory
/// </summary>
/// <param name="filename">path and filename of config file</param>
public void loadConfigFile(string filename)
{
if(string.IsNullOrEmpty(filename))
throw new System.ArgumentException("You must specify a filename");
try
{
configData.Load(filename);
}
catch(Exception ex)
{
throw new Exception("Config file could not be loaded",ex);
}
}
It loads a config file into a private field - which needs to be kept private so that developers do not modify the value directly. Instead the modification will be done through setConfigValue and getConfigValue methods (which I would assume need to be tested separately).
Given this, how would I test that the loadConfigFile actually worked? As I cant access the private configData field.
Where else in that class is configData used?
If, say, you have a method like
public string GetValue()
{
return configData.GetsomeDataFromThis;
}
Then I suggest you have a test like so:
public void ReadValueFromLoadedConfigData()
{
// Arrange.
const string ExpectedValue = "Whatever";
var sut = new ConfigFile();
sut.loadConfigFile(#"C:\PathToTheConfigFile");
// Act.
string actual = sut.GetConfigValue();
// Assert.
Assert.AreEqual(ExpectedValue, actual);
}
In your tests, try to test only the public interactions, since going deep into the class, by having to read values of private fields, means your class isn't test friendly.
You could test that getConfigValue returned the values loaded from the file, basically.
Just because you test setConfigValue/getConfigValue in other tests doesn't mean that you can't use them in the test for loadConfigFile.
As an aside, I'd strongly urge you to follow .NET naming conventions, starting your method names with capital letters. (I'd also urge you to make your field private rather than internal...)
A unit test is basically a way to say "given this input, verify that this happened", it is not a substitute for verifying that your application is in a valid state.
In the example, the loadConfigFile method throws an exception if the precondition is not met, or the load operation failed. This will then be detected in the unit test, which will fail.
If any other validation is needed on the config beyond that no exception is thrown, that should be handled in the class itself.
Are there any multithreaded caching mechanisms that will work in a SQL CLR function without requiring the assembly to be registered as "unsafe"?
As also described in this post, simply using a lock statement will throw an exception on a safe assembly:
System.Security.HostProtectionException:
Attempted to perform an operation that was forbidden by the CLR host.
The protected resources (only available with full trust) were: All
The demanded resources were: Synchronization, ExternalThreading
I want any calls to my functions to all use the same internal cache, in a thread-safe manner so that many operations can do cache reads and writes simultaneously. Essentially - I need a ConcurrentDictionary that will work in a SQLCLR "safe" assembly. Unfortunately, using ConcurrentDictionary itself gives the same exception as above.
Is there something built-in to SQLCLR or SQL Server to handle this? Or am I misunderstanding the threading model of SQLCLR?
I have read as much as I can find about the security restrictions of SQLCLR. In particular, the following articles may be useful to understand what I am talking about:
SQL Server CLR Integration Part 1: Security
Deploy/Use assemblies which require Unsafe/External Access with CLR and T-SQL
This code will ultimately be part of a library that is distributed to others, so I really don't want to be required to run it as "unsafe".
One option that I am considering (brought up in comments below by Spender) is to reach out to tempdb from within the SQLCLR code and use that as a cache instead. But I'm not quite sure exactly how to do that. I'm also not sure if it will be anywhere near as performant as an in-memory cache. See update below.
I am interested in any other alternatives that might be available. Thanks.
Example
The code below uses a static concurrent dictionary as a cache and accesses that cache via SQL CLR user-defined functions. All calls to the functions will work with the same cache. But this will not work unless the assembly is registered as "unsafe".
public class UserDefinedFunctions
{
private static readonly ConcurrentDictionary<string,string> Cache =
new ConcurrentDictionary<string, string>();
[SqlFunction]
public static SqlString GetFromCache(string key)
{
string value;
if (Cache.TryGetValue(key, out value))
return new SqlString(value);
return SqlString.Null;
}
[SqlProcedure]
public static void AddToCache(string key, string value)
{
Cache.TryAdd(key, value);
}
}
These are in an assembly called SqlClrTest, and and use the following SQL wrappers:
CREATE FUNCTION [dbo].[GetFromCache](#key nvarchar(4000))
RETURNS nvarchar(4000) WITH EXECUTE AS CALLER
AS EXTERNAL NAME [SqlClrTest].[SqlClrTest.UserDefinedFunctions].[GetFromCache]
GO
CREATE PROCEDURE [dbo].[AddToCache](#key nvarchar(4000), #value nvarchar(4000))
WITH EXECUTE AS CALLER
AS EXTERNAL NAME [SqlClrTest].[SqlClrTest.UserDefinedFunctions].[AddToCache]
GO
Then they are used in the database like this:
EXEC dbo.AddToCache 'foo', 'bar'
SELECT dbo.GetFromCache('foo')
UPDATE
I figured out how to access the database from SQLCLR using the Context Connection. The code in this Gist shows both the ConcurrentDictionary approach, and the tempdb approach. I then ran some tests, with the following results measured from client statistics (average of 10 trials):
Concurrent Dictionary Cache
10,000 Writes: 363ms
10,000 Reads : 81ms
TempDB Cache
10,000 Writes: 3546ms
10,000 Reads : 1199ms
So that throws out the idea of using a tempdb table. Is there really nothing else I can try?
I've added a comment that says something similar, but I'm going to put it here as an answer instead, because I think it might need some background.
ConcurrentDictionary, as you've correctly pointed out, requires UNSAFE ultimately because it uses thread synchronisation primitives beyond even lock - this explicitly requires access to lower-level OS resources, and therefore requires the code fishing outside of the SQL hosting environment.
So the only way you can get a solution that doesn't require UNSAFE, is to use one which doesn't use any locks or other thread synchronisation primitives. However, if the underlying structure is a .Net Dictionary then the only truly safe way to share it across multiple threads is to use Lock or an Interlocked.CompareExchange (see here) with a spin wait. I can't seem to find any information on whether the latter is allowed under the SAFE permission set, but my guess is that it's not.
I'd also be questioning the validity of applying a CLR-based solution to this problem inside a database engine, whose indexing-and-lookup capability is likely to be far in excess of any hosted CLR solution.
The accepted answer is not correct. Interlocked.CompareExchange is not an option since it requires a shared resource to update, and there is no way to create said static variable, in a SAFE Assembly, that can be updated.
There is (for the most part) no way to cache data across calls in a SAFE Assembly (nor should there be). The reason is that there is a single instance of the class (well, within the App Domain which is per-database per-owner) that is shared across all sessions. That behavior is, more often than not, highly undesirable.
However, I did say "for the most part" it was not possible. There is a way, though I am not sure if it is a bug or intended to be this way. I would err on the side of it being a bug since again, sharing a variable across sessions is a very precarious activity. Nonetheless, you can (do so at your own risk, AND this is not specifically thread safe, but might still work) modify static readonly collections. Yup. As in:
using Microsoft.SqlServer.Server;
using System.Data.SqlTypes;
using System.Collections;
public class CachingStuff
{
private static readonly Hashtable _KeyValuePairs = new Hashtable();
[SqlFunction(DataAccess = DataAccessKind.None, IsDeterministic = true)]
public static SqlString GetKVP(SqlString KeyToGet)
{
if (_KeyValuePairs.ContainsKey(KeyToGet.Value))
{
return _KeyValuePairs[KeyToGet.Value].ToString();
}
return SqlString.Null;
}
[SqlProcedure]
public static void SetKVP(SqlString KeyToSet, SqlString ValueToSet)
{
if (!_KeyValuePairs.ContainsKey(KeyToSet.Value))
{
_KeyValuePairs.Add(KeyToSet.Value, ValueToSet.Value);
}
return;
}
[SqlProcedure]
public static void UnsetKVP(SqlString KeyToUnset)
{
_KeyValuePairs.Remove(KeyToUnset.Value);
return;
}
}
And running the above, with the database set as TRUSTWORTHY OFF and the assembly set to SAFE, we get:
EXEC dbo.SetKVP 'f', 'sdfdg';
SELECT dbo.GetKVP('f'); -- sdfdg
SELECT dbo.GetKVP('g'); -- NULL
EXEC dbo.UnsetKVP 'f';
SELECT dbo.GetKVP('f'); -- NULL
That all being said, there is probably a better way that is not SAFE but also not UNSAFE. Since the desire is to use memory for caching of repeatedly used values, why not set up a memcached or redis server and create SQLCLR functions to communicate with it? That would only require setting the assembly to EXTERNAL_ACCESS.
This way you don't have to worry about several issues:
consuming a bunch of memory which could/should be used for queries.
there is no automatic expiration of the data held in static variables. It exists until you remove it or the App Domain gets unloaded, which might not happen for a long time. But memcached and redis do allow for setting an expiration time.
this is not explicitly thread safe. But cache servers are.
SQL Server locking functions sp_getapplock and sp_releaseapplock can be used in SAFE context. Employ them to protect an ordinary Dictionary and you have yourself a cache!
The price of locking this way is much worse than ordinary lock, but that may not be an issue if you are accessing your cache in a relatively coarsely-grained way.
--- UPDATE ---
The Interlocked.CompareExchange can be used on a field contained in a static instance. The static reference can be made readonly, but a field in the referenced object can still be mutable, and therefore usable by Interlocked.CompareExchange.
Both Interlocked.CompareExchange and static readonly are allowed in SAFE context. Performance is much better than sp_getapplock.
Based on Andras answer, here is my implantation of a "SharedCache" to read and write in a dictionary in SAFE permission.
EvalManager (Static)
using System;
using System.Collections.Generic;
using Z.Expressions.SqlServer.Eval;
namespace Z.Expressions
{
/// <summary>Manager class for eval.</summary>
public static class EvalManager
{
/// <summary>The cache for EvalDelegate.</summary>
public static readonly SharedCache<string, EvalDelegate> CacheDelegate = new SharedCache<string, EvalDelegate>();
/// <summary>The cache for SQLNETItem.</summary>
public static readonly SharedCache<string, SQLNETItem> CacheItem = new SharedCache<string, SQLNETItem>();
/// <summary>The shared lock.</summary>
public static readonly SharedLock SharedLock;
static EvalManager()
{
// ENSURE to create lock first
SharedLock = new SharedLock();
}
}
}
SharedLock
using System.Threading;
namespace Z.Expressions.SqlServer.Eval
{
/// <summary>A shared lock.</summary>
public class SharedLock
{
/// <summary>Acquires the lock on the specified lockValue.</summary>
/// <param name="lockValue">[in,out] The lock value.</param>
public static void AcquireLock(ref int lockValue)
{
do
{
// TODO: it's possible to wait 10 ticks? Thread.Sleep doesn't really support it.
} while (0 != Interlocked.CompareExchange(ref lockValue, 1, 0));
}
/// <summary>Releases the lock on the specified lockValue.</summary>
/// <param name="lockValue">[in,out] The lock value.</param>
public static void ReleaseLock(ref int lockValue)
{
Interlocked.CompareExchange(ref lockValue, 0, 1);
}
/// <summary>Attempts to acquire lock on the specified lockvalue.</summary>
/// <param name="lockValue">[in,out] The lock value.</param>
/// <returns>true if it succeeds, false if it fails.</returns>
public static bool TryAcquireLock(ref int lockValue)
{
return 0 == Interlocked.CompareExchange(ref lockValue, 1, 0);
}
}
}
SharedCache
using System;
using System.Collections.Generic;
namespace Z.Expressions.SqlServer.Eval
{
/// <summary>A shared cache.</summary>
/// <typeparam name="TKey">Type of key.</typeparam>
/// <typeparam name="TValue">Type of value.</typeparam>
public class SharedCache<TKey, TValue>
{
/// <summary>The lock value.</summary>
public int LockValue;
/// <summary>Default constructor.</summary>
public SharedCache()
{
InnerDictionary = new Dictionary<TKey, TValue>();
}
/// <summary>Gets the number of items cached.</summary>
/// <value>The number of items cached.</value>
public int Count
{
get { return InnerDictionary.Count; }
}
/// <summary>Gets or sets the inner dictionary used to cache items.</summary>
/// <value>The inner dictionary used to cache items.</value>
public Dictionary<TKey, TValue> InnerDictionary { get; set; }
/// <summary>Acquires the lock on the shared cache.</summary>
public void AcquireLock()
{
SharedLock.AcquireLock(ref LockValue);
}
/// <summary>Adds or updates a cache value for the specified key.</summary>
/// <param name="key">The cache key.</param>
/// <param name="value">The cache value used to add.</param>
/// <param name="updateValueFactory">The cache value factory used to update.</param>
/// <returns>The value added or updated in the cache for the specified key.</returns>
public TValue AddOrUpdate(TKey key, TValue value, Func<TKey, TValue, TValue> updateValueFactory)
{
try
{
AcquireLock();
TValue oldValue;
if (InnerDictionary.TryGetValue(key, out oldValue))
{
value = updateValueFactory(key, oldValue);
InnerDictionary[key] = value;
}
else
{
InnerDictionary.Add(key, value);
}
return value;
}
finally
{
ReleaseLock();
}
}
/// <summary>Adds or update a cache value for the specified key.</summary>
/// <param name="key">The cache key.</param>
/// <param name="addValueFactory">The cache value factory used to add.</param>
/// <param name="updateValueFactory">The cache value factory used to update.</param>
/// <returns>The value added or updated in the cache for the specified key.</returns>
public TValue AddOrUpdate(TKey key, Func<TKey, TValue> addValueFactory, Func<TKey, TValue, TValue> updateValueFactory)
{
try
{
AcquireLock();
TValue value;
TValue oldValue;
if (InnerDictionary.TryGetValue(key, out oldValue))
{
value = updateValueFactory(key, oldValue);
InnerDictionary[key] = value;
}
else
{
value = addValueFactory(key);
InnerDictionary.Add(key, value);
}
return value;
}
finally
{
ReleaseLock();
}
}
/// <summary>Clears all cached items.</summary>
public void Clear()
{
try
{
AcquireLock();
InnerDictionary.Clear();
}
finally
{
ReleaseLock();
}
}
/// <summary>Releases the lock on the shared cache.</summary>
public void ReleaseLock()
{
SharedLock.ReleaseLock(ref LockValue);
}
/// <summary>Attempts to add a value in the shared cache for the specified key.</summary>
/// <param name="key">The key.</param>
/// <param name="value">The value.</param>
/// <returns>true if it succeeds, false if it fails.</returns>
public bool TryAdd(TKey key, TValue value)
{
try
{
AcquireLock();
if (!InnerDictionary.ContainsKey(key))
{
InnerDictionary.Add(key, value);
}
return true;
}
finally
{
ReleaseLock();
}
}
/// <summary>Attempts to remove a key from the shared cache.</summary>
/// <param name="key">The key.</param>
/// <param name="value">[out] The value.</param>
/// <returns>true if it succeeds, false if it fails.</returns>
public bool TryRemove(TKey key, out TValue value)
{
try
{
AcquireLock();
var isRemoved = InnerDictionary.TryGetValue(key, out value);
if (isRemoved)
{
InnerDictionary.Remove(key);
}
return isRemoved;
}
finally
{
ReleaseLock();
}
}
/// <summary>Attempts to get value from the shared cache for the specified key.</summary>
/// <param name="key">The key.</param>
/// <param name="value">[out] The value.</param>
/// <returns>true if it succeeds, false if it fails.</returns>
public bool TryGetValue(TKey key, out TValue value)
{
try
{
return InnerDictionary.TryGetValue(key, out value);
}
catch (Exception)
{
value = default(TValue);
return false;
}
}
}
}
Source Files:
https://github.com/zzzprojects/Eval-SQL.NET/blob/master/src/Z.Expressions.SqlServer.Eval/EvalManager/EvalManager.cs
https://github.com/zzzprojects/Eval-SQL.NET/blob/master/src/Z.Expressions.SqlServer.Eval/Shared/SharedLock.cs
https://github.com/zzzprojects/Eval-SQL.NET/blob/master/src/Z.Expressions.SqlServer.Eval/Shared/SharedCache.cs
Would your needs be satisfied with a table variable? They're kept in memory, as long as possible anyway, so performance should be excellent. Not so useful if you need to maintain your cache between app calls, of course.
Created as a type, you can also pass such a table into a sproc or UDF.