Aprile 2018

Volume 33 Numero 4

Il presente articolo è stato tradotto automaticamente.

Punti dati - Entità di proprietà di EF Core 2 e soluzioni alternative temporanee

Dal Julie Lerman

Julie LermanLa nuova funzionalità di proprietà dell'entità in Entity Framework Core 2.0 sostituisce la funzionalità di tipo complesso di Entity Framework (EF attraverso EF6) "classico". Proprietà del che entità supportano il mapping di oggetti valore all'archivio dati. È abbastanza frequente disporre di una regola business che consenta le proprietà in base agli oggetti di valore possono essere null. Inoltre, poiché gli oggetti di valore sono modificabili, è anche importante essere in grado di sostituire le proprietà che contengono un oggetto valore. La versione corrente di Core EF non consentono uno di questi scenari per impostazione predefinita, anche se entrambi saranno supportati nelle iterazioni successive. Nel frattempo, anziché considerato le seguenti limitazioni di importanza strategica per coloro che i vantaggi dei modelli di progettazione basate su dominio (DDD), in questo articolo verrà descritto aggirare le limitazioni. Un specialista DDD potrebbe rifiutare questi modelli temporanei che non seguendo i principi di DDD sufficientemente, ma pragmatist Me è soddisfatta, buttressed a conoscenza che sono soluzioni semplicemente temporanee.

Si noti che ho detto "per impostazione predefinita." Si scopre che è possibile disporre dei componenti di base di EF assumersi la responsabilità per applicare il proprio regola sulle proprietà dell'entità null e consentire la sostituzione di oggetti valore, senza influenzare notevolmente le classi di dominio o le regole business. In questa colonna, è possibile mostreremo come eseguire questa operazione.

È possibile risolvere il problema di supporto di valori null, ovvero eseguire il mapping semplicemente il tipo di oggetto di valore a una tabella, pertanto suddividere fisicamente i dati dell'oggetto valore dal resto dell'oggetto a cui appartiene. Sebbene questo possa essere una buona soluzione per alcuni scenari, è in genere uno che non desidera utilizzare. Pertanto, è preferibile utilizzare la soluzione alternativa, ovvero l'obiettivo di questa colonna. Ma prima di tutto si desidera assicurarsi di che comprendere perché questo problema e soluzione sono abbastanza importanti che sta dedicare questa colonna per l'argomento.

Brevi informazioni generali sugli oggetti di valore

Gli oggetti di valore sono un tipo che consente di incapsulare più valori in una singola proprietà. Una stringa è un ottimo esempio di un oggetto valore. Le stringhe sono costituite da un insieme di caratteri. E sono non modificabile, ovvero immutabilità è un facet critico di un oggetto valore. La combinazione e l'ordine delle lettere c, a e r hanno un significato specifico. Se devi per mutare, ad esempio modificando l'ultima lettera "t", è completamente modificherebbe il significato.  L'oggetto è definito dalla combinazione di tutti i relativi valori. Di conseguenza, il fatto che l'oggetto non può essere modificato fa parte del contratto. Ed è presente un altro aspetto importante di un oggetto di valore, ovvero non dispone di una propria identità. E può essere utilizzato solo come proprietà di un'altra classe, come una stringa. Valore oggetti hanno altre regole contrattuali, ma questi sono i più importanti per iniziare se si ha familiarità con il concetto.

Dato che un oggetto valore è costituito dalle relative proprietà e quindi, nel suo complesso, utilizzato come una proprietà in un'altra classe, la persistenza dei dati richiede qualche sforzo speciale. Con un database non relazionali, ad esempio un database di documenti, è facile archiviare il grafico di un oggetto e i relativi oggetti di valore incorporato. Ma che non è il caso durante l'archiviazione in un database relazionale. Inizia con la prima versione, Entity Framework incluso ComplexType, che sa come eseguire il mapping delle proprietà della proprietà per il database in cui Entity Framework è stato salvare in modo permanente i dati. Un esempio di oggetto valore comune è PersonName, può essere costituito da una proprietà FirstName e una proprietà LastName. Se si dispone di un tipo di contatto con una proprietà PersonName, per impostazione predefinita, Core EF archivierà i valori FirstName e LastName come colonne aggiuntive nella tabella a cui viene eseguito il mapping di contatto.

