Entity Framework Core で値オブジェクトを実装する

Entity Framework Core 2.0 の頃から既に値オブジェクトを実装する資料は存在した。

docs.microsoft.com

ただ、自分がやりたかったのは「エンティティの ID を値オブジェクトにすること」だったので、 ちょっと違う。 Entity Framework Core 2.1 から追加された「値の変換」機能を使えば、 やりたかったことが実現できるかもと思ったので試してみた。

using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace ValueObjectSample
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                MainAsync(args).GetAwaiter().GetResult();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
            Console.WriteLine("Press enter key");
            Console.ReadLine();
        }

        static async Task MainAsync(string[] args)
        {
            using (var context = new ApplicationDbContext())
            {
                await context.Database.EnsureDeletedAsync();
                await context.Database.EnsureCreatedAsync();
            }

            using (var context = new ApplicationDbContext())
            {
                var author = new Author()
                {
                    Name = "荒川 弘"
                };
                context.Authors.Add(author);

                context.Books.Add(new Book()
                {
                    Title = "銀の匙 Silver Spoon",
                    AuthorId = author.Id,
                });
                context.Books.Add(new Book()
                {
                    Title = "アルスラーン戦記",
                    AuthorId = author.Id,
                });

                await context.SaveChangesAsync();
            }

            using (var context = new ApplicationDbContext())
            {
                var books = await context.Books
                    .Include(b => b.Author)
                    .ToListAsync();
                foreach (var book in books)
                {
                    Console.WriteLine($"Id : {book.Id}");
                    Console.WriteLine($"Title : {book.Title}");
                    Console.WriteLine($"AuthorName : {book.Author.Name}");
                    Console.WriteLine();
                }
            }

            using (var context = new ApplicationDbContext())
            {
                var authors = await context.Authors
                    .Include(a => a.Books)
                    .ToListAsync();
                foreach (var author in authors)
                {
                    Console.WriteLine($"Id : {author.Id}");
                    Console.WriteLine($"Name : {author.Name}");
                    for (int i = 0; i < author.Books.Count; i++)
                    {
                        Console.WriteLine($"BookTitle{i} : {author.Books[i].Title}");
                    }
                    Console.WriteLine();
                }
            }
        }
    }

    public class ApplicationDbContext : DbContext
    {
        public DbSet<Author> Authors => Set<Author>();

        public DbSet<Book> Books => Set<Book>();

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(
                "Server=(local);Database=ValueObjectSample;Integrated Security=SSPI;");
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Author>(b =>
            {
                b.Property(e => e.Id)
                    .IsRequired()
                    .HasConversion( // データベースには文字列で保存する
                        id => id.Value,
                        value => AuthorId.Parse(value));
                b.Property(e => e.Name)
                    .IsRequired();

                b.HasKey(e => e.Id);
                b.HasMany(e => e.Books);
            });

            modelBuilder.Entity<Book>(b =>
            {
                b.Property(e => e.Id)
                    .IsRequired()
                    .HasConversion( // データベースには文字列で保存する
                        id => id.Value,
                        value => BookId.Parse(value));
                b.Property(e => e.Title)
                    .IsRequired();
                b.Property(e => e.AuthorId)
                    .IsRequired()
                    .HasConversion( // データベースには文字列で保存する
                        id => id.Value,
                        value => AuthorId.Parse(value));

                b.HasKey(e => e.Id);
                b.HasOne(e => e.Author);
            });
        }
    }

    public class Author
    {
        public AuthorId Id { get; set; } = AuthorId.NewAuthorId();

        public string Name { get; set; }

        public IList<Book> Books { get; set; }
    }

    public class Book
    {
        public BookId Id { get; set; } = BookId.NewBookId();

        public string Title { get; set; }

        public AuthorId AuthorId { get; set; }

        public Author Author { get; set; }
    }

    // Author の ID を ValueObject にする
    public class AuthorId
    {
        AuthorId(string value)
        {
            Value = value;
        }

        public string Value { get; }

        public static AuthorId NewAuthorId() =>
            new AuthorId(Guid.NewGuid().ToString());

        public static AuthorId Parse(string value) =>
            new AuthorId(value);

        public override int GetHashCode() =>
            Value.GetHashCode();

        public override bool Equals(object obj)
        {
            if (obj == null || GetType() != obj.GetType())
                return false;
            if (object.ReferenceEquals(this, obj))
                return true;
            if (obj is AuthorId other)
                return Value.Equals(other.Value);
            return false;
        }

        public override string ToString() =>
            $"AuthorId({Value})";
    }

    // Book の ID を ValueObject にする
    public class BookId
    {
        BookId(string value)
        {
            Value = value;
        }

        public string Value { get; }

        public static BookId NewBookId() =>
            new BookId(Guid.NewGuid().ToString());

        public static BookId Parse(string value) =>
            new BookId(value);

        public override int GetHashCode() =>
            Value.GetHashCode();

        public override bool Equals(object obj)
        {
            if (obj == null || GetType() != obj.GetType())
                return false;
            if (object.ReferenceEquals(this, obj))
                return true;
            if (obj is BookId other)
                return Value.Equals(other.Value);
            return false;
        }

        public override string ToString() =>
            $"BookId({Value})";
    }
}

実行結果がこちら。 ID をデータベースには文字列として保存しつつ、 エンティティで扱うときは値オブジェクト化できている。 リレーションシップも OK。

f:id:griefworker:20180606145237p:plain

ID は上手くいくと思っていたけど、 リレーションシップの方は上手くいくかどうか半信半疑だったので一安心。