Skip to content

Relationships Interview Questions & Answers

15 questions Updated 2026-06-23 Share:

EF Core relationships interview questions — one-to-many, many-to-many, cascade delete, owned entities, shadow properties, and concurrency tokens.

Read the in-depth guideConfiguring Relationships in EF Core(opens in new tab)
15 of 15

A one-to-many relationship has one principal entity linked to many dependent entities. EF Core can infer it from navigation properties and FK naming conventions, or you can configure it explicitly with Fluent API.

// Domain model:
public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
    public ICollection<Order> Orders { get; set; } = new List<Order>(); // navigation
}

public class Order
{
    public int Id { get; set; }
    public int CustomerId { get; set; }     // FK property (conventional name → inferred)
    public Customer Customer { get; set; } = null!; // reference navigation
    public decimal Total { get; set; }
}

// EF Core convention: Finds CustomerId FK automatically — no Fluent API needed.

// Explicit Fluent API (preferred for clarity and complex cases):
public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
    public void Configure(EntityTypeBuilder<Order> b)
    {
        b.HasOne(o => o.Customer)           // Order has one Customer
         .WithMany(c => c.Orders)           // Customer has many Orders
         .HasForeignKey(o => o.CustomerId)  // FK column
         .IsRequired()                      // NOT NULL in DB
         .OnDelete(DeleteBehavior.Restrict); // don't cascade
    }
}

// Querying:
var customerWithOrders = await _db.Customers
    .Include(c => c.Orders)
    .FirstOrDefaultAsync(c => c.Id == 1);

Rule of thumb: Declare both the FK property (CustomerId) and the navigation (Customer) on the dependent. Explicit Fluent API beats convention for any non-trivial relationship — clarity is worth the extra lines.

EF Core 5+ supports implicit many-to-many — no join entity class required. EF Core 3 required an explicit join table entity with two one-to-many relationships.

// EF Core 5+ — implicit many-to-many (no join entity):
public class Post
{
    public int Id { get; set; }
    public ICollection<Tag> Tags { get; set; } = new List<Tag>(); // navigation
}

public class Tag
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
    public ICollection<Post> Posts { get; set; } = new List<Post>(); // navigation
}

// EF Core infers a join table "PostTag" (PostsId, TagsId) automatically.
// Fluent API to customise the join table name and columns:
modelBuilder.Entity<Post>()
    .HasMany(p => p.Tags)
    .WithMany(t => t.Posts)
    .UsingEntity(j => j.ToTable("PostTags")); // rename join table

// Explicit join entity (required when join has extra payload columns):
public class PostTag
{
    public int PostId { get; set; }
    public int TagId  { get; set; }
    public DateTime TaggedAt { get; set; } // extra column on the join
    public Post Post { get; set; } = null!;
    public Tag  Tag  { get; set; } = null!;
}

modelBuilder.Entity<PostTag>()
    .HasKey(pt => new { pt.PostId, pt.TagId }); // composite PK

modelBuilder.Entity<PostTag>()
    .HasOne(pt => pt.Post).WithMany(p => p.PostTags).HasForeignKey(pt => pt.PostId);

modelBuilder.Entity<PostTag>()
    .HasOne(pt => pt.Tag).WithMany(t => t.PostTags).HasForeignKey(pt => pt.TagId);

Rule of thumb: Use implicit many-to-many (EF Core 5+) when the join has no extra columns. Use an explicit join entity as soon as you need additional payload on the join (timestamps, flags, ordering).

A one-to-one relationship links exactly one principal to at most one dependent. EF Core requires you to specify which end holds the FK because it can't infer it when both navigations are present.

// Domain model:
public class User
{
    public int Id { get; set; }
    public string Email { get; set; } = "";
    public UserProfile? Profile { get; set; } // optional navigation to dependent
}

public class UserProfile
{
    public int Id { get; set; }
    public int UserId { get; set; }              // FK lives on the dependent
    public User User { get; set; } = null!;
    public string Bio { get; set; } = "";
}

