Using int-Keys takes a bit more work than other types, because KeyedCollection provides indexers for both key and index. However, if the key’s type is int as well, you don’t have that option anymore. This class fills that gap by providing a few things for you:

GetByKey, TryGetByKey, and GetByIndex methods

Simplified exception handling and much better exception messages that include the invalid parameters. You can even customize exception messages by overriding the virtual GetKeyNotFoundMessage method.

An AddRange method which takes an IEnumerable<T>

For security reasons, the indexer has been overwritten and throws an InvalidOperationException if it is invoked. I just caught myself too much using it with the wrong parameters (index rather than key or vice versa). You might want to reverse that if you need to automatically serialize the collection to XML.

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace Hardcodet.Util
{
/// <summary>/// An implementation of a <see cref="KeyedCollection{TKey,TItem}"/> for items that/// have a numeric key, an therefore do not provide individual/// indexers for both key and index. As an alternative, this class provides/// <see cref="GetByIndex"/> and <see cref="GetByKey"/> methods plus more/// sophisticated exception handling for invalid keys.<br/>/// For security measures, the numeric indexer has been disabled, as using it/// is just misleading because one cannot tell whether it returns an item by/// index or by key.../// </summary>publicabstractclass NumericallyKeyedCollection<T> : KeyedCollection<int, T>
{
#region constructors
/// <summary>/// Creates an empty collection./// </summary>public NumericallyKeyedCollection()
{
}
/// <summary>/// Inits the collection by copying all items of another/// collection./// </summary>/// <param name="items">A collection of items to be added/// to this collection.</param>public NumericallyKeyedCollection(IEnumerable<T> items)
{
AddRange(items);
}
#endregion#region get by id
/// <summary>/// Tries to retrieve a given item by its key./// </summary>/// <param name="key">The key that was used to store the/// item.</param>/// <returns>The matching item, if any. Otherwise/// <c>default(T)</c> (null in case of standard objects).</returns>public T TryGetByKey(int key)
{
return Contains(key) ? GetByKey(key) : default(T);
}
/// <summary>/// Overrides the default implementation in order to provide a more sophisticated/// exception handling. In case of an invalid ID, an exception with a message is/// being thrown that is built the <see cref="GetKeyNotFoundMessage"/> method./// </summary>/// <returns>The item with the matching key.</returns>/// <exception cref="KeyNotFoundException">Thrown if no descriptor/// with a matching ID was found.</exception>public T GetByKey(int key)
{
try
{
returnbase[key];
}
catch (KeyNotFoundException)
{
//throw custom exception that contains the keystring msg = GetKeyNotFoundMessage(key);
thrownew KeyNotFoundException(msg);
}
}
/// <summary>/// Overrides the default implementation in order disable usage of the indexer,/// as its usage is no longer clear because index and item keys are both of/// the same type.<br/>/// Invoking this indexer always results in a <see cref="InvalidOperationException"/>./// </summary>/// <returns>Nothing - invoking the indexer results in a <see cref="InvalidOperationException"/>.</returns>/// <exception cref="InvalidOperationException">Thrown if the indexer is being invoked.</exception>publicnewvirtual T this[int key]
{
get
{
string msg = "Using the indexer is disabled because both access through index and item key take the same type.";
msg += " Use the GetByKey or GetByIndex methods instead.";
thrownew InvalidOperationException(msg);
}
}
/// <summary>/// Gets an exception message that is being submitted with/// the <see cref="KeyNotFoundException"/> which is thrown/// if the indexer was called with an unknown key./// This template method might be overridden in order to provide/// a more specific message./// </summary>/// <param name="key">The submitted (and unknown) key.</param>/// <returns>This default implementation returns an error/// message that contains the requested key.</returns>protectedvirtualstring GetKeyNotFoundMessage(int key)
{
string msg = "No matching item found for key '{0}'.";
return String.Format(msg, key);
}
#endregion#region get by index
/// <summary>/// Gets the item at the specified <paramref name="index"/>./// </summary>/// <param name="index">Index within the collection.</param>/// <returns>The item at the specified index.</returns>/// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/>/// if not a valid index of the internal list.</exception>publicvirtual T GetByIndex(int index)
{
return Items[index];
}
#endregion#region add items
/// <summary>/// Adds a number of items to the list./// </summary>/// <param name="items">The items to be appended.</param>publicvoid AddRange(IEnumerable<T> items)
{
foreach (T item in items)
{
Add(item);
}
}
/// <summary>/// Adds an item to the end of the collection./// </summary>/// <param name="item">Item to be added to the collection.</param>/// <remarks>This override just provides a more sophisticated exception/// message.</remarks>publicnewvoid Add(T item)
{
try
{
base.Add(item);
}
catch (ArgumentException e)
{
int key = GetKeyForItem(item);
if (Contains(key))
{
string msg = "An item with key '{0}' has already been added.";
msg = String.Format(msg, key);
thrownew ArgumentException(msg, "item", e);
}
else
{
//in case of any other argument exception, just throw the//original exceptionthrow;
}
}
}
#endregion
}
}

Using it is fairly easy. Imagine you have a user collection that stores User objects by their numeric UserId. All you need to get going is this: