Engineering/Coding Standards/C#/Entity Framework Core/Ef Model Definition/

EF Core Model Definition Standards · EF-01

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

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

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

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 own examples.

Good Example: Getter-only collection navigation

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

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

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

[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.