Pensare l’estendibilità non basta: il caso EmitMapper
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”:
Se avete giocato un po’ con EmitMapper la prima cosa che fate è definire i mapper:
Qualche commento sul codice: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 ) );
- 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<,>;
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:
- Un tipo (che scoprite a vostre spese dover essere generico) che è il tipo di partenza: e.g. IList<>;
- Un tipo (che scoprite a vostre spese dover essere generico, anche se non è sempre vero…) che è il tipo di destinazione: e.g. ISet<>;
- Un oscuro, e quanto meno teribbbbbile , robo che deve implementare l’interfaccia ICustomConverterProvider;
Un CustomConverterDescriptor ha 3 proprietà:
- ConversionMethodName: è il nome del metodo che il custom converter implementerà al fine di eseguire la conversione;
- ConverterImplementation: è il tipo (System.Type) del custom converter;
- ConverterClassTypeArguments: è un Type[] che rappresentano i tipi di ingresso e i tipi di ritorno del famigerato ConversionMethodName;
Ma andiamo avanti perché c’è una sorpresa più interessante….
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:
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.new DefaultMapConfig() .ConvertGeneric ( typeof( ICollection<> ), typeof( Array ), new MyConverterProvider() )
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:
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:new MappingConfiguration( mm ) .OverrideDefaultCollectionConverter( typeof( ICollection<> ), typeof( Array ), myProvider )
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.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 ); } }
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