0Pricing
C# Academy · Lekcja

Rekordy w projektowaniu sterowanym domeną

Modeluj obiekty wartości, DTO i zdarzenia domenowe jako rekordy, aby wymuszać niemutowalność i zwiększać czytelność kodu.

Rekordy w projektowaniu sterowanym domeną to bezpłatna lekcja C# Academy na CoddyKit. To lekcja 4 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej C# Academy, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs C# Academy zawiera 4 lekcji w sumie.

DDD i obiekty wartościowe

W Domain-Driven Design (DDD) obiekt wartościowy jest definiowany przez swoje właściwości, a nie przez tożsamość. Dwie wartości Money(100, "USD") są wzajemnie wymienne. Rekordy są idealnym sposobem reprezentowania obiektów wartościowych w C#.

Obiekty wartościowe jako rekordy

Obiekty wartościowe domeny należy modelować jako rekordy. Ich równość strukturalna, niezmienność i zwięzła składnia doskonale odpowiadają zasadom 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

Walidacja w konstruktorach obiektów wartościowych

Należy dodać konstruktor skrócony do sprawdzania niezmienników. Obiekty wartościowe powinno dać się utworzyć wyłącznie w prawidłowym stanie — nieprawidłowy stan powinien powodować wyjątek już podczas konstrukcji.

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

Encje a obiekty wartościowe

Encje mają tożsamość (śledzoną na przestrzeni czasu za pomocą identyfikatora), natomiast obiekty wartościowe nie mają tożsamości i są wzajemnie wymienne, jeśli ich wartości są takie same. W C# należy modelować je odmiennie.

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

Rozbudowane obiekty wartościowe z metodami domenowymi

Obiekty wartościowe mogą zawierać logikę domenową w postaci metod. Operacje zwracają nowe obiekty wartościowe zamiast modyfikować istniejący obiekt.

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

Zdarzenia domenowe jako rekordy

Zdarzenia domenowe opisują rzeczy, które wydarzyły się w domenie. Rekordy doskonale się do tego nadają: są niezmienne, opisowe i używają równości strukturalnej, co upraszcza testowanie.

// 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 i kontrakty API jako rekordy

DTO żądań i odpowiedzi są naturalnie obiektami wartościowymi — przenoszą dane, ale nie mają tożsamości. Rekordy sprawiają, że są zwięzłe i niezmienne.

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

Przechowywanie obiektów wartościowych w EF Core

Do utrwalania obiektów wartościowych w EF Core należy używać encji należących. Właściwości każdego obiektu wartościowego są przechowywane w tabeli właściciela (lub w osobnej tabeli), bez oddzielnego klucza głównego.

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

Główny agregat z obiektami wartościowymi

Połączmy wszystkie elementy: główny agregat Order używa rekordów dla obiektów wartościowych i zdarzeń domenowych, a wszystkie reguły biznesowe są egzekwowane w metodach.

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

Praktyczny przykład: identyfikatory bezpieczne typowo

Rekordy pozwalają opakować identyfikatory prymitywne i zapobiec przypadkowemu mieszaniu identyfikatorów różnych encji — jest to powszechna technika DDD nazywana "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!

Szybkie sprawdzenie

Co definiuje obiekt wartościowy w DDD i dlaczego rekordy naturalnie się do niego nadają?

Podsumowanie: rekordy w Domain-Driven Design

Najważniejsze wnioski:

  • Obiekty wartościowe: definiowane przez wartości, bez tożsamości — należy używać rekordów
  • Encje: mają tożsamość (identyfikator) i zmienny stan — należy używać klas
  • Niezmienniki należy sprawdzać w konstruktorach skróconych — nieprawidłowy stan nie może być reprezentowany
  • Zdarzenia domenowe jako rekordy: niezmienne, porównywalne i samodokumentujące
  • Encje należące EF Core służą do utrwalania obiektów wartościowych
  • Identyfikatory silnie typowane za pomocą rekordów zapobiegają mieszaniu identyfikatorów encji

Często zadawane pytania

Czy lekcja „Rekordy w projektowaniu sterowanym domeną” jest bezpłatna?

Tak — pełny tekst „Rekordy w projektowaniu sterowanym domeną” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu C# Academy, przejdź na CoddyKit PRO. Kurs C# Academy zawiera 4 lekcji w sumie.

Co nauczysz się w „Rekordy w projektowaniu sterowanym domeną”?

Modeluj obiekty wartości, DTO i zdarzenia domenowe jako rekordy, aby wymuszać niemutowalność i zwiększać czytelność kodu. Ćwiczysz C# Academy z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć C# Academy?

Nie wymagamy żadnego doświadczenia. C# Academy w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 4 z 4.

Ile czasu zajmuje lekcja „Rekordy w projektowaniu sterowanym domeną”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji C# Academy?

Tak. Każda lekcja C# Academy zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Typy record: podstawy i składnia
  2. Niemutowalność z init i with
  3. Równość wartości i dekonstrukcja
  4. Rekordy w projektowaniu sterowanym domeną
← Powrót do C# Academy