EF Core: Mapping & Constraints via Data Annotations
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.
| Attribute | Usage | Description |
[Table] | Class Level | Maps the class to a specific database table and schema. |
[Column] | Property Level | Maps 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.,LastUpdatedtriggers).None: User must supply the value (common for manually assigned GUIDs).
3. Data Constraints & Schema
These attributes restrict the data allowed in the database.
| Attribute | SQL Equivalent | Effect |
[Required] | NOT NULL | Column 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).
Delete Behaviors: You cannot specify
ON DELETE CASCADEorON DELETE SET NULLvia attributes.Default Values: You cannot set a SQL
DEFAULTconstraint (e.g.,DEFAULT GETDATE()).Check Constraints: You cannot add logical checks like
CHECK (Age > 18).Many-to-Many Payload: Simple Many-to-Many works, but if the Join Table has extra columns (payload), you need Fluent API configuration.
Fixed Length Strings: There is no specific attribute for
CHAR(10). You must use the workaround[Column(TypeName="char(10)")].Sequence Configuration: You cannot define or attach custom database sequences (e.g.,
HiLopatterns) using attributes.

