Skip to content

Relationships

Entities don’t exist in isolation. An Order has LineItems. A Reservation belongs to a Guest. In a traditional EF Core project, you write navigation properties, foreign key properties, and Fluent API configuration for each relationship — all manually.

For a project with 50 entities and 80 relationships, this means hundreds of lines of configuration code that all follows the same pattern.

You declare relationships as attributes on the entity class (not on individual properties). The source generator produces the navigation properties, foreign key properties, and EF Core configuration.

// ═══ What YOU write ═══
[Entity<Guid>]
[BelongsTo<BillingBoundary>]
[Relation.OneToMany<LineItem>]
[Relation.ManyToOne<Customer>]
public partial class Order
{
public string OrderNumber { get; private set; } = "";
public decimal Total { get; private set; }
}

Important: [Relation.*] attributes go on the class declaration, not on a property. They tell the source generator “this entity has a relationship with that other entity.”

// ═══ What the SG generates ═══
// Navigation properties (Order.Relations.g.cs)
public partial class Order
{
public ICollection<LineItem> Items { get; } = []; // OneToMany → collection
public Guid CustomerId { get; private set; } // ManyToOne → FK property
public Customer? Customer { get; set; } // ManyToOne → navigation
}
// EF Core configuration (OrderEntityConfiguration.g.cs)
builder.HasMany(e => e.Items)
.WithOne()
.HasForeignKey(e => e.OrderId)
.OnDelete(DeleteBehavior.NoAction);
builder.HasOne(e => e.Customer)
.WithMany()
.HasForeignKey(e => e.CustomerId);

“An Order has many LineItems.” The parent entity declares the relationship.

[Entity<Guid>]
[Relation.OneToMany<LineItem>]
public partial class Order { /* ... */ }

What gets generated:

  • On Order: public ICollection<LineItem> Items { get; } = [];
  • On LineItem: public Guid OrderId { get; private set; } (FK back to parent)
  • EF Core config: HasMany / WithOne / HasForeignKey

“A LineItem belongs to one Order.” The child entity declares the relationship.

[Entity<Guid>]
[Relation.ManyToOne<Order>]
public partial class LineItem { /* ... */ }

What gets generated:

  • On LineItem: public Guid OrderId { get; private set; } (FK)
  • On LineItem: public Order? Order { get; set; } (navigation)
  • EF Core config: HasOne / WithMany / HasForeignKey

You can declare the relationship from either side — the result is the same FK and EF Core configuration.

Declare on…Use when…
Parent ([Relation.OneToMany<Child>])You think of the parent as “owning” the children
Child ([Relation.ManyToOne<Parent>])You think of the child as “belonging to” the parent
Both sidesYou want navigation properties on both entities

If you declare both, the SG detects they refer to the same relationship and generates a single FK + configuration.

“An Order can have many Tags, and a Tag can be on many Orders.”

Many-to-many requires an explicit join entity:

[Entity<Guid>]
[Relation.ManyToMany<Tag>(JoinEntity = typeof(OrderTag))]
public partial class Order { /* ... */ }
// The join entity
[Entity<Guid>]
[Relation.ManyToOne<Order>]
[Relation.ManyToOne<Tag>]
public partial class OrderTag { /* ... */ }

What gets generated:

  • On Order: public ICollection<Tag> Tags { get; } = [];
  • EF Core config: HasMany(e => e.Tags).WithMany().UsingEntity<OrderTag>(...)

By default, the SG generates FK properties (like OrderId on LineItem). But sometimes you need to declare them yourself — for example, when other source generators (like [MapFrom]) need to reference the FK at compile time.

[Entity<Guid>]
[Relation.ManyToOne<Reservation>]
[Relation.ManyToOne<Guest>]
public partial class Invoice
{
// FK kept in source so [MapFrom] DTOs can reference them
public Guid ReservationId { get; private set; }
public Guid GuestId { get; private set; }
}

When you declare a FK property that matches the expected convention ({NavigationType}Id), the SG detects it and uses your property instead of generating a new one. No duplication.


Entities from different boundaries can reference each other, but with limitations.

// Billing boundary
[Entity<Guid>]
[BelongsTo<BillingBoundary>]
[Relation.ManyToOne<Reservation>] // Reservation is in BookingBoundary
public partial class Invoice { /* ... */ }

What works: The FK property (ReservationId) is generated and stored in the database. You can query by FK.

What doesn’t work: EF Core Include() across boundaries. Since Invoice and Reservation live in different DbContexts, you can’t do query.Include(i => i.Reservation). The SG generates the FK but not the navigation property for cross-boundary relations.

If you need cross-boundary data, load it separately:

var invoice = await invoiceRepo.GetByIdAsync(invoiceId, ct);
var reservation = await reservationRepo.GetByIdAsync(invoice.ReservationId, ct);

By default, all generated relationships use OnDelete(DeleteBehavior.NoAction). This means:

  • Deleting a parent does not cascade to children in the database
  • You must handle child cleanup in your application logic (or use [SoftDelete(Cascade = true)])

This is intentional — cascade deletes in the database are dangerous and hard to debug.