Strutturare un'applicazione reale con Entity Framework

Marco De Sanctis

di Marco De Sanctis, in LINQ, 24 marzo 2009

4 pagine in totale: <<Indietro 1 2 [3] 4 Avanti >>

Long running context

La strada che a questo punto verrebbe naturale percorrere è quella allora di utilizzare la Session di ASP.NET per memorizzare, oltre alla entity da gestire, anche l'object context.

private Customer currentCustomer 
{ 
  get { return this.Session["customer"] as Customer; } 
  set { this.Session["customer"] = value; } 
} 
 
private ObjectContext objectContext 
{ 
  get { return this.Session["objectContext"] as ObjectContext; } 
  set { this.Session["objectContext"] = value; } 
}

Il Page_Load della nostra applicazione di esempio diventa allora simile al seguente:

protected void Page_Load(object sender, EventArgs e) 
{ 
  if (!IsPostBack) 
  { 
    this.objectContext = new ObjectContext(); 
    loadCustomer(this.customerId); 
  } 
} 
 
private void loadCustomer(int customerId) 
{ 
  currentCustomer = 
    (from c in this.objectContext.CustomerSet 
    where c.Id == customerId 
    select c).First(); 
 
  this.txtId.Text = customer.Id.ToString(); 
  this.txtName.Text = customer.Name; 
  // e così via... 
}

Il Detach del customer ovviamente non viene più effettuato, dato che il context resta il medesimo per tutta la durata della transazione di business; in questo modo eventuali modifiche effettuate nell'arco dei vari postback della pagina vengono correttamente tracciate, così che il metodo di Save si traduca in un qualcosa di molto simile al seguente snippet:

protected void btnSave_Click(object sender, EventArgs e) 
{ 
  this.objectContext.SaveChanges(); 
  this.objectContext.Dispose(); 
  this.objectContext = null; 
 
  // altro codice qui... 
}

Il codice è divenuto incredibilmente semplice e poco invasivo e la strada scelta sembra sia quella giusta. Purtroppo il grosso svantaggio di questo approccio risiede nello storage scelto (ASP.NET Session) che rischia di minare pesantemente la scalabilità della nostra applicazione. Il problema è che gli oggetti memorizzati in session, se non esplicitamente rimossi, restano raggiungibili (e pertanto occupano risorse) per un tempo solitamente molto lungo. Nel codice precedente, l'unico caso in cui si può effettivamente rimuovere l'object context è in corrispondenza della pressione del button "Salva", ma se l'utente abbandona prematuramente la pagina continuando però a navigare all'interno dello stesso sito web, la sessione viene automaticamente rinnovata, il context istanziato resta pertanto raggiungibile e il garbage collector non potrà rimuoverlo finché non ne scada il timeout.

Uno storage alternativo per l'object context

Una buona alternativa invece può essere l'utilizzo della ASP.NET Cache, infatti:

  • tramite la sliding expiration, è possibile impostare per ogni elemento un distinto tempo di time-out, che verrà rinnovato automaticamente ad ogni utilizzo (similmente a quanto accade per la Session); la differenza però è che tale refresh coinvolge esclusivamente l'elemento acceduto e non l'intero contenuto del dictionary;
  • Allo scadere del timeout, l'ObjectContext viene automaticamente rimosso dalla cache. E' possibile configurare il tutto affinchè venga contestualmente invocato un metodo di callback in cui richiamarne il Dispose;
  • Ad ogni item presente in cache può essere assegnata una priorità che ne regoli la rimozione quando il sistema è in condizioni di memory pressure; in questo modo, ad esempio, si può decidere di sacrificare per primi alcuni dati memorizzati per avere vantaggi prestazionali e non rimuovere affatto gli ObjectContext, che hanno invece un impatto funzionale sul sistema.

Invece che replicare questa logica all'interno di ogni pagina, è preferibile centralizzarne la definizione in una classe specifica, tramite il quale avviare e concludere una transazione di business. Cerchiamo di formalizzarne l'architettura.

