DTO, IL e Reflection nelle nostre applicazioni.

 

Pur essendo io un fan dei DTO (Data Transfer Object) anche in ambienti non service oriented, trovo alquanto noiosa la parte di idratazione dalla entity di DTO mediante “copia” dei dati dalla entity di dominio utilizzata per la persistenza.

Se ci si trova a lavorare con i servizi, le motivazioni per cui una entity di dominio non dovrebbe MAI essere restituita dal servizio possono essere molteplici, alcune delle quali elencate qui di seguito:

  • La entity di dominio potrebbe non essere serializzabile;
  • La entity di dominio potrebbe avere relazioni unidirezionali e bidirezionali verso altre entità;
  • La entity di dominio potrebbe contenere informazioni necessarie alla sua persistenza o informazioni che nascono e muoiono all’interno del servizio;
  • Restituire informazioni non necessarie all’esterno di un servizio va ad influire sulle performance dello stesso, in quanto le informazioni che deve restituire e serializzare sono maggiori di quelle realmente necessarie causando così rallentamenti nella fase di serializzazione/deserializzazione e trasporto dei dati;
  • Un servizio deve vivere di vita propria: non deve quindi necessitare di nessun altro servizio per poter svolgere il suo lavoro e non deve divulgare verso l’esterno informazioni come eccezioni e dominio;

Ora, se si prova ad analizzare il grafo di dominio riportato qui di seguito e si immagina un metodo che restituisca un’istanza di User, ci si renderà subito conto che si entra in conflitto con tutti i punti elencati poc’anzi.

Dominio

 

In un’applicazione perfetta il servizio dovrebbe restituire un’istanza di un oggetto differente da quella di dominio, contenente solo le informazioni necessarie all’esterno, come il suo ID, Nome, Cognome ed Username, come per la entity seguente:

dto

Andando a guardare il lato implementativo si dovrebbe avere un metodo che, in base ai parametri forniti dal client, recupera un’istanza della entity di dominio User, dalla quale verranno poi prese le informazioni necessarie per idratare la entity di dominio UserDTO, che verrà restituita dal servizio in quanto rispetta tutti i punti sopra elencati (è snella, è serializzabile, non contiene informazioni futili esternamente al servizio, ecc).

Sicuramente una soluzione ottimale sarebbe quella di prelevare dal repository (un database per esempio) solo le informazioni necessarie ad idratare la entity di DTO, ma questo spesso può risultare un lavoro dispendioso, in quanto si rischia di avere poco riutilizzo di codice; oppure ci si può trovare nella condizione in cui si dispone di un’intera entity in cache e l’andare a recuperare l’intera entity dalla memoria può risultare meno dispendioso del recupero delle singole informazioni dal repository.
Con l’avvento degli O/RM l’utilizzo dei DTO si è semplificato di parecchio, come mostrato negli snippet seguenti:

Nhibernate:

using(Isession session = SessionHelper.GetSession())
{
    ICriteria myCriteria = session.CreateCriteria(typeof (User));
    myCriteria.Restrictions.Add("Name", name);

    myCriteria.SetProjection(
          Projections.ProjectionList()
            .Add(Projections.Property("Firstname"), "Firstname")
            .Add(Projections.Property("ID"), "ID")
            .Add(Projections.Property("Lastname"), "Lastname")
                .Add(Projections.Property("Username"), "Username"));    
    
    myCriteria.SetResultTransformer(NHibernate.Transform.Transformers.AliasToBean(typeof(UserDTO))); 
    return myCriteria.List();
}

Linq:

var q = from u in Users where u.ID = 1 select new UserDTO { ID = u.ID, Firstname = u.Firstname, Lastname = u.Lastname, Username = u.Username };
q.ToList();

Entity Idratata manualmente:

User usr = //recupero la entity dal database o dalla cache

UserDTO userDto = new UserDTO();
userDto.ID = usr.ID;
userDto.Firstname = usr.Firstname;
userDto.Lastname = usr.Lastname;
userDto.Username = usr.Username;

Come si potrà intuire, finchè si parla di poche proprietà per poche entities la cosa è fattibile, ma quando si tratta di applicazioni complesse l’utilizzo dei DTO può incidere in maniera pesante sulle tempistiche e sui costi di sviluppo, specie se ci si trova con una entity già idratata.

Il problema potrebbe essere facilmente risolto se ci fosse qualcosa che svolge il lavoro di “trasbordo” dei dati per noi, ad esempio la Reflection.
Purtroppo, come è stato detto in numerosi articoli e blog, questa non è propriamente la soluzione migliore, in quanto a compile time non si conoscono né tipi né i membri in azione.
Per poter ovviare a questo problema si può generare codice IL (Intermediate Language) ad hoc per il nostro scopo ed avere le “stesse performance” del codice compilato (per chi fosse interessato all’argomento può avere maggiori info qui).


