🐥note.

小鳥とMicrosoft <3 なエンジニアの技術Blog📚

EntityFrameworkCoreでValueObjectをDBに永続化する

Microsoft Docsのこちらの記事を読んでいたところ…

docs.microsoft.com

ValueObjectをEntity Framework Coreで永続化することができる…だと!全然知らなかった!

調べてみたら2.0の時代から対応してるっぽいですね。

devblogs.microsoft.com

ということでDocs見ながら手を動かしてみました。

つくってみる

テキトーにProject作って...

dotnet new worker -o EFCore.ValueObject.Sample
cd EFCore.ValueObject.Sample
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SQLite

Personクラスと、ValueObjectとしてPersonNameクラスを用意しました。

public class Person
{
    public int Id { get; set; }
    public PersonName Name { get; set; }
}

public class PersonName : ValueObject
{
    public string FirstName { get; private set; }
    public string LastName { get; private set; }
    public string FullName { get; private set; }
    
    private PersonName() { }
    private PersonName(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
        FullName = FirstName + " " + LastName;
    }

    public static PersonName Create(string firstName, string lastName) => new PersonName(firstName, lastName);

    protected override IEnumerable<object> GetAtomicValues()
    {
        yield return FirstName;
        yield return LastName;
        yield return FullName;
    }
}

ValueObjectは先程のMicrosoft Docsを参考に...

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

DbContextはこんな感じ。
PersonEntityTypeConfigurationOwnsOneがキモ。
builder.Entity<Person>(e => e.OwnsOne(o => o.Name));でもOK。

public class MyDbContext : DbContext
{
    public DbSet<Person> Persons { get; set; }

    public MyDbContext(DbContextOptions<MyDbContext> opt) : base(opt)
    {
        this.Database.OpenConnection();
        this.Database.EnsureCreated();
    }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        builder.ApplyConfiguration(new PersonEntityTypeConfiguration());
        base.OnModelCreating(builder);
    }
}

class PersonEntityTypeConfiguration : IEntityTypeConfiguration<Person>
{
    public void Configure(EntityTypeBuilder<Person> builder) => builder.OwnsOne(m => m.Name);
}

Program.csConfigureServicesでDIに登録して...

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureServices((hostContext, services) =>
        {
            services.AddDbContext<MyDbContext>(cfg => cfg.UseSqlite("Data Source=test.db"), ServiceLifetime.Singleton);
            services.AddHostedService<Worker>();
        });

WorkerExecuteAsyncでDBに追加して...

public MyDbContext DbContext { get; }
public Worker(ILogger<Worker> logger, MyDbContext dbContext)
{
    _logger = logger;
    DbContext = dbContext;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    DbContext.Add(new Person { Name = PersonName.Create("Michael", "Westine") });
    DbContext.Add(new Person { Name = PersonName.Create("Sam", "Axe") });
    DbContext.Add(new Person { Name = PersonName.Create("Fiona", "Glenanne") });
    await DbContext.SaveChangesAsync();

    var persons = await DbContext.Set<Person>().ToListAsync();
    foreach (var p in persons)
    {
        Console.WriteLine($"ID = {p.Id}\tFullName = {p.Name.FullName}");
    }
}

結果がこうなりました。
ValueObjectがDBに保存されている...。

ID = 1 FullName = Michael Westine
ID = 2 FullName = Sam Axe
ID = 3 FullName = Fiona Glenanne

LINQPadでDBファイルを参照してみます。

f:id:piyo_esq:20200124231943p:plain
DBファイルの中身をLINQPadで参照した結果

デフォルトだとColumnがName_FirstName, Name_LastName, Name_FullNameになるっぽいですね。

おわり

Entity Framework Coreすごいなぁ...

以下余談ですが...

上記記載の通り.NET Application Architecture Guidanceとかいう気合入ったドキュメントかなりいい感じですね!

以上です。