Relationships in a database context define how two entities relate to each other. Entity Framework Core really shines in supporting relationships. It offers a convention-based configuration, that will configure relationships based on the model provided. For more advanced cases we can use robust Fluent API capabilities, that offer greater flexibility.
I must admit that working with relationships in Entity Framework Core 5 feels very natural for a developer and that might be simply its most important feature.
Types of relationships
Relationships in the database will mean, that two entities relate to each other. They are logically connected. Let’s take a look at a hotel model example:
We have a reservation, that has a single room and a list of guests. The room can be assigned to many reservations. A profile can be assigned to many reservations, but it has only one address related. We have 3 different types of relationships defined here:
- one-to-many – room to a reservation
- many-to-many – reservation to profile
- one-to-one – profile to address
Those types are nicely supported by Entity Framework Core, so let’s have a look at the model that corresponds to this schema. Here is a Reservation
:
public class Reservation
{
public int Id { get; set; }
public int RoomId { get; set; }
public Room Room { get; set; }
public List<Profile> Profiles { get; set; }
public DateTime Created { get; set; }
public DateTime From { get; set; }
public DateTime To { get; set; }
}
And Room
:
public class Room
{
public int Id { get; set; }
public int Number { get; set; }
public string Description { get; set; }
public DateTime LastBooked { get; set; }
public int Level { get; set; }
public RoomType RoomType { get; set; }
public bool WithBathroom { get; set; }
public int NumberOfPlacesToSleep { get; set; }
}
public enum RoomType
{
Standard,
Suite
}
And Profile
:
public class Profile
{
public int Id { get; set; }
public string Ref { get; set; }
public string Salutation { get; set; }
public string Forename { get; set; }
public string Surname { get; set; }
public string TelNo { get; set; }
public string Email { get; set; }
public string Country { get; set; }
public DateTime? DateOfBirth { get; set; }
public Address Address { get; set; }
public List<Reservation> Reservations { get; set; }
}
And finally, Address
:
public class Address
{
public int Id { get; set; }
public string Street { get; set; }
public string HouseNumber { get; set; }
public string City { get; set; }
public string PostCode { get; set; }
public int ProfileId { get; set; }
public Profile Profile { get; set; }
}
And a cherry on top, a PrimeDbContext
:
public class PrimeDbContext : DbContext
{
public PrimeDbContext(DbContextOptions<PrimeDbContext> options)
: base(options)
{
}
public virtual DbSet<Room> Rooms { get; set; }
public virtual DbSet<Profile> Profiles { get; set; }
public virtual DbSet<Reservation> Reservations { get; set; }
public virtual DbSet<Address> Address { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
}
}
Please notice a very important thing, thanks to a convention-based configuration, there is no additional configuration needed in the model classes and PrimeDbContext
class.
Configuration is easy
Have you noticed how easy it is to configure relationships in Entity Framework Core 5? If you have properties named correctly, then EF Core will deduct relationships on its own. A relationship is defined by a navigation property, that is an entity inside an entity. Look at the Reservation
. There is a Room
, that is a navigation property and there is a RoomId
that will be treated as a foreign key to define a constraint.
There are 3 ways to configure model and relationships:
- convention-based – with properly named properties, EF Core will deduct how entities are related
- data annotations – handy attributes that you can put on the entity property
- Fluent API – a fully-featured API for configuring relations and entities as you wish
Here is an example of a data annotation to set a non-standard named foreign key:
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int BlogForeignKey { get; set; }
[ForeignKey("BlogForeignKey")]
public Blog Blog { get; set; }
}
If the foreign key would be named BlogId
, then it would be configured automatically, but a custom name has to be handled manually.
Even with many-to-many relationships there is no need to define a joining table and write additional configuration. It is a new feature available from RC1 version, so documentation available on the official Microsoft page can be misleading. Hopefully, it will be updated soon.
Luckily, in most cases, you would not need to write much of configuration manually, cause it is mainly required when coping with advanced scenarios and custom mappings.
Model First approach
Model First approach lets you define your model and relationships and use Entity Framework Core to generate SQL for you. All you need to do is to create models that you want and when you’re done, just create a database migration. This is true of course when you already have EF Core migrations in place.
It also works for updating the model, when you need to add a related entity, EF Core migrations will handle that surprisingly well.
Let’s say I had a profile entity and I wanted to add an Address
entity in a one-to-one relationship. You can take a look at the code of those both classes above. When I add a new migration from dotnet CLI, I get a new migration like this, already generated for me, based on my model changes.
public partial class AddAddress : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Address",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Street = table.Column<string>(type: "nvarchar(max)", nullable: true),
HouseNumber = table.Column<string>(type: "nvarchar(max)", nullable: true),
City = table.Column<string>(type: "nvarchar(max)", nullable: true),
PostCode = table.Column<string>(type: "nvarchar(max)", nullable: true),
ProfileId = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Address", x => x.Id);
table.ForeignKey(
name: "FK_Address_Profiles_ProfileId",
column: x => x.ProfileId,
principalTable: "Profiles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Address_ProfileId",
table: "Address",
column: "ProfileId",
unique: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Address");
}
}
Nice and easy, but what’s most important – it works!
Summary
Configuring relationships in Entity Framework Core 5 is as simple as it can possibly be. Most of the mapping can be done automatically by the framework, just by naming properties right. If you’re struggling with more advanced scenarios you can go with Fluent API, which offers a lot and is rather compact. In just a few lines you can define how to map a view to an entity.
My favorite part, though, is a Model First approach, where you create a model that you would like to work with and generate SQL with EF Core migrations.
All code mentioned here is available on my GitHub, code that uses those relationships as well! Feel free to drop by.