Now that we've created a Code-First model using Attributes, let's walk through the other manner by which we can create a model: FluentAPI.
The Model
Once again, we will be using the same CourseCatalog model:
To start off, let's create the classes necessary to represent these entities:
public class Student
{
public int ID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime DateOfBirth { get; set; }
public virtual StudentAddress Address { get; set; }
public virtual ICollection<Course> Courses { get; set; }
}
public class StudentAddress
{
public int StudentID { get; set; }
public string StreetAddress { get; set; }
public string City { get; set; }
public string State { get; set; }
public string PostalCode { get; set; }
public virtual Student Student { get; set; }
}
public class Teacher
{
public int ID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public virtual ICollection<Course> Courses { get; set; }
}
public class Course
{
public int ID { get; set; }
public string Name { get; set; }
public virtual ICollection<Student> Students { get; set; }
public int TeacherID { get; set; }
public virtual Teacher Teacher { get; set; }
}
We can also go ahead and create a basic DbContext class:
public class CourseCatalogContext : DbContext
{
public virtual DbSet<Student> Students { get; set; }
public virtual DbSet<StudentAddress> StudentAddresses { get; set; }
public virtual DbSet<Course> Courses { get; set; }
public virtual DbSet<Teacher> Teachers { get; set; }
}
Once we've got all of this in place, we can start binding the model to the database using FluentAPI.
Creating the Model using FluentAPI
FluentAPI uses a method-based syntax rather than the attribute-based syntax we saw in the previous post. This gives us a bit more control over the properties of the database that will get created, but can be a bit more difficult to read.
To get started, let's override the OnModelCreating method in CourseCatalogContext:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
//FluentAPI mapping goes here
}
The DbModelBuilder class is what we can use to build our model rather than using attributes.
For each entity in this model, we need to define a few things:
- On each entity, we need to define a key column.
- For each property in an entity, we need to define the attributes of the corresponding column (string length, nullable, etc).
- For each relationship, we need to define the multiplicity on each side (e.g. one-to-one, one-to-many, many-to-many, etc.)
Let's start with the Student entity. We have a key column on the ID property, which we can define with a call like this:
modelBuilder.Entity<Student>()
.HasKey(x => x.ID);
Notice the syntax here. Each call to an instance of DbModelBuilder needs to define the entity we are targeting, and then apply the change we need.
Remember that in addition to having the ID property be the key, we also need to specify that it is an IDENTITY column, which we can do by making an additional call and specifying the property ID:
modelBuilder.Entity<Student>()
.Property(x => x.ID)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
Next, we need to tackle the other properties of Student: FirstName and LastName. Both of these properties will need a maximum length of 100 characters and cannot be blank. Since we are defining characteristics of these properties, we can chain these methods like so:
modelBuilder.Entity<Student>()
.Property(x => x.FirstName)
.IsRequired()
.HasMaxLength(100);
modelBuilder.Entity<Student>()
.Property(x => x.LastName)
.IsRequired()
.HasMaxLength(100);
FluentAPI's chaining makes creating these characteristics much easier.
Using the methods we've already seen, we can set up the keys and property characteristics for the other three entities:
modelBuilder.Entity<StudentAddress>()
.HasKey(x => x.StudentID);
modelBuilder.Entity<StudentAddress>()
.Property(x => x.StreetAddress)
.IsRequired();
modelBuilder.Entity<StudentAddress>()
.Property(x => x.City)
.IsRequired();
modelBuilder.Entity<StudentAddress>()
.Property(x => x.State)
.IsRequired();
modelBuilder.Entity<StudentAddress>()
.Property(x => x.PostalCode)
.IsRequired();
modelBuilder.Entity<Teacher>()
.HasKey(x => x.ID);
modelBuilder.Entity<Teacher>()
.Property(x => x.ID)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
modelBuilder.Entity<Teacher>()
.Property(x => x.FirstName)
.IsRequired()
.HasMaxLength(100);
modelBuilder.Entity<Teacher>()
.Property(x => x.LastName)
.IsRequired()
.HasMaxLength(100);
modelBuilder.Entity<Course>()
.HasKey(x => x.ID);
modelBuilder.Entity<Course>()
.Property(x => x.ID)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
modelBuilder.Entity<Course>()
.Property(x => x.Name)
.IsRequired();
Setting Up Relationships
The last part of the model definition is to set up the relationships between the entities. In our model, we have the following relationships:
- Student 1-to-0..1 StudentAddress
- Teacher 1-to-n Course
- Student n-to-m Course
Let's start with Student and Student Address.
FluentAPI's naming conventions help us out when defining these relationships. Once we've targeted the entity we want to create a relationship for, we can call methods beginning with "Has" to define the relationship on that side. In the case of Student-StudentAddress, we'd use the method HasOptional, since a Student doesn't have to have a related StudentAddress.
modelBuilder.Entity<Student>()
.HasOptional(x => x.Address)
However, as with all relationships, there are two sides to the story and we still need to define the other side. In order to do this, we use methods starting with "With"; in our case, we want WithRequired because a StudentAddress must have a related Student. The final code that sets up the Student-to-StudentAddress relationship is:
modelBuilder.Entity<Student>()
.HasOptional(x => x.Address)
.WithRequired(x => x.Student);
Now we can make the relationship between Teacher and Course. One teacher can have many courses, and any course must have a related Teacher. Therefore, we need the aptly-named methods HasMany and WithRequired:
modelBuilder.Entity<Teacher>()
.HasMany(x => x.Courses)
.WithRequired(x => x.Teacher)
There's still a piece missing though; you may remember from the class definitions above that Course has a property TeacherID, which is a foreign key to the Teacher table. We need to explicitly define that property as a foreign key, and we do this by using the HasForeignKey method like so:
modelBuilder.Entity<Teacher>()
.HasMany(x => x.Courses)
.WithRequired(x => x.Teacher)
.HasForeignKey(x => x.TeacherID);
Two relationships down, one to go! The last one is a little more difficult, because it's a many-to-many relationship.
Remember that in this kind of relationship in a database, you need a mapping table that related the two tabled together. Entity Framework is smart enough to not count that table as an entity unto itself, rather it just reads it as a relationship between two other entities.
Our relationship between Student and Course can be created using HasMany and WithMany:
modelBuilder.Entity<Course>()
.HasMany(x => x.Students)
.WithMany(x => x.Courses)
However, there's one more step, and that's to create the mapping table. We do this by calling Map(), and then expressing the two columns in the table as well as the name of the table itself:
modelBuilder.Entity<Course>()
.HasMany(x => x.Students)
.WithMany(x => x.Courses)
.Map(c => c.MapLeftKey("StudentID")
.MapRightKey("CourseID")
.ToTable("StudentCourses"));
Now we've got all tables, all properties, and all relationships defined! Our final OnModelBuilding method looks like this:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Student>()
.Property(x => x.ID)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
modelBuilder.Entity<Student>()
.HasKey(x => x.ID);
modelBuilder.Entity<Student>()
.Property(x => x.FirstName)
.IsRequired()
.HasMaxLength(100);
modelBuilder.Entity<Student>()
.Property(x => x.LastName)
.IsRequired()
.HasMaxLength(100);
modelBuilder.Entity<Student>()
.HasOptional(x => x.Address)
.WithRequired(x => x.Student);
modelBuilder.Entity<StudentAddress>()
.HasKey(x => x.StudentID);
modelBuilder.Entity<StudentAddress>()
.Property(x => x.StreetAddress)
.IsRequired();
modelBuilder.Entity<StudentAddress>()
.Property(x => x.City)
.IsRequired();
modelBuilder.Entity<StudentAddress>()
.Property(x => x.State)
.IsRequired();
modelBuilder.Entity<StudentAddress>()
.Property(x => x.PostalCode)
.IsRequired();
modelBuilder.Entity<Teacher>()
.HasKey(x => x.ID);
modelBuilder.Entity<Teacher>()
.Property(x => x.ID)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
modelBuilder.Entity<Teacher>()
.Property(x => x.FirstName)
.IsRequired()
.HasMaxLength(100);
modelBuilder.Entity<Teacher>()
.Property(x => x.LastName)
.IsRequired()
.HasMaxLength(100);
modelBuilder.Entity<Course>()
.HasKey(x => x.ID);
modelBuilder.Entity<Course>()
.Property(x => x.ID)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
modelBuilder.Entity<Course>()
.Property(x => x.Name)
.IsRequired();
modelBuilder.Entity<Teacher>()
.HasMany(x => x.Courses)
.WithRequired(x => x.Teacher)
.HasForeignKey(x => x.TeacherID);
modelBuilder.Entity<Course>()
.HasMany(x => x.Students)
.WithMany(x => x.Courses)
.Map(c => c.MapLeftKey("StudentID")
.MapRightKey("CourseID")
.ToTable("StudentCourses"));
base.OnModelCreating(modelBuilder);
}
Lots of people will split these calls out into separate mapping files (this is what Entity Framework Power Tools does) as that is more maintainable for larger projects.
Now we're done! We've got our model and database setup. Don't forget to check out the sample project on GitHub, and feel free to let me know if this tutorial helped you in the comments!
There's still one more post in this series, and that's going to show how we can use Code-First Migrations to manage changes to our database schema, all without writing any SQL! Stay tuned for the last installment of Entity Framework for Beginners!
Happy Coding!