C# Academy · Lektion

Records i domändriven design

Modellera värdeobjekt, DTO:er och domänhändelser som records för att framtvinga oföränderlighet och göra koden tydligare.

Lektion 4 av 412 steg

Records i domändriven design är en gratis lektion i C# Academy på CoddyKit. Detta är lektion 4 av 4. Ni kan läsa hela lektionen gratis nedan och sedan öva praktiskt i webbläsaren med en inbyggd kodredigerare och en AI-handledare som är tillgänglig dygnet runt. Den ingår i lärvägen för C# Academy, och Era framsteg synkroniseras mellan webben och CoddyKit-appen. Kursen i C# Academy innehåller totalt 4 lektioner.

DDD och värdeobjekt

I Domain-Driven Design (DDD) definieras ett värdeobjekt av sina egenskaper, inte av sin identitet. Två Money(100, "USD")-värden är utbytbara. Records är den perfekta C#-representationen för värdeobjekt.

Värdeobjekt som records

Modellera domänens värdeobjekt som records. Deras strukturella likhet, oföränderlighet och koncisa syntax överensstämmer perfekt med DDD-principerna.

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

Validering i värdeobjektens konstruktorer

Lägg till en kompakt konstruktor för att validera invarianter. Värdeobjekt ska bara kunna skapas i ett giltigt tillstånd — ett ogiltigt tillstånd ska orsaka ett undantag vid skapandet.

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

Entiteter kontra värdeobjekt

Entiteter har en identitet (spåras med ID över tid); värdeobjekt har ingen identitet och är utbytbara om värdena överensstämmer. Modellera dem på olika sätt i 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

Rika värdeobjekt med domänmetoder

Värdeobjekt kan innehålla domänlogik i form av metoder. Operationer returnerar nya värdeobjekt i stället för att ändra det befintliga.

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

Domänhändelser som records

Domänhändelser beskriver sådant som har inträffat i domänen. Records passar perfekt: de är oföränderliga och beskrivande och använder strukturell likhet, vilket förenklar testning.

// 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:er och API-kontrakt som records

DTO:er för förfrågningar och svar är naturligt värdeobjekt — de innehåller data utan identitet. Records gör dem kortfattade och oföränderliga.

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

Lagring av värdeobjekt i EF Core

Använd ägda entiteter för att spara värdeobjekt i EF Core. Egenskaperna i varje värdeobjekt lagras i ägarens tabell (eller i en separat tabell) utan en separat primärnyckel.

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

Aggregatrot med värdeobjekt

Sammanför allt: en Order-aggregatrot som använder records för värdeobjekt och domänhändelser, med alla affärsregler implementerade i metoder.

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

Verklighetsnära: typsäkra ID:n

Använd records för att omsluta primitiva ID:n och förhindra att olika entitets-ID:n blandas ihop av misstag — en vanlig DDD-teknik som kallas "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!

Snabbtest

Vad definierar ett DDD-värdeobjekt, och varför passar records naturligt för detta?

Repetition: records i domändriven design

Viktiga slutsatser:

  • Värdeobjekt: definieras av värden, saknar identitet — använd records
  • Entiteter: har identitet (ID) och föränderligt tillstånd — använd klasser
  • Validera invarianter i kompakta konstruktorer — ett ogiltigt tillstånd kan inte representeras
  • Domänhändelser som records: oföränderliga, jämförbara och självbeskrivande
  • Ägda entiteter i EF Core för att spara värdeobjekt
  • Starkt typade ID:n med records förhindrar att entitets-ID:n blandas ihop
Gratis att börja

Lär dig C# med en AI-lärare – gratis

Skriv och kör riktig kod i webbläsaren, få omedelbar hjälp av en AI-lärare dygnet runt och fortsätt där du slutade – på webben eller i appen.

Kurser
93
Lektioner
346

Vanliga frågor

Är lektionen ”Records i domändriven design” gratis?

Ja – hela texten till ”Records i domändriven design” kan läsas gratis här på webben. Om Ni vill öva interaktivt med en inbyggd kodredigerare och en AI-handledare som är tillgänglig dygnet runt och låsa upp resten av kursen i C# Academy, kan Ni uppgradera till CoddyKit PRO. Kursen i C# Academy innehåller totalt 4 lektioner.

Vad lär jag mig i ”Records i domändriven design”?

Modellera värdeobjekt, DTO:er och domänhändelser som records för att framtvinga oföränderlighet och göra koden tydligare. Ni övar på C# Academy med praktisk kod som körs direkt i webbläsaren, medan en AI-handledare som är tillgänglig dygnet runt svarar på Era frågor under lektionen.

Behöver jag någon erfarenhet för att börja lära mig C# Academy?

Du behöver inga förkunskaper. Utbildningen i C# Academy på CoddyKit är upplagd för allt från nybörjare till avancerade elever, så att du kan börja här eller från början och gå fram i din egen takt. Detta är lektion 4 av 4.

Hur lång tid tar lektionen ”Records i domändriven design”?

De flesta CoddyKit-lektioner tar cirka 5–10 minuter. Varje lektion är kort och interaktiv, så att du gör stadiga framsteg och kan fortsätta precis där du slutade – på webben eller i appen.

Kan jag skriva och köra kod i den här C# Academy-lektionen?

Ja. Varje C# Academy-lektion innehåller en inbyggd kodredigerare, så att du kan skriva och köra riktig kod direkt i webbläsaren och få omedelbar AI-feedback – utan lokal installation.

Alla lektioner i den här kursen

  1. Record-typer: grunder och syntax
  2. Oföränderlighet med init och with
  3. Värdelikhet och dekomponering
  4. Records i domändriven design
← Tillbaka till C# Academy