// Fluent API — must declare which side has the FK:
public class UserProfileConfiguration : IEntityTypeConfiguration<UserProfile>
{
    public void Configure(EntityTypeBuilder<UserProfile> b)
    {
        b.HasOne(p => p.User)           // UserProfile has one User
         .WithOne(u => u.Profile)       // User has one Profile
         .HasForeignKey<UserProfile>(p => p.UserId) // FK on dependent side
         .IsRequired()
         .OnDelete(DeleteBehavior.Cascade); // delete profile when user deleted

        b.HasIndex(p => p.UserId).IsUnique(); // enforce one-to-one at DB level
    }
}

// Querying:
var user = await _db.Users
    .Include(u => u.Profile)
    .FirstOrDefaultAsync(u => u.Id == userId);

Rule of thumb: Always place the FK on the dependent (the entity that can't exist without the other). Add a unique index on the FK column in the database — EF doesn't add it automatically for one-to-one.

Navigation properties are class members that reference related entity objects. EF Core uses them to build SQL JOINs and maintain the object graph.

public class Order
{
    public int Id { get; set; }
    public int CustomerId { get; set; }

    // Reference navigation — points to a single related entity:
    public Customer Customer { get; set; } = null!;

    // Collection navigation — points to multiple related entities:
    public ICollection<OrderItem> Items { get; set; } = new List<OrderItem>();
}

public class OrderItem
{
    public int Id { get; set; }
    public int OrderId { get; set; }
    public int ProductId { get; set; }

    // Inverse reference navigation (back to the principal):
    public Order Order { get; set; } = null!;

    // Second reference navigation:
    public Product Product { get; set; } = null!;
    public int Quantity { get; set; }
}

// Always initialise collection navigations to avoid null checks:
public ICollection<OrderItem> Items { get; set; } = new List<OrderItem>();
// Or in constructor: Items = new HashSet<OrderItem>();

// Reference navigations should be nullable (not always loaded):
public Customer? Customer { get; set; }

// Check if a navigation is loaded:
bool isLoaded = _db.Entry(order).Reference(o => o.Customer).IsLoaded;
bool collLoaded = _db.Entry(order).Collection(o => o.Items).IsLoaded;

// Explicitly load a navigation on demand:
await _db.Entry(order).Reference(o => o.Customer).LoadAsync();
await _db.Entry(order).Collection(o => o.Items).LoadAsync();

Rule of thumb: Initialise collection navigations to empty collections (new List<T>()) to prevent NullReferenceException. Make reference navigations nullable to signal they might not be loaded.

Cascade delete automatically deletes dependent rows when the principal is deleted. EF Core exposes four DeleteBehavior options with different safety profiles.

// DeleteBehavior options:
// Cascade    — DB deletes dependents automatically (or EF tracks and deletes them)
// Restrict   — throws if dependents exist — safest for accidental deletes
// SetNull    — sets the FK to NULL on dependents (FK must be nullable)
// NoAction   — no action on DB delete; the application must manage orphans

public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
    public void Configure(EntityTypeBuilder<Order> b)
    {
        // Cascade — deleting Customer deletes all their Orders:
        b.HasOne(o => o.Customer)
         .WithMany(c => c.Orders)
         .HasForeignKey(o => o.CustomerId)
         .OnDelete(DeleteBehavior.Cascade);

        // Restrict — can't delete a Customer with existing Orders:
        b.HasOne(o => o.Customer)
         .WithMany(c => c.Orders)
         .HasForeignKey(o => o.CustomerId)
         .OnDelete(DeleteBehavior.Restrict);

        // SetNull — deleting Supplier sets Order.SupplierId to NULL:
        b.HasOne(o => o.Supplier)
         .WithMany(s => s.Orders)
         .HasForeignKey(o => o.SupplierId)   // SupplierId is int? (nullable)
         .OnDelete(DeleteBehavior.SetNull);
    }
}

// EF Core's default: Cascade for required relationships, SetNull for optional.
// SQL Server default: NO ACTION (needs EF to cascade explicitly).

// Soft delete avoids all cascade concerns:
order.IsDeleted = true;
await _db.SaveChangesAsync(); // no FK violations

Rule of thumb: Prefer Restrict for business-critical data (orders, invoices) to prevent accidental mass deletes. Use Cascade only for true parent-child where the child has no meaning without the parent (e.g., order → order items).

Owned entities are value-object-like types that belong exclusively to one owner entity. They share the owner's table by default (no separate Id column) and have no independent lifecycle.

