Building ASP.NET Web API RESTful Service – Part 1






4.91/5 (3 votes)
This is the first part of Building ASP.NET Web API RESTful Service Series.
- Source code is available on GitHub
Introduction
This is the first part of building ASP.NET Web API RESTful Service Series. The topics we’ll cover are:
- Building the Database Model using Entity Framework Code First – Part 1 (This Post)
- Applying the Repository Pattern for the Data Access Layer – Part 2
- Getting started with ASP.NET Web API - Part 3
- Implement Model Factory, Dependency Injection and Configuring Formatters - Part 4
- Implement HTTP actions POST, PUT, and DELETE In Web API - Part 5
- Implement Resources Association - Part 6
- Implement Resources Pagination - Part 7
- Securing Web API – Part 8
- Preparing Web API for Versioning – Part 9
- Different techniques to Implement Versioning – Part 10
- Caching resources using CacheCow and ETag - Part 11
Update (2014-March-5) Two new posts which cover ASP.NET Web API 2 new features:
Building the Database Model using Entity Framework Code First
We'll be using Entity Framework Code First approach where we’ll define our model objects using Plain Old CLR Objects POCO. We’ll be code centeric and start by writing standard .NET classes which define the domain model objects that are suitable for our API. Those POCO classes will be responsible to generate our eLearning database.
The eLearning database is simple, we want to be able to define and store Students
and Tutors
. As well, we have to define and store Courses
and Subjects
. We need to allow each Student
to enroll in different Courses
.
The image below shows the final result of the database schema, I’m listing it early so it will facilitate the understanding of the POCO classes we’ll build now:
Step 1: Create a new empty Class Library Project
We’ll start by creating a new empty class library project which will be responsible for all data management operations (Data Layer). Choose File->New Project->Windows->Class Library and name your solution “eLearning” and your class library “Learning.Data
”. You can choose .NET framework 4 or 4.5.
Step 2: Install Entity framework using NuGet
We need to install Entity framework version 5 or 6 using NuGet package manager or NuGet package console, the package we’ll install is named “EntityFramework
“. Our solution will look as below after installing Entity Framework:
Step 3: Creating our Model
As we stated before, we do not have our eLearning database and we need to create it by writing standard .NET classes that define the domain model objects.
Now, add a new folder called Entities, then add five classes called Subject
, Course
, Tutor
, Student
, and Enrollment
. Those classes contain just simple properties and will shape our database:
public class Subject
{
public Subject()
{
Courses = new List<Course>();
}
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Course> Courses;
}
public class Course
{
public Course()
{
Enrollments = new List<Enrollment>();
CourseTutor = new Tutor();
CourseSubject = new Subject();
}
public int Id { get; set; }
public string Name { get; set; }
public Double Duration { get; set; }
public string Description { get; set; }
public Tutor CourseTutor { get; set; }
public Subject CourseSubject { get; set; }
public ICollection<Enrollment> Enrollments { get; set; }
}
public class Tutor
{
public Tutor()
{
Courses = new List<Course>();
}
public int Id { get; set; }
public string Email { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public Enums.Gender Gender { get; set; }
public ICollection<Course> Courses;
}
public class Student
{
public Student()
{
Enrollments = new List<Enrollment>();
}
public int Id { get; set; }
public string Email { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public Enums.Gender Gender { get; set; }
public DateTime DateOfBirth { get; set; }
public DateTime? RegistrationDate { get; set; }
public DateTime? LastLoginDate { get; set; }
public ICollection<Enrollment> Enrollments { get; set; }
}
public class Enrollment
{
public Enrollment()
{
Student = new Student();
Course = new Course();
}
public int Id { get; set; }
public DateTime EnrollmentDate { get; set; }
public Student Student { get; set; }
public Course Course { get; set; }
}
As you noticed, those classes do not derive from any base classes nor have any attributes, having those standard classes give us more data access flexibility and allow us to focus on the application needs without worrying about persistence implementation.
Entity framework Code First by default supports an approach called “Convention over Configuration” for mapping your POCO classes to database objects (Tables, Table fields data types, and FK Relations). I find this approach is useful in scenarios where you are building a demo/simple applications. But in our case we need to override this convention by providing custom database mapping rules using Fluent API.
Step 4: Applying Custom Mapping Rules
Once we apply the custom mapping rules, we will be able to define datatype for each column, set null-ability, map FK relationships between tables, and specify PK and Identity columns.
To do this, we need to create new folder named “Mappers” then add five classes which derive from System.Data.Entity.ModelConfiguration.EntityTypeConfiguration<T>
.
Classes are: CourseMapper
, EnrollmentMapper
, StudentMapper
,
SubjectMapper
, and TutorMapper
.
class CourseMapper : EntityTypeConfiguration<Course>
{
public CourseMapper()
{
this.ToTable("Courses");
this.HasKey(c => c.Id);
this.Property(c => c.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
this.Property(c => c.Id).IsRequired();
this.Property(c => c.Name).IsRequired();
this.Property(c => c.Name).HasMaxLength(255);
this.Property(c => c.Duration).IsRequired();
this.Property(c => c.Description).IsOptional();
this.Property(c => c.Description).HasMaxLength(1000);
this.HasRequired(c => c.CourseSubject).WithMany().Map(s => s.MapKey("SubjectID"));
this.HasRequired(c => c.CourseTutor).WithMany().Map(t => t.MapKey("TutorID"));
}
}
class EnrollmentMapper : EntityTypeConfiguration<Enrollment>
{
public EnrollmentMapper()
{
this.ToTable("Enrollments");
this.HasKey(e => e.Id);
this.Property(e => e.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
this.Property(e => e.Id).IsRequired();
this.Property(e => e.EnrollmentDate).IsRequired();
this.Property(e => e.EnrollmentDate).HasColumnType("smalldatetime");
this.HasOptional(e => e.Student).WithMany(e => e.Enrollments).Map(
s => s.MapKey("StudentID")).WillCascadeOnDelete(false);
this.HasOptional(e => e.Course).WithMany(e => e.Enrollments).Map(
c => c.MapKey("CourseID")).WillCascadeOnDelete(false);
}
}
class StudentMapper : EntityTypeConfiguration<Student>
{
public StudentMapper()
{
this.ToTable("Students");
this.HasKey(s => s.Id);
this.Property(s => s.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
this.Property(s => s.Id).IsRequired();
this.Property(s => s.Email).IsRequired();
this.Property(s => s.Email).HasMaxLength(255);
this.Property(s => s.Email).IsUnicode(false);
this.Property(s => s.UserName).IsRequired();
this.Property(s => s.UserName).HasMaxLength(50);
this.Property(s => s.UserName).IsUnicode(false);
this.Property(s => s.Password).IsRequired();
this.Property(s => s.Password).HasMaxLength(255);
this.Property(s => s.FirstName).IsRequired();
this.Property(s => s.FirstName).HasMaxLength(50);
this.Property(s => s.LastName).IsRequired();
this.Property(s => s.LastName).HasMaxLength(50);
this.Property(s => s.Gender).IsOptional();
this.Property(s => s.DateOfBirth).IsRequired();
this.Property(s => s.DateOfBirth).HasColumnType("smalldatetime");
this.Property(s => s.RegistrationDate).IsOptional();
this.Property(s => s.RegistrationDate).HasColumnType("smalldatetime");
this.Property(s => s.LastLoginDate).IsOptional();
this.Property(s => s.LastLoginDate).HasColumnType("smalldatetime");
}
}
class SubjectMapper : EntityTypeConfiguration<Subject>
{
public SubjectMapper ()
{
this.ToTable("Subjects");
this.HasKey(s => s.Id);
this.Property(s => s.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
this.Property(s => s.Id).IsRequired();
this.Property(s => s.Name).IsRequired();
this.Property(s => s.Name).HasMaxLength(255);
}
}
class TutorMapper : EntityTypeConfiguration<Tutor>
{
public TutorMapper()
{
this.ToTable("Tutors");
this.HasKey(s => s.Id);
this.Property(s => s.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
this.Property(s => s.Id).IsRequired();
this.Property(s => s.Email).IsRequired();
this.Property(s => s.Email).HasMaxLength(255);
this.Property(s => s.Email).IsUnicode(false);
this.Property(s => s.UserName).IsRequired();
this.Property(s => s.UserName).HasMaxLength(50);
this.Property(s => s.UserName).IsUnicode(false);
this.Property(s => s.Password).IsRequired();
this.Property(s => s.Password).HasMaxLength(255);
this.Property(s => s.FirstName).IsRequired();
this.Property(s => s.FirstName).HasMaxLength(50);
this.Property(s => s.LastName).IsRequired();
this.Property(s => s.LastName).HasMaxLength(50);
this.Property(s => s.Gender).IsOptional();
}
}
By looking at the code above, you will notice that we are configuring each POCO class property (Datatype, Null-ability, PK and identity columns, and FK relations). Those configurations will be reflected on the database tables we are building. For more details about mapping/configuring fluent API, you can visit this link.
The relationships between eLearning database tables are simple and described as given below:
- Each
Course
has aSubject
. - Each
Tutor
can teach multipleCourse
s. - Each
Student
can enroll in multipleCourse
s. So we’ll have a Many-to-Many table to persist the relation calledEnrollment
.
Step 5: Creating Context Class to Handle Database Persistence
Now, we need to add a new class named LearningContext
which derives from class System.Data.Entity.DbContext
:
public class LearningContext:DbContext
{
public LearningContext():
base("eLearningConnection")
{
Configuration.ProxyCreationEnabled = false;
Configuration.LazyLoadingEnabled = false;
Database.SetInitializer(new MigrateDatabaseToLatestVersion<LearningContext,
LearningContextMigrationConfiguration>());
}
public DbSet<Course> Courses {get;set;}
public DbSet<Enrollment> Enrollments { get; set; }
public DbSet<Student> Students { get; set; }
public DbSet<Subject> Subjects { get; set; }
public DbSet<Tutor> Tutors { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new StudentMapper());
modelBuilder.Configurations.Add(new SubjectMapper());
modelBuilder.Configurations.Add(new TutorMapper());
modelBuilder.Configurations.Add(new CourseMapper());
modelBuilder.Configurations.Add(new EnrollmentMapper());
base.OnModelCreating(modelBuilder);
}
}
The “LearningContext
” class is responsible for three tasks which are:
- Exposing our POCO classes as
public
DbSet
properties, this means that every POCO class is transferred to a database table. - Overriding the
OnModelCreating
procedure which is used to apply custom mapping rules for each POCO class by adding the new configurations to theDbModelBuilder
configurations. - In “
LearningContext
” class constructor, we have implemented two things:
- Disabled the
ProxyCreationEnabled
andLazyLoadingEnabled
properties which are enabled by default. The Lazy Loading property enables loading the sub-objects of model up front, in our case we want to load them on demand. The Proxy Creation property is used in conjugation with Lazy Loading property, so if is set tofalse
theLearningContext
won’t load sub-objects unlessInclude
method is called. - Configured the initialization and migration strategy of the database to migrate to latest version if a model has changed (i.e., a new property has been added). To implement this, we need to add a new class called
LearningContextMigrationConfiguration
which derives from classSystem.Data.Entity.Migrations.DbMigrationsConfiguration<TContext>
. The code listing is as below:
class LearningContextMigrationConfiguration : DbMigrationsConfiguration<LearningContext>
{
public LearningContextMigrationConfiguration()
{
this.AutomaticMigrationsEnabled = true;
this.AutomaticMigrationDataLossAllowed = true;
}
#if DEBUG
protected override void Seed(LearningContext context)
{
new LearningDataSeeder(context).Seed();
}
#endif
}
The LearningContextMigrationConfiguration
class is responsible for two tasks which are:
- In the constructor of the class, we set the property
AutomaticMigrationsEnabled
totrue
which means that we need EF to handle the automatic migration for us without caring about DB Versioning. As well, we set the propertyAutomaticMigrationDataLossAllowed
totrue
, this is dangerous to set in production environment. If it was set tofalse
, an exception will be thrown if data loss may occur as part of an automatic migration, but for our series it is fine to keep it totrue
. - Overriding the Seed procedure which is used to seed our database with initial data, this procedure gets called every time our application starts, I’ve created a class called “
LearningDataSeeder
” which is responsible to seed the database, I won’t list its code here but you can browse it on GitHub or by downloading the source code for the API.
Till this point, we’ve implemented all the code needed to configure and create our eLearning database depending on model objects we’ve defined. We can stop at this point and consider our data layer completed, but we want to enhance it more and implement “Repository Pattern” which facilitates data access and manipulation once we start building the Web API. So in the next post, we’ll implement the Repository Pattern for our data access layer.