Combinare due query LINQ

Sembra una vita che non riesco più a trovare il tempo per scrivere due righe sul blog… Colpa del lavoro, a cui sto dedicando decisamente troppo tempo, ok, cerco di rimediare con un mini post.

Vi è mai capitato di avere la necessità di combinare due query LINQ? Sembra una baggianata (e in effetti lo è) ma io ho avuto difficoltà a trovare un esempio bello e pronto… Io ho incontrato il caso con i DomainService, ove mi è servito combinare alla query proveniente dal client, degli altri “pezzi” definiti sul server (si tratta di un evoluzione basata sui due post precedenti sui WCF Ria Services e nHibernate, di cui penso prima o poi di scrivere un post) ma la problematica è decisamente generica.

Definite due query, una che chiamerò “sorgente” è quella al cui queryProvider chiederò di eseguire il risultato della composizione e l’altra, “destinazione”, contiene i “pezzi” che voglio aggiungere alla sorgente.

Per far ciò è sufficiente far “visitare” l’espressione “destinazione” (ovvero quella da aggiungere) da un ExpressionVisitor istanziato sull’espressione sorgente e chiedere al queryProvider “sorgente” di creare una nuova query a partire dall’espressione “visitata”.

Ok, qui servirebbero pagine e pagine di spiegazione su LINQ, ma c’è tanto materiale in giro, e così ben fatto che non mi azzardo nemmeno a scrivere due righe… :-)

…Però il codice necessario lo posto subito!

Come dimostrazione ho creato una semplicissima applicazione console

 using  System;
 using  System.Collections.Generic;
 using  System.Linq;
 using  System.Text;
 
 namespace  ConsoleApplication1
 {
     class  Program 
     {
         static  void  Main(string [] args)
         {
             List <MyEntity > myList = new  List <MyEntity >()
             {
                 new  MyEntity ()
                 {
                     Id = 1,
                     MyProperty = "Messina" 
                 },
                 new  MyEntity ()
                 {
                     Id = 2,
                     MyProperty = "Palermo" 
                 },
                 new  MyEntity ()
                 {
                     Id=3,
                     MyProperty="Catania" 
                 },
                 new  MyEntity ()
                 {
                     Id = 4,
                     MyProperty = "Milazzo" 
                 }
             };
 
             //let's obtain an IQueryable... 
             var  query = myList.AsQueryable();
             //and define a query  over it 
             var  otherQuery = query.Skip(1).Take(2).OrderByDescending(x=> x.Id);
 
             //print out original count 
             Console .WriteLine("querCount = "  + query.Count().ToString());
             //combine the two queries... 
             var  newQuery = QueryCombiner .Combine<MyEntity >(query, otherQuery);
             //and see something about the result! 
             Console .WriteLine("newQueryCount  = "  + newQuery.Count().ToString());
 
             foreach  (var  item in  newQuery)
             {
                 Console .WriteLine("rimasti: "  + (item as  MyEntity ).MyProperty);
             }
 
             Console .ReadKey();
 
         }
     }
     public  class  MyEntity 
     {
         public  int  Id { get ; set ; }
         public  string  MyProperty { get ; set ; }
     }
 }
 
 

che dimostra la piccolissima classe a seguire

 using  System;
 using  System.Collections.Generic;
 using  System.Linq;
 using  System.Text;
 using  System.Collections;
 using  System.Linq.Expressions;
 using  System.ServiceModel.DomainServices.Server;
 
 namespace  ConsoleApplication1
 {
     public  class  QueryCombiner  : ExpressionVisitor 
     {
         // Methods 
         public  static  IQueryable <T> Combine<T>(IQueryable  source, IQueryable  query)
         {
             Expression  expression = Combine(source.Expression, query.Expression);
             return  source.Provider.CreateQuery<T>(expression);
         }
         public  static  IQueryable  Combine(IQueryable  source, IQueryable  query)
         {
             Expression  expression = Combine(source.Expression, query.Expression);
             return  source.Provider.CreateQuery(expression);
         }
 
         public  static  Expression  Combine(Expression  source, Expression  query)
         {
             Expression  retExp = new  Visitor (source).Visit(query);
             return  retExp;
         }
         
         //nested types 
         private  class  Visitor  : ExpressionVisitor 
         {
             // Fields 
             private  Expression  _root;
 
             // Methods 
             public  Visitor(Expression  root)
             {
                 _root = root;
             }
 
             protected  override  Expression  VisitMethodCall(MethodCallExpression  mce)
             {
                 if  (((mce.Arguments.Count > 0) 
                     && (mce.Arguments[0].NodeType == ExpressionType .Constant)) 
                     && ((((ConstantExpression ) mce.Arguments[0]).Value != null ) 
                     && (((ConstantExpression ) mce.Arguments[0]).Value is  IQueryable )))
                 {
                     List <Expression > list = new  List <Expression > {
                         _root
                     };
                     list.AddRange(mce.Arguments.Skip<Expression >(1));
                     return  Expression .Call(mce.Method, list.ToArray());
                 }
                 return  base .VisitMethodCall(mce);
             }
         }
     }
 }
 

