Ho già parlato di EmitMapper, lodandone le prestazioni, in questi giorni stiamo però lavorando ad un progetto dove abbiamo la necessità di personalizzare, in piccola parte a dire il vero, il processo di mapping.
Lo scenario
Avete un bel servizio WCF che fa da front-end per un modello, il servizio espone un ampio set di operazioni, siccome gli use case sono molto vari, il servizio WCF modella i DTO che ritorna proprio un base allo use case; quindi avete un Product che viene mappato su un ProductDto, ma avete anche una projection di Product che viene mappata su un ProductSummaryDto.
n.d.r.: i DTO sulla carta sono una gran bella cosa ma in pratica rischiate di passare la vita a manutenere DTO, EmitMapper (o uno strumento per automatizzare il processo di mapping) diventa uno strumento vitale, soprattutto nel caso di un servizio WCF che fa uso di NHibernate e che per ovvio motivi non può far viaggiare on-the-wire il dominio. Una delle cose interessanti di EmitMapper, oltre alle prestazioni, è che facendo uso di IL generato a runtime, sono ovviamente spaziali, è che potete mappare i costruttori quindi nulla vi vieta di avere oltre ad un mapping automatico “modello –> DTO” anche uno “DTO –> modello”
A complicare le cose avete un albero di elementi “Category” e volete un albero di elementi “CategoryDto”, la peculiarità è che “Category” è una classe base da cui ad esempio deriva “SpecialCategory”, quindi se uno dei nodi è una “SpecialCategory” volete che il corrispondente DTO sia uno “SpecialCategoryDto”:
image
Se avete giocato un po’ con EmitMapper la prima cosa che fate è definire i mapper:
var mapperManager = ObjectMapperManager.DefaultInstance;
 
var specialCategoryMapper = mapperManager.GetMapper<SpecialCategory, SpecialCategoryDto>
(
     new DefaultMapConfig()
         .DeepMap()
         .ConvertUsing<Category, Int32>( p => p == null ? -1 : p.Id )
);
 
var categoryMapper = mapperManager.GetMapper<Category, CategoryDto>
(
     new DefaultMapConfig()
         .DeepMap()
         .ConvertUsing<Category, Int32>( p => p == null ? -1 : p.Id )
);
Qualche commento sul codice:
  • DeepMap serve per istruire il mapper che vogliamo che l’intero grafo, e non solo il primo livello, venga mappato;
  • ConvertUsing istruisce il mapper che nel momento in cui trova una proprietà di tipo Category e la vuole mappare su una di tipo Int32 (perché i nomi delle proprietà coincidono) deve chiamare la nostra Func<,>;
La configurazione che abbiamo definito non funziona, ma direi che è anche abbastanza evidente che non possa funzionare, anche se fastidioso, il processo di mapping incontra una lista di nodi che è di tipo IList, esposti dalla proprietà ChildCategories, e non si sogna minimamente di cercare di capire che l’istanza dell’elemento n può essere qualcosa che eredita da Category limitandosi a mappare l’oggetto come se fosse un elemento di tipo Category e basta.
Il primo tentativo che fate è quello di usare ConvertUsing<,> purtroppo senza successo perché non è pensato per gli elementi di una lista ma solo per le proprietà direttamente esposte dagli oggetti coinvolti nel processo di mapping, e già qui io semplicemente storco il naso...
A questo punto vi armate di pazienza e scoprite, non senza fatica per la totale assenza di documentazione, che esiste un metodo ConvertGeneric che prende in pasto 3 parametri:
  1. Un tipo (che scoprite a vostre spese dover essere generico) che è il tipo di partenza: e.g. IList<>;
  2. Un tipo (che scoprite a vostre spese dover essere generico, anche se non è sempre vero…) che è il tipo di destinazione: e.g. ISet<>;
  3. Un oscuro, e quanto meno teribbbbbile Smile, robo che deve implementare l’interfaccia ICustomConverterProvider;
dopo non pochi smadonnamenti (fate molto prima a spulciare nel terrificante sorgente di EmitMapper) scoprite che un ICustomConverterProvider ha lo scopo di fornire al mapper (o meglio alla configurazione) un’istanza di un CustomConverterDescriptor…che è molto peggio…
Un CustomConverterDescriptor ha 3 proprietà:
  1. ConversionMethodName: è il nome del metodo che il custom converter implementerà al fine di eseguire la conversione;
  2. ConverterImplementation: è il tipo (System.Type) del custom converter;
  3. ConverterClassTypeArguments: è un Type[] che rappresentano i tipi di ingresso e i tipi di ritorno del famigerato ConversionMethodName;
