Home > C# > A KeyedCollection for int keys

A KeyedCollection for int keys

March 5th, 2008

I’m using this class so much that I thought I’d share it with you. This generic class is an implementation of the KeyedCollection<TKey, TItem> class for items that have a numeric key:

public abstract class NumericallyKeyedCollection<T> : KeyedCollection<int, T>
{ }

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>
  public abstract class 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
      {
        return base[key];
      }
      catch (KeyNotFoundException)
      {
        //throw custom exception that contains the key
        string msg = GetKeyNotFoundMessage(key);
        throw new 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>
    public new virtual 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.";
        throw new 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>
    protected virtual string 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>
    public virtual 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>
    public void 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>
    public new void 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);
          throw new ArgumentException(msg, "item", e);
        }
        else
        {
          //in case of any other argument exception, just throw the
          //original exception
          throw;
        }
      }
    }

    #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:

/// <summary>
/// Stores <see cref="User"/> instances by their numer
/// <see cref="User.UserId"/>.
/// </summary>
public class UserCollection : NumericallyKeyedCollection<User>
{
  protected override int GetKeyForItem(User item)
  {
    return item.UserId;
  }
}

 

…accordingly, here’s how you manipulate the collection:

public void Test()
{
  UserCollection col = new UserCollection();
  col.Add(new User(123));

  User userByIndex = col.GetByIndex(0);
  User userByKey = col.GetByKey(123);
}