I metodi statici in testa alla classe credo che parlino da soli… :)

Ciao a tutti e a presto (spero)

Marco

Posted da mCasamento con no comments

Sarà una lunga gestazione ?

Da un bel po’ di tempo non sto più scrivendo nulla sul blog e temo ci voglia ancora un po’ prima di scrivere qualche altro post. In questo momento oltre al “solito” lavoro in ufficio sto realizzando un add-in per estendere la già potente PivotTable di Excel 2007.
Per i pochi che non lo sapessero, la PivotTable è uno strumento integrato in Excel che consente di visualizzare facilmente dati da praticamente qualunque origine dati per cui esista un driver ODBC o OLEDB, incluso Analysis Services di cui rappresenta il front-end d’elezione.
Con la mia add-in intendo dare la possibilità di aggiungere misure calcolate in sessione a una PivotTable basata su SSAS2005/2008. Ho visto in giro un utility open source su codeplex che fa già qualcosa di simile a quello che ho in mente io, OlapPivotTableExtension  ma conto di realizzare qualcosa di più user-friendly, con una tree che consenta di selezionare gerarchie, membri, livelli, misure e funzioni MDX, e la textbox di editing che esegua almeno un po’ di Syntax Color di base. Intellisense ? Eh non lo so… magari nella ver. 2!
Quello sotto è una screenshot del form che permette di creare le misure calcolate (e dovrebbe integrare delle funzionalità di salvataggio/recupero misure calcolate da un file, alfine di facilitare la condivisione delle formule.

image
Bhe… io continuo a passi lenti lo sviluppo, dopotutto è pur sempre estate, il codice non è affatto “sexy” e il calore del portatile sulle gambe sempre meno piacevole…
A presto!

Posted da mCasamento con no comments

WCF RIA Services e NHibernate, un passo avanti: NHibernate TypeDescriptor

Oggi voglio parlare di come sia semplice utilizzare NHibernate come back end per i WCF RIA Services. La maggior parte degli esempi di utilizzo di questa nuova tecnologia mostrano con disarmante semplicità  come utilizzare da un client Silverlight un data model creato con EntityFramework o LinqToSql, ma in pochi, tra cui menziono Brad Abrams, hanno dato esempio di come i WCF RIA Services possano essere serviti con la stessa semplicità d’uso, da altri ORM, primo fra tutti, NHibernate. E’ da notare che il caso d’uso “semplice” in cui le classi POCO del domain model sono decorate staticamente, è al di fuori degli scopi di questo articolo ed è già ben documentato da questo post, nella serie di Brad Abrams su SL3 in applicazioni LOB. Qui si vuole che le classi POCO restino tali, senza aggiungere dipendenze per responsabilità che sono di altri strati dell’architettura, e si vuole ottemperare al tanto caro principio “Don’t Repeat Yourself”, evitando di esprimere in più punti le stesse logiche (nel mapping di NHibernate e negli attributi delle classi POCO per i WCF Ria Services)

Cosa c’è già: LinqToEntitiesTypeDescriptor


Dopo questa doverosa premessa passiamo ai fatti, e facciamo un po’ analisi delle logiche che sottintendono alla generazione del DomainContext. Per meglio spiegare tali logiche mi avvarrò di due progetti di esempio, ciascuno dei quali gestisce quattro semplici entità: Order, OrderDetail, Customer e Product, uno mediante EntityFramework e l’altro mediante NHibernate. Farò quindi un po’ di parallelismo tra i due progetti e cercherò di dimostrare come ottenere anche con NHibernate la semplicità di generazione automatica del DomainContext, già ottenibile con EntityFramework
SampleDBModel
Schema grafico dell’ EDMX di Entity Framework.

Quando si aggiunge un Domain Service a un progetto, Visual studio propone la maschera a seguire, in cui, nel caso il progetto o una referenza del progetto contengono un ObjectContext (o un DataContext di LinqToSql) sarà possibile scegliere quale entity esporre tramite la classe DomainService che si sta creando.

image

Raccomando di compilare il progetto che contiene l’ObjectContext prima di aggiungere il DomainService o VS non sarà in grado di recuperare le entities. Devo qui fare una precisazione, la maschera di cui sopra offre si la possibilità di scegliere l’ObjectContext/DataContext anche quando esso è contenuto in un altro assembly referenziato dal progetto corrente, ma, in questo caso, l’opzione di generazione delle classi di metadati sarà disabilitata, poiché VS utilizza il sistema delle classi partial per generare tali classi di metadati e se le classi di origine si trovano in un altro assembly il gioco non funziona. Personalmente penso che questa cosa si potesse fare un po’ meglio, soprattutto nell’ottica del wizard che Visual Studio propone, ma poco importa, l’architettura dei WCF RIA Services ci permettono di fare questo ed altro… ;-)