Lo stato di una transazione applicativa, quindi la presenza in memory di un customer (nell'esempio che stiamo portando avanti in questo articolo) e il relativo tracking delle modifiche, è mantenuto da un oggetto chiamato UnitOfWork tramite un ObjectContext di Entity Framework contenuto internamente:

Uno UnitOfWorkContainer, unico e condiviso da tutti i thread dell'applicazione, si occupa di gestirne il ciclo di vita; il metodo BeginTransaction ha il compito di istanziare una UnitOfWork e memorizzarla in cache, restituendo un Guid che servirà ad identificarla univocamente.

public Guid BeginTransaction&lt;T&gt;() where T : ObjectContext, new() 
{ 
  HttpContext context = HttpContext.Current; 
  UnitOfWork&lt;T&gt; uow = new UnitOfWork&lt;T&gt;(); 
 
  Guid id = Guid.NewGuid(); 
 
  context.Cache.Add(id.ToString(), uow, null, Cache.NoAbsoluteExpiration, 
    new TimeSpan(0, 10, 0), CacheItemPriority.NotRemovable, 
    this.cacheItemRemoved); 
 
  return id; 
}

Si noti come, nel codice precedente, la UnitOfWork venga inserita in cache fissando uno sliding timeout ad esempio di 10 minuti e impostando un metodo di Callback per invocarne il Dispose in caso di rimozione automatica. Questo risolve il problema, presente invece nel caso della Session, dell'utente che, navigando in diverse pagine del sito, lascia numerose UnitOfWork attive all'interno dell'applicazione. La pagina di gestione clienti, a questo punto, ha semplicemente il compito di iniziare una transazione di business al primo accesso e di recuperarne la relativa UnitOfWork ad ogni postback:

protected override void OnLoad(EventArgs e) 
{ 
  if (!IsPostBack) 
  { 
    // Primo caricamento della pagina: iniziamo una nuova 
    // transazione di business 
    this.unitOfWorkId = UnitOfWorkContainer.Instance.BeginTransaction&lt;ObjectContext&gt;(); 
  } 
 
  // ad ogni esecuzione della pagina, recuperiamo la unitOfWork corrente 
  this.unitOfWork = 
    UnitOfWorkContainer.Instance.GetUnitOfWork&lt;ObjectContext&gt;(this.unitOfWorkId); 
 
  if (unitOfWork == null) 
  { 
    // la unitOfWork è scaduta; qui si può prevedere un messaggio 
    // in cui si avvisa l'utente e lo si invita a ri-effettuare 
    // l'operazione dall'inizio 
    // In questo esempio ci limitiamo a sollevare un'eccezione 
    throw new InvalidOperationException("UnitOfWork scaduta!"); 
  } 
 
  base.OnLoad(e); 
}

L'identificativo ha una dimensione minimale e pertanto può essere tranquillamente memorizzato in Session o addirittura nel ViewState. Le operazioni di accesso ai dati, come il caricamento del customer da database, avvengono utilizzando l'ObjectContext contenuto nella UnitOfWork:

private Customer loadCustomer(int customerId) 
{ 
  Customer res = 
  (from c in this.unitOfWork.PersistenceContext.CustomerSet 
  where c.Id == customerId 
  select c).First(); 
 
    return res; 
}

Il salvataggio viene invece effettuato tramite il metodo CommitTransaction di UnitOfWorkContainer

protected void btnSave_Click(object sender, EventArgs e) 
{ 
  UnitOfWorkContainer.Instance.CommitTransaction(this.unitOfWorkId); 
 
  // altro codice qui... 
}

che, come si vede nella definizione in basso, oltre che persistere le modifiche si occupa anche di chiudere la transazione di business rimuovendo il contesto di persistenza dalla cache:

public void CommitTransaction(Guid id) 
{ 
  UnitOfWork uow = this.GetUnitOfWork(id); 
  uow.PersistenceContext.SaveChanges(); 
  uow.Dispose(); 
 
  HttpContext context = HttpContext.Current; 
 
  context.Cache.Remove(id.ToString()); 
}

Quanto visto fino ad ora è facilmente integrabile e riutilizzabile anche all'interno di un'architettura layered, in cui, ad esempio, il codice per caricare la entity Customer, invece che contenuto nelle pagine, sia incapsulato all'interno di servizi di business; il requisito è che questi ultimi siano progettati in modo da utilizzare una UnitOfWork condivisa, agendo quindi di fatto all'interno della stessa transazione applicativa:

public class CustomersService 
{ 
  private UnitOfWork&lt;ObjectContext&gt; unitOfWork; 
  public CustomersService(UnitOfWork&lt;ObjectContext&gt; unitOfWork) 
  { 
    this.unitOfWork = unitOfWork; 
    } 
 
  public Customer GetById(int customerId) 
  { 
    Customer res = 
      (from c in this.unitOfWork.PersistenceContext.CustomerSet 
       where c.Id == customerId 
       select c).First(); 
 
    return res; 
  } 
     
  // altro codice qui... 
}

4 pagine in totale: <<Indietro 1 2 [3] 4 Avanti >>

Attenzione: Questo articolo contiene un allegato

Contenuti dell'articolo

Commenti

Per inserire un commento, devi avere un account.

Fai il login e torna a questa pagina, oppure registrati alla nostra community.



Segnala su: Facebook MSDN Social Twitter Segnalo Wikio Diggita Technorati Stumbleupon Google Yahoo FriendFeed Delicious Furl

TUTORIALS
TOP TEN ARTICOLI
ARTICOLI VIA E-EMAIL

Iscriviti alla nostra newsletter nuoviarticoli per ricevere via e-mail le notifiche!

MEDIA
IN EVIDENZA
MISC