L’allergia continua ed evidentemente peggiora… :-)
Qualche tempo fa ho parlato di NHibernate e degli IUserType, fortunatamente non ho fatto vedere l’implementazione :-) perchè c’è un leggero bug, moltgo subdolo, ma sempre bug!
Un doveroso grazie a Marco De Sanctis che mi ha supportato nella ricerca/soluzione del problema.
Un minimo di storia, concedetemelo, immaginiamo un modello del tipo:
 image
dove avete una classe e delle associazioni, nell’esempio volutamente monodirezionali, non delle collection-association, che hanno la caratteristica di poter essere null. Avete quindi un Prodotto che può avere una Serie e può avere un Formato, ma non è detto che siano valorizzati.
Adesso volete semplicemente fare questo:
using( ISession session = SessionHelper.SessionFactory.OpenSession() )
{
    ICriteria c = session.CreateCriteria<Product>()
        .CreateAlias( "Format", "f", JoinType.LeftOuterJoin )
        .CreateAlias( "Series", "s", JoinType.LeftOuterJoin );

    var res = c.List<Product>();

    var wf = res.Where( p => p.Format == null ).ToArray();
    var ws = res.Where( p => p.Series == null ).ToArray();
}
Ergo avere in blocco uno o più prodotti con precaricate le associazioni, ove esistenti. Finchè le cose sono semplici, esattamente come nel modello di cui sopra, tutto funziona come ci si aspetta.
Ma se aggiungiamo una paio di “ficiur” al nostro dominio ecco che il tutto fa le bizze… per riprodurre il problema è sufficiente aggiungere:
  • Il supporto per il Versioning, io utilizzo i timestamp di Sql Server ma poco importa la scelta tennologica ;
  • i miei custom user type per gestire le primary key;
La cosa estremamente fuorviante è che l’errore l’avete sulla conversione da BinaryBlob verso il Byte[] per la gestione della version, perchè giustamente NH si lamenta perchè state cercando di convertire un DbNull in Byte[]… ma la domanda vera è perchè questo tentativo di conversione?
La query prodotta da NH è una cosa del tipo:
SELECT this_.Id          as Id0_2_,
       this_.Description as Descript2_0_2_,
       this_.Format_id   as Format3_0_2_,
       this_.Series_id   as Series4_0_2_,
       f1_.Id            as Id1_0_,
       f1_.Description   as Descript2_1_0_,
       s2_.Id            as Id2_1_,
       s2_.Description   as Descript2_2_1_
FROM   [Product] this_
       left outer join [Format] f1_
         on this_.Format_id = f1_.Id
       left outer join [Series] s2_
         on this_.Series_id = s2_.Id

Che giustamente nel caso di Prodotti senza Serie e/o senza Formato produce delle righe nel resultset che non contengono valori:
image
Guardando il resultset e confrontandolo con quello dell’esempio funzionante non ci sono sostanziali differenze se non i dati in più per fillare le parti nuove del dominio (Version e PK) mi concentro quindi sull’errore e dopo un po’ di verifiche del mapping sentenzio che il problema non sta li… e faccio una prova banale… commento la parte di mapping relativa alla gestione della Version… e tutto funziona senza errori :-)
Ma non è tutto oro quel che luccica, infatti non ci sono errori ma il risultato non è lo stesso quello atteso, NHibernate fa una cosa stranissima, all’apparenza, crea cioè proxy per tutte le reference anche dove dovrebbe metterci dei valori null, questi proxy hanno l’unica stranezza di avere tutte le proprietà a null… sempre più strano.
Alla fine debuggando un po’ qua e un po’ la e confrontando il risultato con il progetto funzionante scopro l’inghippo…
NHibernate fa un ragionamento all’apparenza strano, almeno per me che non ho studiato :-), si aspetta di avere un valore ben preciso per la PK di una entità che non esiste: null, mentre io gli davo un valore predefinito che nel mio dominio significava “null/empty” ma che naturalmente non era null, quello che succede quindi è che NHibernate “va” dallo IUserType e chiama NullSafeGet e siccome lo IUserType gli ritorna qualcosa di diverso da null NHibernate interpreta questo come: li c’è una entità da idratare… e fallisce!
Morale della favola: il manuale delle istruzioni se l’hanno inventato a qualcosa servirà pure… :-) altrimenti si rischia sempre di dare la colpa agli altri.
.m