Con le opzioni di cui in finestra, Visual Studio genererà due file, uno per la classe che espone il DomainService (SampleDBService.cs) e uno che contiene una definizione di classe partial per ciascuna classe esposta (SampleDBService.metadata.cs).
Il primo contiene una classe che deriva da LinqToEntitiesDomainService<TContext> al cui interno sono esposti quattro metodi per ciascuna entità esposta, ad esempio per Customer:

 

public IQueryable<Customers> GetCustomers()
public void InsertCustomers(Customers customers)
public void UpdateCustomers(Customers currentCustomers)
public void DeleteCustomers(Customers customers)

 

Rappresentanti i 4 metodi CRUD per i quali il wizard di Visual studio ha fatto uso delle naming conventions documentate qui per permettere al DomainService di identificare le funzionalità di ciascun metodo.
Il file SampleDBService.metadata.cs contiene una definizione di classe partial per ciascuna entità esposta, ad esempio per la classe Customer:

 

 

     [MetadataTypeAttribute(typeof(Customers.CustomersMetadata ))]
     public partial class Customers 
     {
         // This class allows you to attach custom attributes to properties 
         // of the Customers class. 
         // 
         // For example, the following marks the Xyz property as a 
         // required property and specifies the format for valid values: 
         //    [Required] 
         //    [RegularExpression("[A-Z][A-Za-z0-9]*")] 
         //    [StringLength(32)] 
         //    public string Xyz { get; set; } 
         internal sealed class CustomersMetadata 
         {
              // Metadata classes are not meant to be instantiated. 
             private  CustomersMetadata()
             {
             }
              public string AddressLine1 { get; set; }
              public string AddressLine2 { get; set; }
              public string EMail { get; set; }
              public string Firstname { get; set; }
              public int Id { get; set; }
              public string Lastname { get; set; }
              public EntityCollection<Orders> Orders { get; set; }
              public string Phone { get; set; }
        }
     }

In cui è soprattutto da notare l’attributo in testa MetadataTypeAttribute  che indica al TypeDescriptor quale classe utilizzare per restituire i Metadati. Il task Msbuild che genera il DomainContext fa uso proprio del TypeDescriptor (invece che direttamente della Reflection) per ottenere tali metadati. Nell’esempio di cui sopra non vi è alcun metadato aggiuntivo, ma la classe rappresenta il punto di ingresso per aggiungerne con semplicità come esemplifica il codice commentato nello snippet sopra.
Tutte le altre informazioni necessarie alla generazione del DomainContext (definito in un file nascosto del progetto Silverlight, SL4_EF_Basic.Web.g.cs) vengono restituiti dalla classe LinqToEntitiesTypeDescriptor che è  iniettata nella catena dei descrittori da LinqToEntitiesDomainServiceDescriptionProviderAttribute, a sua volta presente nella classe LinqToEntitiesDomainService<T> da cui SampleDBService deriva.
Qual è dunque il lavoro svolto da LinqToEntitiesTypeDescriptor ? Esso analizza il generico tipo T : ObjectContext della classe cui è applicato e ne inferisce vincoli e relazioni, aggiungendoli dinamicamente all’insieme degli attributi restituiti dal TypeDescriptor. Più in particolare esso aggiunge:

  • Key
  • ConcurrencyCheck
  • StringLength
  • RoundTripOriginal
  • Required
  • TimeStamp
  • Editable
  • Association

