Table of Contents
Introduction
Web APIs are ubiquitous on the web, used by millions of people every day to buy, sell and book travel. Microsoft ASP.NET Core Cross-Platform provides a powerful and flexible way to create robust and scalable ASP.NET Core Web APIs.
In this article, we will discover the key concepts and step-by-step instructions for creating a robust and scalable ASP.NET Core Web API.
In the previous articles, we created a database using the Entity Framework Code First Approach in .NET 8 and a data access layer using the Generic Repository Pattern C#. It is recommended to read these two articles if you want to follow the example.
What is a web API?
API stands for Application Programming Interface, which provides programming interfaces for the web and allows us to create software applications that can communicate with each other over the internet and exchange information using the HTTP and HTTPS protocols, which are used by most web APIs and are the most widely used protocols for web APIs.
What is a REST API?
REST stands for Representational State Transfer and is a special style of web API that follows certain principles and constraints and is usually implemented via HTTP and HTTPS protocols.
API-first approach
The “Design First” approach means that the API contract is designed first, before the code is written. This allows the developer to integrate the API with frontend applications by mocking the APIs. This includes the definition of resources and API endpoints (resource-based URLs) as well as the mapping of the HTTP to be used by the application and, of course, the documentation of the API.
What is ASP.NET Core Web API?
ASP.NET Core Web API is a framework provided by Microsoft that enables developers to create Restful Web APIs and expose data or services to other software, including mobile devices and browsers, or to other services over HTTP and HTTPS protocols.
ASP.NET Core Web API is based on REST principles and enables the creation of stateless APIs with resource-based URLs and standard HTTP methods (GET
, POST
, PUT
, DELETE
, etc.).
Setting up the project and Designing the Web API
In the previous section we have discovered the basic concepts of web APIs. In this section we will create a REST Web API using ASP.NET Core Web API framework.
In a previous article, we created a database using the Entity Framework Code First Approach in .NET 8 as shown in the following diagram.

Then we created a data access layer using the Generic Repository Pattern C# as shown below.

In this article, we focus on one entity in our schema, which is the Employee
entity, which has a many-to-many relationship with the Project
entity and a one-to-many relationship with the Department
entity.
Creating the Data transfer object
A DTO (Data Transfer Object) is a simple object that acts as a container and has no behavior that is used to transfer data between the application layers (e.g. between the presentation layers and the domain layers).
First add a new class library named Dnc.Objects
to the Staff
solution and then create a new directory in the Dnc.Objects
project named Staff. Then add a new class file named StaffEmployee.cs
as shown below.
namespace Dnc.Objects.Staff
{
public class StaffEmployee
{
public int Id { get; set; }
public string Name { get; set; }
public string Job { get; set; }
public DateTime HireDate { get; set; }
public decimal Salary { get; set; }
public int? ManagerId { get; set; }
public int StaffDepartmentId { get; set; }
public StaffDepartment StaffDepartment { get; set; }
public StaffEmployee Manager { get; set; }
public IEnumerable<StaffProject> StaffProjects { get; set; }
}
}
Finally add two new class files with the namesStaffDepartment.cs
andStaffProject.cs
(see below).
namespace Dnc.Objects.Staff
{
public class StaffDepartment
{
public int Id { get; set; }
public string Name { get; set; }
public string Location { get; set; }
public IEnumerable<StaffEmployee> StaffEmployees { get; set; }
}
}
namespace Dnc.Objects.Staff
{
public class StaffProject
{
public int Id { get; set; }
public DateOnly StartDate { get; set; }
public DateOnly EndDate { get; set; }
public IEnumerable<StaffEmployee> StaffEmployees { get; set; }
}
}
Defining the API Endpoints
First we will identify operation events and Design the API endpoints.
- GET
/api/staffEmployees
View all basicstaffEmploees
- GET
/api/staffEmployees/with-department-and-projects
View allstaffEmploees
with associated department and projects - GET
/api/staffEmployees/{id}
Get theStaffEmployee
by id - GET
/api/staffEmployees/{id}/with-projects
Get theStaffEmployee
by id with associated projects - GET
/api/staffEmployees/{id}/with-department-and-projects
Get theStaffEmployee
by id with associated department and projects - GET
/api/staffEmployees/by-job/{job}
Get basicStaffEmployees
by job - GET
/api/staffEmployees/by-name/{name}/with-department-and-projects
Get basicStaffEmployees
by name with the associated department and projects - POST
/api/staffEmployees
Create a newstaffEmploee
- POST
/api/staffEmployees/range
Create a rangestaffEmploees
- PUT
/api/staffEmployees
Update an existingstaffEmploee
- PUT
/api/staffEmployees/range
Update a rangestaffEmploees
- DELETE
/api/staffEmployees
Delete an existingstaffEmploee
- DELETE
/api/staffEmployees/range
Delete a rangestaffEmploees
Creating the Service layer
In an ASP.NET Web API, the service layer is a design pattern that serves to decouple the business logic from the data access and controller layers. This separation of concerns makes the application maintainable, testable and scalable.
First add a new class library named Dnc.Staff.Services
to the Staff
solution, and add references to Dnc.Objects
and Dnc.Staff.Repository
projects . Then create a new directory in the Dnc.Staff.Services
project named Interfaces and add a new interface file named IStaffEmployeeService.cs
as shown below.
namespace Dnc.Staff.Services.Interfaces
{
public interface IStaffEmployeeService
{
Task<IEnumerable<StaffEmployee>> GetAllStaffEmployees();
Task<IEnumerable<StaffEmployee>> GetAllEmployeesIncludeDepartmentAndProjects();
Task<StaffEmployee> GetStaffEmployeeByKey(int key);
Task<StaffEmployee> GetStaffEmployeeByKeyIncludeProjects(int key);
Task<StaffEmployee> GetStaffEmployeeByKeyIncludeDepartmentAndProjects(int key);
Task<IEnumerable<StaffEmployee>> GetStaffEmployeesByJob(string job);
Task<IEnumerable<StaffEmployee>> GetStaffEmployeesByNameIncludeDepartmentAndProjects(string name);
Task<int> AddStaffEmployee(StaffEmployee staffEmployee);
Task<int> AddRangeStaffEmployees(IEnumerable<StaffEmployee> staffEmployees);
Task<int> UpdateStaffEmployee(StaffEmployee staffEmployee);
Task<int> UpdateRangeStaffEmployees(IEnumerable<StaffEmployee> staffEmployees);
Task<int> DeleteStaffEmployee(StaffEmployee staffEmployee);
Task<int> DeleteRangeStaffEmployees(IEnumerable<StaffEmployee> staffEmployees);
}
}
Before we implement our interface, we need to map our entities to data transfer objects and vice versa. The DTOs are often mapped to the entities using mapping tools like AutoMapper, but I have chosen to map them using extension methods.
Create a new directory in the Dnc.Staff.Services
project named Extensions. Then add a new class file named EmployeeExtensions.cs
as shown below.
namespace Dnc.Staff.Services.Extensions
{
public static class EmployeeExtensions
{
public static StaffEmployee ToStaffEmployee(this Employee employee,bool basic = false)
{
if (employee == null)
{
return null;
}
return new StaffEmployee
{
Id = employee.Id,
Name = employee.Name,
HireDate = employee.HireDate,
Job = employee.Job,
Salary = employee.Salary,
ManagerId = employee.ManagerId ?? 0,
StaffDepartment = basic ? null : employee.Department.ToStaffDepartment(true),
StaffDepartmentId = employee.DepartmentId,
StaffProjects = employee.Projects?.ToStaffProjects()
};
}
public static IEnumerable<StaffEmployee> ToStaffEmployees(this IEnumerable<Employee> employees, bool basic = false)
{
return employees.Select(v => v.ToStaffEmployee(basic));
}
public static Employee ToEmployee(this StaffEmployee staffEmployee)
{
if (staffEmployee == null)
{
return null;
}
return new Employee
{
Id = staffEmployee.Id,
Name = staffEmployee.Name,
HireDate = staffEmployee.HireDate,
Job = staffEmployee.Job,
Salary = staffEmployee.Salary,
ManagerId = staffEmployee.ManagerId,
DepartmentId = staffEmployee.StaffDepartmentId,
Projects = staffEmployee.StaffProjects?.ToProjects()
};
}
public static IEnumerable<Employee> ToEmployees(this IEnumerable<StaffEmployee> staffEmployees)
{
return staffEmployees.Select(v => v.ToEmployee());
}
}
}
For the sake of brevity, the extension methods for Department.cs
and Project.cs
can be found in the full example code for this project in my GitHub repository.
Now we are ready to implement our interface. Add a new class file named StaffEmployeeService.cs
in the Dnc.Staff.Services
project as shown below.
namespace Dnc.Staff.Services
{
public class StaffEmployeeService(IGenericRepository<Employee> employeeRepository) : IStaffEmployeeService
{
private readonly IGenericRepository<Employee> employeeRepository = employeeRepository;
private readonly Expression<Func<Employee, object>>[] includeProjects =
ExpressionBuilder.BuildExpressions<Employee>([nameof(Employee.Projects)]);
private readonly Expression<Func<Employee, object>>[] includeDepartmentAndProjects =
ExpressionBuilder.BuildExpressions<Employee>([nameof(Employee.Department), nameof(Employee.Projects)]);
public async Task<IEnumerable<StaffEmployee>> GetAllStaffEmployees()
{
var employees = await employeeRepository.GetAllAsync();
return employees?.ToStaffEmployees();
}
public async Task<IEnumerable<StaffEmployee>> GetAllEmployeesIncludeDepartmentAndProjects()
{
var employees = await employeeRepository.GetAllIncludeAsync(includeDepartmentAndProjects);
return employees?.ToStaffEmployees();
}
public async Task<StaffEmployee> GetStaffEmployeeByKey(int key)
{
var employee = await employeeRepository.FindByKey(key);
return employee?.ToStaffEmployee();
}
public async Task<StaffEmployee> GetStaffEmployeeByKeyIncludeProjects(int key)
{
var employees = await employeeRepository.GetByIncludeAsync(v => v.Id == key, includeProjects);
var employee = employees?.SingleOrDefault();
return employee?.ToStaffEmployee();
}
public async Task<StaffEmployee> GetStaffEmployeeByKeyIncludeDepartmentAndProjects(int key)
{
var employees = await employeeRepository.GetByIncludeAsync(v => v.Id == key, includeDepartmentAndProjects);
var employee = employees?.SingleOrDefault();
return employee?.ToStaffEmployee();
}
public async Task<IEnumerable<StaffEmployee>> GetStaffEmployeesByJob(string job)
{
var employees = await employeeRepository.FindByAsync(v => v.Job == job);
return employees?.ToStaffEmployees();
}
public async Task<IEnumerable<StaffEmployee>> GetStaffEmployeesByNameIncludeDepartmentAndProjects(string name)
{
var employees = await employeeRepository.GetByIncludeAsync(v => v.Name == name, includeDepartmentAndProjects);
return employees?.ToStaffEmployees();
}
public async Task<int> AddStaffEmployee(StaffEmployee staffEmployee)
{
return await employeeRepository.AddAsync(staffEmployee.ToEmployee());
}
public async Task<int> AddRangeStaffEmployees(IEnumerable<StaffEmployee> staffEmployees)
{
return await employeeRepository.AddRangeAsync(staffEmployees.ToEmployees());
}
public async Task<int> UpdateStaffEmployee(StaffEmployee staffEmployee)
{
return await employeeRepository.UpdateAsync(staffEmployee.ToEmployee());
}
public async Task<int> UpdateRangeStaffEmployees(IEnumerable<StaffEmployee> staffEmployees)
{
return await employeeRepository.UpdateRangeAsync(staffEmployees.ToEmployees());
}
public async Task<int> DeleteStaffEmployee(StaffEmployee staffEmployee)
{
return await employeeRepository.DeleteAsync(staffEmployee.ToEmployee());
}
public async Task<int> DeleteRangeStaffEmployees(IEnumerable<StaffEmployee> staffEmployees)
{
return await employeeRepository.DeleteRangeAsync(staffEmployees.ToEmployees());
}
}
}
As you see above we injected the Generic Repositoy created in the previous article Generic Repository Pattern C# to implement our interface.
We used the lambda expression to build the predicate parameters, as you can see in this section.
Finally, create a new directory called Utilities in the Dnc.Staff.Services
project. Then add a new class file called ExpressionBuilder.cs
(see below).
namespace Dnc.Staff.Services.Utilities
{
public static class ExpressionBuilder
{
public static Expression<Func<TItem, object>>[] BuildExpressions<TItem>(string[] properties)
{
var expressions = new List<Expression<Func<TItem, object>>>();
foreach (var property in properties)
{
var parameter = Expression.Parameter(typeof(TItem), "item");
var member = Expression.PropertyOrField(parameter, property);
var lambda = Expression.Lambda<Func<TItem, object>>(member, parameter);
expressions.Add(lambda);
}
return [.. expressions];
}
}
}
For the sake of brevity, the code for IStaffDepartmentService.cs
StaffDepartmentService.cs
, IStaffProjectService.cs
and StaffProjectService.cs
, can be found in the full sample code for this project in my GitHub repository.
The project should look like this at this point.

