Data Annotations in EF Core

.NET provides a set of data annotation attributes that can be used to define constraints to the tables and the columns in the underlying database and override  Entity Framework Core conventions. The attributes that generally add constraints to the database columns and the tables are in System.ComponentModel.DataAnnotations namespace. They are the following. 

The attributes that affect the schema of the database are in System.ComponentModel.DataAnnotations.Schema namespace. The following are them.

Key

The key attribute can be added to a model property if it is to be used as a primary in the database. The key attribute is required only when no eligible properties are present in the entity class as the primary key by EF Core's convention. In the following example, the key attribute is required to make the EmpId property primary key.

    public class Employee
    {
        [Key]
        public long EmpId { get; set; }
        public string Name { get; set; }
        public string Address { get; set; }
    }

Required

Specifies that a property's value is required. Use this attribute if a database field's value can not be null.

 public class Employee
    {
        [Key]
        public long EmpId { get; set; }
        [Required]
        public string Name { get; set; }
        public string Address { get; set; }
    }

MaxLength

This can be used to specify the maximum length of the corresponding field in the database. In the following example, the length of the Address property is 250 characters. For SQL Server, this will create a nvarchar(250) field in the database. 

 public class Employee
    {
        [Key]
        public long EmpId { get; set; }
        [Required]
        public string Name { get; set; }
        [MaxLength(length:250)]
        public string Address { get; set; }
    }

Timestamp

This attribute is used to specify the data type of the corresponding database column as row version. It is useful to handle concurrency issues in the database when multiple users simultaneously update the same row. Timestamp attribute is valid for byte array properties only. 

public class Employee
    {
        [Key]
        public long EmpId { get; set; }
        [Required]
        public string Name { get; set; }
        [MaxLength(length:250)]
        public string Address { get; set; }
        [Timestamp]
        public byte[] RowVersion { get; set; }
    }

ConcurrencyCheck

Specifies that the corresponding database column participates in optimistic concurrency checks. Check the following example:

using System.ComponentModel.DataAnnotations;
 
public class Employee
    {
        [Key]
        public long EmpId { get; set; }
        [Required]
        public string Name { get; set; }
        [MaxLength(length:250)]
        public string Address { get; set; }

        [Timestamp]
        public byte[] RowVersion { get; set; }
        [StringLength(200)]

        [ConcurrencyCheck]
        public string Email { get; set; }
    }

Suppose two users have taken an employee record for modification, and one user saves it after making the changes. When the other one also modifies the record and tries to save it, the first user's changes will be lost, and it will cause data inconsistency. When the Concurrencycheck attribute is added to a field, it will participate in the where clause of the update statement in Entity Framework Core and throws an error if that field changed after it had been retrieved from the database for modification. Entity Framework Core always includes properties with ConcurrencyCheck attribute in where clause of update and delete statements. The ConcurrencyCheck attribute can be added to any number of properties in an entity class.

StringLength

This is similar to the MaxLength attribute. This can be used to specify the maximum length of the corresponding column in the database. 

  public class Employee
    {
        [Key]
        public long EmpId { get; set; }
        [Required]
        public string Name { get; set; }
        [MaxLength(length:250)]
        public string Address { get; set; }
        [Timestamp]
        public byte[] RowVersion { get; set; }
        [StringLength(200)]
        public string Email { get; set; }
    }

The following are the schema-related attributes in System.ComponentModel.DataAnnotations.Schema namespace

Table

Table attribute is used to specify the table name in the database and its schema. It overrides the default convention in EF Core, where the table name is the name of the DbSet<TEntity> property. The following example shows how the Table attribute can be used. The schema name is optional.

 [Table("DepartmentMaster", Schema="dbo")]
    public class Department
    {      
        public int id { get; set; }       
        public string Name { get; set; }
    }

The table's name will be DepartmentMaster even when DbSet<TEntity> property name is different. 

Column

The Column attribute is used in an entity class to specify the column name, data type, and column order in a database table. The Column attribute overrides the default convention where the column name is the same as the property name.

  [Table("DepartmentMaster", Schema="dbo")]
    public class Department
    {
        [Column("DepartmentID")]
        public int id { get; set; }

        [Column("DepartmentName", Order = 2, TypeName = "nvarchar(50)")]
        public string Name { get; set; }
    }

In the above example, the name of the database columns will be DepartmentID and DepartmentName. Here, Order and TypeName parameters are optional. 

ForeignKey

