La validazione è sicuramente una delle feature più interessanti introdotte con la CTP 5, anche se disponibile solo “in modalità” Code First, almeno nel momento in cui si scrive (su questo punto penso che se ne potrebbe parlare…). Bisogna fare una piccola precisazione: in questo post, per validazione, non intendiamo le Business Rule del Domain che sono generalmente più complicate della validazione effettuata sui dati che devono essere persistiti: richiesto, non richiesto, lunghezza minima di un campo ecc … Prima di iniziare a vedere qualche esempio di codice, qualche piccola precisazione: la validazione è abilitata per default, per eseguirla sono utilizzati gli attributi presenti nel namespace System.ComponentModel.DataAnnotations e derivanti dalla classe ValidationAttribute.
Riprendiamo le classi degli esempi precedenti, decoriamo la classe Developer con qualche attributo in questo modo:
[Table("Sviluppatore")]
public class Developer
{
public Developer()
{
this.Skills = new List<Skill>(3);
this.Projects = new List<Project>(1);
}
public int Id { get; set; }
[Required]
[MaxLength(30, ErrorMessage = "Maximum lenght for name is 30 chars")]
public string Name { get; set; }
[Required]
[MaxLength(30, ErrorMessage = "Maximum lenght for surname is 30 chars")]
public string Surname { get; set; }
[Required]
public ICollection<Skill> Skills { get; set; }
public ICollection<Project> Projects { get; set; }
}
Le proprietà Name e Surname sono decorate con gli attributi Required ad indicare che i campi sono obbligatori e con MaxLenght che specifica quale sia la lunghezza massima in numero di caratteri, per entrambe le proprietà fissata a 30 caratteri. Da notare che abbiamo definito anche un messaggio di errore per capire, come vedremo tra poco, eventuali errori di validazione. Stesso discorso per la Navigation Properties Skills, dove vogliamo che per ogni sviluppatore sia aggiunta almeno una competenza. Modifichiamo il codice del Main in questo modo:
using (Db db = new Db())
{
try
{
int developers = 0;
int projects = 0;
////Add a new Developer
Developer dev1 = new Developer();
db.Developers.Add(dev1);
//////Save data.
int rowsAffected = db.SaveChanges();
developers = (from d in db.Developers select d).Count();
projects = (from p in db.Projects select p).Count();
Console.WriteLine("Projects in db: {0}", developers);
Console.WriteLine("Developers in db: {0}", developers);
}
catch (DbEntityValidationException ex)
{
foreach (DbEntityValidationResult validationResult in ex.EntityValidationErrors)
{
if (!validationResult.IsValid)
{
Console.WriteLine("Error in " + validationResult.Entry.Entity.ToString());
foreach (DbValidationError validationError in validationResult.ValidationErrors)
{
Console.WriteLine("{0} : {1}", validationError.PropertyName, validationError.ErrorMessage);
}
}
}
}
}
Nella parte finale del codice abbiamo aggiunto un try…catch per catturare un’eventuale eccezione di tipo DbEntityValidationException, la quale contiene una collezione di DbEntityValidationResult (una per ogni entità) che sua volta espone la collezione ValidationErrors che possiamo visitare per visualizzare su console (in questo caso) gli eventuali errori di validazione dei dati, come in figura:
Se modifichiamo il codice per la creazione di uno sviluppatore in questo modo:
////Add a new Developer
Developer dev1 = new Developer()
{
Name = "Pinco",
Surname = "Pallo"
};
ed eseguiamo, questa volta tutto ok:
Attenzione, non è vero che è tutto ok: inizialmente abbiamo specificato la collezione Skills come Required, ma dato che nel costruttore di Developer abbiamo scritto:
public Developer()
{
this.Skills = new List<Skill>(3);
this.Projects = new List<Project>(1);
}
per EF è tutto ok. Noi invece vogliamo che la collezione contenga almeno un elemento, per far questo creiamo un nuovo attributo che definiamo NavigationAttribute:
public class NavigationAttribute : ValidationAttribute
{
public int MinLenght { get; set; }
public override bool IsValid(object value)
{
if (value == null)
return false;
int count = (int)value.GetType().GetMethod("get_Count").Invoke(value, null);
return (count >= MinLenght);
}
}
Eseguiamo l’override del metodo IsValid per definire il comportamento della regola di validazione: 1) la collezione non deve essere Null 2) tramite reflection invochiamo il metodo Count della collezione (get_Length nel caso di Array) per verificare quanti elementi questa contenga. Nel caso il valore restituito da Count sia minore di MinLenght indichiamo al motore di validazione di EF che per quest’entità la validazione non è andata a buon fine. Vediamo l’uso dell’attributo Custom:
[Required]
[Navigation(MinLenght = 1, ErrorMessage = "A developer requires al least one skill.")]
public ICollection<Skill> Skills { get; set; }
Anche in questo aggiungiamo un messaggio personalizzato di errore. A questo punto rieseguendo l’applicazione dovremmo ottenere qualcosa di simile:
Per rimuovere ogni forma di validazione è sufficiente impostare a false il corrispondente valore di configurazione:
db.Configuration.ValidateOnSaveEnabled = false;
La validazione delle entità può essere effettuata in qualsiasi momento richiamando GetValidationErrors(), quindi non solo in fase di salvataggio:
IEnumerable<DbEntityValidationResult> validationResults = db.GetValidationErrors();
foreach (DbEntityValidationResult validationResult in validationResults)
{
Console.WriteLine("Error in " + validationResult.Entry.Entity.ToString());
foreach (DbValidationError validationError in validationResult.ValidationErrors)
{
Console.WriteLine("{0} : {1}", validationError.PropertyName, validationError.ErrorMessage);
}
}
Oppure è possibile eseguire la validazione di una singola entità in questo modo:
DbEntityValidationResult validationResult = db.Entry<Developer>(dev1).GetValidationResult();
if (!validationResult.IsValid)
{
foreach (DbValidationError validationError in validationResult.ValidationErrors)
{
Console.WriteLine("{0} : {1}", validationError.PropertyName, validationError.ErrorMessage);
}
Console.ReadLine();
return;
}
Volendo è possibile effettuare la validazione anche di singole proprietà. Ovviamente in tutti i casi il risultato che si ottiene è sempre lo stesso. Un altro attributo di validazione molto interessante è quello che permette di eseguire validazioni tramite
Regular Expression ad esempio nel caso della proprietà e-mail dell’entità architetto potremmo decorare la classe in questo modo:
[Table("Architetto")]
public class Architect : Developer
{
[RegularExpression(@"\b[A-Z0-9._%-]+@[A-Z0-9.-]+\.[A-Z]{2,4}\b")]
public string Email { get; set; }
public string Phone { get; set; }
}
Un altro tassello nel puzzle della CTP 5 di EF Code First.