値オブジェクトの実装Implementing value objects

これまでのエンティティと集計に関するセクションで説明したように、ID はエンティティの基礎です。As discussed in earlier sections about entities and aggregates, identity is fundamental for entities. 一方、システムには、ID と ID の追跡を必要としないオブジェクトとデータ項目が多数あります。たとえば、値オブジェクトなどです。However, there are many objects and data items in a system that do not require an identity and identity tracking, such as value objects.

値オブジェクトは他のエンティティを参照できます。A value object can reference other entities. たとえば、あるポイントから別のポイントに到達する方法を示すルートを生成するアプリケーションの場合、そのルートが値オブジェクトです。For example, in an application that generates a route that describes how to get from one point to another, that route would be a value object. これは特定のルート上にあるポイントのスナップショットですが、内部的には City、Road などのエンティティを参照していても、この提案されるルートに ID はありません。It would be a snapshot of points on a specific route, but this suggested route would not have an identity, even though internally it might refer to entities like City, Road, etc.

図 9-13 は、Order 集計内の Address 値オブジェクトを示しています。Figure 9-13 shows the Address value object within the Order aggregate.

図 9-13.Figure 9-13. Order 集計内の Address 値オブジェクトAddress value object within the Order aggregate

図 9-13 に示すように、通常、エンティティは複数の属性で構成されます。As shown in Figure 9-13, an entity is usually composed of multiple attributes. たとえば、Order エンティティは、ID があるエンティティとしてモデル化し、内部的に OrderId、OrderDate、OrderItems などの一連の属性で構成することができます。ただし、住所は、単に国、市区町村、番地などで構成された複合値であり、このドメイン内に ID はないため、値をモデル化し、値オブジェクトとして扱う必要があります。For example, the Order entity can be modeled as an entity with an identity and composed internally of a set of attributes such as OrderId, OrderDate, OrderItems, etc. But the address, which is simply a complex value composed of country, street, city, etc. and has no identity in this domain, must be modeled and treated as a value object.

値オブジェクトの重要な特性Important characteristics of value objects

値オブジェクトには主に 2 つの特性があります。There are two main characteristics for value objects:

  • ID がない。They have no identity.

  • 不変である。They are immutable.

1 つ目の特性については既に説明しました。The first characteristic was already discussed. 不変性は重要な要件です。Immutability is an important requirement. 値オブジェクトが作成された後は、その値を不変にする必要があります。The values of a value object must be immutable once the object is created. そのため、オブジェクトの構築時に必要な値を指定する必要がありますが、オブジェクトの有効期間中は変更を許可しない必要があります。Therefore, when the object is constructed, you must provide the required values, but you must not allow them to change during the object’s lifetime.

値オブジェクトを使用すると、不変の性質がパフォーマンスのために役立つことがあります。Value objects allow you to perform certain tricks for performance, thanks to their immutable nature. 特に、何千もの値オブジェクト インスタンスが存在する可能性があり、インスタンスの多くが同じ値を持つシステムで役に立ちます。This is especially true in systems where there may be thousands of value object instances, many of which have the same values. 不変の性質なので、再利用することができます。値が同じで、ID を持たないので、相互に交換可能なオブジェクトにすることができます。Their immutable nature allows them to be reused; they can be interchangeable objects, since their values are the same and they have no identity. このような最適化で、低速で実行されるソフトウェアとパフォーマンスが良好なソフトウェアの間で違いが生じる場合があります。This type of optimization can sometimes make a difference between software that runs slowly and software with good performance. 当然ながら、このようないずれの場合でも、アプリケーション環境と展開コンテキストによって変わります。Of course, all these cases depend on the application environment and deployment context.

C# での値オブジェクトの実装Value object implementation in C#