// Domain model — Address is a value object:
public class Address
{
    public string Street { get; set; } = "";
    public string City   { get; set; } = "";
    public string Zip    { get; set; } = "";
    // No Id — Address is owned by Customer
}

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
    public Address ShippingAddress { get; set; } = new();
    public Address BillingAddress  { get; set; } = new();
}

// Configuration:
modelBuilder.Entity<Customer>()
    .OwnsOne(c => c.ShippingAddress, addr =>
    {
        addr.Property(a => a.Street).HasColumnName("Ship_Street").HasMaxLength(100);
        addr.Property(a => a.City).HasColumnName("Ship_City");
        addr.Property(a => a.Zip).HasColumnName("Ship_Zip");
    });

modelBuilder.Entity<Customer>()
    .OwnsOne(c => c.BillingAddress, addr =>
    {
        addr.Property(a => a.Street).HasColumnName("Bill_Street");
        // ...
    });
// Schema: Customers table contains Ship_Street, Ship_City, ... Bill_Street, etc.

// Store as JSON column (EF Core 7+):
modelBuilder.Entity<Customer>()
    .OwnsOne(c => c.ShippingAddress, addr => addr.ToJson());
// Customers.ShippingAddress JSON column: {"Street":"...","City":"...","Zip":"..."}

Rule of thumb: Use OwnsOne / OwnsMany for value objects that belong to one entity and have no independent identity. This maps cleanly to DDD value objects and keeps the schema tidy.

A self-referencing relationship points from an entity back to itself — used for trees and hierarchies (categories, organisational charts, comments with replies).

// Category tree — each category optionally has a parent:
public class Category
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
    public int? ParentId { get; set; }                // nullable FK — root has no parent
    public Category? Parent { get; set; }             // reference navigation (up)
    public ICollection<Category> Children { get; set; } = new List<Category>(); // down
}

// Configuration — works by convention, but explicit is clearer:
modelBuilder.Entity<Category>()
    .HasOne(c => c.Parent)
    .WithMany(c => c.Children)
    .HasForeignKey(c => c.ParentId)
    .IsRequired(false)              // nullable FK — root nodes have null ParentId
    .OnDelete(DeleteBehavior.Restrict); // prevent accidental tree deletion

// Load the full tree with recursive includes (small trees only):
var root = await _db.Categories
    .Include(c => c.Children)
        .ThenInclude(c => c.Children)    // 3 levels deep
    .Where(c => c.ParentId == null)      // root nodes
    .ToListAsync();

// For deep trees, load all and build the hierarchy in memory:
var all = await _db.Categories.AsNoTracking().ToListAsync();
var roots = all.Where(c => c.ParentId == null).ToList();
var byParent = all.Where(c => c.ParentId != null)
                  .GroupBy(c => c.ParentId!.Value)
                  .ToDictionary(g => g.Key, g => g.ToList());

Rule of thumb: Keep self-referencing relationships in relational databases for shallow trees (≤ 3–4 levels). For very deep or often-queried trees, consider a closure table or path enumeration strategy to avoid recursive queries.

A primary key (HasKey) is the entity's identity — one per entity. An alternate key (HasAlternateKey) is an additional unique column that other entities can reference as a foreign key.

public class Product
{
    public int Id { get; set; }       // PK
    public string Sku { get; set; } = ""; // alternate key — unique, referenced externally
    public string Name { get; set; } = "";
}

public class OrderItem
{
    public int Id { get; set; }
    public string ProductSku { get; set; } = ""; // FK pointing to Product.Sku (not PK)
    public Product Product { get; set; } = null!;
}

// Configuration:
modelBuilder.Entity<Product>()
    .HasAlternateKey(p => p.Sku); // creates UNIQUE constraint on Sku

modelBuilder.Entity<OrderItem>()
    .HasOne(i => i.Product)
    .WithMany()
    .HasForeignKey(i => i.ProductSku)
    .HasPrincipalKey(p => p.Sku); // FK targets the alternate key, not PK

// Composite alternate key:
modelBuilder.Entity<Product>()
    .HasAlternateKey(p => new { p.Brand, p.ModelNumber });

