Freeteo


Pensieri e C#dice di Matteo Raumer

Entity Framework: Il mio approccio Code-First allo strato dati

Negli ultimi tempi Microsoft ha sfornato versioni sempre migliori del pacchetto Entity Framework, un "tassello" dopo l'altro ha rinforzato lo strato dati in  maniera sempre più solida, scalabile e soprattutto agile dal punto di vista sviluppo.
Io ovviamente non potevo che utilizzarlo e beneficiare di un modo abbastanza standard di approcciarmi allo strato dati, che si tratti di un'applicazione Web o Windows fa poca differenza, se parlo con un Db Sql (o cmq uno qualsiasi supportato da EF) , il mio modo di procedere è Code-First e poi design del db.
Un mio "pattern" che ha portato a migliorare i tempi/modi di sviluppo.

Per quanto riguarda lo strato dati solitamente procedo così:
1) Definisco a codice le entità che faranno parte del progetto (Domain Driven Design) e comincio a lavorare sul modello

2) Creo il database da SqlManagement in locale, poi andrò a distribuirlo (sul server del cliente o su Azure) se posso dare il nome alle colonne uguale alle proprietà che ho degli oggetti, la mappatura si riduce al solo nome della tabella

3) Uso Entity Framework code first con un BaseProvider<T> che si occupa della CRUD semplice, che nel caso di miglioramenti posso estendere


Il tutto in divenire ovviamente, parto sempre da un oggetto base (vedi articolo qui) , creo via via qualche classe del progetto ma man mano che vado avanti ne aggiungo di nuove e faccio la mappatura relativa, un pochi minuti.
Se poi vado a dare lo stesso nome della proprietà al nome del campo, allora proprio non scrivo nemmeno la mappatura...fantastico.
La cosa non è fattibile se si parte da un db già presente con cui integrarsi, ma poco male, basterà ritoccare un attimo le varie mappature, il processo di sviluppo cambia di poco.

Sostanzialmente il tutto è fatto da poche classi:
1) un ContextHelper per la connessione al db ed altri metodi comodi

public class ContextHelper
{
     public const string NomeConnessione = "DbConnection";

     public virtual DbContext GetContext()
     {

         //--- importante per evitare che crei lui il db su sql
        
Database
.SetInitializer<ContextVideo>(null);
         DbContext ctx = new ContextVideo();

         return ctx;

     }

    public virtual SqlConnection GetConnection()
    {
        
return new SqlConnection(ConfigurationManager.ConnectionStrings[NomeConnessione].ConnectionString);
    }

}



2) il context di Entity Framework che mappa sul db

public partial class ContextVideo : DbContext
{   
    public
ContextVideo() : base(ContextHelper.NomeConnessione)
   
{ }


    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {

         //--- tutte le mappature degli oggetti del domain model

         modelBuilder.Configurations.Add(new MappingUtente());

         modelBuilder.Configurations.Add(new MappingCategoria());

         ....

         base.OnModelCreating(modelBuilder);
    }

}



3) Un MappingBase<T> che estende EntityTypeConfiguration<T> che si occupa dei campi base, dato che gli oggetti hanno sempre alcuni campi "vitali" come ID,Descrizione,DataModifica etc... la mappatura viene semplificata tenendo conto dei campi o meno, a seconda dell'oggetto

public abstract class MappingBase<T> : EntityTypeConfiguration<T> where T : OggettoBase
{

    public bool IgnoraCampiSys { get; set; }

    public abstract void Mappature(); //--- qui mi imposti le tue mappature

    public MappingBase()
    {

       Mappature();

       HasKey(x => x.ID);   //--- il campo ID è Identity
       Property(x => x.ID).HasDatabaseGeneratedOption(
DatabaseGeneratedOption.Identity);


       //--- campi di sistema, a volte possono essere ignorati
      
if (IgnoraCampiSys)
      
{

           Ignore(x => x.SysComputer);
           Ignore(x => x.SysData)
           Ignore(x => x.SysUtente);
       }

    }

}



4) le Mappature degli oggetti (es: Utente) che appunto estendono la mappatura base di quell'oggetto:

public class MappingUtente : MappingBase<Utente
{
 
  public override void Mappature()
   {
        ToTable("UTENTI");

        //--- eventuali altre configurazioni specifiche per l'entita
       
//--- ma se i campi sono tutti mappati, ed il nome corrisponde alla colonna
       
//--- non c'è altro da fare
   
}
}


5) Il mio BaseProvider<T> per la CRUD semplice è l'ultimo tassello del processo, ed è standard, infatti per diversi progetti referenzio direttamente la libreria. Il codice è questo:

public class BaseProvider<T> where T : OggettoBase
{

    static ContextHelper c;

    public virtual ContextHelper ContextHelper
    {
   
   get
      
{
          if (c == null)
             
c = new ContextHelper();

           return c;
       }
    }


    protected virtual void BuildObject(T item) 
    {
       //--- utile pre l'implementazione specifica
    }
    protected virtual IEnumerable<T> BuildLista(IEnumerable<T> lista)
   
{
       
foreach (var item in lista)
            BuildObject(item);
       
        return
lista;
    }


