Inquadriamo prima lo scenario:
Abbiamo la classica classe Customer che ha una proprietà che rappresenta la sua chiave primaria: nel mio caso tendo sempre a scegliete come tipo, per queste proprietà, un Guid perchè ritengo che i vantaggi surclassino di gran lunga gli eventuali svantaggi (ma non è questo l'oggetto del post Tongue out); questa proprietà, nel 90% o più dei casi, è mappata sulla colonna del db che rappresenta la PK... ed è proprio questo che non mi piace per nulla.
Supponiamo che un giorno la fonte dati della mia applicazione cambi e non supporti i Guid (scenario poco probabile è vero, ma questo è un esempio Tongue out) ci ritroveremmo con una bella magagna perchè il nostro domain model ha una dipendenza dallo strato dati che apparentemente sembra di poco o nessun conto, ma evidentemente non è così...
L'ispirazione viene da un vecchissimo post di Pierre Greborio ex MVP Solution Architect e ora dipendente Microsoft in quel di San Francisco.
Cerchiamo quindi di capire come potremmo risolvere la questione: una Primary Key astratta.
    [Serializable]
    public abstract class Key : IConvertible, IComparable
    {
        /*
         * Riporto solo una parte dell'implementazione
         * per chiarezza. Tutti i metodi sono definiti
         * come abstract ed è compito della classe derivata
         * implementarne le funzionalità
         */
        #region IConvertible Members

        public abstract TypeCode GetTypeCode();

        #endregion

        #region IComparable Members

        public abstract int CompareTo( object obj );

        #endregion
    }
Cominciamo con il definire una classe che rappresenta una potenziale chiave primaria, essendo un reference type vogliamo che questa classe (o meglio chi deriverà da questa classe) sia obbligato ad implementare i meccanismi di conversione e confronto in modo da poter confrontare due istanze di Key come se facessimo un confronto per valore e quindi non confrontando i "reference".
    [Serializable]
    public class Key : Key where T : IComparable
    {
        private T value;

        public Key() 
        {
            value = default( T );
        }

        public Key( T value )
        {
            if( value == null )
            {
                this.value = default( T );
            }
            else
            {
                this.value = value;
            }
        }

        public override string ToString()
        {
            return ( this.value == null ) ? "" : this.value.ToString();
        }

        public T Value
        {
            get { return this.value; }
        }

        public static implicit operator Key( T value )
        {
            return new Key( value );
        }

        public static implicit operator T( Key pk )
        {
            if( pk == null )
            {
                return default( T );
            }
            else
            {
                return pk.value;
            }
        }

        public static Boolean operator ==( Key pk1, Key pk2 )
        {
            return pk1.Equals( pk2 );
        }

        public static Boolean operator ==( Key pk1, T value )
        {
            return pk1.Equals( value );
        }

        public static Boolean operator ==( T value, Key pk1 )
        {
            return pk1.Equals( value );
        }

        public static Boolean operator !=( Key pk1, Key pk2 )
        {
            return !pk1.Equals( pk2 );
        }

        public static Boolean operator !=( Key pk1, T value )
        {
            return !pk1.Equals( value );
        }

        public static Boolean operator !=( T value, Key pk1 )
        {
            return !pk1.Equals( value );
        }

        public override bool Equals( object obj )
        {
            if( obj == null )
            {
                return false;
            }
            else if( obj is T )
            {
                return this.value.Equals( ( T )obj );
            }
            else if( obj is Key )
            {
                return this.value.Equals( ( ( Key )obj ).value );
            }

            throw new NotSupportedException();
        }

        public override int GetHashCode()
        {
            return this.Value.GetHashCode();
        }

        #region IConvertible Members
    /*
     * Tralascio per l'implementazione di IConvertible 
     * che altro non fa che richiamare i metodi dstatici della
     * classe System.Convert
     */
        #endregion

        public override int CompareTo( object obj )
        {
            return ( ( IComparable )this.Value ).CompareTo( obj );
        }
    }
Definiamo quindi una classe generica Key che deriva da Key e impone che il tipo T sia almeno di tipo IComparable, in questo modo possiamo demandare al tipo T l'onere di gestire la comparazione tra i valori di due istanze.
Abbiamo anche definito l'overload di alcuni operatori e gestiamo le conversioni implicite , quest'ultimo passaggio non è necessario ma è decisamente comodo Wink
Siamo a questo punto, grazie agli operatori di conversione impliciti, in grado di fare qualcosa del tipo:
    Key<Int32> pk1 = 0;
    Key<Int32> pk2 = 0;

    Console.WriteLine( pk1 == pk2 );
Possiamo istanziare 2 oggetti di tipo Key assegnando lo stesso valore (0) e confrontando le due istanze otteniamo che sono uguali nonostante siano 2 reference diversi, ma cosa più interessante possiamo scrivere qualcosa del tipo:
    Key b_pk1 = pk1;
    Key b_pk2 = pk2;

    Console.WriteLine( b_pk1 == b_pk2 );
In questo caso eseguiamo lo stesso confronto ma senza avere la più pallida idea di che tipo reale di Key stiamo trattando, dal punto di vista del linguaggio il passaggio è decisamente banale e ovvio ma implica una cosa molto importante dal punto di vista del design: siamo a questo punto in grado di fornire ad un nostro ipotetio oggetto di dominio una chiave primaria senza che l'oggetto stesso abbia reale conoscenza di quale sia l'implementazione concreta di tale chiave.
Questo implica anche una seconda conseguenza, siamo adesso liberi di "far girare" la chiave primaria all'interno/tra i nostri layer/tier senza che questi siano legati a filo doppio alla sua implementazione.

Vediamo adesso in concreto questo cosa comporta: siccome amiamo renderci la vita facile (è sempre meglio fare due chiacchere con quelle che lanciano gli scatoloni, e chi ha occhio per intendere intenda..., piuttosto che scervellarsi su codice farraginoso) definiamo un'interfaccia che la nostra entità di dominio dovrà implementare per comunicare al mondo che lei possiede una chiave primaria:
    public interface IKey 
    {
        Key GetKey();
    }

    public interface IKey : IKey where T : IComparable
    {
        Key Key { get; }
    }
Anche in questo caso abbiamo due livelli:
  1. un livello molto alto/astratto "IKey" che ha a che fare solo ed esclusivamente con la classe base Key;
  2. un livello "più" concreto "IKey", che deriva da IKey, che determina il tipo concreto che la chiave primaria dovrà incapsulare;
Facciamo un esempio:
    public class Customer : IKey<Int32>
    {
        public Customer()
        {
            this._pk = -1;
        }

        public Customer( Int32 pk )
        {
            this._pk = pk;
        }

        Key<Int32> _pk;
        #region IKey Members

        Key<Int32> IKey<Int32>.Key
        {
            get { return this._pk; }
        }

        #endregion

        #region IKey Members

        Key IKey.GetKey()
        {
            return ( ( IKey<Int32> )this ).Key;
        }

        #endregion
    }
Definiamo una classe Customer che implementa (in questo caso in maniera esplicita, ma poco importa) l'interfaccia IKey imponendo che la chiave primaria sia di tipo Int32. Siamo a questo punto in grado di scrivere una cosa del tipo:
    Customer c = new Customer();
    Key c_pk = ( ( IKey )c ).GetKey();
L'istanza di Key (c_pk) che abbiamo in mano sarà in realtà di tipo "Key" ma al codice che la gestisce in questo caso poco importa e non ha nessuna necessità di saperlo, la cosa importante è che sappia che quell'istanza rappresenta in maniera univoca un determinato cliente all'interno del nostro modello ad oggetti.
Il codice sarà quindi in grado di passare quell'istanza ad un ipotetico DAL (implementato tramite abstract factory) e farsi dare tutti gli ordini di quel cliente, anche in questo caso sarà solo il DAL Concreto che sa che la chiave primaria passata è di tipo Key e saprà quindi come usarla per accedere al Database e fare quello che meglio crede.
A questo punto potrebbe sorgere spontanea un'obiezione: la classe Customer sopra esposta però sa che l'implementazione della PK è fatta con un intero a 32 bit e questo in parte si scontra con il nostro intento iniziale, rendere cioè totalmente agnostico il nostro modello ad oggetti.
Anche in questo caso la soluzione è semplice anche se la spiegazione non è proprio breve, cercherò di essere sintetico... Tongue out, anche qui l'incipit viene da un post del collega M.kino Smile
Una soluzione (che uso abitualmente con soddisfazione) è quella di definire il proprio domain model totalmente astratto (cioè definire tutte le classi come abstract) e di inserire l'implementazione concreta dello stesso all'interno (definendola come internal) del DAL Concreto. Vediamo uno "scheletrico" esempio (tralasciando tutta la parte di abstract factory perchè non è oggetto del presente post):
    public abstract class Customer : IKey
    {
        protected Customer()
        {
            this._companyName = "";
        }

        protected Customer( String compannyName )
        {
            this._companyName = compannyName;
        }

        private String _companyName;
        public String CompanyName
        {
            get { return this._companyName; }
            set { this._companyName = value; }
        }

        #region IKey Members

        public abstract Key GetKey();

        #endregion
    }
iniziamo con il definire un po' meglio la nostra classe Customer, aggiungendo una proprietà CompanyName (per rendere il tutto un po' più credibile) e definendo solo l'implementazione di IKey in maniera astratta in modo da obbligare la classe concreta a determinarne il tipo concreto.
Introduciamo quindi un semplicissimo DAL astratto:

    public abstract class CustomersDataProvider
    {
        public abstract Customer FindByCompanyName( String cn );
    }