Un esempio di un oggetto di valore in uso

Ritengo che esaminare numerosi esempi di oggetti valore consentito per comprendere meglio il concetto, pertanto, si utilizzerà un ulteriore esempio, un'entità SalesOrder e un oggetto valore PostalAddress. Un ordine include in genere sia un indirizzo di spedizione e un indirizzo di fatturazione. Mentre gli indirizzi possono essere presenti per altri scopi, all'interno del contesto dell'ordine, ma sono parte integrante della relativa definizione. Se un utente passa a un nuovo percorso, si desidera conoscere in cui è stato eseguito tale ordine, in modo da risultare opportuno per incorporare gli indirizzi nell'ordine. Ma per poter considerare gli indirizzi in modo coerente del sistema, preferisco incapsulare i valori che costituiscono un indirizzo nella propria classe, PostalAddress, come illustrato figura 1.

Figura 1 PostalAddress ValueObject

public class PostalAddress : ValueObject<PostalAddress>
{
  public static PostalAddress Create (string street, string city,
                                      string region, string postalCode)   {
    return new PostalAddress (street, city, region, postalCode);
  }
  private PostalAddress () { }
  private PostalAddress (string street, string city, string region,
                         string postalCode)   {
    Street = street;
    City = city;
    Region = region;
    PostalCode = postalCode;
  }
  public string Street { get; private set; }
  public string City { get; private set; }
  public string Region { get; private set; }
  public string PostalCode { get; private set; }
  public PostalAddress CopyOf ()   {
    return new PostalAddress (Street, City, Region, PostalCode);
  }
}

PostalAddress eredita da una classe di base ValueObject creata Jimmy Bogard (bit.ly/2EpKydG). ValueObject fornisce parte della logica obbligatoria richiesta a un oggetto valore. Ad esempio, viene eseguito un override di Object. Equals, che assicura che tutte le proprietà deve essere confrontata. Tenere presente che rende un uso massiccio della reflection, che può influire sulle prestazioni in un'app di produzione.

Altre due funzionalità importanti dell'oggetto valore PostalAddress sono che non dispone di alcuna proprietà chiave di identità e il relativo costruttore impone la regola invariante che tutte le proprietà devono essere popolata. Tuttavia, per un'entità appartenente a essere in grado di eseguire il mapping di un tipo definito come un oggetto di valore, l'unica regola è che non avere alcuna chiave di identità di un proprio. Un'entità di proprietà non viene considerata con gli altri attributi di un oggetto valore.

Con PostalAddress definito, non è possibile ora utilizzarlo come proprietà ShippingAddress e BillingAddress della classe SalesOrder (vedere figura 2). Non sono le proprietà di navigazione per i dati correlati, ma solo altre proprietà in modo analogo le note sulla scalare e OrderDate.

Figura 2, la classe SalesOrder contiene proprietà che sono tipi PostalAddress

public class SalesOrder {
  public SalesOrder (DateTime orderDate, decimal orderTotal)   {
    OrderDate = orderDate;
    OrderTotal = orderTotal;
    Id = Guid.NewGuid ();
  }
  private SalesOrder () { }
  public Guid Id { get; private set; }
  public DateTime OrderDate { get; private set; }
  public decimal OrderTotal { get; private set; }
  private PostalAddress _shippingAddress;
  public PostalAddress ShippingAddress => _shippingAddress;
  public void SetShippingAddress (PostalAddress shipping)
  {
    _shippingAddress = shipping;
  }
  private PostalAddress _billingAddress;
  public PostalAddress BillingAddress => _billingAddress;
  public void CopyShippingAddressToBillingAddress ()
  {
    _billingAddress = _shippingAddress?.CopyOf ();
  }
  public void SetBillingAddress (PostalAddress billing)
  {
    _billingAddress = billing;
  }
}