    public virtual T Get(string codice)
    {
        return Get(x => x.Codice == codice);
    }


    public virtual T Get(Expression<Func<T, bool>> filtri)
   
{

       T u = null;

       //--- safety code
       if (filtri == null)
           return u;

       using (DbContext ctx = ContextHelper.GetContext())
      
{
                  u = ctx.Set<T>().FirstOrDefault(filtri);
             }

       BuildObject(u);
       return u;

     }


    public virtual IEnumerable<T> GetAll()
   
{
        return GetAll(null);
    }

    public virtual IEnumerable<T> GetAll(Expression<Func<T, bool>> filtri)
   
{
              using (DbContext ctx = ContextHelper.GetContext())
       
{
                      var q = ctx.Set<T>().AsQueryable();

            if (filtri != null)

                q = q.Where(filtri);

            return BuildLista(q.ToArray()
                               .OrderBy(x => x.Nome));
        }
    }

    public virtual T Create(T item)
    {

        item.ID = 0;

        Save(item);

        return item;
    }


    public virtual bool Save(T item)
   
{
              item.SysData = DateTime.Now;

        ControllaDate(item);

        using (DbContext ctx = ContextHelper.GetContext())
        {

            DbSet<T> set = ctx.Set<T>();

            if (item.ID > 0)
           
{
                             set.Attach(item);
                             ctx.Entry(item).State = EntityState.Modified;

            }
            else
                set.Add(item);

            return SaveChanges(ctx);

         }
      }

      public virtual bool Delete(T item)
     
{
          return Delete(item.Codice);
           }
     
public virtual bool Delete(string codice)
     
{
          return Delete(x => x.Codice == codice);
     
}
      public virtual bool Delete(Expression<Func<T, bool>> filtri)
      {
         
using (DbContext ctx = ContextHelper.GetContext())
          {

              DbSet<T> set = ctx.Set<T>();
              
var mm = set.Where(filtri);

              if (mm != null)

                 set.RemoveRange(mm);

             return SaveChanges(ctx);
          }
      }


      protected bool SaveChanges(DbContext ctx)
     
{
          bool ok = false;

          try
         
{
             ok = ctx.SaveChanges() > 0;
          }
          catch { }

          return ok;
      }

     

      private void ControllaDate(T item)
     
{
          if (item == null)
            
return;

          //--- Sql e DateTime.Minvalue non vanno d'accordo
         
foreach (var p in item.GetType().GetProperties().Where(x => x.PropertyType == typeof(DateTime)))
         
{
              try
              {

                 DateTime d = (DateTime)p.GetValue(item, null);

                 if (d == DateTime.MinValue)
                    
p.SetValue(item, SqlDateTime.MinValue.Value, null);

              }
              catch { }
          }
      }

}

(* come si nota, si tratta di un gestore di CRUD semplice, nel caso complesso meglio implementare un provider specifico)


A questo punto, nel momento in cui debba aggiungere una nuova entità, nel caso ppiù semplice mi basta creare l'entita (che estende sempre OggettoBase ovviamente), e creare un Manager relativo dove ci metto le logiche che usa nel caso più semplice un BaseProvider<Entita> e sono posto...fantastico.
Nel caso in cui voglia specificare delle funzioni diverse nel provider dati, creare anche il provider relativo, estendendo sempre il Base per beneficiare della CRUD "semplice" e mettendo i metodi con Join o altre migliorie relative allo strato dati.


Questa modo di operare, mi da una serie di vantaggi:
Per il 90% dei casi mi basta definire una proprietà con un certo nome che riporto nella colonna, EF fa il lavoro sporco per me.
Creando il Manager con metodi che accettano come parametri Lambda Expression, posso avere la libertà di essere flessibile nello strato Business, con tutta l'affidabilità della tipizzazione.

Ma soprattutto,il motivo principale che mi fa piacere questo approccio, è che in poco tempo riesco a partire con un progetto reale, scalabile, ed affidabile senza dover ogni volta preoccuparmi dello strato dati.
Posso quasi dimenticarmi di parlare con un database e concentrarmi sulle logiche dell'applicativo, poi, via via che il progetto cresce, posso intervenire con poche operazioni semplici per integrare nuovi oggetti.

Un vero piacere, pensando poi a qualche anno fa, quando bene o male lo strato dati andava sempre fatto e testato...


Un progetto d'esempio lo potete trovare qui: http://www.dotnethell.it/users/files/1958_Freeteo.CodeFirst.zip



Categoria: .net
giovedì, 27 feb 2014 Ore. 12.29





  • Views Home Page: 243.204
  • Views Posts: 420.645
  • Views Gallerie: 564.822
  • n° Posts: 163
  • n° Commenti: 148
Anno 2014

Anno 2013

Anno 2012

Anno 2011

Anno 2010

Anno 2009

Anno 2008

Anno 2007

Anno 2006

Anno 2005
Copyright © 2002-2007 - Blogs 2.0
dotNetHell.it | Home Page Blogs
ASP.NET 2.0 Windows 2003