Ora…già qui io sono inorridito…perché mi sono chiesto: a che serve un’interfaccia? e mi è sorto il primo sospetto che l’autore di EmitMapper non avesse mai provato neanche di striscio il suo meccanismo di estendibilità.
Ma andiamo avanti perché c’è una sorpresa più interessante…. Confused smile
Tornando al nostro problema quello che abbiamo bisogno di fare è intercettare la conversione di ogni singolo item della lista sorgente al fine di decidere come convertirlo, vi armate di pazienza, molta…ma proprio molta, e cercate di capire come funziona il meccanismo basato sul modello che abbiamo appena esposto, la prima cosa che cercate di fare è quindi quella di iniettare un vostro custom ICustomConverterProvider in cui piazzare un bel breakpoint per cominciare a capire come gira il fumo. Una cosa del tipo:
new DefaultMapConfig()
      .ConvertGeneric
      (
          typeof( ICollection<> ),
          typeof( Array ),
          new MyConverterProvider()
      )
Non viene mai invocato… ma proprio mai. Anche qui dopo un po’ di enumerazioni di santi scoprite che la simpatica DefaultMapConfig ha un metodo protected RegisterDefaultCollectionConverters che registra un “collection converter” che fondamentalmente prende in pasto tutte le casistiche di fatto rendendo pressoché impossibile fare l’override del comportamento di EmitMapper.
Quello che fate quindi è derivare una nuova configurazione da quella di default e cercare di cambiare quel comportamento, con l’obiettivo di rendere iniettabile dall’esterno il default converter. Approfondendo scoprite che non è così semplice perché nella configurazione di default non c’è nulla di lazy e nello specifico quel metodo viene chiamato dal costruttore, quindi in modalità top-down, rendendo di fatto impossibile intercettarlo dall’esterno.
Il primo obiettivo è quindi poter fare questo:
new MappingConfiguration( mm )
     .OverrideDefaultCollectionConverter( typeof( ICollection<> ), typeof( Array ), myProvider )
La soluzione passa dal capire quale sia la relazione tra l’ObjectsMapperManager e la configurazione: una volta che la configurazione è stata creata la prima cosa che l’ObjectsMapperManager fa è chiamare GetMappingOperations, perché quindi non fare una cosa del tipo:
public class MappingConfiguration : DefaultMapConfig
{
     protected override sealed void RegisterDefaultCollectionConverters()
     {
         //NOP :-)
     }

     Boolean defaultCollectionConverterInitialized = false;

     public MappingConfiguration OverrideDefaultCollectionConverter( Type from, Type to, ICustomConverterProvider converterProvider )
     {
         if( !this.defaultCollectionConverterInitialized )
         {
             this.ConvertGeneric
             (
                 from,
                 to,
                 converterProvider
             );

             this.defaultCollectionConverterInitialized = true;
         }

         return this;
     }

     void RegisterDefaultCollectionConverterIfRequired()
     {
         if( !this.defaultCollectionConverterInitialized )
         {
             this.ConvertGeneric
             (
                 typeof( ICollection<> ),
                 typeof( Array ),
                 new CollectionArrayConverterProvider()
             );

             this.defaultCollectionConverterInitialized = true;
         }
     }

     public override IMappingOperation[] GetMappingOperations( Type from, Type to )
     {
         this.RegisterDefaultCollectionConverterIfRequired();
         return base.GetMappingOperations( from, to );
     }
}
Ci limitiamo a bloccare la funzionalità che non ci piace, per fortuna che il metodo è virtual, e poi esponiamo la nostra invocandola al momento giusto. A questo punto possiamo fare quello che vogliamo con la configurazione, ma che fatica.
Nelle prossime puntate vedremo un bel set di novità che possiamo esporre dalla nostra configurazione custom al fine di renderla veramente malleabile per l’utilizzatore. Questo caso è di sicuro interesse perché ci fa capire come pensare gli extension point senza mai di fatto usarli abbia come risultato solo ed esclusivamente sovra ingegnerizzazione inutile, totalmente inutile.
.m