実装の観点からは、(値オブジェクトは ID に基づいてはならないので) すべての属性と他の基本的な特性の比較に基づいて、等値などの基本的なユーティリティ メソッドを持つ値オブジェクトの基底クラスを持つことができます。In terms of implementation, you can have a value object base class that has basic utility methods like equality based on comparison between all the attributes (since a value object must not be based on identity) and other fundamental characteristics. 次の例は、eShopOnContainers の注文マイクロサービスで使用される値オブジェクトの基底クラスを示しています。The following example shows a value object base class used in the ordering microservice from 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, null) || left.Equals(right);
    }

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

    protected abstract IEnumerable<object> GetAtomicValues();

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

        ValueObject other = (ValueObject)obj;
        IEnumerator<object> thisValues = GetAtomicValues().GetEnumerator();
        IEnumerator<object> otherValues = other.GetAtomicValues().GetEnumerator();
        while (thisValues.MoveNext() && otherValues.MoveNext())
        {
            if (ReferenceEquals(thisValues.Current, null) ^
                ReferenceEquals(otherValues.Current, null))
            {
                return false;
            }

            if (thisValues.Current != null &&
                !thisValues.Current.Equals(otherValues.Current))
            {
                return false;
            }
        }
        return !thisValues.MoveNext() && !otherValues.MoveNext();
    }

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

次の例に示す Address 値オブジェクトと同様に、実際の値オブジェクトを実装するときにこのクラスを使用できます。You can use this class when implementing your actual value object, as with the Address value object shown in the following example:

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

    private 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> GetAtomicValues()
    {
        // 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;
    }
}

EF Core 2.0 でデータベース内の値オブジェクトを永続化する方法How to persist value objects in the database with EF Core 2.0

ここまでは、ドメイン モデルで値オブジェクトを定義する方法について説明しました。You just saw how to define a value object in your domain model. それでは、通常は ID のあるエンティティをターゲットとする Entity Framework (EF) Core を使用して、データベースに永続化するにはどうすればよいでしょうか。But, how can you actually persist it into the database through Entity Framework (EF) Core which usually targets entities with identity?

EF Core 1.1 を使用する背景と以前のアプローチBackground and older approaches using EF Core 1.1

背景として、EF Core 1.0 と 1.1 を使用する場合、従来の .NET Framework で EF 6.x で定義されているような複合型を使用できないという制限がありました。As background, a limitation when using EF Core 1.0 and 1.1 was that you cannot use complex types as defined in EF 6.x in the traditional .NET Framework. そのため、EF Core 1.0 または 1.1 を使用する場合、値オブジェクトを ID フィールドを持つ EF エンティティとして格納する必要がありました。Therefore, if using EF Core 1.0 or 1.1, you needed to store your value object as an EF entity with an ID field. そこで、ID を持たない値オブジェクトのように見えるように、ID を非表示にすることがありました。これで、値オブジェクトの ID がドメイン モデルで重要ではないことがはっきりします。Then, so it looked more like a value object with no identity, you could hide its ID so you make clear that the identity of a value object is not important in the domain model. この ID を非表示にするには、シャドウ プロパティとして ID を使用します。You could hide that ID by using the ID as a shadow property. モデル内の ID を非表示にする構成は EF インフラストラクチャ レベルで設定されるため、ドメイン モデルでも透過的になります。Since that configuration for hiding the ID in the model is set up in the EF infrastructure level, it would be kind of transparent for your domain model.

eShopOnContainers の初期バージョン (.NET Core 1.1) では、EF Core インフラストラクチャに必要な非表示の ID は、次のように、インフラストラクチャ プロジェクトで Fluent API を使用して DbContext レベルで実装されていました。In the initial version of eShopOnContainers (.NET Core 1.1), the hidden ID needed by EF Core infrastructure was implemented in the following way in the DbContext level, using Fluent API at the infrastructure project. そのため、ID はドメイン モデルの観点からは非表示でしたが、インフラストラクチャには存在していました。Therefore, the ID was hidden from the domain model point of view, but still present in the infrastructure.

// 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
}

ただし、その値オブジェクトのデータベースへの永続化は、別のテーブルの通常のエンティティと同様に実行されていました。However, the persistence of that value object into the database was performed like a regular entity in a different table.

EF Core 2.0 には、値オブジェクトを永続化するための新しく優れた方法があります。With EF Core 2.0, there are new and better ways to persist value objects.

EF Core 2.0 で所有エンティティ型として値オブジェクトを永続化するPersist value objects as owned entity types in EF Core 2.0

DDD の標準の値オブジェクト パターンと EF Core の所有エンティティ型の間にいくつかのギャップがあるとしても、現在は EF Core 2.0 を使用して値オブジェクトを永続化する方法が最善です。Even with some gaps between the canonical value object pattern in DDD and the owned entity type in EF Core, it's currently the best way to persist value objects with EF Core 2.0. 制限事項については、このセクションの末尾を参照してください。You can see limitations at the end of this section.

