În treacăt fie spus, nu-i vorba de ceva legat de Telerik Report Engine în sine, cât de faptul că reprezintă un mediu unde asemenea probleme pot lesne apărea (și, în definitiv, a ieșit de sub macat după rezolvarea investigației precedente). Așadar, avem un serviciu de raportare ce-și extrage dinamic rapoartele dorite dintr-o tabelă. Fiecare raport conține și DLL-ul ce unde trebuie încărcat și clasa ce-l produce.
Bun, pentru unul din ele, achiziția de date troznea sever cu sânge pe pereți și suficientă confuzie cât să-și reclame propriul gen pe buletin:
An error occurred while invoking data retrieval method. ---> AutoMapper.AutoMapperMappingException: Missing type map configuration or unsupported mapping. Mapping types: Object -> SmufItemViewModel System.Object -> Zbuf.Stuf.Common.ViewModels.SmufItemViewModel
O astfel de eroare înseamnă, pe scurt, că AutoMapper trebuie să creeze un obiect de un tip dintr-un alt obiect de un alt tip fără să aibă cunoștință despre cum s-o facă. Și atunci zice fie Nu știu, d-astea, nu știu!, fie I have no memory of this place. Și mesajul de mai sus este dovada. Atâta doar că, urmărind șirul sfiicios de firimituri lăsat de excepție, la linia în cauză se află:
StaticMapper.Map<SmufItemViewModel>( item ); //where item is of type SmufStatusSearch
Mai descurcă lucrurile dacă poți! Nu doar că nu are o regulă de corespondență, nici măcar nu pare să știe ce fel de obiect a primit! Iar dacă AutoMapper nu știe, nu semnifică oare că runtime-ul .NET nu știe? Acest nu știe, însă, dacă ar fi adevărat, ar fi foarte bizar: ar trebui să însemne că de fapt nu poate să-ncarce DLL-ul unde se află clasa, iar asta ar produce o excepție. Dar instanța există bine-mersi.
Prima ipoteză reală a fost că o instanță a SmufStatusSearch traversează granița dintre două DLL-uri (executabilul rădăcină și cel ce implementează raportul executabil), cu schepsisul că tipul exista în ambele DLL-uri, rezultând de fapt două identități diferite: ReportService.EXE -> SmufStatusSearch, respectiv BoomBoomReport.DLL -> SmufStatusSearch. Căzu totuși repede, întrucât o scurtă analiză a codului a relevat că instanța e garantat să aparțină DLL-ului raportului. Deci, deși duplicarea era reală, nu avea niciun cuvânt de spus.
Așadar, pentru elucidarea problemei, mi-am pus cămașa roșie de scandal și pantalonii maro de fugă și m-am avântat în sursele AutoMapper. Acel StaticMapper apelează AutoMapper.Mapper.Map<TDestination>(object source). Mai departe devine interesant – ajunge la un MapCore unde se construiește pachetul folosit la căutarea unui convertor potrivit:
private TDestination MapCore<TSource, TDestination>(TSource source,
TDestination destination,
ResolutionContext context,
Type sourceType = null,
Type destinationType = null,
IMemberMap memberMap = null)
{
return ConfigurationProvider
.GetExecutionPlan<TSource, TDestination>(MapRequest())(source,
destination,
context);
MapRequest MapRequest()
{
var runtimeTypes = new TypePair(
source?.GetType() ?? sourceType
?? typeof(TSource),
destination?.GetType() ?? destinationType
?? typeof(TDestination)
);
var requestedTypes = new TypePair(typeof(TSource),
typeof(TDestination));
return new MapRequest(requestedTypes,
runtimeTypes,
memberMap);
}
}
De reținut aici că MapRequest primește atât tipurile cerute (adică la compilare), cât și tipurile propriu-zise la rulare. Cum de fapt se cere construcția printr-o metodă al cărei singur argument generic este tipul destinație, cel sursă este specificat la compilare ca System.Object. Mai departe, microfonul este predat către MapperConfiguration:
public LambdaExpression BuildExecutionPlan(MapRequest mapRequest)
{
var typeMap = ResolveTypeMap(mapRequest.RuntimeTypes)
?? ResolveTypeMap(mapRequest.RequestedTypes);
if (typeMap != null)
{
return GenerateTypeMapExpression(mapRequest, typeMap);
}
var mapperToUse = FindMapper(mapRequest
.RuntimeTypes);
return GenerateObjectMapperExpression(mapRequest,
mapperToUse);
}
Deci convertorul este căutat după tipurile propriu-zise la rularea programului. Dar cum sunt raportate dacă de fapt nu se reușește conversia? Contrar primului impuls, excepția nu provine din FindMapper, ci din GenerateObjectMapperExpression:
private LambdaExpression GenerateObjectMapperExpression(
MapRequest mapRequest,
IObjectMapper mapperToUse
)
{
//...
if (mapperToUse == null)
{
fullExpression = Throw(
"Missing type map configuration or unsupported mapping.",
Constant(null,
typeof(Exception))
);
}
else
{
//..
}
//...
BlockExpression Throw(string message, Expression innerException) =>
Block(Expression.Throw(New(
ExceptionConstructor,
Constant(message),
innerException,
Constant(mapRequest.RequestedTypes)
)),
Default(destinationType));
}
Acel ExceptionConstructor aparține AutoMapperMappingException, dar nu mai are rost să mergem mai departe, contează ce parametru primește: mapRequest.RequestedTypes. Deci, dacă se întâmplă să-i ceri să convertească niște valori puchinoase, nu va găsi o corespondență și va raporta perechea de tipuri cerute. Nu cunosc motivul și nu importă, măcar am fixat cauza excepției, chit că eram la fel de-n ceață vis-a-vis de cauza cauzei.
M-am mai învârtit un pic în jurul cozii, până am luat la puricat ambele DLL-uri din nou. După cum ziceam, duplicarea era reală și am vrut să verific cam cât de întinsă e problema. Și ce să vezi, mi-am zis și eu ca Scotty: Imagine that, it never occurred to me that space was the thing that was moving! Anume, că, pe lângă o pleiadă întreagă de clase, și StaticMapper exista de două ori:
– în BoomBoomReport.DLL conținând o instanță neconfigurată de AutoMapper.Mapper,
– iar în ReportService.EXE sălășluind una diferită, configurată încă de la pornirea serviciului.
Jean Claude Van Damme în Dublu Impact n-ar fi iscat mai multă confuzie. Poate că merită amintită și cauza cauzei cauzei: ca ultim pas al unei împachetări de producție se face o fuziune între mai multe DLL-uri (folosind ILMerge) și se pare că a fost dusă un pic prea departe la această ultimă versiune adăugându-se încă unul, cel ce mi-a dat de furcă:
– în DLL-ul BoomBoomReport.DLL, căpătând o identitate.
– în executabilul ReportService.EXE, căpătând o altă identitate.
Mielul blând suge la două capre, no?