Alternate keys differ from HasIndex(...).IsUnique():

  • Alternate keys can be referenced by FKs; unique indexes cannot.
  • Both add a UNIQUE constraint to the schema.

Rule of thumb: Use alternate keys when an external system references your entity by a natural key (SKU, ISBN, slug). Use unique indexes for uniqueness constraints that don't need to be referenced by FKs.

EF Core supports three strategies for mapping class hierarchies to relational tables. The strategy affects schema shape, query performance, and null handling.

// Base entity:
public abstract class Payment
{
    public int Id { get; set; }
    public decimal Amount { get; set; }
}
public class CardPayment : Payment { public string Last4 { get; set; } = ""; }
public class BankPayment : Payment { public string IBAN  { get; set; } = ""; }

// === 1. Table-Per-Hierarchy (TPH) — EF Core default ===
// One table, Discriminator column per row:
// Payments: Id | Amount | Discriminator | Last4 | IBAN
modelBuilder.Entity<Payment>()
    .HasDiscriminator<string>("Discriminator")
    .HasValue<CardPayment>("Card")
    .HasValue<BankPayment>("Bank");
// Pro: no joins; Con: sparse nullable columns

// === 2. Table-Per-Type (TPT) — each type has its own table ===
modelBuilder.Entity<CardPayment>().ToTable("CardPayments"); // separate table
modelBuilder.Entity<BankPayment>().ToTable("BankPayments");
// Pro: normalised, no nulls; Con: JOIN on every query

// === 3. Table-Per-Concrete-Type (TPC) — EF Core 7+ ===
modelBuilder.Entity<CardPayment>().UseTpcMappingStrategy();
modelBuilder.Entity<BankPayment>().UseTpcMappingStrategy();
// Each concrete type gets a full table with all columns (no FK to base table)
// Pro: no joins, normalised; Con: queries over base type need UNION ALL

// Querying polymorphically works the same regardless of strategy:
var allPayments = await _db.Set<Payment>().ToListAsync();
var cardPayments = await _db.Set<CardPayment>().ToListAsync();

Rule of thumb: Start with TPH — it's the default, simplest, and fastest for small hierarchies. Use TPT when nullable columns become a schema problem. Use TPC (EF Core 7+) for separate tables with no JOINs on concrete-type queries.

Shadow properties exist in the EF Core model and database schema but are NOT present on the entity class. They're useful for auditing, tenancy, and soft-delete fields you don't want to expose in the domain model.

// Define a shadow property in OnModelCreating:
modelBuilder.Entity<Order>()
    .Property<DateTime>("CreatedAt")   // no matching property on Order class
    .HasDefaultValueSql("GETUTCDATE()");

modelBuilder.Entity<Order>()
    .Property<string>("CreatedBy")
    .HasMaxLength(256);

// Read and write shadow properties via Entry:
var order = new Order { Total = 99.99m };

_db.Entry(order).Property("CreatedBy").CurrentValue = "alice@example.com";
_db.Orders.Add(order);
await _db.SaveChangesAsync();

// Read shadow property from a tracked entity:
var createdAt = (DateTime)_db.Entry(order).Property("CreatedAt").CurrentValue!;

// Filter by a shadow property in a query:
var aliceOrders = await _db.Orders
    .Where(o => EF.Property<string>(o, "CreatedBy") == "alice@example.com")
    .ToListAsync();

// Use in SaveChangesInterceptor for automatic auditing:
foreach (var entry in db.ChangeTracker.Entries<Order>())
{
    if (entry.State == EntityState.Added)
        entry.Property("CreatedAt").CurrentValue = DateTime.UtcNow;
}

Rule of thumb: Use shadow properties for cross-cutting metadata (audit timestamps, tenant ID, row version) that shouldn't pollute the domain model. For shared auditing across many entities, combine shadow properties with a SaveChangesInterceptor.

Table splitting maps multiple entity types to a single database table. It's the inverse of owned entities — useful when you want to load a "lightweight" view of a table by default and load the heavy columns separately on demand.

// Split a table into a lightweight head and a heavy detail entity:
public class OrderSummary
{
    public int Id { get; set; }
    public string Status { get; set; } = "";
    public decimal Total { get; set; }
    public OrderDetail Detail { get; set; } = null!; // navigation to same table
}

