Șpârle cu rapoarte Telerik Report Engine (II) – Când nu ești chiar tu însuți

Î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?