Also read https://dotnetcoder.com/client-credentials-flow-in-entra-id/
Creating the ASP.NET Core Web API project
In this part, we will implement our endpoints API using the ASP.NET Core Web API projects.
First, add a new ASP.NET Core Web API project template named Dnc.Staff.ApiApp
to the Staff
solution. Select .NET 8 (Long Term Support) as the target Framework, set it as the startup project and then add references to the Dnc.Objecs
and Dnc.Staff.Services
projects.

Then add a new controller with the name StaffEmployeeController.cs
to the controller directory, as shown in the following code.
namespace Dnc.Staff.ApiApp.Controllers
{
[Route("api/staffEmployees")]
[ApiController]
public class StaffEmployeeController(IStaffEmployeeService staffEmployeeService) : ControllerBase
{
private readonly IStaffEmployeeService staffEmployeeService = staffEmployeeService;
[HttpGet]
public async Task<ActionResult<IEnumerable<StaffEmployee>>> GetAllStaffEmployees()
{
return Ok(await staffEmployeeService.GetAllStaffEmployees());
}
[HttpGet("with-department-and-projects")]
public async Task<ActionResult<IEnumerable<StaffEmployee>>> GetAllEmployeesIncludeDepartmentAndProjects()
{
return Ok(await staffEmployeeService.GetAllEmployeesIncludeDepartmentAndProjects());
}
[HttpGet("{id}")]
public async Task<ActionResult<StaffEmployee>> GetStaffEmployeeByKey(int id)
{
var employee = await staffEmployeeService.GetStaffEmployeeByKey(id);
if (employee == null)
{
return NotFound();
}
return Ok(employee);
}
[HttpGet("{id}/with-projects")]
public async Task<ActionResult<StaffEmployee>> GetStaffEmployeeByKeyIncludeProjects(int id)
{
var employee = await staffEmployeeService.GetStaffEmployeeByKeyIncludeProjects(id);
if (employee == null)
{
return NotFound();
}
return Ok(employee);
}
[HttpGet("{id}/with-department-and-projects")]
public async Task<ActionResult<StaffEmployee>> GetStaffEmployeeByKeyIncludeDepartmentAndProjects(int id)
{
var employee = await staffEmployeeService.GetStaffEmployeeByKeyIncludeDepartmentAndProjects(id);
if (employee == null)
{
return NotFound();
}
return Ok(employee);
}
[HttpGet("by-job/{job}")]
public async Task<ActionResult<IEnumerable<StaffEmployee>>> GetStaffEmployeesByJob(string job)
{
var employees = await staffEmployeeService.GetStaffEmployeesByJob(job);
if (!employees.Any())
{
return NotFound();
}
return Ok(employees);
}
[HttpGet("by-name/{name}/with-department-and-projects")]
public async Task<ActionResult<IEnumerable<StaffEmployee>>> GetStaffEmployeesByNameIncludeDepartmentAndProjects(string name)
{
var employees = await staffEmployeeService.GetStaffEmployeesByNameIncludeDepartmentAndProjects(name);
if (!employees.Any())
{
return NotFound();
}
return Ok(employees);
}
[HttpPost]
public async Task<ActionResult> CreateStaffEmployee(StaffEmployee staffEmployee)
{
var effected = await staffEmployeeService.AddStaffEmployee(staffEmployee);
if(effected == 0)
{
return NotFound();
}
return NoContent();
}
[HttpPost("range")]
public async Task<ActionResult> CreateStaffEmployees(IEnumerable<StaffEmployee> staffEmployees)
{
var effected = await staffEmployeeService.AddRangeStaffEmployees(staffEmployees);
if (effected == 0)
{
return NotFound();
}
return NoContent();
}
[HttpPut]
public async Task<ActionResult> UpdateStaffEmployee(StaffEmployee staffEmployee)
{
var effected = await staffEmployeeService.UpdateStaffEmployee(staffEmployee);
if (effected == 0)
{
return NotFound();
}
return NoContent();
}
[HttpPut("range")]
public async Task<ActionResult> UpdateStaffEmployees(IEnumerable<StaffEmployee> staffEmployees)
{
var effected = await staffEmployeeService.UpdateRangeStaffEmployees(staffEmployees);
if (effected == 0)
{
return NotFound();
}
return NoContent();
}
[HttpDelete]
public async Task<ActionResult> DeleteStaffEmployee(StaffEmployee staffEmployee)
{
var effected = await staffEmployeeService.DeleteStaffEmployee(staffEmployee);
if (effected == 0)
{
return NotFound();
}
return NoContent();
}
[HttpDelete("range")]
public async Task<ActionResult> DeleteStaffEmployees(IEnumerable<StaffEmployee> staffEmployees)
{
var effected = await staffEmployeeService.DeleteRangeStaffEmployees(staffEmployees);
if (effected == 0)
{
return NotFound();
}
return NoContent();
}
}
}
The StaffEmployeeControlle
r receives the IStaffEmployeeService
via dependency injection, which can be added to the DI container in the Program.cs
file.
Add the following code to the Program.cs
file to inject the required services into the dependent components.
// Removed code for brevity
builder.Services.AddDbContext<StaffDbContext>(options =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnectionString"));
});
builder.Services.AddScoped(typeof(IGenericRepository<>), typeof(GenericRepository<>));
builder.Services.AddScoped<IStaffDepartmentService, StaffDepartmentService>();
builder.Services.AddScoped<IStaffEmployeeService, StaffEmployeeService>();
builder.Services.AddScoped<IStaffProjectService, StaffProjectService>();
builder.Services.AddControllers()
.AddJsonOptions(options => {
options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
});
Finally change the connection string to your connection string in the appsettings.Development.json
file as shown in the following code.
{
"DefaultConnectionString": "Server=(localdb)\\MSSQLLocalDB;Database=Dnc-Staff-Database;Trusted_Connection=True;MultipleActiveResultSets=true"
}
Avoid storing security information in your code. It is recommended to securely manage the secret by using Azure Key Vault instead of embedding it directly in your code.
Finally, run the application and test it.

Tips for creating a better web API
- Think like a user during API design
- Avoid the use of verbs in your URL paths
- Make your URL paths clean and easy to understand
- Return HTTP status code
- Don’t expose your entities
- Use data transfer object
- Use caching for better performance
- Use API Documentation like Swagger
- Don’t create instances of
HttpClient
- Use
HttpClientFactory
instead
Conclusion
In this post, we learned about the Web API key concepts, then created a simple ASP.NET Core Web API with Entity Framework, and finally learned some tips for creating better Web APIs.
Also read https://dotnetcoder.com/creating-a-blazor-loading-spinner-component/
Sample Code
You can find the complete example code for this project on my GitHub repository