# EF Core Model Definition Standards · EF-01

## Standard Metadata

- Area: ORM
- Owner: Backend Steering group
- Last reviewed: 2026-09-09
- Next review due from: 2026-12-01
- Enforcement: CodeAnalysis / Rosyln Analyser / code review
- Status: Active

## Use navigation properties to represent relationships · EF-01.1 · **MUST**

When creating entities which map to other entities within the same domain of the codebase, navigation properties must be used.

### Navigation Property Example

```cs
public class Book
{
    public Guid Id { get; set; }

    public Guid AuthorId { get; set; }

    public required Author Author { get; set; }
}

public class Author
{
    public Guid Id { get; set; }

    public required string Name { get; set; }

    public ICollection<Book> Books { get; } = new List<Book>();
}
```

### **EXCEPTION**: Scoping boundaries

The exception applies to entity references which are outside the boundaries of your system, or (depending on architecture) feature area.

#### Scoping Boundary Exception Example

When storing `TenantId` or `ExternalApplicationUserId` within your application, these navigation properties cannot be defined because the full entity is not stored or managed by your application.

---

## Prefer non-nullable references for required properties · EF-01.2 · **MUST**

Utilise EF Core's ability to leverage the nullability of C# types to determine column nullability.

By convention, `string` becomes `NOT NULL` and `string?` becomes nullable.

### Good Example: Nullability expressed through the type system

```cs
public class Book
{
    public Guid Id { get; set; }

    public required string Title { get; set; }

    public required string Isbn { get; set; }

    public string? Subtitle { get; set; }

    public DateOnly? PublishedDate { get; set; }
}
```

### Bad Example: Nullability expressed through attributes

```cs
public class Book
{
    public Guid Id { get; set; }

    // Nullable despite being a required field, forcing null checks throughout the codebase and allowing NULLs into the database.
    public string? Title { get; set; }

    // Uses attribute which duplicates what the type system should express.
    [Required]
    public string Isbn { get; set; }
}
```

### **EXCEPTION**: Nullability cannot always be enabled

Enabling nullability on existing projects should not be done lightly and should be a planned action.

---

## Avoid exposing collection setters on entities · EF-01.3 · **MUST**

When defining collection navigation properties, use a getter `get;` only, and initialise them inline.

This safe-guards against the possibility of the caller overwriting the entire collection. And is used in [Microsoft's](https://learn.microsoft.com/en-us/ef/core/modeling/relationships/navigations#collection-navigations) own examples.

### Good Example: Getter-only collection navigation

```cs
public class Author
{
    public Guid Id { get; set; }

    public required string Name { get; set; }

    // Getter only, initialised inline
    public ICollection<Book> Books { get; } = new List<Book>();
}
```

### Bad Example: Exposed collection setter

```cs
public class Author
{
    public Guid Id { get; set; }

    public required string Name { get; set; }

    // Allows caller to replace the collection and silently discard any entities EF Core has populated.
    public ICollection<Book> Books { get; set; } = new List<Book>();
}
```

---

## Keep entities free of persistence and infrastructure concerns · EF-01.4 · **MUST**

Keep entities readable by limiting the class to the shape of the data and nothing else. Avoid using data annotations such as `[Table]`, `[Column]`, `[MaxLength]`, `[Index]`, and `[ForeignKey]` and encourage isolation of entity definition and configuration by using the Fluent API within `IEntityTypeConfiguration<T>` files.

### Good Example: Separation of entity definition and configuration

```cs
public class Book
{
    public Guid Id { get; set; }

    public required string Title { get; set; }

    public Guid AuthorId { get; set; }

    public required Author Author { get; set; }
}
```

```cs
public class BookConfiguration : IEntityTypeConfiguration<Book>
{
    public void Configure(EntityTypeBuilder<Book> builder)
    {
        builder.ToTable("Books");

        builder.HasKey(b => b.Id);

        builder.Property(b => b.Title)
            .HasMaxLength(200)
            .HasColumnName("BookTitle");

        builder.HasOne(b => b.Author)
            .WithMany(a => a.Books)
            .HasForeignKey(b => b.AuthorId);
    }
}
```

### Bad Example: Entity polluted with persistence concerns

```cs
[Table("Books")]
public class Book
{
    [Key]
    public Guid Id { get; set; }

    [Required]
    [MaxLength(200)]
    [Column("BookTitle")]
    public string Title { get; set; }

    [ForeignKey(nameof(Author))]
    public Guid AuthorId { get; set; }

    public Author Author { get; set; }
}
```

### **EXCEPTION**: Domain-Driven Development

When developing in a DDD project, it is encouraged to use private setters and only mutate property values through business-logic named methods which sit directly on the entity - [Microsoft](https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/net-core-microservice-domain-model#implement-domain-entities-as-poco-classes).
