Condividi tramite


Implementare oggetti di valore

Suggerimento

Questo contenuto è un estratto dell'eBook, Architettura di microservizi .NET per applicazioni .NET containerizzati, disponibile in documentazione .NET o come PDF scaricabile gratuitamente leggibile offline.

Architettura di Microservizi .NET per Applicazioni .NET Containerizzate miniatura della copertina dell'eBook.

Come descritto nelle sezioni precedenti sulle entità e sulle aggregazioni, l'identità è fondamentale per le entità. Esistono tuttavia molti oggetti e elementi di dati in un sistema che non richiedono né identità né il tracciamento dell'identità, come gli oggetti valore.

Un oggetto valore può fare riferimento ad altre entità. Ad esempio, in un'applicazione che genera una route che descrive come ottenere da un punto a un altro, tale route sarebbe un oggetto valore. Sarebbe uno snapshot di punti su un percorso specifico, ma questo percorso suggerito non avrebbe un'identità, anche se internamente potrebbe fare riferimento a entità come City, Road e così via.

La figura 7-13 mostra l'oggetto valore Address all'interno dell'aggregazione Order.

Diagramma che mostra l'oggetto valore Address all'interno dell'aggregazione degli ordini.

Figura 7-13. Oggetto di valore dell'indirizzo all'interno dell'aggregato Order

Come illustrato nella figura 7-13, un'entità è in genere costituita da più attributi. Ad esempio, l'entità Order può essere modellata come entità con un'identità e composta internamente da un set di attributi, ad esempio OrderId, OrderDate, OrderItems e così via. Ma l'indirizzo, che è semplicemente un valore complesso composto da paese/area geografica, strada, città e così via, e non ha identità in questo dominio, deve essere modellato e considerato come oggetto valore.

Caratteristiche importanti degli oggetti valore

Esistono due caratteristiche principali per gli oggetti valore:

  • Non hanno identità.

  • Sono immutabili.

La prima caratteristica è già stata discussa. L'immutabilità è un requisito importante. I valori di un oggetto valore devono essere non modificabili dopo la creazione dell'oggetto. Pertanto, quando l'oggetto viene costruito, è necessario specificare i valori necessari, ma non consentire la modifica durante la durata dell'oggetto.

Gli oggetti valore consentono di eseguire alcune ottimizzazioni per le prestazioni, grazie alla loro natura immutabile. Ciò vale soprattutto nei sistemi in cui possono essere presenti migliaia di istanze di oggetti valore, molte delle quali hanno gli stessi valori. La loro natura immutabile permette loro di essere riutilizzati; possono essere oggetti intercambiabili, poiché i valori sono uguali e non hanno identità. Questo tipo di ottimizzazione può talvolta fare la differenza tra il software che viene eseguito lentamente e il software con buone prestazioni. Naturalmente, tutti questi casi dipendono dall'ambiente dell'applicazione e dal contesto di distribuzione.

Implementazione dell'oggetto valore in C#

In termini di implementazione, è possibile avere una classe base di oggetti valore con metodi di utilità di base come l'uguaglianza in base al confronto tra tutti gli attributi (poiché un oggetto valore non deve essere basato sull'identità) e altre caratteristiche fondamentali. Nell'esempio seguente viene illustrata una classe base di oggetti valore usata nel microservizio di ordinamento da eShopOnContainers.

public abstract class ValueObject
{
    protected static bool EqualOperator(ValueObject left, ValueObject right)
    {
        if (ReferenceEquals(left, null) ^ ReferenceEquals(right, null))
        {
            return false;
        }
        return ReferenceEquals(left, right) || left.Equals(right);
    }

    protected static bool NotEqualOperator(ValueObject left, ValueObject right)
    {
        return !(EqualOperator(left, right));
    }

    protected abstract IEnumerable<object> GetEqualityComponents();

    public override bool Equals(object obj)
    {
        if (obj == null || obj.GetType() != GetType())
        {
            return false;
        }

        var other = (ValueObject)obj;

        return this.GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());
    }

    public override int GetHashCode()
    {
        return GetEqualityComponents()
            .Select(x => x != null ? x.GetHashCode() : 0)
            .Aggregate((x, y) => x ^ y);
    }
    // Other utility methods
}

ValueObject è un tipo abstract class, ma in questo esempio non sovraccarica gli operatori == e !=. È possibile scegliere di eseguire questa operazione, delegando i confronti all'override Equals. Ad esempio, si considerino i seguenti overload dell'operatore per il tipo ValueObject:

public static bool operator ==(ValueObject one, ValueObject two)
{
    return EqualOperator(one, two);
}

public static bool operator !=(ValueObject one, ValueObject two)
{
    return NotEqualOperator(one, two);
}

È possibile usare questa classe quando si implementa l'oggetto valore effettivo, come con l'oggetto valore illustrato nell'esempio Address seguente:

public class Address : ValueObject
{
    public String Street { get; private set; }
    public String City { get; private set; }
    public String State { get; private set; }
    public String Country { get; private set; }
    public String ZipCode { get; private set; }

    public Address() { }

    public Address(string street, string city, string state, string country, string zipcode)
    {
        Street = street;
        City = city;
        State = state;
        Country = country;
        ZipCode = zipcode;
    }

    protected override IEnumerable<object> GetEqualityComponents()
    {
        // Using a yield return statement to return each element one at a time
        yield return Street;
        yield return City;
        yield return State;
        yield return Country;
        yield return ZipCode;
    }
}

L'implementazione dell'oggetto valore di Address non ha identità e pertanto non viene definito alcun campo ID nella definizione della Address classe o nella definizione della ValueObject classe.

Non è stato possibile usare alcun campo ID in una classe da usare da Entity Framework (EF) fino a EF Core 2.0, che consente di implementare oggetti valore migliori senza ID. Questa è esattamente la spiegazione della sezione successiva.

Si potrebbe sostenere che gli oggetti valore, non modificabili, devono essere di sola lettura (ovvero avere proprietà get-only) e questo è vero. Tuttavia, gli oggetti valore vengono solitamente serializzati e deserializzati per transitare attraverso le code dei messaggi, e il fatto che siano di sola lettura impedisce al deserializzatore di assegnare valori. Quindi, basta lasciarli come private set, il che è sufficientemente di sola lettura per essere pratico.

Semantica del confronto degli oggetti di valore

È possibile confrontare due istanze del Address tipo usando tutti i metodi seguenti:

var one = new Address("1 Microsoft Way", "Redmond", "WA", "US", "98052");
var two = new Address("1 Microsoft Way", "Redmond", "WA", "US", "98052");

Console.WriteLine(EqualityComparer<Address>.Default.Equals(one, two)); // True
Console.WriteLine(object.Equals(one, two)); // True
Console.WriteLine(one.Equals(two)); // True
Console.WriteLine(one == two); // True

Quando tutti i valori sono uguali, i confronti vengono valutati correttamente come true. Se non hai scelto di sovraccaricare gli operatori == e !=, l'ultimo confronto di one == two sarebbe valutato come false. Per altre informazioni, vedere Operatori di uguaglianza ValueObject di overload.

Come rendere persistenti gli oggetti valore nel database con EF Core 2.0 e versioni successive

Si è appena visto come definire un oggetto valore nel modello di dominio. Ma come è possibile salvarlo in modo permanente nel database usando Entity Framework Core perché in genere è destinato alle entità con identità?

Contesto e approcci precedenti con EF Core 1.1

Come contesto, una limitazione quando si usa EF Core 1.0 e 1.1 era che non era possibile usare tipi complessi come definito in EF 6.x nel .NET Framework tradizionale. Pertanto, se si usa EF Core 1.0 o 1.1, è necessario archiviare l'oggetto valore come entità EF con un campo ID. Quindi, sembrava più simile a un oggetto valore senza identità, è possibile nasconderne l'ID in modo da chiarire che l'identità di un oggetto valore non è importante nel modello di dominio. È possibile nascondere l'ID utilizzando l'ID come proprietà ombra. Poiché tale configurazione per nascondere l'ID nel modello è impostata a livello dell'infrastruttura di Entity Framework, risulterebbe praticamente trasparente per il tuo modello di dominio.

Nella versione iniziale di eShopOnContainers (.NET Core 1.1), l'ID nascosto necessario per l'infrastruttura di EF Core è stato implementato nel modo seguente nel livello DbContext, usando l'API Fluent nel progetto di infrastruttura. Pertanto, l'ID è stato nascosto dal punto di vista del modello di dominio, ma ancora presente nell'infrastruttura.

// Old approach with EF Core 1.1
// Fluent API within the OrderingContext:DbContext in the Infrastructure project
void ConfigureAddress(EntityTypeBuilder<Address> addressConfiguration)
{
    addressConfiguration.ToTable("address", DEFAULT_SCHEMA);

    addressConfiguration.Property<int>("Id")  // Id is a shadow property
        .IsRequired();
    addressConfiguration.HasKey("Id");   // Id is a shadow property
}

Tuttavia, la persistenza di tale oggetto valore nel database è stata eseguita come un'entità regolare in una tabella diversa.

Con EF Core 2.0 e versioni successive, esistono modi nuovi e migliori per rendere persistenti gli oggetti valore.

Rendere persistenti gli oggetti valore come tipi di entità di proprietà in EF Core 2.0 e versioni successive

Anche con alcune lacune tra il modello di oggetto valore canonico in DDD e il tipo di entità di proprietà in EF Core, è attualmente il modo migliore per rendere persistenti gli oggetti valore con EF Core 2.0 e versioni successive. È possibile visualizzare le limitazioni alla fine di questa sezione.

La funzionalità del tipo di entità di proprietà è stata aggiunta a EF Core dalla versione 2.0.

Un tipo di entità di proprietà consente di eseguire il mapping dei tipi che non hanno una propria identità definita in modo esplicito nel modello di dominio e vengono usati come proprietà, ad esempio un oggetto valore, all'interno di una delle entità. Un tipo di entità di proprietà condivide lo stesso tipo CLR con un altro tipo di entità, ovvero è solo una classe normale. L'entità contenente la navigazione definita è l'entità proprietaria. Quando si esegue una query sul proprietario, i tipi di proprietà vengono inclusi per impostazione predefinita.

Esaminando solo il modello di dominio, un tipo di proprietà sembra che non abbia alcuna identità. Tuttavia, sotto le quinte, i tipi di proprietà hanno l'identità, ma la proprietà di navigazione del proprietario fa parte di questa identità.

L'identità delle istanze di tipi posseduti non è completamente autonoma. È costituito da tre componenti:

  • Identità del proprietario

  • La proprietà di navigazione che punta a loro

  • Nel caso di raccolte di tipi di proprietà, si tratta di un componente indipendente (supportato in EF Core 2.2 e versioni successive).

Ad esempio, nel modello di dominio Ordering in eShopOnContainers, come parte dell'entità Order, l'oggetto valore Address viene implementato come tipo di entità di proprietà all'interno dell'entità proprietario, ovvero l'entità Order. Address è un tipo senza proprietà Identity definita nel modello di dominio. Viene usato come proprietà del tipo Order per specificare l'indirizzo di spedizione per uno specifico ordine.

Per convenzione, viene creata una chiave primaria nascosta per il tipo di entità posseduto e sarà mappata alla stessa tabella del proprietario utilizzando la suddivisione delle tabelle. In questo modo è possibile usare tipi di proprietà in modo analogo al modo in cui i tipi complessi vengono usati in EF6 in .NET Framework tradizionale.

È importante notare che i tipi di proprietà non vengono mai individuati per convenzione in EF Core, quindi è necessario dichiararli in modo esplicito.

In eShopOnContainers, nel file OrderingContext.cs, all'interno del OnModelCreating() metodo vengono applicate più configurazioni dell'infrastruttura. Uno di essi è correlato all'entità Order.

// Part of the OrderingContext.cs class at the Ordering.Infrastructure project
//
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.ApplyConfiguration(new ClientRequestEntityTypeConfiguration());
    modelBuilder.ApplyConfiguration(new PaymentMethodEntityTypeConfiguration());
    modelBuilder.ApplyConfiguration(new OrderEntityTypeConfiguration());
    modelBuilder.ApplyConfiguration(new OrderItemEntityTypeConfiguration());
    //...Additional type configurations
}

Nel codice seguente l'infrastruttura di persistenza viene definita per l'entità Order:

// Part of the OrderEntityTypeConfiguration.cs class
//
public void Configure(EntityTypeBuilder<Order> orderConfiguration)
{
    orderConfiguration.ToTable("orders", OrderingContext.DEFAULT_SCHEMA);
    orderConfiguration.HasKey(o => o.Id);
    orderConfiguration.Ignore(b => b.DomainEvents);
    orderConfiguration.Property(o => o.Id)
        .ForSqlServerUseSequenceHiLo("orderseq", OrderingContext.DEFAULT_SCHEMA);

    //Address value object persisted as owned entity in EF Core 2.0
    orderConfiguration.OwnsOne(o => o.Address);

    orderConfiguration.Property<DateTime>("OrderDate").IsRequired();

    //...Additional validations, constraints and code...
    //...
}

Nel codice precedente il orderConfiguration.OwnsOne(o => o.Address) metodo specifica che la Address proprietà è un'entità di proprietà del Order tipo.

Per impostazione predefinita, le convenzioni di EF Core denominano le colonne di database per le proprietà del tipo di entità di proprietà come EntityProperty_OwnedEntityProperty. Pertanto, le proprietà interne di Address verranno visualizzate nella Orders tabella con i nomi Address_Street, Address_City (e così via per State, Countrye ZipCode).

È possibile aggiungere il Property().HasColumnName() metodo Fluent per rinominare tali colonne. Nel caso in cui Address si tratta di una proprietà pubblica, i mapping saranno simili ai seguenti:

orderConfiguration.OwnsOne(p => p.Address)
                            .Property(p=>p.Street).HasColumnName("ShippingStreet");

orderConfiguration.OwnsOne(p => p.Address)
                            .Property(p=>p.City).HasColumnName("ShippingCity");

È possibile concatenare il metodo OwnsOne in un mapping fluente. Nell'esempio ipotetico seguente, OrderDetails possiede BillingAddress e ShippingAddress, che sono entrambi Address tipi. Quindi OrderDetails è di proprietà del tipo Order.

orderConfiguration.OwnsOne(p => p.OrderDetails, cb =>
    {
        cb.OwnsOne(c => c.BillingAddress);
        cb.OwnsOne(c => c.ShippingAddress);
    });
//...
//...
public class Order
{
    public int Id { get; set; }
    public OrderDetails OrderDetails { get; set; }
}

public class OrderDetails
{
    public Address BillingAddress { get; set; }
    public Address ShippingAddress { get; set; }
}

public class Address
{
    public string Street { get; set; }
    public string City { get; set; }
}

Dettagli aggiuntivi sui tipi di entità di proprietà

  • I tipi di proprietà vengono definiti quando si configura una proprietà di navigazione su un particolare tipo usando l'API Fluent OwnsOne.

  • La definizione di un tipo di proprietà nel modello di metadati è composta da: il tipo di proprietario, la proprietà di navigazione e il tipo CLR del tipo di proprietà.

  • L'identità (chiave) di un'istanza di tipo di proprietà nello stack è composta dall'identità del tipo di proprietario e dalla definizione del tipo di proprietà.

Funzionalità delle entità di proprietà

  • I tipi di proprietà possono fare riferimento ad altre entità, di proprietà (tipi di proprietà annidati) o non di proprietà (proprietà di navigazione di riferimento regolari ad altre entità).

  • È possibile eseguire il mapping dello stesso tipo CLR come diversi tipi posseduti nella stessa entità proprietaria tramite proprietà di navigazione separate.

  • La suddivisione delle tabelle è configurata per convenzione, ma puoi scegliere di non seguire la convenzione mappando il tipo posseduto a una tabella diversa tramite ToTable.

  • Il caricamento eager viene eseguito automaticamente sui tipi posseduti, cioè non è necessario chiamare .Include() sulla query.

  • Può essere configurato con l'attributo [Owned], usando EF Core 2.1 e versioni successive.

  • Può gestire raccolte di tipi posseduti (usando la versione 2.2 e successive).

Limitazioni delle entità di proprietà

  • Non è possibile creare un DbSet<T> di un tipo posseduto (per progettazione).

  • Non è possibile chiamare ModelBuilder.Entity<T>() sui tipi posseduti (attualmente per progettazione).

  • Nessun supporto per i tipi di proprietà facoltativi (ovvero nullable) di cui è stato eseguito il mapping con il proprietario nella stessa tabella , ovvero tramite la suddivisione delle tabelle. Questo perché il mapping viene eseguito per ogni proprietà, non esiste alcuna sentinella separata per il valore complesso nullo nel suo insieme.

  • Nessun supporto per il mapping dell'ereditarietà per i tipi posseduti, ma dovrebbe essere possibile eseguire il mapping di due tipi finali delle stesse gerarchie di ereditarietà come tipi posseduti distinti. EF Core non prenderà in considerazione il fatto che facciano parte della stessa gerarchia.

Differenze principali con i tipi complessi di EF6

  • La suddivisione delle tabelle è facoltativa, ovvero possono essere mappate a una tabella separata e rimanere tipi di proprietà.

Risorse aggiuntive