所有エンティティ型の機能は、EF Core バージョン 2.0 以降に追加されました。The owned entity type feature was added to EF Core since version 2.0.

所有エンティティ型を使用すると、ドメイン モデルで明示的に定義された独自の ID を持たない型をマップし、任意のエンティティ内で値オブジェクトなどのプロパティとして使用することができます。An owned entity type allows you to map types that do not have their own identity explicitely defined in the domain model and are used as properties, such as a value object, within any of your entities. 所有エンティティ型は、同じ CLR 型を別のエンティティ型と共有します。An owned entity type shares the same CLR type with another entity type. 定義となるナビゲーションを含むエンティティは、所有者エンティティです。The entity containing the defining navigation is the owner entity. 所有者のクエリを実行すると、所有型が既定で含まれます。When querying the owner, the owned types are included by default.

ドメイン モデルのみを見ると、所有型には ID がないように見えます。Just by looking at the domain model, an owned type looks like it doesn’t have any identity. 実際には所有型に ID はありますが、所有者ナビゲーション プロパティはこの ID の一部です。However, under the covers, owned types do have identity, but the owner navigation property is part of this identity.

所有型のインスタンスの ID は、完全に独自のものではありません。The identity of instances of own types is not completely their own. この ID は 3 つのコンポーネントで構成されています。It consists of three components:

  • 所有者の IDThe identity of the owner

  • これらを指すナビゲーション プロパティThe navigation property pointing to them

  • 所有型のコレクションの場合は、独立したコンポーネント (EF Core 2.0 ではまだサポートされていません)。In the case of collections of owned types, an independent component (not yet supported in EF Core 2.0).

たとえば、eShopOnContainers の Ordering ドメイン モデルでは、Order エンティティの一部である Address 値オブジェクトは、所有者エンティティ (Order エンティティ) 内の所有エンティティ型として実装されます。For example, in the Ordering domain model at eShopOnContainers, as part of the Order entity, the Address value object is implemented as an owned entity type within the owner entity, which is the Order entity. Address は、ドメイン モデルに定義されている ID プロパティのない型です。Address is a type with no identity property defined in the domain model. 特定の注文の配送先住所を指定するために、Order 型のプロパティとして使用されます。It is used as a property of the Order type to specify the shipping address for a particular order.

規約によって、所有されている型に対してシャドウ主キーが作成され、テーブル分割を利用し、同じテーブルに所有者としてマップされます。By convention, a shadow primary key is created for the owned type and it will be mapped to the same table as the owner by using table splitting. そのため、従来の .NET Framework の EF6 で複合型を使用する方法と同様に所有型を使用できます。This allows to use owned types similarly to how complex types are used in EF6 in the traditional .NET Framework.

EF Core の規約で所有型が検出されることはないので、明示的に宣言する必要がある点に注意してください。It is important to note that owned types are never discovered by convention in EF Core, so you have to declare them explicitly.

eShopOnContainers では、OnModelCreating() メソッド内の OrderingContext.cs に複数のインフラストラクチャ構成が適用されています。In eShopOnContainers, at the OrderingContext.cs, within the OnModelCreating() method, there are multiple infrastructure configuration being applied. そのうちの 1 つが Order エンティティに関連しています。One of them is related to the Order entity.

// 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
}

次のコードでは、Order エンティティについて永続化インフラストラクチャが定義されています。In the following code, the persistence infrastructure is defined for the Order entity:

// 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...
    //...
}

前のコードでは、orderConfiguration.OwnsOne(o => o.Address) メソッドは、Address プロパティが Order 型の所有エンティティであることを指定しています。In the previous code, the orderConfiguration.OwnsOne(o => o.Address) method specifies that the Address property is an owned entity of the Order type.

既定の EF Core 規約では、所有エンティティ型のプロパティのデータベース列に EntityProperty_OwnedEntityProperty と名前が付けられます。By default, EF Core conventions name the database columns for the properties of the owned entity type as EntityProperty_OwnedEntityProperty. そのため、Address の内部プロパティは、Orders テーブルで Address_StreetAddress_City (StateCountryZipCode など) という名前で表示されます。Therefore, the internal properties of Address will appear in the Orders table with the names Address_Street, Address_City (and so on for State, Country and ZipCode).