Ci rendiamo subito conto che il nostro DAL Concreto non potrà mai istanziare un oggetto di tipo Customer perchè è astratto abbiamo quindi bisogno di una classe concreta da ritornare al chiamante:
    internal class InnerCustomer : Customer, IKey<Int32>
    {
        public InnerCustomer(Int32 pk, String companyName )
            : base( companyName )
        {
            this._pk = pk;
        }

        Key<Int32> _pk;
        #region IKey Members

        public IKey<Int32> Key
        {
            get { return this._pk; }
        }

        #endregion

        #region IKey Members

        public override Key GetKey()
        {
            return this.Key;
        }

        #endregion
    }
Sarà qui che avremo la reale implementazione della chiave primaria (IKey), a questo punto il DAL Concreto è in grado di trattare con qualcosa di concreto (questi si che sono concreti giochi di parole Open-mouthed):
    public class SqlCustomersDataProvider : CustomersDataProvider
    {
        public override Customer FindByCompanyName( String cn )
        {
            /*
             * Esegue le operazioni di select
             * su Sql Server e recupera un DataReader
             * 
             * Con i dati dal db istanzia un nuovo
             * Customer e lo ritorna al chiamante
             */
            Customer c = new InnerCustomer( /* pk from db */, /* companyName from db */ );
            return c;
        }
    }
...e il cerchio si chiude. Possiamo quindi avere un'ulteriore estensione del tipo:
    public abstract class OrdersDataProvider
    {
        public abstract OrderCollection FindByCustomerKey( Key pk );
    }
e interfacciarci con questo nuovo DAL Astratto in questo modo:
     CustomersDataProvider cdp = CustomersDataProvider.Create();
     Customer c = cdp.FindByCompanyName( "name" );
     Key customerKey = c.GetKey();

     OrdersDataProvider odp = OrdersDataProvider.Create();
     OrderCollection list = odp.FindByCustomerKey( customerKey );
A questo punto il nostro domain model è agnostico in tutto e per tutto riguardo la chiave primaria che le entity possono dover gestire.
.m