posts - 644, comments - 1982, trackbacks - 137

My Links

News

Raffaele Rialdi website

Su questo sito si trovano i miei articoli, esempi, snippet, tools, etc.

Archives

Post Categories

Image Galleries

Blogs

Links

Thursday, September 1, 2011

RIA Services ed EF 4.1 code-first non si sposano bene

Il primo test che ho effettuato è già concluso miseramente.

Il modello di prova ha una serie di entities e ciascuna di queste ha i corrispettivi 'details' (Contact => ContactDetails, Document => DocumentDetails, etc.).

Ho scelto di avere una sola tabella Details per cui non posso avere la FK in Details che punta alla Contact e alla Document.

Creo quindi il DomainService in un progetto RIA, dopo aver cancellato il progetto Silverlight che è inutile per questo esempio:

[EnableClientAccess] public class TestService : DbDomainService<BizContext> { [System.ServiceModel.DomainServices.Server.Query(IsDefault = true)] public IQueryable<Contact> GetProducts() { return this.DbContext.Contacts; } ...

Configuro l'endpoint (nel mio caso oData ma potrebbe essere anche SOAP o Json):

<system.serviceModel> <domainServices> <endpoints> <add name="OData" type="System.ServiceModel.DomainServices.Hosting.ODataEndpointFactory, System.ServiceModel.DomainServices.Hosting.OData, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" /> </endpoints> </domainServices>

 

E purtroppo ottengo una YSOD con questo errore:

Unable to retrieve association information for association '….DataLayer.Contact_Details'. Only models that include foreign key information are supported

Un vecchio post conferma i sospetti, la FK è indispensabile.

A questo va aggiunto il noto problema delle relazioni molti a molti (http://forums.silverlight.net/p/90385/346173.aspx) (http://social.msdn.microsoft.com/Forums/is/adodotnetentityframework/thread/cc48da63-502d-4429-b90a-0c558b1b9c94) che addirittura è sfociato in un progetto di workaround su codeplex (http://m2m4ria.codeplex.com/).

I RIA services sono indubbiamente una manna per chi fa RAD e le più recenti aggiunte dell'endpoint JSon per supportare i client jQuery sono vere killer features.

Ma qui stiamo partendo dal presupposto che il Modello venga prima di tutto. Quindi:

  • per i più svariati motivi viene prima fatto il design delle entity
  • poi mappo le entity al database con la fluent-API della Code-first, e genero il DB di conseguenza
  • quindi voglio 'nascondere' tutto dietro un servizio in modo da poter presentare i dati con la fondamentale IQueryable e per client di tipo differente:
    • Silverlight via soap
    • jQuery via json
    • altri client via oData

In quest'ottica non posso stravolgere il modello solo perché RIA non capisce bene il mapping di EF 4.1 e quindi per certi scopi non sono la soluzione. Un vero peccato perché si perdono un certo numero di facilities che sarebbero realmente molto comode.

Altri esperimenti in vista …

posted @ Friday, September 2, 2011 2:54 PM | Feedback (3) |

EF 4.1 tracking di entity disconnesse (scenario 'POCO')

In uno scenario POCO che utilizza Entity Framework 4.1 (code-first) di default abbiamo in regalo il tracking dei cambiamenti apportati alle entities.

Caso 1.

Category cat; System.Data.EntityState state; using (BizContext ctx = new BizContext()) { cat = ctx.Categories.First(); cat.Name += "X"; state = ctx.Entry(cat).State; Console.WriteLine(state); }

Il risultato di questo codice è "Modified". Dietro le quinte infatti EF chiama ctx.ChangeTracker.DetectChanges(); che esegue il confronto delle proprietà delle entity dichiarate nel context BizContext che deriva da DbContext.

Nessun timore per le performance, l'automatismo si può disabilitare ed eseguire il controllo, se richiesto, on-demand:

using (BizContext ctx = new BizContext()) { ctx.Configuration.AutoDetectChangesEnabled = false; cat = ctx.Categories.First(); cat.Name += "X"; ctx.ChangeTracker.DetectChanges(); state = ctx.Entry(cat).State; Console.WriteLine(state); }

Naturalmente il controllo verifica che i valori siano effettivamente differenti, per cui se si scrive una cosa di questo tipo:

cat.Name = string.Copy(cat.Name);

l'entity non verrà rilevata come modificata.

CASO 2.

Fin qui tutto ok. Lo scenario però è troppo semplice perché abbiamo imparato che gli scenari reali sono n-tier o comunque disconnessi (come nel caso delle web application) di conseguenza serve fare qualcosa di più.

using (BizContext ctx = new BizContext()) { cat = ctx.Categories.First(); } cat.Name += "X"; using (BizContext ctx = new BizContext()) { ctx.Categories.Attach(cat); state = ctx.Entry(cat).State; Console.WriteLine(state); ctx.MarkAsModified(cat); // manuale! state = ctx.Entry(cat).State; Console.WriteLine(state); }

Il metodo MarkAsModified l'ho aggiunto alla mia classe BizContext ed è definito in questo modo:

public void MarkAsModified(object entity) { var ctx = ((System.Data.Entity.Infrastructure.IObjectContextAdapter)this).ObjectContext; ctx.ObjectStateManager.ChangeObjectState(entity, System.Data.EntityState.Modified); }

In questa seconda casistica:

  • dopo aver chiuso il context, modifichiamo l'entity che perciò è disconnessa
  • riapriamo il context ed eseguiamo l'attach, dopo il quale sfortunatamente l'entity viene segnata come 'unchanged'
  • dobbiamo quindi essere noi a marcarla come modificata (MarkAsModifid) e finalmente entity framework potrà lanciare le update sull'invocazione della SaveChanges (non presente nello snippet)

CASO 3.

Il problema di questa soluzione è che abbiamo perso del tutto il tracking che dovrà esser fatto perciò manualmente. Lo scenario è comunque valido per certe applicazioni e l'unica accortezza sarà quella di impostare AutoDetectChanges a false in modo da evitare inutili perdite di tempo da parte di EF.

Volendo recuperare il meccanismo di tracking, ecco il terzo caso:

using (BizContext ctx = new BizContext()) { cat = ctx.Categories.First(); } cat.Name += "X"; using (BizContext ctx = new BizContext()) { IObjectContextAdapter adapter = ctx as IObjectContextAdapter; var objctx = adapter.ObjectContext; var current = ctx.Categories.Where(c => c.Id == cat.Id).FirstOrDefault(); if (current == null) { ctx.Categories.Add(cat); // era stato cancellato da un altro utente } else { // ObjectStateEntry entry = objctx.ObjectStateManager.GetObjectStateEntry(current); // var s1 = entry.State; // entry.ApplyCurrentValues(cat); // var s2 = entry.State; objctx.ApplyCurrentValues<Category>("Categories", cat); state = ctx.Entry(current).State; Console.WriteLine(state); } }

Il nuovo flusso dopo la solita modifica è il seguente:

  • recupero l'ObjectContext a partire dal DbContext
  • carico in memoria la versione attualmente su DB (current)
  • se non la trovo significa che un altro utente l'ha cancellata. Nell'esempio la re-inserisco ma potrei ovviamente decidere di lanciare un'eccezione
  • se la trovo chiedo a EF di impostare i valori 'attuali' con quelli che ho modificato (cat) quando ero disconnesso
  • leggo lo stato della Entity e ottengo finalmente 'Modified'.

Le righe commentate sono un modo alternativo di eseguire la ApplyCurrentValues. Entrambe arrivano allo stesso risultato.

Tutto questo prescinde dall'uso di WCF Data Services (ancora in CTP) o di RIA che sono stati aggiornati per lavorare in tandem con EF 4.1 ed il tracking.

Una precisazione: perché uno scenario POCO? Perché sempre di più sarà utile lavorare serializzando in json e reidratare le entity sul lato server. Perciò POCO e disconnesso diventeranno sempre più strumenti ordinari. Se poi abbiamo i nuovi RIA con il supporto a json tanto meglio ma ovviamente dipende dal contesto lato server.

posted @ Thursday, September 1, 2011 3:38 PM | Feedback (7) |

Powered by:
Powered By Subtext Powered By ASP.NET