Notare che l’elenco di  cui sopra potrebbe non essere completo, non proviene da alcuna fonte ufficiale, bensì è frutto di prove sul campo e dell’insostituibile Reflector.
Preciso che per “Dinamicamente” non intendo “a Runtime” bensì durante la generazione del DomainService da parte del wizard di VS, o durante la generazione del DomainContext da parte del task di MsBuild.

Dopo questo breve lavoro di analisi e un po’ di reverse engineering possiamo elencare che cosa serve per avere con NHibernate la stessa semplicità d’uso dei WCF Ria Services che avremmo con Entity Framework o LinqToSql.

NHibernate TypeDescriptor

Dopo aver capito come le classi di MS restituiscono i metadati, possiamo passare alla definizione di un TypeDescriptor che analizzi una Configuration di NHibernate e restituisca quantomeno gli attributi di cui sopra. Per definire un TypeDescriptor bisogna implementare ICustomTypeDescriptor o derivare dalla classe abstract CustomTypeDescriptor ed eseguire l'override dei metodi appropriati. Ho scelto questo secondo approccio e fatto l’override del costruttore per poter avere riferimento alla Configuration di NHibernate, e del metodo GetProperties () per restituire i PropertyDescriptor contenenti gli attributi appropriati.

public override PropertyDescriptorCollection GetProperties()


         {
             if  (properties == null )
             {
                 bool  hasEntityAttributes = false ;
                 properties = base .GetProperties();
                 var  list = new  List <PropertyDescriptor >();
                 foreach  (PropertyDescriptor  descriptor in  properties)
                 {
                     List <Attribute > attrs = GetEntityMemberAttributes(descriptor).ToList();
                     if  (metaDataAttributes.ContainsKey(descriptor.Name))
                         attrs.AddRange(metaDataAttributes[descriptor.Name]);
                     if  (attrs.Count() > 0)
                     {
                         hasEntityAttributes = true ;
                         list.Add(new  PropertyDescriptorWrapper (descriptor, attrs.ToArray()));
                     }
                     else 
                     {
                         list.Add(descriptor);
                     }
                 }
                 if  (hasEntityAttributes)
                     properties = new  PropertyDescriptorCollection (list.ToArray(), true );
             }
             return  properties;
         }

 

Il cuore di tutto è il metodo GetEntityMemberAttributes che dopo aver identificato nel ClassMapping della Configuration di NHibernate, il member descritto dal PropertyDescriptor passato come argomento (la riga di codice a seguire)

member = classMetadata.GetProperty(propertyDescriptor.Name);

esegue una serie di controlli per “mappare” le proprietà di configuration di NHibernate, sugli Attributi necessari ai WCF Ria Services; ad esempio se una proprietà è marcata come “nullable = false”, aggiunge l’attributo “Required” nella table del descrittori.
Le altri classi presenti nel progetto di esempio servono soprattutto a integrare NHibernateTypeDescriptor nella catena: NHibernateMetadataProviderAttribute, che deriva da  DomainServiceDescriptionProviderAttribute è l’attributo che è necessario apporre al Domain Service per indicare a .NET il DescriptionProvider da utilizzare, infatti nel metodo CreateProvider (è un override) restituisce un NHibernateTypeDescriptionProvider, che, a sua volta, nel metodo GetTypeDescriptor, istanzia infine  il nostro NHibernateTypeDescriptor.
Un punto critico di questo metodo è la determinazione del nome delle associazioni e l’individuazione delle proprietà che rappresentano la chiave di detta associazione.
Uno dei vincoli che i WCF RIA Services impongono è infati la presenza in forma “primitiva” delle chiavi per ciascuna associazione presente. Ad esempio, la classe Order, avendo un associazione con Customer deve avere dei campi che rappresentano la chiave della classe Customer, in questo caso Customer_Id.  Dalla configurazione di NHibernate non c’è modo (o meglio non lo conosco) di recuperare tale campo, in quanto lo stesso è esterno alla definizione della foreign key, e l’unico modo che ho trovato è stato quello di ricorrere a delle naming convention, secondo la quale il nome di detti campi deve essere uguale al nome del campo “Classe” + underscore + il nome del campo chiave nella classe destinazione.
Ad esempio Order ha una proprietà Customer, e il campo chiave di Customer si chiama Id, dunque Order deve avere un campo di nome Customer_Id e di tipo uguale al tipo del campo chiave di Customer (int). Se Order avesse un’altra proprietà di tipo Customer, poniamo “BillToCustomer” il nome del campo chiave corrispondente sarebbe stato “BillToCustomer_Id”.
La stessa convenzione è necessaria anche per determinare i campi chiave nel caso di associazioni uno a molti, in cui la chiave è contenuta nella classe destinazione e non nella classe corrente, ad esempio Order ha una ICollection<OrderDetail>, ovviamente i campi di foreign key si trovano nella classe “OrderDetail”, ma occorre una convenzione per utilizzarli. Io ho assunto che i campi chiave necessari si chiamino come il nome della proprietà di relazione (OrderDetails) a cui sottraggo il nome della classe che viene esposta in collection (OrderDetail) reso “plurale” in inglese dall’Inflector (cioè OrderDetails") a cui aggiungo il nome della classe che contiene detta collection (quindi Order"), un underscore e il nome del campo chiave di quest’ultima (cioè il campo chiave di Order, Id). Da quanto affermato ne risulta : Order ha una collection di OrderDetail chiamata OrderDetails, quindi il nome della colonna in OrderDetail che fa da foreign key alla collection deve essere:
OrderDetails - (OrderDetail al plurale) + Order + “_” + Id = Order_Id.
“OrderDetails - (OrderDetail al plurale)” potrebbe sembrare un inutile stravaganza, in quanto in questo caso restituisce una stringa vuota, ma diventa fondamentale nel caso in cui Order abbia due distinte collezioni di OrderDetail; se un ipotetica seconda collection si chiamasse “ShippedOrderDetails” il nome della colonna risultante sarebbe “ShippedOrder_Id”.