Con l’aiuto di Ricciolo ho creato degli Extension Methods che generano IL a runtime ed effettuano il “trasbordo” dei dati dalla entity di dominio alla entity di DTO, senza avere un decadimento delle performance e senza violare i principi esposti precedentemente.
Per prima cosa si realizza l’Extension Method che leggerà tutte le proprietà della entity di Dominio, e questa operazione viene fatta tramite la reflection (se pur la reflection sia lenta, quanta questa operazione verrà effettuata solo alla prima richiesta ed il risultato viene “cachato“ in una hashtable statica) .

 

Come si può notare, all’interno del metodo vengono invocati altri due Extension Methods (FastCreateInstance e FastCopyValue), che hanno il compito di generare l’IL necessario al nostro scopo.
Il metodo FastCreateInstance ha lo scopo di creare una nuova istanza: per ottimizzare le performance viene “cachato” il delegate in una hashtable.

private static readonly Hashtable createInstanceInvokers = new Hashtable();
private delegate object CreateInstanceInvoker();

private static object FastCreateInstance(this Type type)
{
    if (type == null)
        throw new ArgumentNullException("type");

    var invoker = (CreateInstanceInvoker)createInstanceInvokers[type];
    if (invoker == null)
    {
        LambdaExpression e = Expression.Lambda(typeof(CreateInstanceInvoker), Expression.New(type), null);
        invoker = (CreateInstanceInvoker)e.Compile();
        createInstanceInvokers[type] = invoker;

    }
    return invoker();
}

Per il metodo FastCopyValues il discorso è più o meno lo stesso, si ha un’hashtable per il caching del delegate ed il codice IL necessario a copiare i valori:

private static readonly Hashtable getAndSetValuesInvokers = new Hashtable();
private delegate void GetSetValuesInvoker(object source, object target);

private static void FastCopyValues(this IEnumerable property, object source, object target)
{
    if (property == null)
        throw new ArgumentNullException("property");

    if (source == null)
        throw new ArgumentNullException("source");

    if (target == null)
        throw new ArgumentNullException("target");

    GetSetValuesInvoker invoker = GetGetAndSetCachedInvoker(property, source.GetType(), target.GetType());
    invoker(source, target);
}

private static GetSetValuesInvoker GetGetAndSetCachedInvoker(IEnumerable properties, Type sourceType, Type targetType)
{
    var invoker = (GetSetValuesInvoker)getAndSetValuesInvokers[properties];
    if (invoker == null)
    {
        var method = new DynamicMethod("test", null, new[] { typeof(object), typeof(object) }, typeof(object), true);
        ILGenerator il = method.GetILGenerator();
        il.DeclareLocal(sourceType);
        il.DeclareLocal(targetType);

        il.Emit(OpCodes.Nop);
        il.Emit(OpCodes.Ldarg_0);
        if (sourceType.IsClass)
            il.Emit(OpCodes.Castclass, sourceType);
        else
            il.Emit(OpCodes.Unbox_Any, sourceType);
        il.Emit(OpCodes.Stloc_0);

        il.Emit(OpCodes.Ldarg_1);
        if (targetType.IsClass)
            il.Emit(OpCodes.Castclass, targetType);
        else
            il.Emit(OpCodes.Unbox_Any, targetType);
        il.Emit(OpCodes.Stloc_1);

        foreach (PropertyInfo property in properties)
        {
            PropertyInfo sourceProperty = sourceType.GetProperty(property.Name, BindingFlags.Instance | BindingFlags.FlattenHierarchy | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.SetProperty);
            PropertyInfo targetProperty = targetType.GetProperty(property.Name, BindingFlags.Instance | BindingFlags.FlattenHierarchy | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.SetProperty);

            if (sourceProperty != null && targetProperty != null)
            {
                il.Emit(OpCodes.Ldloc_1);
                il.Emit(OpCodes.Ldloc_0);
                il.EmitCall(OpCodes.Callvirt, sourceProperty.GetGetMethod(true), null);
                il.EmitCall(OpCodes.Callvirt, targetProperty.GetSetMethod(true), null);
                il.Emit(OpCodes.Nop);
            }
        }
        il.Emit(OpCodes.Ret);

        invoker = (GetSetValuesInvoker)method.CreateDelegate(typeof(GetSetValuesInvoker));
        getAndSetValuesInvokers[properties] = invoker;
    }

    return invoker;
}

Come si può vedere dal codice precedente, spesso viene utilizzata un hashtable per il caching dei delegate e delle proprietà da copiare. Seppure ad ogni lettura viene effettuato un cast, l’hashtable risulta molto più adatta rispetto ad un Dictionary tipizzato, in quanto è già thread-safe, evitando così l’inserimento di vari lock nel codice.
Se a prima vista può sembrare complesso, basterà guardare il codice seguente per capire la sua semplicità di utilizzo.

User item = new User
                {
                    ID = 45, 
                    Username = "imperugo", 
                    Firstname = "Ugo", 
                    Lastname = "Lattanzi", 
                    CreationDate = DateTime.Now, 
                    LastLoginDate = DateTime.Now.AddDays(-5)
                };


UserDTO returnItem = item.CopySameValues();

L’unica accortezza che bisogna avere riguarda le proprietà che si vogliono copiare tra le due entità, che devono avere lo stesso nome ed essere dello stesso tipo, per il resto è semplicissimo.


Comments