【DDD】C# EntityFramework CoreでValueObjectをマップする方法

【DDD】C# EntityFramework CoreでValueObjectをマップする方法

ドメイン駆動設計(DDD)のValueObject(値オブジェクト)をEntityFramework Core(EFCore)で自動的にマッピングする方法を紹介します。
EFCore2.0以降では、所有エンティティ型としてValueObjectを永続化する機能が提供されています。
マッピングだけではなく、ValueObjectとして定義したEntityからマイグレーションを生成することも可能です。

EntityとValueObjectを定義する

注文Orderと配送先住所Addressの場合の各クラスを以下のように定義します。
配送先住所に含まれる郵便番号、都道府県、住所等をValueObjectとして定義しています。

クラス図

Order (Entity)

using System;
using System.Collections.Generic;
using System.Linq;

namespace OrderDomain.Models.OrderAggregate
{
    public class Order
    {
        protected Order()
        {
        }

        public Order(DateTime orderDate, Address address)
        {
            if (address == null) throw new ArgumentNullException(nameof(address));

            OrderDate = orderDate;
            Address = address;
        }

        public DateTime OrderDate { get; private set; }
        public Address Address { get; private set; }
    }
}

Address (ValueObject)

using System;
using System.Collections.Generic;

namespace OrderDomain.Models.OrderAggregate
{
    public class Address
    {
        public Address() { }

        public Address(string postalCode, int prefectureId, string address1, string address2)
        {
            if (postalCode.Length > 7) throw new ArgumentOutOfRangeException(nameof(postalCode));
            if (address1 == null) throw new ArgumentNullException(nameof(address1));
            if (address1.Length > 50) throw new ArgumentOutOfRangeException(nameof(address1));
            if (address2 == null) throw new ArgumentNullException(nameof(address2));
            if (address2.Length > 50) throw new ArgumentOutOfRangeException(nameof(address2));

            PostalCode = postalCode;
            PrefectureId = prefectureId;
            Address1 = address1;
            Address2 = address2;
        }

        public string PostalCode { get; private set; }
        public string PrefectureId { get; private set; }
        public string Address1 { get; private set; }
        public string Address2 { get; private set; }
    }
}

EntityTypeConfigurationを作成する

IEntityTypeConfigurationインターフェースを実装したConfigurationクラスを定義します。

・OwnsOneメソッドを使用してAddressプロパティがOrderの所有エンティティであることを明示する
・AddressプロパティからWithOwnerメソッドを引数なしで実行してAddress自身は所有権の関係にないことを明示する

 

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using OrderDomain.Models.OrderAggregate;

namespace Infrastructure.EntityConfigurations
{
    public class OrderConfiguration : IEntityTypeConfiguration<Order>
    {
        public void Configure(EntityTypeBuilder<Order> builder)
        {
            builder.HasKey(o => o.Id);
            builder.OwnsOne(o => o.Address, a => a.WithOwner());
        }
    }
}

DbContextのOnModelCreatingに読み込み定義

EF CoreのDbContextのOnModelCreatingメソッドをオーバーライドして、先に作成したEntityConfigurationを読み込み、反映する様に定義します。

using Infrastructure.EntityConfigurations;
using Microsoft.EntityFrameworkCore;
using OrderDomain.Models.OrderAggregate;

namespace Infrastructure
{
    public class EcDbContext : DbContext
    {
        public EcDbContext(DbContextOptions<EcDbContext> options) : base(options)
        {
        }


        public DbSet<Order> Orders { get; set; }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            builder.ApplyConfiguration(new OrderConfiguration());

        }
    }
}

マイグレーションを生成する

あとは、通常通りdotnet ef migrations...からマイグレーションファイルを生成します。
Address(ValueObject)はorderテーブル内に追加され、各カラムのプレフィックスにValueObjectクラス名のAddressが追加されます。

protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.CreateTable(
        name: "orders",
        schema: "order",
        columns: table => new
        {
            Id = table.Column<int>(nullable: false)
                .Annotation("Sqlite:Autoincrement", true),
            OrderDate = table.Column<DateTime>(nullable: false),
            Address_PostalCode = table.Column<string>(nullable: true),
            Address_Prefecture = table.Column<int>(nullable: true),
            Address_Address1 = table.Column<string>(nullable: true),
            Address_Address2 = table.Column<string>(nullable: true)
        },
        constraints: table =>
        {
            table.PrimaryKey("PK_orders", x => x.Id);
        });
}

取得すると自動的にValueObjectがマッピングされる

EF Coreからテーブルに永続化されているOrderエンティティを取得します。
Orderに含むAddressクラスがマッピングされた状態で取得できることが確認できます。

using (var context = new EcDbContext()) {
    var order = context.Order.First();
    Console.WriteLine(order.Address);
}

 

プログラミングカテゴリの最新記事