Metadata class

Rispetto alla soluzione di MS manca ancora la possibilità di aggiungere degli attributi alle classi POCO senza intaccare le classi stesse, ciò che EntityFramework raggiunge mediante le classi di metadati. Il type descriptor è già predisposto a un’eventualità di questo tipo, infatti nel metodo GetTypeDescriptor di NHibernateTypeDescriptionProvider ricerca all’interno dell’assembly che contiene il DomainService, un tipo con il medesimo nome della classe da descrivere + “_Metadata” , ad esempio per Customer ricerca Customer_Metadata. La classe di metadati fornita viene poi letta da NHibernateTypeDescriptor che aggiunge questi attributi all’insieme.

Conclusioni

A questo punto, per raggiungere un livello di semplicità simile a quello offerto da MS, manca ancora  la generazione del DomainService contenente i metodi per il CRUD e la generazione automatica delle classi di metadati. Con un collega abbiamo già realizzato anche questo passaggio, avvalendoci di T4 e del comodissimo T4 Toolbox. Anche in questo caso viene analizzata la configurazione di NHibernate per aggiungere altri attributi, soprattutto di validazione che potrebbero richiedere un messaggio personalizzato. Non dettaglio qui la soluzione, ma se qualcuno è interessato possiamo parlarne e magari si farà un altro post!
Lo scopo di questo post era solo quello di fornire un idea di come decorare in automatico le classi poco di un domain model per nHibernate. Il domain service utilizzato ha solo i metodi “Get” e per di più restituisce una query che non è filtrabile nei confronti della base dati, perdendo di fatto una delle più belle caratteristiche di RIA, ovvero il filtering determinato dal client ma eseguito poi sul server. Grazie a LINQ to NHibernate quest’implementazione è davvero semplice, ma la versione attualmente disponibile come Global Availability ha parecchi limiti, ed è meglio utilizzare quella che verrà poi resa pubblica con NHibernate 3.  Magari dettaglierò meglio quest’aspetto, ma… in un altro post! :)

Abbiamo visto una piccola parte dell’eleganza e delle possibilità offerte dai WCF RIA Services, ma non è che la punta dell’iceberg; un WCF RIA Services è a tutti gli effetti un WCF e offre le stesse magnifiche possibilità di personalizzazione del framework WCF. In post successivi conto di affrontare altre tematiche connesse all’utilizzo dei DomainServices, ma…. mi serve qualche spunto! Il blog è ancora neonato e devo fargli un po’ di pubblicità o nessuno ne saprà dell’esistenza e nessuno potrà darmi critiche e consigli su quanto posto!

Dimenticavo! Lo zip con i due progetti di esempio è scaricabile sempre dal mio SkyDrive a questo indirizzo

