…sembra una parolaccia, e per molti versi ritengo che lo sia, ma andiamo con ordine:
diciamo che avete il vostro “bel” dominio mappato con il fido Linq to Sql e siete decisamente felici di come funzionano le cose, nonostante gli evidenti limiti di L2S, ma ad un certo punto succede che i requisiti vi impongono di integrare all’interno della vostra applicazione dati che arrivano da una sorgente dati diversa, nello specifico un preistorico database dbase (v.2), che ha il piccolo difetto di non supportare in alcun modo la multiutenza ergo il primo che arriva “locca” e gli altri si attaccano (in una “conversazione” via mail ho usato un termine più colorito :-D). La soluzione più ovvia è nascondere i file di dbase dietro un servizio (ho scelto Wcf ma è poco rilevante la tecnologia) e gestire li gli accessi concorrenti.
Adesso siete di fronte ad un problema, siete evidentemente abituati a scrivere una cosa del tipo:
using( var dc = new DomainDataContext() )
{
    var subjects = dc.Subjects.Where( s => s.Addresses.Count > 0 );
}
Sfruttando tutta la potenza di Linq con l’effetto collaterale che il dominio di L2S si propaga a macchia d’olio nella vostra applicazione. Il problema di fondo è che il dominio in questo caso non è il vostro ma bensì il suo e questo comporta che metterci mano è assai arduo, mentre siete proprio nella condizione in cui avreste bisogno di aggiungere classi al dominio ma gestire queste classi, dietro le quinte, in maniera diversa lasciando alla facciata la potenza di Linq… diciamolo subito: non c’è mezzo :-)
In soldoni quello di cui avete bisogno è semplicemente questo:
using( var dc = new DomainDataContext() )
{
    var subjects = dc.Subjects.Where( s => s.Addresses.Count > 0 );

    var other = dc.DifferentSourceData.Where( ... ).Single();
    other.SampleProperty = "new value...";

    dc.SubmitChanges();
}
perchè? perchè è un gran comodo:
  • programmate a fronte di una singola API;
  • gestite il ciclo di vita della “sessione” sempre allo stesso modo e il comportamente sulle entità di dominio è sempre lo stesso;
  • gestite la persistenza in un solo punto;
  • gestite la persistenza in maniera transazionale su tutti gli storage, ove abbia senso, in maniera trasparente;
  • etc…
Sta di fatto che è impossibile da fare con Linq2Sql, tanto quanto con qualsiasi altro tool, proprio perchè il tool non è pensato per fare quel lavoro, ma è pensato per fare il suo lavoro.
Diciamo che la cosa vi serve, e studiando “scoprite” che l’unica possibilità, è wrappare l’ORM dietro un vostro Repository al fine di poter intergrare e gestire le informazioni che arrivano da fonti diverse. Avete 2 fronti su cui lavorare:
  • Backend: è tutta la parte che sta dietro il vostro “data context”, è a tutti gli effetti un ORM a cui manca solo la parte di mapping e di generazione delle query (praticamente le 2 parti veramente toste), ma di cui dovete realizzare, tra i tanti pezzi, come minimo:
    • Motore di persistenza;
    • Motore di data mapping;
    • IdentityMap;
  • Frontend: è tutta la parte, l’API pubblica, che viene esposta e che consente di interrogare il vostro modello;
La prima parte non è oggetto del post di oggi, è per certi versi molto complessa e per certi altri di una semplicità disarmante l’importante è affrontarla nel modo giusto.
E’ invece decisamente più interessante l’approccio da utilizzare per la realizzazione dell’API pubblica; ma prima di iniziare alcune considerazioni, la domanda di fondo è:
Ha senso wrappare?
La risposta dipendende fortemente dal contesto:
  1. se lo faccio perchè è bello si chiama seg* architetturale => non ha senso;
  2. se lo faccio perchè sono capace e sono figo si chiama seg* architetturale => non ha senso;
  3. se lo faccio perchè penso di poter rimpiazzare l’ORM a caldo si chiama seg*issima architetturale => non ha senso;
  4. se lo faccio perchè ritengo che il dominio generato dal tool non sia un dominio decente molto probabilmente è una se*a architetturale => ergo molto probabilmente non ha senso;
  5. se lo faccio perchè penso che in questo modo posso passare da n-layer a n-tier senza problemi, semplicemente “s c o r d a t e v e l o” => non ha senso, e forse è meglio che rivediate le vostre convinzioni :-);
  6. se lo faccio perchè devo integrare informazioni che arrivano da storage diversi e voglio gestirle in un dominio unificato allora probabilmente ho trovato un motivo decente, anche qui però dipende molto da quanto queste informazioni devono essere intergrate => forse ha senso;
Diciamo che siete nell’ultima casistica e decidete di farlo. Per semplicità degli esempi e dei concetti che ci interessa approfondire pensiamo di essere nella 4a casistica, è tutto più facile da spiegare e i concetti sono identici. D’istinto la prima cosa che fate è questa:
interface IDataContextWrapper
{
    IOrderedQueryable<Linq2Sql.Subject> Subjects { get; }

    void SubmitChanges();
}
ma è evidente che non può funzionare perchè il tipo Subject è ancora il Subject generato da Linq2Sql e non avete risolto proprio un bel nulla, il secondo tentativo è piuttosto simile (se non altro nel fallimento):
interface IDataContextWrapper
{
    IOrderedQueryable<MyDomain.Subject> Subjects { get; }

    void SubmitChanges();
}
Adesso il tipo Subject è parte del vostro dominio quindi all’apparenza la cosa funziona ma:
  • esporre IQueryable ha il pesantissimo difetto, in questo scenario, di produrre un expression tree che non potete “eseguire” ma che dovete tradurre nell’expression tree che il motore di query sottostante si aspetta… un delirio;
  • caliamo la cosa in uno scenario più realistico, diciamo che Subject arriva da un servizio Wcf… domanda che ve ne fate di un expression tree sul “client”? proprio un bel nulla…
Il problema è quindi facilmente identificabile, se espongo IQueryable, dove T è per forza parte del mio dominio, ho un expression tree pensato per il mio dominio e non ho mezzo di mapparlo, con semplicità, verso il dominio di destinazione.
Prima di andare avanti cambiamo per un attimo punto di vista e chiediamoci se ha veramente senso esporre un query provider, come IQueryable, e quindi esporre Linq, direttamente dalla nostra API pubblica: la risposta è evidentemente no.
Se ci pensiamo la cosa ha molto senso:
  • state sviluppando un’applicazione non un framework quindi si suppone che conosciate mostruosamente bene il dominio;
  • avete un set di informazioni e di possibili interrogazioni noto, probabilmente vasto, ma decisamente noto;
In uno scenario del genere che cosa me ne faccio di una API super-generica? nulla, semplice seg* architetturale :-) del resto chi ha pensato Linq o la ICriteria API di Hibernate/NHibernate aveva/ha uno scopo decisamente diverso dal nostro.
Public API
Torniamo a noi, dopo una bella serie di ragionamenti, test, prove e riprove, chilometri a piedi avanti e indietro dalla palestra, giungete a partorire quella che a prima vista sembra la soluzione di tutti i mali, e in parte lo è:
interface IDataContext
{
    IEnumerable GetByQuery( ?? querySpec );
    T GetScalar( ?? scalarSpec );
}
Probabilmente molti di voi in base al naming dei parametri hanno già capito dove voglio andare a parare. Cosa abbiamo fatto, semplicemente abbiamo generalizzato e ridotto all’osso le nostre necessità, del resto non ci serve di più: abbiamo bisogno di poter fare query che ritornano “liste” di elementi e abbiamo bisogno di poter recuparare elementi singoli, scalari appunto.
In questo momento non ci interessa la fase di persistenza, è evidente che anche quella dovrà essere esposta dalla nostra API pubblica ma li non ci sono problemi di sorta.
Abbiamo comunque altri problemi da risolvere:
Cosa ci mettiamo come tipi di dato dei parametri?
All’inizio la cosa è abbastanza semplice, lo faccio con un esempio che è decisamente meglio:
interface IQuery{ }

class SubjectByCode : IQuery
{
    public SubjectByCode( String code )
    {
        this.Code = code;
    }

    public String Code
    {
        get;
        private set;
    }
}
Interessante non trovate? mi limito semplicemente a creare una gerarchia di classi, più o meno complesse, che modellano le possibili query che l’applicazione farà sullo storage.
IDataContext dc;
using( dc )
{
    var query = new SubjectByCode( "my code" );
    var result = dc.GetByQuery<Subject>( query );
}
Sarebbe una pessima idea quella di pensare di modellare un vostro linguaggio di query per il semplice fatto che vi infilereste in un ginepraio mai visto, ma andiamo avanti.
La firma dei metodi soddisfa le nostre esigenze?
La risposta è evidentemente no, altrimenti cosa avrei fatto a fare la domanda… il problema è che molti tool offrono una bellissima feature: le projection.
la nostra necessità è quindi un filino più articolata e la possiamo modellare così:
interface IDataContext
{
    IEnumerable GetByQuery( ?? querySpec );
    TResult GetScalar( ?? scalarSpec );
}
Diciamo quindi al nostro motore da dove vogliamo pescare i dati (TSource) ma anche in che formato vogliamo i dati (TResult), il nostro esempio di cui sopra diventa semplicemente:
var result = dc.GetByQuery<Subject, Subject>( query );
è evidente che la cosa è bruttina e che ha una serie di piccole complicazioni “interne”, non oggetto di questo post, che però ci portano alla necessità di “spostare” quel requisito ad un livello più alto:
public interface IQuerySpecification { }

public interface IQuerySpecification : IQuerySpecification { }
definiamo direttamente a livello di query quale sia il suo scopo in termini di projection, e quindi produciamo un qualcosa del tipo:
class SubjectByCode : IScalarSpecification<Subject, Subject>
{
    public SubjectByCode( String code )
    {
        this.Code = code;
    }