Questi indirizzi ora risiedono entro l'ordine di vendita e possono offrire informazioni accurate indipendentemente dall'indirizzo corrente della persona che ha emesso l'ordine. È sempre aggiornato in cui tale ordine è verificato un errore.

Mapping di un oggetto di valore come un'entità di proprietà dei componenti di base EF

Nelle versioni precedenti, Entity Framework è stato possibile riconoscere automaticamente le classi che devono essere mappate tramite un elemento ComplexType individuando che la classe è stata utilizzata come proprietà di un'altra entità e non aveva nessuna proprietà chiave. Entity Framework Core, tuttavia, non è possibile automaticamente dedurre proprietà entità. È necessario specificare questo nei mapping API Fluent DbContext nel metodo OnModelCreating utilizzando il nuovo metodo OwnsOne per specificare quale proprietà dell'entità è l'entità di proprietà:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
  modelBuilder.Entity<SalesOrder>().OwnsOne(s=>s.BillingAddress);
  modelBuilder.Entity<SalesOrder>().OwnsOne(s=>s.ShippingAddress);
}

Le migrazioni di EF Core ho utilizzato per creare un file di migrazione che descrive il database a cui viene mappato il modello. Figura 3 viene mostrata la sezione della migrazione che rappresenta la tabella SalesOrder. È possibile vedere che EF Core riconosciuto che le proprietà di PostalAddress per ognuno dei due indirizzi sono parte dell'ordine di vendita. I nomi delle colonne sono in base alla convenzione di Entity Framework Core, anche se è possibile modificare quelli con l'API Fluent.

Figura 3 della migrazione per la tabella SalesOrder, incluse tutte le colonne per le proprietà PostalAddress