Silverlight DomainDataSource e ViewModel

Da qualche tempo lavoro su un progetto di medie dimensioni che architetturalmente si tratta di una soluzione che usa SQL Server come base dati, nHibernate + LINQ2nHibernate, RIA Services come esposizione dati e Silverlight come interfaccia. Conto di fare dei post sui problemi che ho incontrato/sto incontrando/incontrerò e discutere le stesse in questo spazio.
Oggi parlo di uno dei problemi che ho incontrato nello sviluppo dell’interfaccia client e nell’adozione del celeberrimo MVVM, del quale John Papa da una buona e semplice spiegazionequi.

Non voglio in questo momento parlare dei vantaggi dell'utilizzo di tale pattern o quali helpers utilizzare (PRISM, MVVMLight...) ma cercherò di focalizzare l'attenzione su alcuni problemi che l'utilizzo dello stesso pone.
In particolare, come visibile dall'immagine sopra (rubata vergognosamente dal post di John Papa), la View non deve vedere nient'altro che il ViewModel, quindi qualunque componente all'interno della View... Può referenziare solo il ViewModel! Fin qui semplicissimo, valga come premessa.
Poniamo che sia voglia creare una semplice View con due griglie relazionate tra loro come master/details, il classico esempio di Order e OrderDetails per intenderci.

A seguire le classi Order e OrderDetail

Codice 1
public Order(int id, string description)
{
_id = id;
_description = description;
_orderDetails = new List<OrderDetail>();
}

[Key]
public int Id
{
get { return _id; }
set { _id = value; }
}

public string Description
{
get { return _description; }
set { _description = value; }
}

[Include]
[Association("OrderDetail","Id","OrderId")]
public IList<OrderDetail> OrderDetails
{
get
{
return _orderDetails;
}
set { _orderDetails = value; }
}
}



Codice 2

public class OrderDetail
{
private string _productName;
private int _id;

[Key]
public int Id
{
get { return _id; }
set { _id = value; }
}
public int OrderId { get; set; }

public OrderDetail(int id, string productName)
{
_id = id;
_productName = productName + "_" + id;
}

public string ProductName
{
get { return _productName; }
set { _productName = value; }
}
}

il ViewModel


Codice 3

public class SampleViewModel : DomainContextBase
{
private IEnumerable<Order> _orders;
private Order _selectedOrder;
private DomainService1 _domainContext;

public ViewModelSimulator(DomainService1 domainContext) : base (domainContext)
{
_domainContext = domainContext;
}

public virtual Order SelectedOrder
{
get { return _selectedOrder; }
set
{
_selectedOrder = value;
NotifyPropertyChanged("SelectedOrder");
}
}

public virtual IEnumerable<Order> Orders
{
get
{
return _domainContext.Orders;
}
}
}





e la parte della view che mostra il binding delle due griglie

Codice 4



<Grid x:Name="LayoutRoot" Background="White">
<sdk:DataGrid AutoGenerateColumns="True" Height="148" HorizontalAlignment="Left" Margin="12,12,0,0" Name="dataGrid1"
VerticalAlignment="Top" Width="376" SelectedItem="{Binding SelectedOrder, Mode=TwoWay}" ItemsSource="{Binding Orders}"/>

<sdk:DataGrid AutoGenerateColumns="True" Height="100" HorizontalAlignment="Left" Margin="12,188,0,0" Name="dataGrid2"
VerticalAlignment="Top" Width="376" ItemsSource="{Binding SelectedOrder.OrderDetails}"/>
</Grid>




Ovviamente si vorrebbe che quando l’utente seleziona un Order dalla griglia principale, la griglia secondaria automaticamente mostri le righe di dettaglio, il famoso “lazy loading”.


Se si potesse utilizzare un DomainDataSource la richiesta sarebbe “automagicamente” risolta, insieme ad altre eventuali necessità quali paging, filtering, sorting eccetera; purtroppo però il DomainDataSource è stato pensato per essere dichiarato direttamente nella view e ha una dipendenza sul DomainContext il che significa violare quanto dettato da MVVM (non esporre alla View nient’altro che il ViewModel). Se al contrario si prova a istanziare il DomainDataSource si perdono alcune caratteristiche, prima fra tutte l’AutoLoad.


Jeff Handley spiega bene la cosa nel suo blog: http://jeffhandley.com/archive/2010/03/21/domaindatasource-viewmodel.aspx.



