ドメイン駆動設計におけるRecord
値オブジェクト、DTO、ドメインイベントをrecordとしてモデル化し、不変性を保証してコードの明瞭性を高めます。
「ドメイン駆動設計におけるRecord」はCoddyKit上の無料C# Academyレッスンです。 これはレッスン4/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはC# Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 C# Academyコースには全4レッスンが含まれています。
DDDと値オブジェクト
ドメイン駆動設計(DDD)では、値オブジェクトは同一性ではなくプロパティによって定義されます。Money(100, "USD") の2つの値は、互いに置き換えて使用できます。レコードは、値オブジェクトをC#で表現するのに最適です。
値オブジェクトとしてのレコード
ドメインの値オブジェクトはレコードとしてモデル化します。構造的な等価性、不変性、簡潔な構文は、DDDの原則に完全に適合します。
// Value objects — defined by their values, not identity
public record Money(decimal Amount, string Currency);
public record Email(string Value);
public record PhoneNumber(string CountryCode, string Number);
public record Address(string Street, string City, string Country, string PostalCode);
// Two Money objects with same values are equal:
var price = new Money(99.99m, "USD");
var same = new Money(99.99m, "USD");
Console.WriteLine(price == same); // True値オブジェクトのコンストラクターでの検証
コンパクトコンストラクターを追加して、不変条件を検証します。値オブジェクトは有効な状態でのみ作成できるようにし、無効な状態では構築時に例外をスローする必要があります。
public record Email(string Value)
{
// Compact constructor for validation
public Email : this(Value)
{
if (string.IsNullOrWhiteSpace(Value) || !Value.Contains('@'))
throw new ArgumentException("Invalid email address", nameof(Value));
Value = Value.Trim().ToLowerInvariant();
}
}
// Usage:
var email = new Email(" Alice@Example.COM "); // normalized to alice@example.com
new Email("not-an-email"); // throws ArgumentExceptionエンティティと値オブジェクトの違い
エンティティには同一性があり、時間をまたいでIDによって追跡されます。一方、値オブジェクトには同一性がなく、値が一致すれば互いに置き換えて使用できます。C#では、両者を異なる方法でモデル化します。
// ENTITY: has identity, mutable state
public class Customer
{
public Guid Id { get; } = Guid.NewGuid();
public string Name { get; set; } = "";
public Email Email { get; set; } = default!; // value object
}
// VALUE OBJECT: no identity, immutable
public record Email(string Value);
public record Money(decimal Amount, string Currency);
// Two customers with same name are DIFFERENT entities
// Two Money(100, "USD") are the SAME valueドメインメソッドを持つリッチな値オブジェクト
値オブジェクトには、メソッドとしてドメインロジックを含めることができます。操作では既存の値オブジェクトを変更せず、新しい値オブジェクトを返します。
public record Money(decimal Amount, string Currency)
{
public Money Add(Money other)
{
if (Currency != other.Currency)
throw new DomainException("Cannot add different currencies");
return this with { Amount = Amount + other.Amount };
}
public Money Subtract(Money other) =>
Amount >= other.Amount
? this with { Amount = Amount - other.Amount }
: throw new DomainException("Insufficient funds");
public Money ApplyDiscount(decimal percent) =>
this with { Amount = Amount * (1 - percent / 100) };
public static Money Zero(string currency) => new(0, currency);
}レコードとしてのドメインイベント
ドメインイベントは、ドメイン内で起きたことを表します。レコードは、不変で説明的なうえ、構造的な等価性を使用するためテストも簡単になり、最適な選択肢です。
// Domain events as records
public abstract record DomainEvent(DateTime OccurredAt);
public record OrderPlaced(
Guid OrderId,
Guid CustomerId,
Money Total,
DateTime OccurredAt) : DomainEvent(OccurredAt);
public record OrderShipped(
Guid OrderId,
string TrackingNumber,
DateTime OccurredAt) : DomainEvent(OccurredAt);
public record PaymentReceived(
Guid OrderId,
Money Amount,
DateTime OccurredAt) : DomainEvent(OccurredAt);レコードとしてのDTOとAPIコントラクト
リクエスト/レスポンスDTOは、同一性を持たずデータを運ぶだけなので、自然に値オブジェクトとして扱えます。レコードを使うと、簡潔で不変な型にできます。
// Request DTOs
public record CreateOrderCommand(
Guid CustomerId,
IReadOnlyList<OrderLineDto> Lines);
public record OrderLineDto(Guid ProductId, int Quantity);
// Response DTOs
public record OrderCreatedResponse(
Guid OrderId,
string Status,
decimal Total,
DateTime CreatedAt);
// These can be compared in tests by value:
var expected = new OrderCreatedResponse(id, "Pending", 99m, now);
Assert.Equal(expected, actual);EF Coreでの値オブジェクトの保存
EF Coreで値オブジェクトを永続化するには、所有エンティティを使用します。各値オブジェクトのプロパティは、個別のPKを持たずに、所有者のテーブル(または別のテーブル)に保存されます。
public class Order
{
public int Id { get; set; }
public Money Total { get; set; } = new(0, "USD");
public Address ShipTo { get; set; } = default!;
}
// EF Core config:
modelBuilder.Entity<Order>(e =>
{
e.OwnsOne(o => o.Total, money =>
{
money.Property(m => m.Amount).HasColumnType("decimal(18,2)");
money.Property(m => m.Currency).HasMaxLength(3);
});
e.OwnsOne(o => o.ShipTo);
});値オブジェクトを使った集約ルート
ここまでの内容をまとめ、値オブジェクトとドメインイベントにレコードを使用し、すべてのビジネスルールをメソッド内で適用する Order 集約ルートを作成します。
public class Order
{
private readonly List<DomainEvent> _events = new();
public IReadOnlyList<DomainEvent> DomainEvents => _events;
public Guid Id { get; } = Guid.NewGuid();
public Address ShipTo { get; private set; } = default!; // value obj
public Money Total { get; private set; } = Money.Zero("USD");
public void PlaceOrder(Address shipTo, IEnumerable<OrderLine> lines)
{
ShipTo = shipTo;
Total = lines.Aggregate(Money.Zero("USD"),
(acc, l) => acc.Add(l.Price));
_events.Add(new OrderPlaced(Id, CustomerId, Total, DateTime.UtcNow));
}
}実践例:型安全なID
レコードを使ってプリミティブなIDをラップし、異なるエンティティのIDを誤って混在させないようにします。これはDDDでよく使われる「Strongly Typed IDs」という手法です。
// Strongly typed IDs — can't confuse CustomerId with OrderId
public record CustomerId(Guid Value)
{
public static CustomerId New() => new(Guid.NewGuid());
public static implicit operator Guid(CustomerId id) => id.Value;
}
public record OrderId(Guid Value)
{
public static OrderId New() => new(Guid.NewGuid());
}
// Type safety:
void ShipOrder(OrderId orderId, CustomerId customerId) { }
ShipOrder(OrderId.New(), CustomerId.New()); // OK
ShipOrder(CustomerId.New(), OrderId.New()); // COMPILE ERROR!確認問題
DDDの値オブジェクトは何によって定義され、なぜレコードが自然な選択肢になるのでしょうか。
まとめ:ドメイン駆動設計におけるレコード
重要なポイント:
- 値オブジェクト:値によって定義され、同一性を持ちません — レコードを使用します
- エンティティ:同一性(ID)と可変な状態を持ちます — クラスを使用します
- コンパクトコンストラクターで不変条件を検証します — 無効な状態を表現できないようにします
- レコードとしてのドメインイベント:不変で比較可能、かつ自己説明的です
- 値オブジェクトの永続化には、EF Coreの所有エンティティを使用します
- レコードによるStrongly Typed IDsで、エンティティIDの混在を防ぎます
よくある質問
「ドメイン駆動設計におけるRecord」レッスンは無料ですか?
はい。「ドメイン駆動設計におけるRecord」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、C# Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 C# Academyコースには全4レッスンが含まれています。
「ドメイン駆動設計におけるRecord」で何を学びますか?
値オブジェクト、DTO、ドメインイベントをrecordとしてモデル化し、不変性を保証してコードの明瞭性を高めます。 ブラウザで直接実行するハンズオンコードでC# Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
C# Academyを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのC# Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン4/4です。
「ドメイン駆動設計におけるRecord」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このC# Academyレッスンでコードを書いて実行できますか?
はい。すべてのC# Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- Record型:基礎と構文
- initとwithによる不変性
- 値の等価性と分解
- ドメイン駆動設計におけるRecord