The ForeignKey attribute is used to specify a foreign key in the relationship between two entities. It overrides the default naming convention in EF Core for foreign key properties. The foreign key attribute can be applied in 3 ways. 

  1. On the navigation property of the dependent class, pass the name of the foreign key property
  public class Country
    {  
        public int CountryID { get; set; }
        [MaxLength(length:50)]
        public string CountryName { get; set; }
        [MaxLength(length: 20)]
        public string CountryCode { get; set; }
        public ICollection<State> States { get; set; }
        
    }
 public class State
    {
        public int Id { get; set; }
        public string Name { get; set; }
        [ForeignKey("Cntryid")]
       public Country Country { get; set; }
        public int Cntryid { get; set; }
    }
  2. On the foreign key property of the dependent class, pass the name of the navigation property
 public class State
    {
        public int Id { get; set; }
        public string Name { get; set; }
       
       public Country Country { get; set; }
        [ForeignKey("Country")]
        public int Cntryid { get; set; }
    }

3. On the navigational property of the principal class, pass the foreign key property name in the dependant class 

    public class Country
    {  
        public int CountryID { get; set; }
        [MaxLength(length:50)]
        public string CountryName { get; set; }
        [MaxLength(length: 20)]
        public string CountryCode { get; set; }
        [ForeignKey("Cntryid")]
        public ICollection<State> States { get; set; }        
    }

Fluent API can also be used to set foreign key properties. 

DatabaseGenerated

The DatabaseGenerated attribute specifies how the database generates values for a property. The attribute takes the following DatabaseGeneratedOption enumeration values.

  1. DatabaseGenerated.Identity
  2. DatabaseGenerated.Computed
  3. DatabaseGenerated.None

Consider the following example. 

public class Headline
    {
        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        public int id { get; set; }
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int SrNo { get; set; }
        public string Headlinetext { get; set; }
        [DatabaseGenerated(DatabaseGeneratedOption.Computed)]
        public DateTime LastModified { get; set; }

    }
 protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Headline>()
                    .Property(s => s.LastModified)
                    .HasDefaultValueSql("GETUTCDate()");
        }

In the above example, the id property is the primary key column. EF Core considers numeric primary key properties as identity columns if the database is SQL Server. To overcome this default behavior, DatabaseGenerated.None option is used for the id property. The SrNo property will be treated as an identity column by Entity Framework Core as DatabaseGenerated.Identity option is used.

DatabaseGeneratedOption.Computed option is used to specify that the underlying database will generate the value of such properties. In the above example, LastModified property value is generated in the database when a record is added. A default value is given to that property in AppDbContext class using Fluent API to make this work. GETUTCDate() is an SQL Server function to get UTC date and time. 

NotMapped 

This attribute specifies that a property or an entity with this attribute should not be mapped to a table column or table in the database. In the following example, the CountryName field will not be present in the State table. Similarly, the StateLocation table will not be created in the database. 

public class State
    {
        public int Id { get; set; }
        public string Name { get; set; }       
        public Country Country { get; set; }      
        public int Cntryid { get; set; }
        [NotMapped]
        public string CountyName { get; set; }
    }
    [NotMapped]
    public class StateLocation
    {
        public string StateName { get; set; }
        public string Longitude { get; set; }
        public string Latitude { get; set; }
    }

InverseProperty

If there are multiple relationships between two entities, EF cannot identify them automatically. It should be specified using Inverse Property Attribute. In the following example, if the InverseProperty attribute is commented, the following error will be shown when the add-migration command is run.

"Unable to determine the relationship represented by navigation 'Employee.HeadlineEnteredBy' of type 'ICollection<Headline>'. Either manually configure the relationship, or ignore this property using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating.'"

 

public class Headline
    {
        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        public int id { get; set; }
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int SrNo { get; set; }
        public string Headlinetext { get; set; }
        [DatabaseGenerated(DatabaseGeneratedOption.Computed)]
        public DateTime LastModified { get; set; }
        public Employee EnteredPerson { get; set; }
        public Employee ModifiedPerson { get; set; }   

    }
public class Employee
    {
        [Key]
        public long EmpId { get; set; }
        [Required]
        public string Name { get; set; }       
        public string Address { get; set; }      
        public string Email { get; set; }
        [InverseProperty("EnteredPerson")]
        public ICollection<Headline> HeadlineEnteredBy { get; set; }
        [InverseProperty("ModifiedPerson")]
        public ICollection<Headline> HeadlineModifiedBy { get; set; }
    }

To fix this issue, the InverseProperty attribute is used to point to the navigation property at the other end of the relationship. 


Search