ASP.NET 5 REST API tutorial
In this video we will discuss how to build a REST API from scratch using .NET 5. Along the way you will learn various aspects of building effective APIs using the latest framework from Microsoft .NET 5.0.
Download Project Source Code
The following are the steps to download and setup the project on your local development machine.
- Click here to download project source code
- .NET 5 REST API tutorial.zip contains the source.
- Extract all the files.
- Double click on the solution file - BlazorWebAssemblyTutorial.sln file. You will find this file in BlazorWebAssemblyTutorial folder.
- Once you have the solution open. Execute the command (
Update-Database
) from Package Manager Console. Make sureBlazorProject.Server
project is set as the Default project. - This migration creates the Employee database and the required tables.
- Run the project using
CTRL+F5
You can find all the videos, slides and notes in sequence on the following page.
Blazor Webassembly tutorial notes, slides and videos
If you have couple of minutes, please leave your rating and valuable feedback on our course page on the Feedback tab at the following URL.
What we will build in this course
An API that provides Employee and Department data. For example, we want the employee data to be available at an endpoint that looks like the following.
- The protocol that is used here is
http
. We can also useHTTPS
to be a bit more secure. pragimtech.com
is the domain.- The path
/api
in the URI indicates that this is an API. This is just a convention that most people follow. With this convention just by looking at the URL, we can say this is a REST API URL. - Finally
/employees
is the endpoint at which we have the resource i.e list of employees in this case.
Similarly, we want the Departments
data to be available at an endpoint that looks like the following.
So in a REST API, each resource, is identified by a specific URI. For example, the list of employees are available at the URI http://pragimtech.com/api/employees
. Similarly, the list of Departments are available at the URI http://pragimtech.com/api/departments
.
Along with the URI, we also need to send an HTTP verb to the server. The following are the common HTTP verbs.
- GET
- POST
- PUT
- PATCH
- DELETE
It is this HTTP verb that tells the API what to do with the resource. Four common operations that we do on most resources - CREATE, READ, UPDATE or DELETE. To CREATE a resource we use the HTTP verb POST, to READ - GET. to UPDATE - PUT or PATCH and to DELETE - DELETE.
So the combination of the URI and the HTTP verb, that is sent with each request tells the API what to do with the resource.
For example
- The HTTP verb GET to the URI (/api/employees) gets the list of all employees.
- The same HTTP verb GET to this URI (/api/employees/1) gets the employee with Id = 1.
- POST verb to the URI (/api/employees) creates a new employee.
- The verb DELETE to the URI (/api/employees/1), Deletes employee with Id = 1
- The verb PUT or PATCH to the same URI (/api/employees/1), updates employee with Id = 1
PUT V/S PATCH
PUT updates the entire object i.e FirstName, LastName, DOB, Gender, Email etc of an employee. Basically all the properties of the object are updated.
PATCH is used when you want to do a partial update i.e only a subset of the properties. May be just FirstName and Gender of an employee object.
If you are new to REST APIs, we discussed what is a REST API in detail in the following video.
Solution Layout
Take a look at the projects we have in the solution explorer. We created this project using Blazor WebAssembly App
template. We have 3 projects generted by Visual Studio 2019.
- BlazorProject.Client - This is a Blazor Web Project that runs on the client side in the browser.
- BlazorProject.Server - This project does two things. Contains REST API that provides data to Blazor client project and also hosts the blazor client project.
- BlazorProject.Shared - As the name implies this projects is shared both by the Client and Server projects. It contains the model classes used by both the projects.
At the moment in the Shared project we have 2 model classes (i.e Employee.cs and Department.cs) and we also have 1 enum (and that is Gender.cs)
Employee class in Employee.cs
using System;
using System.ComponentModel.DataAnnotations;
namespace BlazorProject.Shared
{
public class Employee
{
public int EmployeeId { get; set; }
[Required]
[MinLength(2, ErrorMessage = "FirstName must contains at least 2 charcters")]
public string FirstName { get; set; }
[Required]
public string LastName { get; set; }
public string Email { get; set; }
public DateTime DateOfBrith { get; set; }
public Gender Gender { get; set; }
public int DepartmentId { get; set; }
public string PhotoPath { get; set; }
public Department Department { get; set; }
}
}
Department class in Department.cs
public class Department
{
public int DepartmentId { get; set; }
public string DepartmentName { get; set; }
}
Gender enum in Gender.cs
public enum Gender
{
Male,
Female,
Other
}
Adding database support
To add database support we will use Entity Framework Core 5.0. Install the following 3 NuGet packages in the order specified.
Install-Package Microsoft.EntityFrameworkCore
Install-Package Microsoft.EntityFrameworkCore.SqlServer
Install-Package Microsoft.EntityFrameworkCore.Tools
Entity Framework Core DbContext class
One of the very important classes in Entity Framework Core is the DbContext
class. This is the class that we use in our application code to interact with the underlying database. It is this class that manages the database connection and is used to retrieve and save data in the database.
To use the DbContext class in our application
- We create a class that derives from the DbContext class.
- DbContext class is in Microsoft.EntityFrameworkCore namespace.
public class AppDbContext : DbContext
{ }
DbContextOptions in Entity Framework Core
- For the
DbContext
class to be able to do any useful work, it needs an instance of theDbContextOptions
class. - The
DbContextOptions
instance carries configuration information such as the connection string, database provider to use etc. - We pass the
DbContextOptions
to the baseDbContext
class constructor using thebase
keyword.
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options)
{
}
}
Entity Framework Core DbSet
- The
DbContext
class includes aDbSet<TEntity>
property for each entity in the model. - At the moment in our application we have, 2 entity classes - Employee and Department.
- So in our
AppDbContext
class we have 2 correspondingDbSet
properties.
DbSet<Employee>
DbSet<Department>
- We will use these
DbSet
properties to query and save instances of Employee and Department classes. - The LINQ queries against the
DbSet
properties will be translated into SQL queries against the underlying database.
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options)
{
}
public DbSet<Employee> Employees { get; set; }
public DbSet<Department> Departments { get; set; }
}
Seeding Data
Override OnModelCreating
method to seed Employee and Department data.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Code to seed data
}
AppDbContext class complete code
Create Models
folder and include the following AppDbContext
class in it.
using BlazorProject.Shared;
using Microsoft.EntityFrameworkCore;
using System;
namespace BlazorProject.Server.Models
{
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options)
{
}
public DbSet<Employee> Employees { get; set; }
public DbSet<Department> Departments { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
//Seed Departments Table
modelBuilder.Entity<Department>().HasData(
new Department { DepartmentId = 1, DepartmentName = "IT" });
modelBuilder.Entity<Department>().HasData(
new Department { DepartmentId = 2, DepartmentName = "HR" });
modelBuilder.Entity<Department>().HasData(
new Department { DepartmentId = 3, DepartmentName = "Payroll" });
modelBuilder.Entity<Department>().HasData(
new Department { DepartmentId = 4, DepartmentName = "Admin" });
// Seed Employee Table
modelBuilder.Entity<Employee>().HasData(new Employee
{
EmployeeId = 1,
FirstName = "John",
LastName = "Hastings",
Email = "David@pragimtech.com",
DateOfBrith = new DateTime(1980, 10, 5),
Gender = Gender.Male,
DepartmentId = 1,
PhotoPath = "images/john.png"
});
modelBuilder.Entity<Employee>().HasData(new Employee
{
EmployeeId = 2,
FirstName = "Sam",
LastName = "Galloway",
Email = "Sam@pragimtech.com",
DateOfBrith = new DateTime(1981, 12, 22),
Gender = Gender.Male,
DepartmentId = 2,
PhotoPath = "images/sam.jpg"
});
modelBuilder.Entity<Employee>().HasData(new Employee
{
EmployeeId = 3,
FirstName = "Mary",
LastName = "Smith",
Email = "mary@pragimtech.com",
DateOfBrith = new DateTime(1979, 11, 11),
Gender = Gender.Female,
DepartmentId = 1,
PhotoPath = "images/mary.png"
});
modelBuilder.Entity<Employee>().HasData(new Employee
{
EmployeeId = 4,
FirstName = "Sara",
LastName = "Longway",
Email = "sara@pragimtech.com",
DateOfBrith = new DateTime(1982, 9, 23),
Gender = Gender.Female,
DepartmentId = 3,
PhotoPath = "images/sara.png"
});
}
}
}
Database Connection String
"ConnectionStrings": {
"DBConnection": "server=(localdb)\\MSSQLLocalDB;database=EmployeeDB;Trusted_Connection=true"
}
ConfigureServices in Startup class
Read the connection string from appsettings.json
file
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DBConnection")));
services.AddControllers();
}
Create and execute database migrations
Use the following 2 commands to create and execute the initial database migration
Add-Migration InitialCreate
Update-Database
Repository pattern for data access layer
What is Repository Pattern
Repository Pattern is an abstraction of the Data Access Layer. It hides the details of how exactly the data is saved or retrieved from the underlying data source. The details of how the data is stored and retrieved is in the respective repository. For example, you may have a repository that stores and retrieves data from an in-memory collection. You may have another repository that stores and retrieves data from a database like SQL Server. Yet another repository that stores and retrieves data from an XML file.
Repository Pattern Interface
The interface in the repository pattern specifies
- What operations (i.e methods) are supported by the repository
- The data required for each of the operations i.e the parameters that need to be passed to the method and the data the method returns
- As you can see the repository interface only contains what it can do, but not, how it does, what it can do
- The actual implementation details are in the respective repository class that implements the repository Interface
public interface IEmployeeRepository
{
Task<IEnumerable<Employee>> Search(string name, Gender? gender);
Task<IEnumerable<Employee>> GetEmployees();
Task<Employee> GetEmployee(int employeeId);
Task<Employee> GetEmployeeByEmail(string email);
Task<Employee> AddEmployee(Employee employee);
Task<Employee> UpdateEmployee(Employee employee);
Task DeleteEmployee(int employeeId);
}
IEmployeeRepository interface supports the following operations
- Search employees by name and gender
- Get all the employees
- Get a single employee by id
- Get an employee by their email address
- Add a new employee
- Updat an employee
- Delete an employee
The details of how these operations are implmented are in the repository class that implments this IEmployeeRepository
interface.
Repository Pattern - SQL Server Implementation
The following EmployeeRepository
class provides an implementation for IEmployeeRepository
. This specific implementation stores and retrieves employees from a sql server database.
using BlazorProject.Shared;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BlazorProject.Server.Models
{
public class EmployeeRepository : IEmployeeRepository
{
private readonly AppDbContext appDbContext;
public EmployeeRepository(AppDbContext appDbContext)
{
this.appDbContext = appDbContext;
}
public async Task<Employee> AddEmployee(Employee employee)
{
if (employee.Department != null)
{
appDbContext.Entry(employee.Department).State = EntityState.Unchanged;
}
var result = await appDbContext.Employees.AddAsync(employee);
await appDbContext.SaveChangesAsync();
return result.Entity;
}
public async Task DeleteEmployee(int employeeId)
{
var result = await appDbContext.Employees
.FirstOrDefaultAsync(e => e.EmployeeId == employeeId);
if (result != null)
{
appDbContext.Employees.Remove(result);
await appDbContext.SaveChangesAsync();
}
}
public async Task<Employee> GetEmployee(int employeeId)
{
return await appDbContext.Employees
.Include(e => e.Department)
.FirstOrDefaultAsync(e => e.EmployeeId == employeeId);
}
public async Task<Employee> GetEmployeeByEmail(string email)
{
return await appDbContext.Employees
.FirstOrDefaultAsync(e => e.Email == email);
}
public async Task<IEnumerable<Employee>> GetEmployees()
{
return await appDbContext.Employees.ToListAsync();
}
public async Task<IEnumerable<Employee>> Search(string name, Gender? gender)
{
IQueryable<Employee> query = appDbContext.Employees;
if (!string.IsNullOrEmpty(name))
{
query = query.Where(e => e.FirstName.Contains(name)
|| e.LastName.Contains(name));
}
if (gender != null)
{
query = query.Where(e => e.Gender == gender);
}
return await query.ToListAsync();
}
public async Task<Employee> UpdateEmployee(Employee employee)
{
var result = await appDbContext.Employees
.FirstOrDefaultAsync(e => e.EmployeeId == employee.EmployeeId);
if (result != null)
{
result.FirstName = employee.FirstName;
result.LastName = employee.LastName;
result.Email = employee.Email;
result.DateOfBrith = employee.DateOfBrith;
result.Gender = employee.Gender;
if (employee.DepartmentId != 0)
{
result.DepartmentId = employee.DepartmentId;
}
else if (employee.Department != null)
{
result.DepartmentId = employee.Department.DepartmentId;
}
result.PhotoPath = employee.PhotoPath;
await appDbContext.SaveChangesAsync();
return result;
}
return null;
}
}
}
DepartmentRepository Interface
public interface IDepartmentRepository
{
Task<IEnumerable<Department>> GetDepartments();
Task<Department> GetDepartment(int departmentId);
}
DepartmentRepository Implementation
using BlazorProject.Shared;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace BlazorProject.Server.Models
{
public class DepartmentRepository : IDepartmentRepository
{
private readonly AppDbContext appDbContext;
public DepartmentRepository(AppDbContext appDbContext)
{
this.appDbContext = appDbContext;
}
public async Task<Department> GetDepartment(int departmentId)
{
return await appDbContext.Departments
.FirstOrDefaultAsync(d => d.DepartmentId == departmentId);
}
public async Task<IEnumerable<Department>> GetDepartments()
{
return await appDbContext.Departments.ToListAsync();
}
}
}
Which implementation to use
With the following 2 lines of code, ASP.NET provides an instance of EmployeeRepository
class when an instance of IEmployeeRepository
is requested. Similarly an instance of DepartmentRepository
class is provided when an instance of IDepartmentRepository
is requested.
public void ConfigureServices(IServiceCollection services)
{
// Rest of the code
services.AddScoped<IDepartmentRepository, DepartmentRepository>();
services.AddScoped<IEmployeeRepository, EmployeeRepository>();
}
We are using AddScoped() method because we want the instance to be alive and available for the entire scope of the given HTTP request. For another new HTTP request, a new instance of EmployeeRepository class will be provided and it will be available throughout the entire scope of that HTTP request.
We discussed the difference between AddSingleton(), AddScoped() and AddTransient() methods in detail in Part 44 of ASP.NET Core Tutorial.
Throughout our entire application, in all the places where IEmployeeRepository
is injected an instance of EmployeeRepository
is provided. If you want your application to use a different implementation instead, all you need to change is the following one line of code.
services.AddScoped<IEmployeeRepository, EmployeeRepository>();
Benefits of Repository Pattern
- The code is cleaner, and easier to reuse and maintain.
- Enables us to create loosely coupled systems. For example, if we want our application to work with oracle database instead of sql server database, implement an OracleRepository that knows how to read and write to Oracle database and register OracleRepository with the dependency injection system.
- In an unit testing project, it is easy to replace a real repository with a fake implementation for testing.
Building a REST API
Our next step is to create the REST API itself. In .NET to create a REST API, we create a Controller class that derives from the built-in ControllerBase
class
Controller class can either derive from the built-in Controller
or ControllerBase
class. One confusion here is, which built-in class to use as the base class. Well, the answer is very simple.
If you are creating a REST API, make your Controller class derive from ControllerBase and not Controller class. Controller derives from ControllerBase and adds support for MVC views.
So create a controller that derives from Controller class if you are building an MVC web application. On the other hand, if you are creating a REST Web API, create a controller class that derives from ControllerBase class. So in short, Controller is for MVC web applications and ControllerBase is for MVC Web APIs.
If you are planning to use the controller both for a web application and for a web api, then derive it from the Controller class.
EmployeeController.cs
using BlazorProject.Server.Models;
using BlazorProject.Shared;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BlazorProject.Server.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class EmployeesController : ControllerBase
{
private readonly IEmployeeRepository employeeRepository;
public EmployeesController(IEmployeeRepository employeeRepository)
{
this.employeeRepository = employeeRepository;
}
[HttpGet("{search}")]
public async Task<ActionResult<IEnumerable<Employee>>> Search(string name, Gender? gender)
{
try
{
var result = await employeeRepository.Search(name, gender);
if (result.Any())
{
return Ok(result);
}
return NotFound();
}
catch (Exception)
{
return StatusCode(StatusCodes.Status500InternalServerError,
"Error retrieving data from the database");
}
}
[HttpGet]
public async Task<ActionResult> GetEmployees()
{
try
{
return Ok(await employeeRepository.GetEmployees());
}
catch (Exception)
{
return StatusCode(StatusCodes.Status500InternalServerError,
"Error retrieving data from the database");
}
}
[HttpGet("{id:int}")]
public async Task<ActionResult<Employee>> GetEmployee(int id)
{
try
{
var result = await employeeRepository.GetEmployee(id);
if (result == null)
{
return NotFound();
}
return result;
}
catch (Exception)
{
return StatusCode(StatusCodes.Status500InternalServerError,
"Error retrieving data from the database");
}
}
[HttpPost]
public async Task<ActionResult<Employee>> CreateEmployee(Employee employee)
{
try
{
if (employee == null)
return BadRequest();
var emp = await employeeRepository.GetEmployeeByEmail(employee.Email);
if(emp != null)
{
ModelState.AddModelError("Email", "Employee email already in use");
return BadRequest(ModelState);
}
var createdEmployee = await employeeRepository.AddEmployee(employee);
return CreatedAtAction(nameof(GetEmployee),
new { id = createdEmployee.EmployeeId }, createdEmployee);
}
catch (Exception ex)
{
return StatusCode(StatusCodes.Status500InternalServerError,
"Error creating new employee record");
}
}
[HttpPut("{id:int}")]
public async Task<ActionResult<Employee>> UpdateEmployee(int id, Employee employee)
{
try
{
if (id != employee.EmployeeId)
return BadRequest("Employee ID mismatch");
var employeeToUpdate = await employeeRepository.GetEmployee(id);
if (employeeToUpdate == null)
{
return NotFound($"Employee with Id = {id} not found");
}
return await employeeRepository.UpdateEmployee(employee);
}
catch (Exception ex)
{
return StatusCode(StatusCodes.Status500InternalServerError,
"Error updating employee record");
}
}
[HttpDelete("{id:int}")]
public async Task<ActionResult> DeleteEmployee(int id)
{
try
{
var employeeToDelete = await employeeRepository.GetEmployee(id);
if (employeeToDelete == null)
{
return NotFound($"Employee with Id = {id} not found");
}
await employeeRepository.DeleteEmployee(id);
return Ok($"Employee with Id = {id} deleted");
}
catch (Exception ex)
{
return StatusCode(StatusCodes.Status500InternalServerError,
"Error deleting employee record");
}
}
}
}
DepartmentController.cs
using BlazorProject.Server.Models;
using BlazorProject.Shared;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Threading.Tasks;
namespace BlazorProject.Server.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class DepartmentsController : ControllerBase
{
private readonly IDepartmentRepository departmentRepository;
public DepartmentsController(IDepartmentRepository departmentRepository)
{
this.departmentRepository = departmentRepository;
}
[HttpGet]
public async Task<ActionResult> GetDepartments()
{
try
{
return Ok(await departmentRepository.GetDepartments());
}
catch (Exception)
{
return StatusCode(StatusCodes.Status500InternalServerError,
"Error retrieving data from the database");
}
}
[HttpGet("{id:int}")]
public async Task<ActionResult<Department>> GetDepartment(int id)
{
try
{
var result = await departmentRepository.GetDepartment(id);
if (result == null)
{
return NotFound();
}
return result;
}
catch (Exception)
{
return StatusCode(StatusCodes.Status500InternalServerError,
"Error retrieving data from the database");
}
}
}
}
© 2020 Pragimtech. All Rights Reserved.