    public String Code { get; private set; }
}
SubjectByCode è un esempio di query in cui non vogliamo projection ma semplicemente un valore scalare, o qualcosa del tipo:
class AddressStringBySubjectQuery : IQuerySpecification<Address, String>
{
    public AddressStringBySubjectQuery( Subject owner )
    {
        this.Subject = owner;
    }

    public Subject Subject { get; private set; }
}
Dove c’è una richiesta di projection da Address a String.
Un assaggio di “internals”
Ok, ma la domanda legittima è perchè tutto questo giro? perchè quello che vi volete limitare a fare è questo:
class NHDataContext : IDisposable, IDataContext
{
    ISession session;
    IQuerySystemManager querySystemManager;
    
    public NHDataContext( ISession session, IQuerySystemManager querySystemManager )
    {
        this.session = session;
        this.querySystemManager = querySystemManager;
    }

    public IEnumerable GetByQuery( IQuerySpecification querySpec )
    {
        var engine = this.querySystemManager.GetQueryEngine( querySpec );
        
        var provider = this.session.Linq();
        var result = engine.ExecuteQuery( querySpec, provider );

        return result;
    }

    public TResult GetScalar( IScalarSpecification scalarSpec )
    {
        var evaluator = this.querySystemManager.GetScalarEvaluator( scalarSpec );

        var provider = this.session.Linq();
        var result = evaluator.Evaluate( scalarSpec, provider );

        return result;
    }
}
Nell’esempio, perfettamente funzionante wrappo NHibernate, ho scelto NHibernate per semplicità d’implementazione dell’esempio. La cosa degna di nota per chi non l’avesse notato è al chiamata a quell’istanza querySystemManager, una sorta di motore di IoC:
var engine = this.querySystemManager.GetQueryEngine( querySpec );
che è dichiarata così:
interface IQuerySystemManager
{
    IQueryEngine GetQueryEngine<TSource, TResult, TProvider>( IQuerySpecification querySpec );
    IScalarEvaluator GetScalarEvaluator<TSource, TResult, TProvider>( IScalarSpecification scalarSpec );
}
Qui è decisamente più evidente che per avere un “query engine” sia necessario specificare quale sia la sorgente dati nel dominio (TSource), il vostro dominio, quale sia il risultato desiderato (TResult) e anche quale sia la sorgente dati dello storage (TProvider) permettondoci quindi di specificare ad esempio una cosa del tipo: GetQueryEngine( … ).
Un query engine infine altro non è che qualcuno che sa trattare una certa query, ed eventualmente sa fare pure il mapping e le projection:
class AddressStringBySubjectQueryEngine : 
AbstractQueryEngine<AddressStringBySubjectQuery, Address, String, Address> {
public overrideIEnumerable<string> ExecuteQuery( AddressStringBySubjectQueryquerySpec, IQueryable<Address> provider ) { var address = "{0} n° {1}, {2} {3} - {4}"; return provider.Where( a => a.Subject == querySpec.Subject )
.Select( a => new
{
Street = a.Street,
Number = a.Number,
ZipCode = a.ZipCode,
City = a.City,
Country = a.Country
} )
.AsEnumerable()
.Select( a => String.Format( address, a.Street, a.Number, a.ZipCode, a.City, a.Country ) );
}
}
bello, facile e linearissimo, inoltre decisamente componentizzato, “molto tanti e molto piccoli” componenti che fanno ognuno il loro sporco lavoro senza battere ciglio.
.m
p.s. 1
Il concetto di Specification non è nulla di nuovo e su Google Code trovate un interessantissmo progetto che implementa proprio lo Specification pattern, il difetto di quell’implementazione è che nel nostro contesto non è applicabile perchè l’implementazione parte dal presupposto che possa operare con Linq direttamente sul vostro dominio senza avere a che fare con un ipotetico mapping da storage diversi.
p.s. 1.1
attenzione che se usate solo NHibernate e non avete nessuna delle necessità di cui sopra utilizzare una tecnica come quella esposta insieme allo Specification Pattern potrebbe darvi vantaggi notevoli. Con l’uscita di NHibernate 2 e di NHibernate.Linq avete in mano uno strumento potentissimo che, imvho, non ha rivali in questo momento l’implementazione di Linq che abbiamo a disposizione non è però esaustiva e potreste trovarvi nella condizione di dover ricorrere ad ICriteria o ad hql; in questa direzione è evidente che poter incapsulare la costruzione della query ha una mostruosità di vantaggi.
p.s. 2
giusto una coriosità su come potrebbe ragionare un IQuerySystemManager:
public IQueryEngine GetQueryEngine( IQuerySpecification querySpec )
{
    var qbt = typeof( IQueryEngine<,,,> )
        .MakeGenericType( 
            querySpec.GetType(),
            typeof( TSource ), 
            typeof( TResult ), 
            typeof( TProvider ) );

    return this.container.GetService( qbt ) as IQueryEngine;
}
Inversion Of Control Rulez :-)