Author: Categories: C# Tags:
  1. August 12th, 2008 at 23:39 | #1

    Thanks!!

    This is my finall version.

    ///
    /// Clase base para las colecciones de entidades de negocio
    ///
    ///
    /// *-----------------------------------------------------------------------------------------*
    /// * Copyright (C) 2008 CNT Sistemas de Información S.A., Todos los Derechos Reservados
    /// * http://www.cnt.com.co - mailto:produccion_panacea@cnt.com.co
    /// *
    /// * Archivo: EntidadesCollection.cs
    /// * Tipo: Entidad de Negocio
    /// * Autor: Jaimir Guerrero
    /// * Fecha: 2008 Jun 03
    /// * Propósito: Clase base para las colecciones de entidades de negocio
    /// *-----------------------------------------------------------------------------------------*
    ///

    [DataContract()]
    public class EntidadesCollection : KeyedCollection, IEntidadesCollection
    where TLlave : IComparable
    where TEntidad : EntidadBase, new()
    {
    #region Variable
    private long ultimo;
    private bool asiganar;
    #endregion

    #region Constructor / Destructor
    ///
    /// Constructor por defecto
    ///
    public EntidadesCollection()
    : base()
    {
    VerificarUltimo();
    }
    ///
    /// Constructor para relacionar entidades de detalle con su entidad Padre.
    ///
    ///
    public EntidadesCollection(EntidadBase padre)
    : base()
    {
    this.Padre = padre;
    VerificarUltimo();
    }

    ///
    /// Destructor de la clase
    ///
    ~EntidadesCollection()
    {
    Dispose(false);
    }
    ///
    /// Destructor de la clase
    ///
    public void Dispose()
    {
    Dispose(true);
    }
    ///
    /// Destructor para la herencia
    ///
    /// Llamado por el destructor por defecto
    protected virtual void Dispose(bool disposing)
    {
    if (disposing)
    {
    }
    }
    #endregion

    #region Propiedades
    ///
    /// Lista de llaves contenidad en la colección
    ///
    public ICollection Keys
    {
    get { return base.Dictionary.Keys; }
    }
    ///
    /// Lista de entidades contenidas en la colección
    ///
    public ICollection Values
    {
    get { return base.Dictionary.Values; }
    }
    ///
    /// Sí la colección es de sólo lectura
    ///
    public bool IsReadOnly
    {
    get { return false; }
    }
    ///
    /// Retorna una colección con las entidades que estan marcadas como: modificados, borrados ó creados
    ///
    public EntidadesCollection EntidadesCambiadas
    {
    get
    {
    var res = from c in this
    where c.EstadoRegistro != EstadosEntidad.Original
    select c;

    return this.InstanciarColeccionTipo(res);
    }
    }
    ///
    /// Retorna verdero si alguna de los elementos de la colección es diferete a “Original”
    ///
    public bool TieneCambios
    {
    get
    {
    return (this.Count(enti => enti.EstadoRegistro != EstadosEntidad.Original) > 0);
    }
    }
    ///
    /// Retorna todas las entidades excepto las marcadas como Borrados
    ///
    public EntidadesCollection EntidadesActuales
    {
    get
    {
    var res = from c in this
    where c.EstadoRegistro != EstadosEntidad.Borrado
    select c;

    return this.InstanciarColeccionTipo(res);
    }
    }
    internal EntidadBase Padre { get; set; }
    #endregion

    #region Metodos
    ///
    /// Devuelve la cantidad de entidades que se encuentran en un estado específico
    ///
    /// Estado de la entidad
    ///
    public int CantidadEstado(EstadosEntidad estadosEntidad)
    {
    return this.Count(enti => enti.EstadoRegistro == estadosEntidad);
    }
    ///
    /// Adiciona un elemento a la colección
    ///
    /// Identificador de la nueva entidad
    /// Entidad para adicionar
    public virtual TEntidad Add(TLlave key, TEntidad item)
    {
    item.Padre = this.Padre;
    base.Add(item);
    return item;
    }
    ///
    /// Intenta traer el valor asociado a una llave. sí la llave no existe no se genera error
    ///
    /// Llave para buscar
    /// Entidad donde se asignará el valor encontrado
    ///
    public bool TryGetValue(TLlave key, out TEntidad item)
    {
    return base.Dictionary.TryGetValue(key, out item);
    }
    ///
    /// Intenta Traer una entidad por su llave.
    ///
    /// La llave que se utilzó para asociar la entidad
    /// The matching item, if any. Otherwise default(T) (null in case of standard objects).
    public TEntidad TryGetByKey(TLlave key)
    {
    return Contains(key) ? GetByKey(key) : default(TEntidad);
    }
    ///
    /// 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 method.
    ///
    /// The item with the matching key.
    /// Thrown if no descriptor
    /// with a matching ID was found.
    public TEntidad GetByKey(TLlave key)
    {
    TEntidad retorno;
    if (TryGetValue(key, out retorno))
    return retorno;
    else
    {
    string msg = GetKeyNotFoundMessage(key);
    throw new KeyNotFoundException(GetKeyNotFoundMessage(key));
    }
    }
    ///
    /// 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.
    /// Invoking this indexer always results in a .
    ///
    /// Nothing – invoking the indexer results in a .
    /// Thrown if the indexer is being invoked.
    public new virtual TEntidad this[TLlave key]
    {
    get
    {
    if (key.GetType().FullName == “System.Int32”)
    {
    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.”;
    throw new InvalidOperationException(msg);
    }
    else
    return base[key];
    }
    }
    ///
    /// Gets an exception message that is being submitted with
    /// the 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.
    ///
    /// The submitted (and unknown) key.
    /// This default implementation returns an error
    /// message that contains the requested key.
    protected virtual string GetKeyNotFoundMessage(TLlave key)
    {
    return String.Format(“No se econtró entidad para la llave‘{0}’.”, key);
    }
    ///
    /// Gets the item at the specified .
    ///
    /// Index within the collection.
    /// The item at the specified index.
    ///
    /// if not a valid index of the internal list.
    public virtual TEntidad GetByIndex(int index)
    {
    return Items[index];
    }
    ///
    /// Adds an item to the end of the collection.
    ///
    /// Item to be added to the collection.
    /// This override just provides a more sophisticated exception
    /// message.
    public new void Add(TEntidad item)
    {
    try
    {
    this.Add(item.Identificador, item);
    }
    catch (ArgumentException e)
    {
    TLlave key = item.Identificador;
    if (Contains(key))
    throw new ArgumentException(String.Format(“Una entidad con llave ‘{0}’ ya existe en la colección.”, key), “item”, e);
    else
    throw;
    }
    }
    ///
    /// Busca la llave para un Item
    ///
    /// Item a buscar la llave
    ///
    protected override TLlave GetKeyForItem(TEntidad item)
    {
    return item.Identificador;
    }
    ///
    /// Colección de entidades que se encuntran en el estado especifico
    ///
    /// Estado de la entidad
    ///
    public EntidadesCollection ObtenerEstado(EstadosEntidad estadosEntidad)
    {
    var res = from c in this
    where c.EstadoRegistro == estadosEntidad
    select c;

    return this.InstanciarColeccionTipo(res);
    }
    ///
    /// Importa un dicionario de items TEntidad a la colección
    ///
    /// Diccionario con la información
    public void ImportarDiccionario(IDictionary diccionario)
    {
    //
    // Crear registros que no existen
    //
    var res = from dic in diccionario
    join este in this on dic.Key equals este.Identificador into subres
    from nuevo in subres.DefaultIfEmpty(new TEntidad() { Usuario = “xx” })
    where nuevo.Usuario == “xx”
    select dic;

    foreach (var item in res)
    {
    item.Value.Padre = this.Padre;
    base.Add(item.Value);
    }
    //
    // Pasar los registros existentes
    //
    res = from dic in diccionario
    join este in this on dic.Key equals este.Identificador
    select dic;
    //
    // Ojo como linq me asegura que ya existe el registro no valido si el indexof
    // Treae un numero valido.
    foreach (var item in res)
    {
    item.Value.Padre = this.Padre;
    base.Dictionary[item.Key] = item.Value;
    }

    VerificarUltimo();
    }
    ///
    /// Adiciona los elementos de la colección de origen a la colección Actual
    ///
    /// Colección origen de los datos
    public void Unir(EntidadesCollection coleccionOrigen)
    {
    this.ImportarDiccionario(coleccionOrigen.Dictionary);
    }
    ///
    /// Crear un elemento de la colección relizando las validaciones de datos
    ///
    /// Nuevo elemento para ingresar a la colección
    public void Crear(TEntidad item)
    {
    item.EstadoRegistro = EstadosEntidad.Creado;
    item.Padre = this.Padre;
    if (!item.Identificador.Equals(default(TLlave)))
    item.Identificador = this.SiguienteAsignacion();

    item.ValidarInformacion();
    this.Add(item);
    }
    ///
    /// Modificar un elemento de la colección relizando las validaciones de datos
    ///
    /// Elemento de la colección modificado
    public void Modificar(TEntidad item)
    {
    item.EstadoRegistro = EstadosEntidad.Modificado;
    item.ValidarInformacion();
    try
    {
    this.SetItem(IndexOf(item), item);
    }
    catch (Exception exError)
    {
    throw new ErrorException(string.Format(“No se econtró el elemento [{0}] en la colección”, item.Identificador), exError);
    }
    }
    ///
    ///
    ///
    ///
    public void Borrar(TLlave key)
    {
    try
    {
    if (this[key].EstadoRegistro == EstadosEntidad.Creado)
    this.Remove(key);
    else
    this[key].EstadoRegistro = EstadosEntidad.Borrado;
    }
    catch (Exception exError)
    {
    throw new ErrorException(string.Format(“No se econtró el elemento [{0}] en la colección”, key), exError);
    }
    }
    #endregion

    #region Funciones
    private void VerificarUltimo()
    {
    if (typeof(TLlave) != typeof(string) && typeof(TLlave) != typeof(byte))
    {
    this.asiganar = true;
    this.ultimo = -1;

    if (this.Count > 0)
    {
    this.ultimo = this.Min(p => Convert.ToInt64(p.Identificador));
    if (this.ultimo > -1)
    this.ultimo = -1;
    }
    }
    }
    private TLlave SiguienteAsignacion()
    {
    if (!this.asiganar) return default(TLlave);
    TLlave valor = default(TLlave);
    do
    {
    valor = (TLlave)Convert.ChangeType(this.ultimo–, typeof(TLlave));
    } while (this.Contains(valor));
    return valor;
    }
    private EntidadesCollection InstanciarColeccionTipo(IEnumerable res)
    {
    Type tipo = this.GetType();
    ConstructorInfo constructorInfo = tipo.GetConstructor(new Type[0]);
    EntidadesCollection retorno = constructorInfo.Invoke(new object[0]) as EntidadesCollection;
    if (retorno != null)
    retorno.ImportarDiccionario(res.ToDictionary(p => p.Identificador));

    return retorno;
    }
    #endregion
    }

  2. May 13th, 2009 at 03:52 | #2

    Nice collection – i recently came across the same problem and implemented a similar solution as i already had a class derived from KeyedCollection.

    One question – shouldnt you be overriding the set behaviour of this[index] as well since it also suffers the same issues as get?

    • May 13th, 2009 at 09:21 | #3

      XCalibur

      Keep in mind that the KeyedCollection class is not a dictionary. Accordingly, there is no setter at all (the base class only gives you a getter indexer).

  1. No trackbacks yet.