public class OrderDetail
{
    public int Id { get; set; }         // same PK — shared with OrderSummary
    public string Notes { get; set; } = "";
    public byte[]? AttachmentData { get; set; } // large column — only loaded when needed
    public OrderSummary Summary { get; set; } = null!;
}

// Map both entities to the same table:
modelBuilder.Entity<OrderSummary>().ToTable("Orders");
modelBuilder.Entity<OrderDetail>().ToTable("Orders");

// Configure the 1:1 relationship between the two entity "halves":
modelBuilder.Entity<OrderSummary>()
    .HasOne(s => s.Detail)
    .WithOne(d => d.Summary)
    .HasForeignKey<OrderDetail>(d => d.Id); // same PK — required for table splitting

// Query the lightweight summary — no blob loaded:
var summaries = await _db.Set<OrderSummary>()
    .AsNoTracking()
    .Where(s => s.Status == "Pending")
    .ToListAsync();  // SELECT Id, Status, Total FROM Orders WHERE Status='Pending'

// Load the heavy detail only when needed:
var detail = await _db.Set<OrderDetail>()
    .AsNoTracking()
    .FirstOrDefaultAsync(d => d.Id == orderId);  // includes Notes and AttachmentData

When to use table splitting:

  • A table has large or rarely-needed columns (BLOBs, long text) you want to defer.
  • You can't or don't want to physically split the table in the schema.
  • You want a strongly-typed "projection" at the entity level.

Rule of thumb: Use table splitting when a table has a natural head/detail split and the detail columns are large and infrequently needed. For most cases, projection via Select into a DTO is simpler and doesn't require a second entity class.

Optimistic concurrency assumes conflicts are rare and detects them at save time rather than locking rows. EF Core supports it via concurrency tokens (a column included in WHERE clauses on UPDATE).

// Option 1 — RowVersion (SQL Server rowversion / timestamp):
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
    public int Stock { get; set; }

    [Timestamp]                    // maps to SQL Server rowversion column
    public byte[] RowVersion { get; set; } = Array.Empty<byte>();
}

// EF generates: UPDATE Products SET Stock=@p0 WHERE Id=@id AND RowVersion=@version
// If RowVersion changed (another writer), 0 rows affected → EF throws DbUpdateConcurrencyException

// Option 2 — Concurrency token (works with any column):
public class Product
{
    [ConcurrencyCheck]
    public int Stock { get; set; }
}
// EF adds: AND Stock=@originalStock to the WHERE clause

// Handle the exception:
try
{
    await _db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException ex)
{
    foreach (var entry in ex.Entries)
    {
        var proposed = entry.CurrentValues;    // what we tried to write
        var dbValues  = await entry.GetDatabaseValuesAsync(); // what's in DB now

        // Option A — keep proposed values (last writer wins):
        entry.OriginalValues.SetValues(dbValues!);

        // Option B — reload DB values (first writer wins):
        await entry.ReloadAsync();

        // Option C — merge/alert user
    }
    await _db.SaveChangesAsync(); // retry after resolving
}

Rule of thumb: Use [Timestamp] / rowversion on SQL Server for row-level optimistic concurrency — it's automatic and requires no application logic to update. Handle DbUpdateConcurrencyException explicitly in high-contention paths.

Yes. EF Core lets you define foreign keys and relationships without navigation properties — useful when you want to enforce referential integrity at the database level without polluting the domain model with references to unrelated aggregates.

// Domain model — Order references CustomerId but has no Customer navigation:
public class Order
{
    public int Id { get; set; }
    public int CustomerId { get; set; } // FK column only, no navigation property
    public decimal Total { get; set; }
}

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
    // No Orders collection navigation — Order is in a different bounded context
}

// Configuration — relationship with no navigation properties:
modelBuilder.Entity<Order>()
    .HasOne<Customer>()             // no lambda — no navigation on Order
    .WithMany()                     // no lambda — no collection on Customer
    .HasForeignKey(o => o.CustomerId)
    .IsRequired()
    .OnDelete(DeleteBehavior.Restrict);

// EF Core still creates the FK constraint in the schema:
// ALTER TABLE Orders ADD CONSTRAINT FK_Orders_Customers_CustomerId
//   FOREIGN KEY (CustomerId) REFERENCES Customers(Id)