Property().HasColumnName() fluent メソッドを付加して、これらの列の名前を変更することができます。You can append the Property().HasColumnName() fluent method to rename those columns. Address がパブリック プロパティの場合、マッピングは次のようになります。In the case where Address is a public property, the mappings would be like the following:

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

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

fluent マッピングでは、OwnsOne メソッドを連鎖させることができます。It is possible to chain the OwnsOne method in a fluent mapping. 次の仮定例では、OrderDetailsBillingAddressShippingAddress を所有しています (いずれも Address 型です)。In the following hypothetical example, OrderDetails owns BillingAddress and ShippingAddress, which are both Address types. また、OrderDetailsOrder 型に所有されています。Then OrderDetails is owned by the Order type.

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; }
}

所有エンティティ型に関するその他の詳細情報Additional details on owned entity types

• 所有型は、OwnsOne fluent API を使用してナビゲーション プロパティを特定の型に構成するときに定義されます。• Owned types are defined when you configure a navigation property to a particular type using the OwnsOne fluent API.

• メタデータ モデルの所有型の定義は、所有者型、ナビゲーション プロパティ、所有型の CLR 型のコンポジットです。• The definition of an owned type in our metadata model is a composite of: the owner type, the navigation property, and the CLR type of the owned type.

• スタック内の所有型インスタンスの ID (キー) は、所有者型の ID と所有型の定義のコンポジットです。• The identity (key) of an owned type instance in our stack is a composite of the identity of the owner type and the definition of the owned type.

所有エンティティの機能:Owned entities capabilities:

• 所有型は、他の所有 (入れ子にされた所有型) エンティティまたは非所有 (他のエンティティに対する通常の参照ナビゲーション プロパティ) エンティティを参照できます。• Owned type can reference other entities, either owned (nested owned types) or non-owned (regular reference navigation properties to other entities).

• 同じ所有者エンティティの同じ CLR 型を、個別のナビゲーション プロパティを使用して異なる所有型としてマップすることができます。• You can map the same CLR type as different owned types in the same owner entity through separate navigation properties.

• テーブル分割は規約で設定されますが、ToTable を使用して所有型を別のテーブルにマップすることでオプト アウトすることができます。• Table splitting is setup by convention, but you can opt out by mapping the owned type to a different table using ToTable.

• Eager の読み込みは、所有型に対して自動的に実行されます。つまり、クエリで Include () を呼び出す必要はありません。• Eager loading is performed automatically on owned types, i.e. no need to call Include() on the query.

所有エンティティの制限事項:Owned entities limitations:

• 所有型の DbSet を作成することはできません (仕様)。• You cannot create a DbSet of an owned type (by design).

• 所有型に対して ModelBuilder.Entity() を呼び出すことはできません (現時点では仕様)。• You cannot call ModelBuilder.Entity() on owned types (currently by design).

• 所有型のコレクションはまだありません (ただし、EF Core 2.0 の後のバージョンではサポートされる予定です)。• No collections of owned types yet (but they will be supported in versions after EF Core 2.0).

• 属性を介した構成はサポートされていません。• No support for configuring them via an attribute.

• 同じテーブル内の所有者とマップされている (つまり、テーブル分割を使用している) 省略可能な (つまり、null を許容する) 所有型はサポートされていません。• No support for optional (i.e. nullable) owned types that are mapped with the owner in the same table (i.e. using table splitting). これは、null の場合に個別の監視機能を持たないためです。This is because we don't have a separate sentinel for the null.

• 所有型の継承マッピングはサポートされていませんが、異なる所有型と同じ継承階層の 2 つのリーフ型をマップすることはできます。• No inheritance mapping support for owned types, but you should be able to map two leaf types of the same inheritance hierarchies as different owned types. EF Core は、同じ階層に属することの理由にはなりません。EF Core will not reason about the fact that they are part of the same hierarchy.

EF6 の複合型との主な違いMain differences with EF6's complex types

• テーブル分割は省略可能です。つまり、所有型のまま、必要に応じて別のテーブルにマップすることができます。• Table splitting is optional, i.e. they can optionally be mapped to a separate table and still be owned types.

• 他のエンティティを参照することができます (つまり、他の非所有型との関係で依存側として機能することができます)。• They can reference other entities (i.e. they can act as the dependent side on relationships to other non-owned types).

その他の技術情報Additional resources