migrationBuilder.CreateTable(
  name: "SalesOrders",
  columns: table => new
  {
    Id = table.Column(nullable: false)
              .Annotation("Sqlite:Autoincrement", true),
    OrderDate = table.Column(nullable: false),
    OrderTotal = table.Column(nullable: false),
    BillingAddress_City = table.Column(nullable: true),
    BillingAddress_PostalCode = table.Column(nullable: true),
    BillingAddress_Region = table.Column(nullable: true),
    BillingAddress_Street = table.Column(nullable: true),
    ShippingAddress_City = table.Column(nullable: true),
    ShippingAddress_PostalCode = table.Column(nullable: true),
    ShippingAddress_Region = table.Column(nullable: true),
    ShippingAddress_Street = table.Column(nullable: true)
  }

Inoltre, come indicato in precedenza, gli indirizzi di inserimento nella tabella SalesOrder è convenzione e preferenza. Questo codice alternativo suddividerà essere adattati per separare le tabelle e di evitare completamente il problema di supporto di valori null:

modelBuilder.Entity<SalesOrder> ().OwnsOne (
  s => s.BillingAddress).ToTable("BillingAddresses");
modelBuilder.Entity<SalesOrder> ().OwnsOne (
  s => s.ShippingAddress).ToTable("ShippingAddresses");

Creazione di un ordine di vendita nel codice

Inserimento di un ordine di vendita con l'indirizzo di fatturazione e l'indirizzo di spedizione è semplice:

private static void InsertNewOrder()
{
  var order=new SalesOrder{OrderDate=DateTime.Today, OrderTotal=100.00M};
  order.SetShippingAddress (PostalAddress.Create (
    "One Main", "Burlington", "VT", "05000"));
  order.SetBillingAddress (PostalAddress.Create (
    "Two Main", "Burlington", "VT", "05000"));
  using(var context=new OrderContext()){
    context.SalesOrders.Add(order);
    context.SaveChanges();
  }
}

Ma si supponga che le regole di business consentono un ordine da archiviare anche se l'indirizzo di spedizione e di fatturazione non sono ancora stati immessi e un utente può completare l'ordine in un secondo momento. Sarà impostare come commento il codice che riempie la proprietà BillingAddress:

// order.BillingAddress=new Address("Two Main","Burlington", "VT", "05000");

Quando viene chiamato SaveChanges, Core EF tenta di determinare quali sono le proprietà del BillingAddress in modo che è possibile inviarli nella tabella SalesOrder. Ma ha esito negativo in questo caso perché BillingAddress è null. Internamente, dei componenti di base di Entity Framework dispone di una regola che una proprietà di tipo proprietà tradizionalmente mappato non può essere null.

Entity Framework Core è dare per scontato che il tipo di proprietà è disponibile in modo che le relative proprietà possono essere letti. Gli sviluppatori di questo tipo vengono visualizzati come impediva di procedere per poter usare gli oggetti di valore o, ancora peggio, per poter usare Entity Framework Core, a causa di valore come critico gli oggetti con alla progettazione software. Che è stata la modalità è ritenuto inizialmente, ma è stato in grado di creare una soluzione alternativa.

Risolvere temporaneamente per consentire oggetti con valore Null

L'obiettivo di risolvere è garantire che EF Core riceveranno un ShippingAddress, BillingAddress o altri tipi di proprietà, se l'utente ha fornito uno. Ciò significa che l'utente non è obbligato a fornire un indirizzo di spedizione o fatturazione solo per soddisfare il livello di persistenza. Se l'utente non ne fornisce uno, quindi un oggetto PostalAddress con valori null nelle proprietà aggiunti durante l'elemento DbContext quando è necessario salvare un SalesOrder.

Dopo aver apportato un adattamento secondario alla classe PostalAddress mediante l'aggiunta di un secondo metodo factory, vuoto, per consentire l'elemento DbContext creare facilmente un PostalAddress vuota:

public static PostalAddress Empty()
{
  return new PostalAddress(null,null,null,null);
}

Inoltre, è possibile migliorata la classe di base ValueObject con un nuovo metodo IsEmpty, racchiusa figura 4, per consentire al codice di determinare facilmente se un oggetto dispone di tutti i valori null nelle relative proprietà. IsEmpty sfrutta codice già esistente nella classe ValueObject. Scorre le proprietà e se uno di essi ha un valore, restituisce false, che indica che l'oggetto non è vuoto; in caso contrario, restituisce true.

Figura 4 il metodo IsEmpty aggiunto alla classe di Base ValueObject

public bool IsEmpty ()
{
  Type t = GetType ();
  FieldInfo[] fields = t.GetFields
    (BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
  foreach (FieldInfo field in fields)
  {
    object value = field.GetValue (this);
    if (value != null)
    {
      return false;
    }
  }
  return true;
}

Ma la soluzione per consentire a entità di proprietà null non è stata ancora completata. Ancora necessario utilizzare tutta questa nuova logica per assicurarsi che SalesOrders nuovo sempre avrebbe un ShippingAddress e un BillingAddress affinché Entity Framework Core essere in grado di archiviarli nel database. Dopo aver aggiunto inizialmente l'ultima parte della soluzione, è stato soddisfatto del risultato perché tale ultimo frammento di codice (che è possibile non sarà necessario creare anche la condivisione) che la classe SalesOrder applicare regole di Entity Framework di base, ovvero deleterie per progettazione basati su dominio.

Voila! Una soluzione elegante

Fortunatamente, è dato dal fatto di pronuncia in DevIntersection, come posso eseguire ogni passaggio, dove Diego Vega e Andrew Peters dal team di Entity Framework sono stati inoltre la presentazione. Loro riportata la soluzione alternativa e illustrato ciò che è stata richiedere l'intervento dell'utente, ovvero la necessità di applicare ShippingAddress non null e BillingAddress nell'ordine di vendita, e sono concordati. Andrew rapidamente realizzato un modo per utilizzare il lavoro che è possibile avevo nella classe basa ValueObject e tweak apportate al PostalAddress per forzare EF Core per svolgere il problema senza inserire l'onere su SalesOrder. Il trucco avviene nell'override del metodo SaveChanges della classe DbContext, racchiusa figura 5.

Figura 5 viene sottoposto a override SaveChanges per fornire valori per i tipi di proprietà Null

public override int SaveChanges()
{
  foreach (var entry in ChangeTracker.Entries()
             .Where(e => e.Entity is SalesOrder && e.State == EntityState.Added))
  {
    if (entry.Entity is SalesOrder)
    {
      if (entry.Reference("ShippingAddress").CurrentValue == null)
      {
        entry.Reference("ShippingAddress").CurrentValue = PostalAddress.Empty();
      }
      if (entry.Reference("BillingAddress").CurrentValue == null)
      {
        entry.Reference("BillingAddress").CurrentValue = PostalAddress.Empty();
      }
  }
  return base.SaveChanges();
}

Dalla raccolta di voci che sta rilevando l'elemento DbContext, SaveChanges si ripete attraverso quelle SalesOrders contrassegnato per essere aggiunti al database e assicurano che popolati delle rispettive controparti vuoto.

Per quanto riguarda l'esecuzione di query di questi tipi di proprietà vuoti?

Avere soddisfatto la necessità di EF dei componenti di base archiviare gli oggetti di valore null, è ora per eseguire una query nuovamente dal database. Ma EF Core risolve tali proprietà nel relativo stato vuoto. Qualsiasi ShippingAddress o BillingAddress che era originariamente null vengono restituiti come un'istanza con valori null nelle relative proprietà. Dopo qualsiasi query, è necessario la logica per sostituire tutte le proprietà PostalAddress vuote con il valore null.

È possibile impiegato un certo tempo cercando un modo elegante per ottenere questo risultato. Purtroppo, non è ancora un ciclo di vita hook per modificare gli oggetti come è in corso materializzate dai risultati della query. È un servizio sostituibile nella pipeline di query denominata CreateReadValueExpression nella classe EntityMaterializerSource interna, ma che può essere usato solo su valori scalari, non oggetti. Ho tentato di numerosi altri approcci che sono state più complicato e infine ha una lunga parlato con personalmente il fatto che si tratta di una soluzione alternativa temporanea, pertanto è possibile accettare una soluzione più semplice anche se ha un po' sviluppato codice. E questa attività non è troppo difficile controllare se le query vengono incapsulate in una classe dedicata all'esecuzione di chiamate dei componenti di base di EF al database.

È possibile denominato il metodo FixOptionalValueObjects:

private static void FixOptionalValueObjects (SalesOrder order) {
  if (order.ShippingAddress.IsEmpty ()) { order.SetShippingAddress (null); }
  if (order.BillingAddress.IsEmpty ()) { order.SetBillingAddress (null); }
}

Ora è possibile disporre di una soluzione in cui l'utente può lasciare oggetti con valore null e consentire EF Core archiviare e recuperare li come valori non null, anche se restituirle al codice base come valori null comunque.

Sostituzione di oggetti valore

Ho detto in un altro limite nella versione corrente di EF Core 2, che è l'impossibilità di sostituire le proprietà di entità. Gli oggetti di valore sono per definizione non modificabile. Pertanto, se è necessario modificare uno, l'unico modo per sostituirlo. Logicamente ciò significa che si modifica l'ordine di vendita, come se fosse stato modificato la proprietà OrderDate. Tuttavia, a causa della modalità EF Core tiene traccia delle relative proprietà entità, sempre considererà che viene aggiunto per la sostituzione, anche se l'host, SalesOrder, ad esempio, non è una novità.

Dopo aver apportato una modifica all'override del metodo SaveChanges per risolvere il problema (vedere figura 6). La sostituzione ora Filtra per ordini di vendita che vengono aggiunte o modificate e con le due nuove righe di codice che modificano lo stato delle proprietà del riferimento, assicura che ShippingAddress e BillingAddress abbiano lo stesso stato dell'ordine, ovvero che o essere aggiunte o Modificato. Gli oggetti modificati SalesOrder verranno ora anche in grado di includere i valori delle proprietà ShippingAddress e BillingAddress nei comandi di aggiornamento.

Figura 6 effettua SaveChanges comprendere sostituito tipi di proprietà, contrassegnandoli come modificata

public override int SaveChanges () {
  foreach (var entry in ChangeTracker.Entries ().Where (
    e => e.Entity is SalesOrder &&
    (e.State == EntityState.Added || e.State == EntityState.Modified))) {
    if (entry.Entity is SalesOrder order) {
      if (entry.Reference ("ShippingAddress").CurrentValue == null) {
        entry.Reference ("ShippingAddress").CurrentValue = PostalAddress.Empty ();
      }
      if (entry.Reference ("BillingAddress").CurrentValue == null) {
        entry.Reference ("BillingAddress").CurrentValue = PostalAddress.Empty ();
      }
      entry.Reference ("ShippingAddress").TargetEntry.State = entry.State;
      entry.Reference ("BillingAddress").TargetEntry.State = entry.State;
    }
  }
  return base.SaveChanges ();
}

Questo modello funziona poiché sta per il salvataggio con un'istanza diversa di OrderContext che è possibile eseguire una query, che pertanto non ha alcun concetto preconcette dello stato degli oggetti PostalAddress. È possibile trovare un alternative con i criteri per gli oggetti rilevati nei commenti del problema in GitHub bit.ly/2sxMECT.

Pragmatic soluzione per il a breve termine

Se le modifiche per consentire facoltativo di proprietà dell'entità e sostituendo proprietà dell'entità non erano presenti il limite, è possibile richiederebbe molto probabilmente passaggi per creare un modello di dati separato per gestire la persistenza dei dati nel software. Ma questa soluzione temporanea Salva me tale sforzo aggiuntivo e investimento e si nota che non appena sono in grado rimuovere my a soluzioni alternative e facilmente my i modelli di dominio di eseguire il mapping direttamente al database consentendo EF Core di definire il modello di dati. Si è lieta di investire del tempo, impegno e considerato in presentarsi con a soluzioni alternative in modo che è possibile usare gli oggetti di valore ed EF Core 2 quando si progetta il soluzioni e consentire agli altri utenti di essere in grado di eseguire la stessa operazione.

Si noti che il download che accompagna questo articolo si trova in un'applicazione console per testare la soluzione, nonché rendere persistenti i dati in un database SQLite. Utilizzo del database anziché di semplice scrittura di test con il provider InMemory perché si desidera esaminare il server per essere certi che i dati è stati archiviati nel modo che previsto pari al 100%.


Julie Lermanè un direttore regionale Microsoft, Microsoft MVP, istruttore team software e consulente vive massimi di Vermont. È possibile trovare la presentazione di accesso ai dati e altri argomenti in gruppi di utenti e conferenze tutto il mondo. Un blog all'utente e la indirizzo thedatafarm.com ed è l'autore di "Programmazione di Entity Framework", nonché un Code First e un'edizione DbContext, tutto da o ' Reilly Media. Seguire proprio su Twitter: @julielerman e vedere proprio corsi Pluralsight PS/juliel.me-video.

Grazie per il seguente esperto tecnico di Microsoft per la revisione dell'articolo: Andriy Svyryd
Andriy Svyryd è uno sviluppatore .NET ucraino che ha lavorato il team di Entity Framework poiché 2010. Vedere tutti i progetti che contribuisce a in github.com/AndriySvyryd.


Viene illustrato in questo articolo nel forum di MSDN Magazine