You almost had it. Here is a solution:
using System.Collections.ObjectModel;
public class Entity<TPrimKey>
{
public TPrimKey Key { get; set; }
}
public class Entities<TPrimKey> : KeyedCollection<TPrimKey, Entity<TPrimKey>>
{
protected override TPrimKey GetKeyForItem( Entity<TPrimKey> item )
{ return item.Key; }
}
public class MyEntity1 : Entity<int>
{
}
public class MyEntity2 : Entity<int>
{
}
public class MyEntities : Entities<int>
{
public static void Test()
{
var entities = new MyEntities();
var entity1 = new MyEntity1();
entities.Add( entity1 );
var entity2 = new MyEntity2();
entities.Add( entity1 );
}
}
To be clear, this is a slightly different way of thinking about your goal. MyEntities
still does specify key type int
; any generic methods you want go into Entities<TPrimKey>
. OR you create a generic class MyEntitiesA<TPrimKey>
between Entities
and MyEntities
, to hold those methods:
public abstract class MyEntitiesA<TPrimKey> : Entities<TPrimKey>
{
// Methods that apply to all TPrimKeys go here.
// Some methods may need to be abstract, and implemented
// in each subclass, that has a concrete key (or at least a key with a constraint).
}
public class MyEntities : MyEntitiesA<int>
...
MyEntitiesA<TPrimKey>
satisfies your requirement without specifying key type int. (At some point, you will create classes and/or variable declarations that have a concrete type; the "goal" is to be able to stay generic for all methods that can be described generically.)
What you were missing is in this line:
public class Entities<TPrimKey> : KeyedCollection<TPrimKey, Entity<TPrimKey>>
There, the type of value is defined in terms of the type of key. This allows you to define GetKeyForItem
in such a way that it works for all TPrimKey
s.
Note that I've included two classes deriving from Entity<int>
, to show the full power of this collection. Also Test()
to show it in action.
You might also find these links relevant:
"Generic constraint to match numeric types"
use "where TPrimKey : class"
"Blind Interfaces to Models"
This technique is even more powerful if base the collection on an interface
instead of a base class. This allows any class which supports the correct interface (on that key type) to be added to the collection. Here, we see that this allows us to define generic classes EntityA<TPrimKey>
and EntityB<TprimKey>
, with various subclasses of those:
using System.Collections.ObjectModel;
public interface IEntity<TPrimKey>
{
TPrimKey Key { get; set; }
}
public class Entities<TPrimKey> : KeyedCollection<TPrimKey, IEntity<TPrimKey>>
{
protected override TPrimKey GetKeyForItem( IEntity<TPrimKey> item )
{ return item.Key; }
}
public class EntityA<TPrimKey> : IEntity<TPrimKey>
{
public TPrimKey Key { get; set; }
}
public class MyEntityA1 : EntityA<int>
{
}
public class MyEntityA2 : EntityA<int>
{
}
public class EntityB<TPrimKey> : IEntity<TPrimKey>
{
public TPrimKey Key { get; set; }
}
public class MyEntityB1 : EntityB<int>
{
}
public class MyEntities : Entities<int>
{
public static void Test()
{
var entities = new MyEntities();
var entityA1 = new MyEntityA1();
entities.Add( entityA1 );
var entityA2 = new MyEntityA2();
entities.Add( entityA2 );
var entityB1 = new MyEntityB1();
entities.Add( entityB1 );
}
}
NOTE: You don't have to go to interface to create multiple generic subclasses EntityA
and EntityB
. I'm just saying that if you start getting that complicated, its time to consider defining an interface, so that you don't get "boxed in" to a single base class. Defining an interface makes it easy to refactor your class hierarchy, if you ever need to.