Il DomainDataSource è a quanto ne so l’unico strumento “già pronto” per fare lazy load dal client ma ponendo di non voler legare View e ViewModel diventa inutilizzabile a tale scopo.

Per risolvere questo ho pensato di mettere un proxy tra il binding e il ViewModel in modo da intercettare le chiamate “Get” che il binding esegue per ottenere i dati e invocare (via Reflection) i metodi opportuni che carichino questi dati.


Scrivere manualmente un proxy per ogni ViewModel lo ritengo davvero oneroso e credo sarebbe più semplice scrivere la logica di loading direttamente nel ViewModel, ma un DynamicProxy sarebbe perfetto. Ho cercato in internet e scaricato CastleProject DynamicProxy, da più parti considerato validissimo e soprattutto uno dei pochi se non l’unico che fornisca meccanismi di intercettazioni delle chiamate anche su Silverlight.



Ho impostato il DataContext della View sul proxy e scritto un interceptor che mediante reflection individua le chiamate da effettuare sul DomainService. Il pezzo di codice che segue si trova nella parte di codice della View, il che non è certamente una buona pratica, ma questo è solo un post “didattico” più una prova concettuale che altro.



Codice 5

ProxyGenerator _generator = new ProxyGenerator();
SampleViewModelInterceptor _interceptor = new SampleViewModelInterceptor ();
DomainService1 dmnSvc = new DomainService1();

var proxy = _generator.CreateClassProxy(typeof(SampleViewModel), new object[]{dmnSvc}, _interceptor);

this.DataContext = proxy;



Il cuore di tutto è l’implementazione di IInterceptor, interfaccia di Castle qui a seguire:


Codice 6
public class ViewModelSimulatorInterceptor : IInterceptor
{
private static bool isSelf = false;
private List<MethodInfo> _methodsCall = new List<MethodInfo>();

public void Intercept(IInvocation invocation)
{
if (!isSelf)
PreInvocation(invocation);
invocation.Proceed();
}

private void PreInvocation(IInvocation invocation)
{
if (!invocation.Method.Name.StartsWith("get_"))
return;
var propertyInfo = invocation.Proxy.GetType().GetProperty(invocation.Method.Name.Replace("get_", ""));

if (propertyInfo == null)
return;

Attribute[] customAttributes = Attribute.GetCustomAttributes(propertyInfo)
.Where(x => x is AutoLoadAttribute || x is LazyLoadAttribute).ToArray();
if (customAttributes.Count() == 0)
return;


var wdc = invocation.Proxy;
var _svc = ((DomainContextBase)wdc).DomainContext;

//Auto Load
//Generic Load Method (Asynchronous Call To DomainServices)
var loadMethod = GetMethod(x => x.Name == "Load" && !x.ContainsGenericParameters, invocation);

if (customAttributes.Count(x => x is AutoLoadAttribute) > 0)
{
//Get EntityQuery GetQuery Method
var entityQueryCallMethod = GetMethod(x => x.Name.EndsWith("Query")
&& x.ReturnType.GetGenericArguments().Contains(propertyInfo.PropertyType.GetGenericArguments()[0])
&& x.GetParameters().Count() == 0, invocation);
var entityQuery = entityQueryCallMethod.Invoke(_svc, null);
loadMethod.Invoke(_svc, new object[] { entityQuery, LoadBehavior.MergeIntoCurrent, null, null });
}
else //LazyLoadAttribute
{
var dependencyProps = propertyInfo.PropertyType.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(
x => x.PropertyType.IsGenericType
&& Attribute.GetCustomAttribute(x, typeof(AssociationAttribute)) != null
);

var associationAttributes = propertyInfo.PropertyType.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(x => x.PropertyType.IsClass)
.SelectMany(Attribute.GetCustomAttributes)
.Where(z => z is AssociationAttribute).Cast<AssociationAttribute>();


foreach (var item in dependencyProps)
{
var associationAtt= (AssociationAttribute)Attribute.GetCustomAttribute(item, typeof(AssociationAttribute));

object[] keys = new object[associationAtt.ThisKeyMembers.Count()];

isSelf = true;
var itemValue = propertyInfo.GetValue(invocation.Proxy, null);
isSelf = false;
if (itemValue == null)
continue;
int i = 0;

foreach (string thisKeyMember in associationAtt.ThisKeyMembers)
{
keysIdea = propertyInfo.PropertyType.GetProperty(thisKeyMember).GetValue(itemValue, null);
i++;
}

var queryMethod = GetMethod(x => x.Name.EndsWith("Query")
&& x.ReturnType.GetGenericArguments().Contains(item.PropertyType.GetGenericArguments()[0])
&& x.GetParameters().Count() == 1, invocation);

var entityQuery = queryMethod.Invoke(_svc, keys);

loadMethod.Invoke(_svc, new object[] { entityQuery, LoadBehavior.MergeIntoCurrent, null, null });

}
}
}

private MethodInfo GetMethod(Func<MethodInfo, bool> predicate, IInvocation invocation)
{
var returnValue = _methodsCall.FirstOrDefault(predicate);
if (returnValue == null)
{
var wdc = invocation.Proxy;
var _svc = ((DomainContextBase)wdc).DomainContext;

returnValue = _svc.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public).FirstOrDefault(predicate);
if (returnValue != null)
_methodsCall.Add(returnValue);
}
return returnValue;
}
}





