Skip to main content

Command Palette

Search for a command to run...

EF Core: Mapping & Constraints via Data Annotations

Published
5 min read

Data Annotations are attribute-based configurations applied directly to entity classes and properties. They serve two purposes: defining the database schema (DDL) and providing runtime validation for ASP.NET Core.

Namespaces Required:

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

1. Table & Column Mapping

By default, EF Core maps classes to tables named after the DbSet property, and properties to columns of the same name. Annotations override this convention.

AttributeUsageDescription
[Table]Class LevelMaps the class to a specific database table and schema.
[Column]Property LevelMaps a property to a specific column name and SQL data type.

Example:

[Table("tbl_customers", Schema = "sales")]
public class Customer
{
    // Maps 'FullName' in C# to 'cust_name' column as VARCHAR(100)
    [Column("cust_name", TypeName = "varchar(100)")]
    public string FullName { get; set; }
}

2. Primary Keys & Identity Generation

EF Core automatically looks for Id or ClassNameId. Use annotations when your keys deviate from this standard.

Single Key & Identity

public class Order
{
    // 1. Marks this as the Primary Key
    [Key]

    // 2. Database generates value on INSERT (Auto-Increment / Identity)
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)] 
    public int OrderCode { get; set; }
}

Composite Keys (EF Core 7+)

Prior to EF Core 7, this required Fluent API. Now, you can do it via attributes on the class.

[PrimaryKey(nameof(StateCode), nameof(LicenseNumber))]
public class Car
{
    public string StateCode { get; set; }
    public string LicenseNumber { get; set; }
}

Generation Options (DatabaseGenerated)

  • Identity: DB generates value on Insert (Auto-increment).

  • Computed: DB generates value on Insert and Update (e.g., LastUpdated triggers).

  • None: User must supply the value (common for manually assigned GUIDs).


3. Data Constraints & Schema

These attributes restrict the data allowed in the database.

AttributeSQL EquivalentEffect
[Required]NOT NULLColumn cannot be null.
[MaxLength(N)]NVARCHAR(N)Restricts string/array length.
[StringLength(N)]NVARCHAR(N)Similar to MaxLength but allows minimum length validation (MinLength is API only).
[Precision(p, s)]DECIMAL(p, s)(EF Core 6+) Sets precision/scale for decimals.
[Unicode(false)]VARCHAR(EF Core 6+) Forces non-unicode (varchar) instead of nvarchar.

Example:

public class Product
{
    public int Id { get; set; }

    [Required]              // SQL: NOT NULL
    [StringLength(50)]      // SQL: NVARCHAR(50)
    [Unicode(false)]        // SQL: VARCHAR (instead of NVARCHAR)
    public string Sku { get; set; }

    [Precision(18, 4)]      // SQL: DECIMAL(18, 4)
    public decimal Price { get; set; }
}

4. Relationship Mapping

EF Core usually detects relationships automatically. You need annotations for explicit configuration or ambiguous relationships.

Foreign Keys

Use [ForeignKey] when the FK property name doesn't match the standard EntityId pattern.

public class Employee
{
    public int Id { get; set; }

    // The scalar value
    public int DeptCode { get; set; }

    // Links 'DeptCode' to the Department entity
    [ForeignKey(nameof(DeptCode))]
    public Department Department { get; set; }
}

Inverse Property (Ambiguous Relations)

Required when two entities have multiple relationships with each other (e.g., A Flight has a Departure Airport and an Arrival Airport).

public class Flight
{
    public int Id { get; set; }

    public Airport DepartureAirport { get; set; }
    public Airport ArrivalAirport { get; set; }
}

public class Airport
{
    public int Id { get; set; }

    // Tells EF: "This list maps to the DepartureAirport property in Flight"
    [InverseProperty(nameof(Flight.DepartureAirport))]
    public ICollection<Flight> DepartingFlights { get; set; }

    // Tells EF: "This list maps to the ArrivalAirport property in Flight"
    [InverseProperty(nameof(Flight.ArrivalAirport))]
    public ICollection<Flight> ArrivingFlights { get; set; }
}

5. Indexing & Concurrency

Database Indexes

Used to speed up lookups or enforce uniqueness.

// Creates an index on 'Email'. IsUnique adds a UNIQUE constraint.
[Index(nameof(Email), IsUnique = true)]
// Composite Index on FirstName + LastName
[Index(nameof(FirstName), nameof(LastName))]
public class User
{
    public int Id { get; set; }
    public string Email { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

Concurrency Tokens

Used to handle "Optimistic Concurrency" (detecting if someone else edited the record while you were looking at it).

public class BankAccount
{
    public int Id { get; set; }

    [Timestamp] // SQL: ROWVERSION (Auto-updates on every edit)
    public byte[] Version { get; set; }

    // OR apply to a specific column
    [ConcurrencyCheck]
    public decimal Balance { get; set; }
}

Excluding Properties

Use [NotMapped] for properties that exist in C# logic but should not exist in the database.

[NotMapped]
public string FullName => $"{FirstName} {LastName}";

6. Exceptions: What Annotations CANNOT Do

Data Annotations are limited. For the following scenarios, you must use the Fluent API (OnModelCreating).

  1. Delete Behaviors: You cannot specify ON DELETE CASCADE or ON DELETE SET NULL via attributes.

  2. Default Values: You cannot set a SQL DEFAULT constraint (e.g., DEFAULT GETDATE()).

  3. Check Constraints: You cannot add logical checks like CHECK (Age > 18).

  4. Many-to-Many Payload: Simple Many-to-Many works, but if the Join Table has extra columns (payload), you need Fluent API configuration.

  5. Fixed Length Strings: There is no specific attribute for CHAR(10). You must use the workaround [Column(TypeName="char(10)")].

  6. Sequence Configuration: You cannot define or attach custom database sequences (e.g., HiLo patterns) using attributes.

More from this blog

E

EF Core

31 posts