Relationships
The Problem
Section titled “The Problem”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.
The Solution: [Relation.*] Attributes
Section titled “The Solution: [Relation.*] Attributes”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);Relationship Types
Section titled “Relationship Types”One-to-Many
Section titled “One-to-Many”“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
Many-to-One
Section titled “Many-to-One”“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
When to use OneToMany vs ManyToOne
Section titled “When to use OneToMany vs ManyToOne”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 sides | You 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.
Many-to-Many
Section titled “Many-to-Many”“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>(...)
Explicitly Declaring FK Properties
Section titled “Explicitly Declaring FK Properties”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.
Cross-Boundary Relationships
Section titled “Cross-Boundary Relationships”Entities from different boundaries can reference each other, but with limitations.
// Billing boundary[Entity<Guid>][BelongsTo<BillingBoundary>][Relation.ManyToOne<Reservation>] // Reservation is in BookingBoundarypublic 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);Delete Behavior
Section titled “Delete Behavior”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.