Il metodo “Intercept” viene chiamato da Castle DynamicProxy al posto di qualunque metodo cui si tenti di chiamare sulla classe target (in questo caso SampleViewModel) e la chiamata invocation.Proceed() scritta in esso, consente di effettuare la chiamata sulla classe target, all’uscita da tale chiamata l’esecuzione è ovviamente ancora all’ interno del metodo “Intercept”. 


Vediamo un po’ più in dettaglio cosa succede quando la classe di binding richiede la proprietà “Order”, da me decorata con un attributo “AutoLoad”.


Come visibile dal codice “5” il datacontext non è direttamente il ViewModel, ma il dynamic proxy di esso, per cui il binding chiamerà il metodo “get_Orders” del proxy che verrà intercettato nel metodo “Intercept” fornendo il relativo MethodInfo nel parametro “invocation”.

Interceptor chiama quindi “PreInvocation” (isSelf è senz’altro false nella prima chiamata) che dopo qualche controllo si appoggia alla reflection per ottenere dall’oggetto proxato (il SampleViewModel) il MethodInfo del metodo “Load[EntityQuery]” del DomainService, il metodo cioè che fornisce un surrogato di un IQueryable sul quale è possibile applicare Where, OrderBy, ThanBy, Skip, Take…


Qui cerco di fare un minimo di ottimizzazione (ma non sono nemmeno certo di quanto sia utile in effetti) mettendo in _methodCalls tutti i riferimenti ai metodi che cerco tramite Reflection, e questo è lo scopo del metodo “GetMethod” che applica il predicato fornito prima sulla “cache” dei methodinfo e solo in caso di assenza in cache interroga direttamente il type, sempre con lo stesso predicato (ma quanto mi piace LINQ ?? )


Ottenuto il methodinfo della query faccio lo stesso per il metodo “Load”, che come parametro vuole l’entityquery ottenuta sopra e invoco quest’ultimo: la “magia” è fatta, da adesso in poi ci pensano le classi del DomainService a effettuare la chiamata (REST) opportuna al DomainService sul server.


Nel caso di SelectedOrder viene fatta più o meno la stessa cosa, cambiano solo i parametri del metodo “Load[EntityQuery]” che vengono recuperati nel ciclo, ma con il meraviglioso debugger di VS2010 vi annoierete meno che a leggere la spiegazione che posso fornire io.




Ovviamente si tratta di un prototipo che ha diversi problemi e diverse assunzioni, ma spero possa essere utile come spunto a chiunque si sia imbattuto in un problema analogo. A proposito dei problemi, per esempio, bisogna asssolutamente evitare le chiamate “multiple” che questo proxy genererebbe ogni volta che si tenta di accedere a una proprietà, vedic SelectedOrder che è bindata sia alla prima che alla seconda griglia => due chiamate get => due chiamate al metodo load sul server, magari mentre le proprietà di SelectedOrder sono già tutte nel DomainContext!! e qui si potrebbe ovviare leggendo l’invocation list dell’evento “PropertyChanged” o ricorrendo al timing… non so bene ancora, comunque se questo diverrà codice di produzione e qualcuno è interessato magari faccio un altro post!


L’intero progetto è scaricabile qui


Un osservazione sul lazy loading: non è affatto una panacea, e soprattutto da un client web come silverlight potrebbe addirittura peggiorare le prestazioni anziché migliorarle, ma… quant’è comodo ? :) 


Posted da mCasamento con no comments