Getting Started with Entity Framework Core: Building an ASP.NET Core Application with Web API and Code First Development
Introduction
In the previous article, we’ve learned the high-level overview of what Entity Framework is all about and learned how to build a web application quickly using Entity Framework’s Database-First development.
This time we will build a simple, yet realistic ASP.NET Core application and showcase the features of Entity Framework Core. But before we dig down further, let’s take a quick recap about the high-level differences and key benefits of Code-First and Database-First design workflows then we’ll decide which we are going to use in our project.
Design Workflows
Just so you remember, there are two main design workflows that is supported by Entity Framework Core: The Code-First approach which is you create your classes (POCO Entities) and generate a new database out from it. The Database-First approach allows you to use an existing database and generate classes based on your database schema. The table below shows the high-level process of how each design workflow works.
Code First (New Database) | Database First (Existing Database) |
Figure 1: EF Core Design Workflows
Code First (New Database)
- Good option if you don't know the whole picture of your database as you can just update your Plain Old Class Object (POCO) entities and let EF sync your changes to your database. In other words, you can easily add or remove features defined in your class without worrying about syncing your database using Migrations.
- You don't have to worry about your database as EF will handle the creation for you. In essence, a database is just a storage medium with no logic.
- You will have full control over the code. You simply define and create POCO entities and let EF generate the corresponding Database for you. The downside is if you change something in your database manually, you will probably lose these changes because your code defines the database.
- It is easy to modify and maintain as there will be no auto-generated code.
Database First (Existing Database)
- Good option if you are good at database design or you have a database designed by DBAs, or you already have an existing database developed.
- You don't have to worry about creating POCO objects as EF will generate them for you based on your existing database.
- You can create partial classes if you want additional features in POCO entities.
- You can manually modify your database because the database defines your POCO entities (domain model).
- Provides more flexibility and control over database configuration.
Wrapping Up
You may go for the database-first route when you have an existing database developed separately or when time is important for you. Database first enables us to build applications faster as you can generate your domain models in just a few clicks. For larger applications or for long-term client projects, code first provides us the control we need to create the most efficient program and also gives us the protection and consistency of a versioned controlled database while reducing bloat.
Keep in mind that both design workflows have their advantages. Choosing which approach to use is very important decision to make when you start a new project, so be sure to know what your project requires and expects.
The table below outlines my own personal recommendation as to which design workflow to consider when building an application.
Database First | Code First | |
Code Generation | Create the database schema using your preferred tools (e.g SQL Server Management Studio) and import that database and schema using a script to scaffold everything for you. | Write classes with properties you want to keep track of and then define relationships between other classes. |
Syncing Changes | Create a migration that represents the changes and runs the script against the database. | You create the migrations when you need them, and the code will use those files (and a corresponding table in the target database) to keep the schema changes in sync with what the code expects. In other words, you can manage your database schema without ever writing any SQL. |
Type of Project Best Suited for | It makes more sense to use database-first for existing project since you don’t need to make big changes to scale the database. You could also use this approach for new projects when a database is developed separately by DBA’s and owned by another team. | Code-first makes more sense if you are developing a new application from scratch as you can grow and evolve your model as you develop. |
Taking a Choice
As we can easily see by judging the differences between the two approaches, there is no such thing as an overall better or best approach: conversely, we could say that each project scenario will likely have a most suitable approach.
Now with regards to the application that we are going to build, considering the fact we don’t have a database yet and we’re aiming for a flexible, mutable small-scale data structure, adopting the Code-First approach would probably be a good choice. That’s what we’re going to do, build an ASP.NET Core application from scratch using Entity Framework Core's Code-First approach.
The ASP.NET Core Revolution
To summarize what happened in the ASP.NET world within the last year is not an easy task: in short words, we could say that we’re undoubtedly facing the most important series of changes in the .NET Framework since the year it came to life. ASP.NET Core is a complete re-implementation of ASP.NET which unites all the previous web application technologies such as MVC, Web API and Razor Pages into a single programming module, formerly known as MVC6. The new framework introduces a fully-featured cross-platform component, also known as .NET Core, shipped with a brand-new open-source .NET Compiler Platform (currently known as Roslyn), a cross-platform runtime known as CoreCLR.
ASP.NET Core is a new open-source and cross-platform framework for building modern cloud-based web applications. It has been built from the ground up to provide an optimized development framework for web apps that are either deployed to the cloud or to your local servers. Adding to that, it was redesigned to make ASP.NET leaner, modular (so you can just add features that your application requires), cross-platform (so you can easily develop and run your app on Windows, Mac or Linux that is pretty awesome) and cloud-optimized (so you can deploy and debug apps over the cloud).
Three Players, One Goal
Our main goal is to build a data-driven web application using the three cutting-edge technologies: ASP.NET Core MVC, Web API, and Entity Framework Core.
ASP.NET Web API is a framework used to build HTTP services and is an ideal platform for building RESTful applications on the .NET Framework.
ASP.NET Core MVC is a pattern-based way to build dynamic websites that enables a clean separation of concerns and that gives you full control over markup or HTML.
We will use ASP.NET Core to build REST API's (Web API) and as our front-end framework (MVC) to generate pages or views (a.k.a User Interface). On the other hand, we will be using Entity Framework Core as our data access mechanism to work with data from the database. The figure below illustrates how each technology interacts with each other.
Figure 2: High-level process flow
The figure above illustrates how the process works when a user accesses information from the database. When a user requests a page, it will bring up the View/UI (MVC). When a user wants to view some data, MVC app will then talk to the Web API service to process it. Web API will then talk to EF Core to handle the actual request (CRUD operations) and then updates the database when necessary.
Setting Up an ASP.NET Core Web API Project
Now that we already know the high-level overview of ASP.NET Core and EF Core, it’s time for us to get our hands dirty by building a simple, yet data-driven web application using ASP.NET Core with Entity Framework Core. Let’s create the Web API project first.
Fire-up Visual Studio 2017 and let’s create a new ASP.NET Core Web Application project. To do this, select File > New > Project. In the New Project dialog select Visual C# > Web > ASP.NET Core Web Application (.NET Core) just like in the figure below:
*Figure 3: Create New ASP.NET Core Project *
In the dialog, enter the name of your project and then click the OK button to proceed to the next step. Note that for this specific demo we named the project as "StudentApplication.API". Now click on Web API template as shown in the figure below.
Figure 4: ASP.NET Core Web API Template
With the new ASP.NET Core project template dialog, you can now choose to use ASP.NET Core 1.0 or 1.1 using the combo-box at the top of the screen. The default template as of this time of writing is ASP.NET Core 1.1. Additionally, you can choose to add Container support to your project with the checkbox at the bottom-left of the screen. This option adds:
- Docker files to your project for building Container images
- a Docker Compose project to define how you instance your Containers
- Multi-project/container debugging support for Containers
- For the simplicity of this exercise, let’s just omit the use of containers.
Now click again the OK button to let Visual Studio generate the default files for you. The figure below shows the generated files:
Figure 5: Visual Studio Default Generated Files
Let’s take a quick overview of all generated file.
If you already know the core significant changes of ASP.NET Core then you may skip this part, but if you are new to ASP.NET Core then I would like to highlight some of those changes. If you have worked with previous versions of ASP.NET before then you will notice that the new project structure is totally different. The project now includes these files:
- Connected Services: allow service providers to create Visual Studio extensions that can be added to a project without leaving the IDE. It also allows you to connect your ASP.NET Core Application or Mobile Services to Azure Storage services. Connected Services takes care of all the references and connection code, and modifies your configuration files accordingly
- Controllers: this is where you put all your Web API or MVC controller classes. Note that the Web API project will by default generate the ValuesController.cs file.
- wwwroot: is a folder in which all your static files will be placed. These are the assets that the web app will serve directly to the clients, including HTML, CSS, Image and JavaScript files.
- appsettings.json: contains application settings.
- Startup.cs: this is where you put your startup and configuration code.
- Program.cs: this is where you initialize all services needed for your application.
Running the Project for the First Time
To ensure that we got everything we need for our Web API project, let’s try to build and run the project. We can do this by pressing the CTRL + F5 keys. This will compile, build and automatically launches the browser window. If you are getting the following result below, then we can safely assume that our project is good to go.
Setting Up Entity Framework Core
Let’s install Entity Framework Core packages into our project. Go to Tools > NuGet Package Manager -> Package Manager Console and run the following command individually:
Install-Package Microsoft.EntityFrameworkCore.SqlServer
Install-Package Microsoft.EntityFrameworkCore.Tools
After successfully installing the packages above, make sure to check your project references to ensure that we got them in the project. See the figure below.
Figure 7: Entity Framework Core Package References
These packages are used to provide Entity Framework Core migration. Migration enables us to generate database, sync and update tables based on our Entity.
Creating the Model
We will start by creating the “Models” folder within the root of the application. Add a new class file to the Models folder and name it as “Student.cs”, then replace the default generated code with the following:
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace StudentApplication.API.Models
{
public class Student
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long StudentId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Gender { get; set; }
public DateTime? DateOfBirth { get; set; }
public DateTime? DateOfRegistration { get; set; }
public string PhoneNumber { get; set; }
public string Email { get; set; }
public string Address1 { get; set; }
public string Address2 { get; set; }
public string City { get; set; }
public string State { get; set; }
public string Zip { get; set; }
}
}
The code above is nothing but just a simple class that houses some properties. You may have noticed that we decorated the StudentId property with Key and DatabaseGenerated attributes. This is because we will be converting this class into a database table and the StundentId will serve as our primary key with auto-incremented identity. These attributes reside within System.ComponentModel.DataAnnotations. For more details about Data Annotations see: Using Data Annotations for Model Validation
Creating an Empty Database
Now it's time for us to create our database. Open SQL Express Management Studio and then execute the following command:
CREATE DATABASE StundentApplication
The command above should create a blank database in your local machine.
Defining a DBContext
Entity Framework Core Code-First development approach requires us to create a data access context class that inherits from the DbContext class. Now let's add a new class to the Models folder. Name the class as "ApplicationContext" and then copy the following code below:
using Microsoft.EntityFrameworkCore;
namespace StudentApplication.API.Models
{
public class ApplicationContext: DbContext
{
public ApplicationContext(DbContextOptions opts) : base(opts)
{
}
public DbSet<Student> Students { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
}
}
}
The code above defines a context and an entity class that makes up our model. A DbContext must have an instance of DbContextOptions in order to execute. This can be configured by overriding OnConfiguring or supplied externally via a constructor argument. In our example above, we opt to choose constructor argument for configuring our DbContext.
Registering the DbContext with Dependency Injection
Our ApplicationContext class will act as a service. This service will be registered with Dependency Injection (DI) during application startup. This would enable our API Controller or other services gain access to the ApplicationContext via constructor parameters or properties.
In order for other services to make use of ApplicationContext, we will register it as a service. To enable the service, do following steps:
- Define the following database connection string under appsettings.json file:
,
"ConnectionString": {
"StudentApplicationDB": "Server=SSAI-L0028-HP\\SQLEXPRESS;Database=StundentApplication;Trusted_Connection=True;"
}
- Open Startup.cs and declare the following namespace references:
using Microsoft.EntityFrameworkCore;
using StudentApplication.API.Models;
using StudentApplication.API.Models.DataManager;
using StudentApplication.API.Models.Repository
- Add the following code within ConfigureServices method to register our ApplicationContext as a service.
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddDbContext<ApplicationContext>(opts => opts.UseSqlServer(Configuration["ConnectionString:StudentApplicationDB"]));
services.AddSingleton(typeof(IDataRepository<Student, long>), typeof(StudentManager));
services.AddMvc();
}
Adding Migrations
Our next step is to add Code-First migrations. Migrations automate the creation of database based on our Model. Run the following command in the PM console:
PM> Add-Migration StudentApplication.API.Models.ApplicationContext
When the command is executed successfully, it should generate the migration files as shown in the figure below:
*
Figure 8: Generated EF Migration Files*
Now, switch back to the Package Manager Console and run the following command:
PM> update-database
The command above should generate the database table based on our Entity. Here’s the sample screenshot of the generated database table:
Implementing a Simple Data Repository
We don't want our API Controller to access directly our ApplicationContext service, instead, we will let other services handle the communication between our ApplicationContext and API Controller. Having said that, we are going to implement a basic generic repository for handling data access within our application. Add a new folder under Models and name it as “Repository”. Create a new interface and name it as “IDataRepository”. Update the code within that file so it would look similar to the following code below:
using System.Collections.Generic;
namespace StudentApplication.API.Models.Repository
{
public interface IDataRepository<TEntity, U> where TEntity : class
{
IEnumerable<TEntity> GetAll();
TEntity Get(U id);
long Add(TEntity b);
long Update(U id, TEntity b);
long Delete(U id);
}
}
The code above defines our IDataRepository interface. An interface is just a skeleton of a method without the actual implementation. This interface will be injected into our API Controller so we will only need to talk to the interface rather than the actual implementation of our repository. One of the main benefits of using an interface is to make our code reusable and easy to maintain.
Creating the DataManager
Next is we are going to create a concrete class that implements the IDataRepository interface. Add a new folder under Models and name it as “DataManager”. Create a new class and name it as “StudentManager”. Update the code within that file so it would look similar to the following code below:
using StudentApplication.API.Models.Repository;
using System.Collections.Generic;
using System.Linq;
namespace StudentApplication.API.Models.DataManager
{
public class StudentManager : IDataRepository<Student, long>
{
ApplicationContext ctx;
public StudentManager(ApplicationContext c)
{
ctx = c;
}
public Student Get(long id)
{
var student = ctx.Students.FirstOrDefault(b => b.StudentId == id);
return student;
}
public IEnumerable<Student> GetAll()
{
var students = ctx.Students.ToList();
return students;
}
public long Add(Student stundent)
{
ctx.Students.Add(stundent);
long studentID = ctx.SaveChanges();
return studentID;
}
public long Delete(long id)
{
int studentID = 0;
var student = ctx.Students.FirstOrDefault(b => b.StudentId == id);
if (student != null)
{
ctx.Students.Remove(student);
studentID = ctx.SaveChanges();
}
return studentID;
}
public long Update(long id, Student item)
{
long studentID = 0;
var student = ctx.Students.Find(id);
if (student != null)
{
student.FirstName = item.FirstName;
student.LastName = item.LastName;
student.Gender = item.Gender;
student.PhoneNumber = item.PhoneNumber;
student.Email = item.Email;
student.DateOfBirth = item.DateOfBirth;
student.DateOfRegistration = item.DateOfRegistration;
student.Address1 = item.Address1;
student.Address2 = item.Address2;
student.City = item.City;
student.State = item.State;
student.Zip = item.Zip;
studentID = ctx.SaveChanges();
}
return studentID;
}
}
}
The DataManager class handles all database operations for our application. The purpose of this class is to separate the actual data operation logic from our API Controller, and to have a central class for handling create, update, fetch and delete (CRUD) operations.
At the moment, the DataManager class contains five methods: The Get() method gets a specific student record from the database by passing an Id. It uses a LINQ syntax to query the data and returns a Student object. The GetAll() method gets all students records from the database as you could probably guess based on the method name. The Add() method creates a new student record in the database. The Delete() method removes a specific student record from the database based on the Id. Finally, the Update() method updates a specific student record in the database. All methods above use LINQ to query the data from the database.
Creating an API Controller
Now that our DataManager is all set, it's time for us to create the API Controller and expose some endpoints for handling CRUD operations.
Go ahead and right click on the Controllers folder and then select Add > New Item > Web API Controller class. Name the class as "StudentController" just like in the figure below and then click Add.
Figure 10: New Web API Controller Class
Now replace the default generated code so it would look similar to the following code below:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using StudentApplication.API.Models;
using StudentApplication.API.Models.Repository;
using StudentApplication.API.Models.DataManager;
namespace StudentApplication.API.Controllers
{
[Route("api/[controller]")]
public class StudentController : Controller
{
private IDataRepository<Student,long> _iRepo;
public StudentController(IDataRepository<Student, long> repo)
{
_iRepo = repo;
}
// GET: api/values
[HttpGet]
public IEnumerable<Student> Get()
{
return _iRepo.GetAll();
}
// GET api/values/5
[HttpGet("{id}")]
public Student Get(int id)
{
return _iRepo.Get(id);
}
// POST api/values
[HttpPost]
public void Post([FromBody]Student student)
{
_iRepo.Add(student);
}
// POST api/values
[HttpPut]
public void Put([FromBody]Student student)
{
_iRepo.Update(student.StudentId,student);
}
// DELETE api/values/5
[HttpDelete("{id}")]
public long Delete(int id)
{
return _iRepo.Delete(id);
}
}
}
The StudentController derives from the Controller base and by decorating it with the Route attribute enables this class to become a Web API Controller.
If you notice, we have used Controller Injection to subscribed to the IDataRepository interface. So instead of taking directly to the StudentManager class, we just simply talk to the interface and make use of the methods that are available.
The StudentController has five (5) main methods/endpoints. The first Get() method class the GetAll() method from IDataRepository interface and basically returns all the list of Student from the database. The second Get() method returns a specific Student object based on the Id. Notice that the second Get() method is decorated with [HttpGet("{id}")], which means append the "id" to the route template. The "{id}" is a placeholder variable for the ID of the Student object. When the second Get() method is invoked, it assigns the value of "{id}" in the URL to the method's id parameter. The Post() method adds a new record to the database. The Put() method updates a specific record from the database. Notice that both Post() and Put() uses a [FromBody] tag. When a parameter has [FromBody], Web API uses the Content-Type header to select a formatter. Finally, the Delete() method removes a specific record from the database.
Creating an ASP.NET Core MVC Application
At this point, we already have all the requirement we need to build the UI part of this demo. First we created the database, set up our data access using Entity Framework Core Code-First and finally, we already have our Web API ready. Now it's time for us to create a web application that consumes our Web API endpoints.
Now go ahead and create a new ASP.NET Core project. Right click on the Solution and then select Add > New Project > ASP.NET Core Web Application (.NET Core). You should be able to see something like this:
Figure 11: New ASP.NET Core Web Application Project
To be more consistent, name the project as "StudentApplication.Web" and then click OK. In the next dialog, select "Web Application" under ASP.NET Core 1.1 templates and then click OK to let Visual Studio generate the default files for us.
Integrating Newtonsoft.Json
Let's continue by installing Newtonsoft.Json package in the project. Right-click on the project and then select Manage Nuget Packages. In the browse tab, type in "NewtonSoft" and it should result in something like this:
Figure 12: Adding Newtonsoft.Json Package
Click "install" to add the latest package version to the project.
We will be using Newtonsoft.Json later in our code to serialize and deserialize an object from an API request.
The Helper Class
Create a new class at the root of the application and name it as "Helper.cs" and then copy the following code below:
using System;
using System.Net.Http;
using System.Net.Http.Headers;
namespace StudentApplication.Web.Helper
{
public class StudentAPI
{
private string _apiBaseURI = "http://localhost:60883";
public HttpClient InitializeClient()
{
var client = new HttpClient();
//Passing service base url
client.BaseAddress = new Uri(_apiBaseURI);
client.DefaultRequestHeaders.Clear();
//Define request data format
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
return client;
}
}
public class StudentDTO
{
public long StudentId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Gender { get; set; }
public DateTime? DateOfBirth { get; set; }
public DateTime? DateOfRegistration { get; set; }
public string PhoneNumber { get; set; }
public string Email { get; set; }
public string Address1 { get; set; }
public string Address2 { get; set; }
public string City { get; set; }
public string State { get; set; }
public string Zip { get; set; }
}
}
The Helper.cs file contains two (2) main classes: The StudentAPI and StudentDTO. The StudentAPI class has a method InitializeClient() to initialize and HttpClient object. We will be needing this method later to access the API endpoints. The StudentDTO is nothing but a plain class that mimics the Student class from our API Controller. We will be using this class as object parameter for API request.
Note: When testing this in your local, you must change the value in the _apiBaseURI variable to where your Web API project is hosted, or if you are running locally using IISExpress then change the value of the port number generated by Visual Studio.
The Controller
Add a new controller under the Controllers folder and name it as "StudentsController". Replace the default generated code with the following code below:
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using StudentApplication.Web.Helper;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
namespace StudentApplication.Web.Controllers
{
public class StudentsController : Controller
{
StudentAPI _studentAPI = new StudentAPI();
public async Task<IActionResult> Index()
{
List<StudentDTO> dto = new List<StudentDTO>();
HttpClient client = _studentAPI.InitializeClient();
HttpResponseMessage res = await client.GetAsync("api/student");
//Checking the response is successful or not which is sent using HttpClient
if (res.IsSuccessStatusCode)
{
//Storing the response details received from web api
var result = res.Content.ReadAsStringAsync().Result;
//Deserializing the response received from web api and storing into the Employee list
dto = JsonConvert.DeserializeObject<List<StudentDTO>>(result);
}
//returning the employee list to view
return View(dto);
}
// GET: Students/Create
public IActionResult Create()
{
return View();
}
// POST: Students/Create
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create([Bind("StudentId,FirstName,LastName,Gender,DateOfBirth,DateOfRegistration,PhoneNumber,Email,Address1,Address2,City,State,Zip")] StudentDTO student)
{
if (ModelState.IsValid)
{
HttpClient client = _studentAPI.InitializeClient();
var content = new StringContent(JsonConvert.SerializeObject(student), Encoding.UTF8,"application/json");
HttpResponseMessage res = client.PostAsync("api/student", content).Result;
if (res.IsSuccessStatusCode)
{
return RedirectToAction("Index");
}
}
return View(student);
}
// GET: Students/Edit/1
public async Task<IActionResult> Edit(long? id)
{
if (id == null)
{
return NotFound();
}
List<StudentDTO> dto = new List<StudentDTO>();
HttpClient client = _studentAPI.InitializeClient();
HttpResponseMessage res = await client.GetAsync("api/student");
if (res.IsSuccessStatusCode)
{
var result = res.Content.ReadAsStringAsync().Result;
dto = JsonConvert.DeserializeObject<List<StudentDTO>>(result);
}
var student = dto.SingleOrDefault(m => m.StudentId == id);
if (student == null)
{
return NotFound();
}
return View(student);
}
// POST: Students/Edit/1
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Edit(long id, [Bind("StudentId,FirstName,LastName,Gender,DateOfBirth,DateOfRegistration,PhoneNumber,Email,Address1,Address2,City,State,Zip")] StudentDTO student)
{
if (id != student.StudentId)
{
return NotFound();
}
if (ModelState.IsValid)
{
HttpClient client = _studentAPI.InitializeClient();
var content = new StringContent(JsonConvert.SerializeObject(student), Encoding.UTF8, "application/json");
HttpResponseMessage res = client.PutAsync("api/student", content).Result;
if (res.IsSuccessStatusCode)
{
return RedirectToAction("Index");
}
}
return View(student);
}
// GET: Students/Delete/1
public async Task<IActionResult> Delete(long? id)
{
if (id == null)
{
return NotFound();
}
List<StudentDTO> dto = new List<StudentDTO>();
HttpClient client = _studentAPI.InitializeClient();
HttpResponseMessage res = await client.GetAsync("api/student");
if (res.IsSuccessStatusCode)
{
var result = res.Content.ReadAsStringAsync().Result;
dto = JsonConvert.DeserializeObject<List<StudentDTO>>(result);
}
var student = dto.SingleOrDefault(m => m.StudentId == id);
if (student == null)
{
return NotFound();
}
return View(student);
}
// POST: Students/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public IActionResult DeleteConfirmed(long id)
{
HttpClient client = _studentAPI.InitializeClient();
HttpResponseMessage res = client.DeleteAsync($"api/student/{id}").Result;
if (res.IsSuccessStatusCode)
{
return RedirectToAction("Index");
}
return NotFound();
}
}
}
Let's see what we just did above.
Notice that our MVC Controller uses an HttpClient object to access our Web API endpoints. We also use the Newtonsoft's JsonConvert.SerializeObject() and JsonConvert.DeserializeObject() to serialize and deserialize the data back and forth.
The Index() action method gets all Student records from the database. This method is defined as asynchronous which returns the corresponding Index View along with the list of Students entity.
The Create() action method simply returns it's corresponding View. The overload Create() action method is decorated with the [HttpPost] attribute which signifies that the method can only be invoked for POST requests. This method is where we actually handle the adding of new data to database if the model state is valid on posts.
The Edit() action method takes an id as the parameter. If the id is null, it returns NotFound(), otherwise it returns the corresponding data to the View. The overload Edit() action method updates the corresponding record to the database based on the id.
Just like the other action, the Delete() action method takes an id as the parameter. If the id is null, it returns NotFound(), otherwise it returns the corresponding data to the View. The overload Delete() action method deletes the corresponding record to the database based on the id.
The Views
The following are the corresponding markup for Index, Create, Edit and Delete Views.
Index
@model IEnumerable<StudentApplication.Web.Helper.StudentDTO>
@{
ViewData["Title"] = "Index";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>Index</h2>
<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.FirstName)
</th>
<th>
@Html.DisplayNameFor(model => model.LastName)
</th>
<th>
@Html.DisplayNameFor(model => model.Gender)
</th>
<th>
@Html.DisplayNameFor(model => model.DateOfBirth)
</th>
<th>
@Html.DisplayNameFor(model => model.DateOfRegistration)
</th>
<th>
@Html.DisplayNameFor(model => model.PhoneNumber)
</th>
<th>
@Html.DisplayNameFor(model => model.Email)
</th>
<th>
@Html.DisplayNameFor(model => model.Address1)
</th>
<th>
@Html.DisplayNameFor(model => model.Address2)
</th>
<th>
@Html.DisplayNameFor(model => model.City)
</th>
<th>
@Html.DisplayNameFor(model => model.State)
</th>
<th>
@Html.DisplayNameFor(model => model.Zip)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.FirstName)
</td>
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.Gender)
</td>
<td>
@Html.DisplayFor(modelItem => item.DateOfBirth)
</td>
<td>
@Html.DisplayFor(modelItem => item.DateOfRegistration)
</td>
<td>
@Html.DisplayFor(modelItem => item.PhoneNumber)
</td>
<td>
@Html.DisplayFor(modelItem => item.Email)
</td>
<td>
@Html.DisplayFor(modelItem => item.Address1)
</td>
<td>
@Html.DisplayFor(modelItem => item.Address2)
</td>
<td>
@Html.DisplayFor(modelItem => item.City)
</td>
<td>
@Html.DisplayFor(modelItem => item.State)
</td>
<td>
@Html.DisplayFor(modelItem => item.Zip)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.StudentId">Edit</a> |
<a asp-action="Details" asp-route-id="@item.StudentId">Details</a> |
<a asp-action="Delete" asp-route-id="@item.StudentId">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Create
@model StudentApplication.Web.Helper.StudentDTO
@{
ViewData["Title"] = "Create";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>Create</h2>
<form asp-action="Create">
<div class="form-horizontal">
<h4>Student</h4>
<hr />
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="FirstName" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="FirstName" class="form-control" />
<span asp-validation-for="FirstName" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="LastName" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="LastName" class="form-control" />
<span asp-validation-for="LastName" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Gender" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Gender" class="form-control" />
<span asp-validation-for="Gender" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="DateOfBirth" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="DateOfBirth" class="form-control" />
<span asp-validation-for="DateOfBirth" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="DateOfRegistration" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="DateOfRegistration" class="form-control" />
<span asp-validation-for="DateOfRegistration" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="PhoneNumber" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="PhoneNumber" class="form-control" />
<span asp-validation-for="PhoneNumber" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Email" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Address1" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Address1" class="form-control" />
<span asp-validation-for="Address1" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Address2" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Address2" class="form-control" />
<span asp-validation-for="Address2" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="City" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="City" class="form-control" />
<span asp-validation-for="City" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="State" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="State" class="form-control" />
<span asp-validation-for="State" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Zip" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Zip" class="form-control" />
<span asp-validation-for="Zip" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</div>
</div>
</form>
<div>
<a asp-action="Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Edit
@model StudentApplication.Web.Helper.StudentDTO
@{
ViewData["Title"] = "Edit";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>Edit</h2>
<form asp-action="Edit">
<div class="form-horizontal">
<h4>Student</h4>
<hr />
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="StudentId" />
<div class="form-group">
<label asp-for="FirstName" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="FirstName" class="form-control" />
<span asp-validation-for="FirstName" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="LastName" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="LastName" class="form-control" />
<span asp-validation-for="LastName" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Gender" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Gender" class="form-control" />
<span asp-validation-for="Gender" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="DateOfBirth" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="DateOfBirth" class="form-control" />
<span asp-validation-for="DateOfBirth" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="DateOfRegistration" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="DateOfRegistration" class="form-control" />
<span asp-validation-for="DateOfRegistration" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="PhoneNumber" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="PhoneNumber" class="form-control" />
<span asp-validation-for="PhoneNumber" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Email" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Address1" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Address1" class="form-control" />
<span asp-validation-for="Address1" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Address2" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Address2" class="form-control" />
<span asp-validation-for="Address2" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="City" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="City" class="form-control" />
<span asp-validation-for="City" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="State" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="State" class="form-control" />
<span asp-validation-for="State" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Zip" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Zip" class="form-control" />
<span asp-validation-for="Zip" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
</div>
</form>
<div>
<a asp-action="Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Delete
@model StudentApplication.Web.Helper.StudentDTO
@{
ViewData["Title"] = "Delete";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>Delete</h2>
<h3>Are you sure you want to delete this?</h3>
<div>
<h4>Student</h4>
<hr />
<dl class="dl-horizontal">
<input type="hidden" asp-for="StudentId" />
<dt>
@Html.DisplayNameFor(model => model.FirstName)
</dt>
<dd>
@Html.DisplayFor(model => model.FirstName)
</dd>
<dt>
@Html.DisplayNameFor(model => model.LastName)
</dt>
<dd>
@Html.DisplayFor(model => model.LastName)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Gender)
</dt>
<dd>
@Html.DisplayFor(model => model.Gender)
</dd>
<dt>
@Html.DisplayNameFor(model => model.DateOfBirth)
</dt>
<dd>
@Html.DisplayFor(model => model.DateOfBirth)
</dd>
<dt>
@Html.DisplayNameFor(model => model.DateOfRegistration)
</dt>
<dd>
@Html.DisplayFor(model => model.DateOfRegistration)
</dd>
<dt>
@Html.DisplayNameFor(model => model.PhoneNumber)
</dt>
<dd>
@Html.DisplayFor(model => model.PhoneNumber)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Email)
</dt>
<dd>
@Html.DisplayFor(model => model.Email)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Address1)
</dt>
<dd>
@Html.DisplayFor(model => model.Address1)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Address2)
</dt>
<dd>
@Html.DisplayFor(model => model.Address2)
</dd>
<dt>
@Html.DisplayNameFor(model => model.City)
</dt>
<dd>
@Html.DisplayFor(model => model.City)
</dd>
<dt>
@Html.DisplayNameFor(model => model.State)
</dt>
<dd>
@Html.DisplayFor(model => model.State)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Zip)
</dt>
<dd>
@Html.DisplayFor(model => model.Zip)
</dd>
</dl>
<form asp-action="Delete">
<div class="form-actions no-color">
<input type="submit" value="Delete" class="btn btn-default" /> |
<a asp-action="Index">Back to List</a>
</div>
</form>
</div>
All markup above is a strongly-typed View as all of them are referencing the same object Model which is StudentApplication.Web.Helper.StudentDTO. They also used tag-helpers to define the HTML and data binding.
Testing the Application
Since this demo is composed of two projects and our MVC application relies on our Web API application, we need to make the Web API accessible when testing the page. Typically, we will host or deploy both projects in IIS web server to be able to connect between projects. Luckily, one of the cool features in Visual Studio 2017 is to enable multiple start-up projects. This means we could run both our Web API and MVC application within Visual Studio and be able to test them right away without having them to deploy in IIS. All you need to do is:
- Right click on the Solution
- Select Set Startup Projects
- Select Multiple Startup Projects radio button
- Select "Start" as the action for both Web API and MVC project
- Click Apply and then OK
Now build and run the application using CTRL + 5 keys. In the browser, navigate to /students/Create. It should bring up the following page.
Supply the fields and click the "Create" button and it should navigate you to the Index page with the new student record added as shown in the figure below:
Of course the Edit and Delete works too :) Feel free to download the source code and have fun!
Summary
In this series, we’ve learned to build an ASP.NET Core MVC application using Entity Framework Core with a new database. We also learned how to create a Web API Controller and have the Controller talked to an Interface. We also learned how to implement a simple Data Access using a repository pattern to handle CRUD operations. Finally, we've learned how to create a basic ASP.NET Core MVC application that consumes a Web API endpoints to perform CRUD in the UI.
Source Code
Feel free to download the source code at the Gallery section here: EF Core Code-First Development
See Also
- Introduction to ASP.NET Core
- Getting Started with Entity Framework Core: Database-First Development
- Getting Started with EF Core on ASP.NET Core with a New database
- Configuring a DbContext
- Learn About ASP.NET Web API from the Official Website