Prologue

In this article, I’ll talk about data annotation in Entity Framework Core, including data type, basic use of primary and foreign key. I won’t go too far, just simple usage.

Because what we do in code is designing logical data model, the eventual result may differ depends on the database you use. In this article, the physical database I used is SQLite, by default.

One more thing, you can also use Fluent API to achieve the same purpose, but here I’ll only talk about data annotation.

If you want to learn this a step further, I recommend a great place for you.


Data Annotation

Data Type

First, we can simply create an entity without any annotation, which is totally OK. But what’s the default behavior?

1
2
3
4
5
6
7
8
public class User
{
public Guid Id { get; set; }
public string Username { get; set; }
public byte[] PasswordHash { get; set; }
public int Age { get; set; }
public bool IsAdmin { get; set; }
}

For int, bool, these are easy to convert into physical database. However, what about other types? By default, string and byte[] will be mapped to varchar and varbinary of maximum length. And all of the fields are set to NOT NULL. Others including primary key and whatever.

If you want to configure these by your self, then data annotation is what you want.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class User
{
[Key] // primary key
[DatabaseGenerated(DatabaseGeneratedOption.Identity)] // auto create
public Guid Id { get; set; }

[Column(TypeName = "varchar(63)")] // manual data type
[Required] // required field
public string Username { get; set; }

[Column(TypeName = "varchar(63)")]
[EmailAddress] // optional option
public string? Email { get; set; } // optional field

[Column(TypeName = "varbinary(256)")]
[Required]
public byte[] PasswordHash { get; set; }
}

With these, you can declare almost all common types of data.


Foreign Key

Reference: Foreign Key Attribute

This is the most confusing part when I first come across EF Core. First of all, you should get to know two properties, Foreign Key property and Reference Navigation property.

To put it simple, Foreign Key property is the actual column that will appear in the database, it is what we write in creating a table via SQL statement. It is the primary key of another table, or entity here. Then Reference Navigation property will not appear in database (actually it cannot since it is an object), and merely a method to navigate through entities, to the table it references.

One-to-One

Reference: One-to-One Relationship

For basic use of foreign key, it comes with two members - one is the referenced key, the other is the reference navigation. It’s OK to only declared in one entity, but it will then become a one-way reference.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class User
{
[Key]
public int Id { get; set; }

public int AddressId { get; set; }
[ForeignKey("AddressId")]
public Address Address { get; set; }
}

public class Address
{
[Key]
public int Id { get; set; }
public string Street { get; set; }
public int No { get; set; }

public int UserId { get; set }
[ForeignKey("UserId")]
public User User { get; set; }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class User
{
[Key]
public int Id { get; set; }

[ForeignKey("Address")]
public int AddressId { get; set; }
public Address Address { get; set; }
}

public class Address
{
[Key]
public int Id { get; set; }
public string Street { get; set; }
public int No { get; set; }

[ForeignKey("User")]
public int UserId { get; set }
public User User { get; set; }
}

One-to-Many

Reference: One-to-Many Relationship

One to many relationship usually requires no extra configurations.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class User
{
[Key]
public int Id { get; set; }

public ICollection<Address> Addresses { get; set; }
}

public class Address
{
[Key]
public int Id { get; set; }
public string Street { get; set; }
public int No { get; set; }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class User
{
[Key]
public int Id { get; set; }

public ICollection<Address> Addresses { get; set; }
}

public class Address
{
[Key]
public int Id { get; set; }
public string Street { get; set; }
public int No { get; set; }

public int UserId { get; set }
[ForeignKey("UserId")]
public User User { get; set; }
}

Many-to-Many

Reference: Many-to-Many Relationship

Well, this is a little complicated, and should be configured with Fluent API.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Book
{
public int BookId { get; set; }
public string Title { get; set; }
public Author Author { get; set; }
public ICollection<BookCategory> BookCategories { get; set; }
}

public class Category
{
public int CategoryId { get; set; }
public string CategoryName { get; set; }
public ICollection<BookCategory> BookCategories { get; set; }
}

public class BookCategory
{
public int BookId { get; set; }
public Book Book { get; set; }
public int CategoryId { get; set; }
public Category Category { get; set; }
}

Then, in your custom DbContext, create this method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// joint primary key
modelBuilder.Entity<BookCategory>()
.HasKey(bc => new { bc.BookId, bc.CategoryId });

// assign relation manually
modelBuilder.Entity<BookCategory>()
.HasOne(bc => bc.Book)
.WithMany(b => b.BookCategories)
.HasForeignKey(bc => bc.BookId);
modelBuilder.Entity<BookCategory>()
.HasOne(bc => bc.Category)
.WithMany(c => c.BookCategories)
.HasForeignKey(bc => bc.CategoryId);
}

Data Creation

Well, it is not enough just declare the entities. Then how to actually instantiate them?

1
2
3
4
5
6
// Assume we use the One-to-One relationship example
using (var context = new YourDbContext())
{
var address = context.Add(new Address {Street = "St. A", No = 1}).Entity;
var user = new User { Address = Address };
}

Or it could be simpler if you use Repository and Unit of Work Pattern.

Just one more thing, unlike Django ORM, EF Core will NOT get reference navigation properties by default. Which means, when you get a fresh data from DbContext, all navigation properties are null. You have to get them manually, or use eager loading when you query them.

Check out Eager Loading of Related Data for more information. 😶‍🌫️


Epilogue

This is hard to understand… Just a beginning. Will update when I have new ideas. 🤡