// Query using explicit join instead of navigation:
var report = await _db.Orders
    .Join(_db.Set<Customer>(),
          o => o.CustomerId,
          c => c.Id,
          (o, c) => new { c.Name, o.Total })
    .ToListAsync();

Rule of thumb: Omit navigation properties across aggregate boundaries in DDD designs. Configure the FK with HasOne<T>().WithMany() (no lambda) to enforce referential integrity in the schema while keeping aggregates decoupled in code.

Skip navigations are the collection properties that link two entities directly across a join table, without exposing the join entity in the domain model. They are the navigations EF Core creates for implicit many-to-many relationships (EF Core 5+).

// Implicit many-to-many — skip navigations on both sides:
public class Student
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
    public ICollection<Course> Courses { get; set; } = new List<Course>(); // skip nav
}

public class Course
{
    public int Id { get; set; }
    public string Title { get; set; } = "";
    public ICollection<Student> Students { get; set; } = new List<Student>(); // skip nav
}

// EF creates a hidden join table (StudentCourse) with StudentId and CourseId FKs.

// Add to a many-to-many relationship via the skip navigation:
var student = await _db.Students.Include(s => s.Courses).FirstAsync(s => s.Id == 1);
var course  = await _db.Courses.FindAsync(10);
student.Courses.Add(course!);          // inserts into StudentCourse join table
await _db.SaveChangesAsync();

// Remove from the relationship:
student.Courses.Remove(course!);
await _db.SaveChangesAsync();          // deletes from StudentCourse

// Direct query through the skip navigation:
var studentsInCourse = await _db.Courses
    .Where(c => c.Id == 10)
    .SelectMany(c => c.Students)       // expands through the join table
    .AsNoTracking()
    .ToListAsync();
// SQL: SELECT s.* FROM Students s
//      INNER JOIN StudentCourse sc ON sc.StudentsId = s.Id
//      WHERE sc.CoursesId = 10

// Inspect the underlying join entity type (EF manages it internally):
var joinEntity = modelBuilder.Entity<Student>()
    .HasMany(s => s.Courses)
    .WithMany(c => c.Students)
    .UsingEntity(j => j.ToTable("StudentCourse")); // rename the join table

Rule of thumb: Use skip navigations (implicit many-to-many) when the join table is a pure relationship with no extra columns. The moment you need metadata on the join (e.g., EnrolledAt, Grade), switch to an explicit join entity.

A required relationship means the dependent entity must have a principal — the foreign key is NOT NULL. An optional relationship allows the FK to be NULL, meaning the dependent can exist without a principal.

// Required relationship — FK is NOT NULL:
public class Order
{
    public int Id { get; set; }
    public int CustomerId { get; set; }       // non-nullable int → required
    public Customer Customer { get; set; } = null!;
}

// Fluent API — explicit required:
modelBuilder.Entity<Order>()
    .HasOne(o => o.Customer)
    .WithMany(c => c.Orders)
    .HasForeignKey(o => o.CustomerId)
    .IsRequired();                            // NOT NULL in schema
// EF default for non-nullable FK types: already required by convention.

// Optional relationship — FK is nullable:
public class Order
{
    public int Id { get; set; }
    public int? SupplierId { get; set; }      // nullable int → optional
    public Supplier? Supplier { get; set; }   // nullable reference navigation
}

// Fluent API — explicit optional:
modelBuilder.Entity<Order>()
    .HasOne(o => o.Supplier)
    .WithMany(s => s.Orders)
    .HasForeignKey(o => o.SupplierId)
    .IsRequired(false);                       // NULL allowed in schema

// Cascade delete default differs:
// Required → EF default is Cascade (delete dependents when principal deleted)
// Optional → EF default is SetNull (null out the FK when principal deleted)

// Practical consequence for loading:
var order = await _db.Orders.Include(o => o.Supplier).FirstAsync(o => o.Id == 1);
if (order.Supplier is not null) // must null-check optional navigation
    Console.WriteLine(order.Supplier.Name);

Rule of thumb: Make a relationship required when the dependent has no business meaning without the principal (order must have a customer). Make it optional when the principal can legitimately be absent (order might not have a supplier yet). Reflect this in the FK nullability and reference navigation nullability in C#.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel