Introduction
CRUD stands for Create, Read, Update, and Delete, which are the four basic operations commonly performed on application data.
In this tutorial, we will build an Employee Management application using ASP.NET Core, Entity Framework Core, SQL Server, Web API, and ASP.NET Core MVC.
The application is divided into two projects:
Employee Web Application
|
v
ASP.NET Core MVC
|
| HTTP Requests
v
ASP.NET Core Web API
|
v
Entity Framework Core
|
v
SQL Server
The Web API is responsible for database operations, while the MVC application consumes the API and provides the user interface.
The application will support:
Creating employees
Viewing employees
Updating employees
Deleting employees
Validating employee information
Persisting employee data in SQL Server
Technologies Used
The implementation uses the following technologies:
ASP.NET Core Web API
ASP.NET Core MVC
Entity Framework Core
SQL Server
C#
Razor Views
HttpClientDependency Injection
Step 1: Create the Employee Model
Create an Employee class in the API project.
using System.ComponentModel.DataAnnotations;
public class Employee
{
[Key]
public int Id { get; set; }
[Required]
[Display(Name = "Employee Name")]
public string Name { get; set; } = string.Empty;
public string Designation { get; set; } = string.Empty;
[DataType(DataType.MultilineText)]
public string Address { get; set; } = string.Empty;
public DateTime? RecordCreatedOn { get; set; }
}
The properties represent the employee information stored in the database.
Idis the primary key.Namestores the employee's name.Designationstores the employee's job designation.Addressstores the employee's address.RecordCreatedOnstores the record creation date.
The [Required] attribute ensures that the employee name is required.
Step 2: Install Entity Framework Core Packages
Install the required Entity Framework Core packages in the API project.
For the original .NET 6 implementation, the package references are:
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.1" />
These packages provide the core EF functionality, SQL Server provider, and migration tooling.
For a new application, use package versions compatible with the .NET version targeted by the project rather than copying an old package version unchanged.
Step 3: Create the Data Folder
Create a folder named:
Data
Inside the folder, create:
ApplicationDbContext.cs
The ApplicationDbContext class connects the application model to Entity Framework Core.
using EmployeeCRUD.Models;
using Microsoft.EntityFrameworkCore;
namespace EmployeeCRUD.Data
{
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(
DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public DbSet<Employee> Employees { get; set; }
}
}
The important correction here is that ApplicationDbContext must inherit from DbContext.
The DbSet<Employee> property represents the Employees table managed by Entity Framework Core.
Step 4: Configure the SQL Server Connection
Open appsettings.json and add a connection string.
{
"ConnectionStrings": {
"DefaultConnection": "Server=EnterServerName;Database=EmployeeDatabase;User Id=sa;Password=EnterPassword;TrustServerCertificate=True;"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
Replace the placeholder server and authentication values with the appropriate configuration for your development environment.
For production applications, avoid storing database passwords directly in source-controlled configuration files. Use an appropriate secrets-management mechanism.
Step 5: Register Entity Framework Core
Open Program.cs and register ApplicationDbContext.
using EmployeeCRUD.Data;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<ApplicationDbContext>(
options =>
options.UseSqlServer(
builder.Configuration
.GetConnectionString("DefaultConnection")));
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
app.Run();
AddDbContext registers the database context with ASP.NET Core's built-in Dependency Injection container.
The connection string is retrieved from appsettings.json.
Step 6: Create the Database Using EF Core Migrations
After configuring the DbContext, create the initial migration.
Using the Package Manager Console:
Add-Migration Initial
Update-Database
Alternatively, the .NET CLI can be used:
dotnet ef migrations add Initial
dotnet ef database update
The migration creates the database schema based on the Employee model.
After the database update, the Employees table will contain columns corresponding to the model properties.
Step 7: Create DTOs for POST and PUT
Instead of using the database entity directly for every API request, create DTOs for create and update operations.
EmployeeCreateDto
This DTO is used when creating an employee.
using System.ComponentModel.DataAnnotations;
public class EmployeeCreateDto
{
[Required]
public string Name { get; set; } = string.Empty;
public string Designation { get; set; } = string.Empty;
public string Address { get; set; } = string.Empty;
}
EmployeeUpdateDto
This DTO is used when updating an employee.
using System.ComponentModel.DataAnnotations;
public class EmployeeUpdateDto
{
[Required]
public string Name { get; set; } = string.Empty;
public string Designation { get; set; } = string.Empty;
public string Address { get; set; } = string.Empty;
}
The DTOs intentionally do not contain Id or RecordCreatedOn.
These values are controlled by the application rather than being supplied by the client.
Step 8: Create the Employee API Controller
Create EmployeeController.cs in the API project.
The original code contained spelling inconsistencies such as EmployeeControler and ApplicaitonDbContext. They should be corrected to EmployeeController and ApplicationDbContext.
using EmployeeCRUD.Data;
using EmployeeCRUD.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace EmployeeCRUD.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class EmployeeController : ControllerBase
{
private readonly ApplicationDbContext _context;
public EmployeeController(ApplicationDbContext context)
{
_context = context;
}
// GET: api/Employee
[HttpGet]
public async Task<ActionResult<IEnumerable<Employee>>> GetAllEmployees()
{
return await _context.Employees.ToListAsync();
}
// GET: api/Employee/5
[HttpGet("{id}")]
public async Task<ActionResult<Employee>> GetEmployeeById(int id)
{
var employee = await _context.Employees.FindAsync(id);
if (employee == null)
{
return NotFound(new
{
message = $"Employee with ID {id} not found."
});
}
return Ok(employee);
}
// POST: api/Employee
[HttpPost]
public async Task<ActionResult<Employee>> CreateEmployee(
EmployeeCreateDto employeeDto)
{
var employee = new Employee
{
Name = employeeDto.Name,
Designation = employeeDto.Designation,
Address = employeeDto.Address,
RecordCreatedOn = DateTime.UtcNow
};
_context.Employees.Add(employee);
await _context.SaveChangesAsync();
return CreatedAtAction(
nameof(GetEmployeeById),
new { id = employee.Id },
employee);
}
// PUT: api/Employee/5
[HttpPut("{id}")]
public async Task<IActionResult> UpdateEmployee(
int id,
EmployeeUpdateDto employeeDto)
{
var existingEmployee =
await _context.Employees.FindAsync(id);
if (existingEmployee == null)
{
return NotFound(new
{
message = $"Employee with ID {id} does not exist."
});
}
existingEmployee.Name = employeeDto.Name;
existingEmployee.Designation = employeeDto.Designation;
existingEmployee.Address = employeeDto.Address;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!EmployeeExists(id))
{
return NotFound();
}
throw;
}
return NoContent();
}
// DELETE: api/Employee/5
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteEmployee(int id)
{
var employee =
await _context.Employees.FindAsync(id);
if (employee == null)
{
return NotFound(new
{
message = $"Employee with ID {id} not found."
});
}
_context.Employees.Remove(employee);
await _context.SaveChangesAsync();
return NoContent();
}
private bool EmployeeExists(int id)
{
return _context.Employees.Any(e => e.Id == id);
}
}
}
Step 9: Understand the API Endpoints
The controller exposes the following endpoints:
HTTP Method | Endpoint | Purpose |
|---|---|---|
GET |
| Get all employees |
GET |
| Get one employee |
POST |
| Create an employee |
PUT |
| Update an employee |
DELETE |
| Delete an employee |
GET All Employees
GET /api/Employee
The API queries the database:
await _context.Employees.ToListAsync();
and returns the employee collection.
GET Employee by ID
GET /api/Employee/5
The API searches for the employee with ID 5.
If the employee does not exist, the API returns:
404 Not Found
Create Employee
POST /api/Employee
Example request:
{
"name": "John",
"designation": "Software Developer",
"address": "Kolkata"
}
The API creates the entity and assigns RecordCreatedOn automatically.
A successful creation returns:
201 Created
Update Employee
PUT /api/Employee/5
Example request:
{
"name": "John Smith",
"designation": "Senior Developer",
"address": "Kolkata"
}
The existing entity is loaded first, and only the editable properties are changed.
Id and RecordCreatedOn remain unchanged.
The API returns:
204 No Content
Delete Employee
DELETE /api/Employee/5
The employee is removed from the database.
A successful deletion returns:
204 No Content
Step 10: Test the Web API
Before connecting the MVC application, test the API independently.
Swagger, Postman, or another HTTP client can be used to test the endpoints.
A typical CRUD sequence is:
POST
|
v
Create Employee
|
v
GET
|
v
Verify Employee
|
v
PUT
|
v
Update Employee
|
v
DELETE
|
v
Remove Employee
Testing the API separately makes it easier to determine whether a problem belongs to the API or the MVC application.
Step 11: Connect the MVC Web Project
Now create or open the ASP.NET Core MVC project that will consume the Web API.
The architecture is:
+-----------------------+
| ASP.NET Core MVC |
| Employee Web Project |
+-----------+-----------+
|
| HttpClient
v
+-----------------------+
| ASP.NET Core Web API |
+-----------+-----------+
|
| EF Core
v
+-----------------------+
| SQL Server |
+-----------------------+
The MVC application does not directly access the employee database. It communicates with the Web API through HTTP.
Step 12: Register HttpClient in the MVC Project
Open the MVC project's Program.cs and register a named HttpClient.
builder.Services.AddHttpClient("EmployeeAPI", client =>
{
client.BaseAddress =
new Uri("http://localhost:5112/");
});
Make sure the base address matches the actual URL and port used by the Web API.
The named client can then be retrieved through IHttpClientFactory.
Step 13: Create the MVC ViewModel
Create EmployeeViewModel in the MVC project.
using System.ComponentModel.DataAnnotations;
namespace Core_Web.Models
{
public class EmployeeViewModel
{
public int Id { get; set; }
[Required]
[Display(Name = "Employee Name")]
public string Name { get; set; } = string.Empty;
[Required]
public string Designation { get; set; } = string.Empty;
[Required]
public string Address { get; set; } = string.Empty;
[Display(Name = "Created On")]
public DateTime? RecordCreatedOn { get; set; }
}
}
The MVC ViewModel represents the data required by the user interface.
Step 14: Create the MVC Employee Controller
Create EmployeeController.cs in the MVC project.
using Core_Web.Models;
using Microsoft.AspNetCore.Mvc;
using System.Text;
using System.Text.Json;
namespace Core_Web.Controllers
{
public class EmployeeController : Controller
{
private readonly HttpClient _httpClient;
private readonly JsonSerializerOptions _jsonOptions;
public EmployeeController(
IHttpClientFactory httpClientFactory)
{
_httpClient =
httpClientFactory.CreateClient("EmployeeAPI");
_jsonOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
}
// GET: Employee
public async Task<IActionResult> Index()
{
List<EmployeeViewModel> employees = new();
var response =
await _httpClient.GetAsync("api/Employee");
if (response.IsSuccessStatusCode)
{
var data =
await response.Content.ReadAsStringAsync();
employees =
JsonSerializer.Deserialize<
List<EmployeeViewModel>>(
data,
_jsonOptions) ?? new();
}
return View(employees);
}
// GET: Employee/Create
[HttpGet]
public IActionResult Create()
{
return View();
}
// POST: Employee/Create
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(
EmployeeViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var json =
JsonSerializer.Serialize(model);
var content = new StringContent(
json,
Encoding.UTF8,
"application/json");
var response =
await _httpClient.PostAsync(
"api/Employee",
content);
if (response.IsSuccessStatusCode)
{
return RedirectToAction(nameof(Index));
}
ModelState.AddModelError(
"",
"Unable to create employee record.");
return View(model);
}
// GET: Employee/Edit/5
[HttpGet]
public async Task<IActionResult> Edit(int id)
{
var response =
await _httpClient.GetAsync(
$"api/Employee/{id}");
if (!response.IsSuccessStatusCode)
{
return NotFound();
}
var data =
await response.Content.ReadAsStringAsync();
var employee =
JsonSerializer.Deserialize<EmployeeViewModel>(
data,
_jsonOptions);
if (employee == null)
{
return NotFound();
}
return View(employee);
}
// POST: Employee/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(
int id,
EmployeeViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var json =
JsonSerializer.Serialize(model);
var content = new StringContent(
json,
Encoding.UTF8,
"application/json");
var response =
await _httpClient.PutAsync(
$"api/Employee/{id}",
content);
if (response.IsSuccessStatusCode)
{
return RedirectToAction(nameof(Index));
}
ModelState.AddModelError(
"",
"Unable to update employee record.");
return View(model);
}
// POST: Employee/Delete/5
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(int id)
{
var response =
await _httpClient.DeleteAsync(
$"api/Employee/{id}");
if (!response.IsSuccessStatusCode)
{
return NotFound();
}
return RedirectToAction(nameof(Index));
}
}
}
The controller uses IHttpClientFactory to communicate with the API.
It handles the following operations:
Index -> GET employees
Create -> POST employee
Edit -> GET + PUT employee
Delete -> DELETE employee
The Delete action is implemented as a POST operation rather than using a GET request to perform a state-changing operation.
Step 15: Create the Index View
Create:
Views
|
+-- Employee
|
+-- Index.cshtml
Add the following code:
@model IEnumerable<Core_Web.Models.EmployeeViewModel>
<div class="container mt-4">
<div class="d-flex justify-content-between
align-items-center mb-3">
<h2>Employee List</h2>
<a asp-action="Create"
class="btn btn-primary">
Add New Employee
</a>
</div>
<table class="table table-bordered table-striped">
<thead class="table-dark">
<tr>
<th>Name</th>
<th>Designation</th>
<th>Address</th>
<th>Created Date</th>
<th class="text-center">Actions</th>
</tr>
</thead>
<tbody>
@if (!Model.Any())
{
<tr>
<td colspan="5"
class="text-center text-muted">
No records found.
</td>
</tr>
}
else
{
@foreach (var emp in Model)
{
<tr>
<td>@emp.Name</td>
<td>@emp.Designation</td>
<td>@emp.Address</td>
<td>
@emp.RecordCreatedOn?.ToString("g")
</td>
<td class="text-center">
<a asp-action="Edit"
asp-route-id="@emp.Id"
class="btn btn-sm btn-warning">
Edit
</a>
<form asp-action="Delete"
asp-route-id="@emp.Id"
method="post"
class="d-inline">
@Html.AntiForgeryToken()
<button type="submit"
class="btn btn-sm btn-danger"
onclick="return confirm(
'Are you sure you want to delete this employee?');">
Delete
</button>
</form>
</td>
</tr>
}
}
</tbody>
</table>
</div>
This view displays all employees retrieved from the API.
The Edit button navigates to the edit page, while the Delete button submits a POST request to the MVC controller.
Step 16: Create the Create View
Create:
Views/Employee/Create.cshtml
@model Core_Web.Models.EmployeeViewModel
<div class="container mt-4"
style="max-width: 500px;">
<h2>Add Employee</h2>
<hr />
<form asp-action="Create">
<div asp-validation-summary="ModelOnly"
class="text-danger mb-3">
</div>
<div class="mb-3">
<label asp-for="Name"
class="form-label">
</label>
<input asp-for="Name"
class="form-control" />
<span asp-validation-for="Name"
class="text-danger">
</span>
</div>
<div class="mb-3">
<label asp-for="Designation"
class="form-label">
</label>
<input asp-for="Designation"
class="form-control" />
<span asp-validation-for="Designation"
class="text-danger">
</span>
</div>
<div class="mb-3">
<label asp-for="Address"
class="form-label">
</label>
<textarea asp-for="Address"
class="form-control"
rows="3">
</textarea>
<span asp-validation-for="Address"
class="text-danger">
</span>
</div>
<button type="submit"
class="btn btn-success">
Save
</button>
<a asp-action="Index"
class="btn btn-secondary">
Cancel
</a>
</form>
</div>
The form uses ASP.NET Core Tag Helpers to bind input fields to the ViewModel.
Step 17: Create the Edit View
Create:
Views/Employee/Edit.cshtml
@model Core_Web.Models.EmployeeViewModel
<div class="container mt-4"
style="max-width: 500px;">
<h2>Edit Employee</h2>
<hr />
<form asp-action="Edit">
<input type="hidden"
asp-for="Id" />
<div asp-validation-summary="ModelOnly"
class="text-danger mb-3">
</div>
<div class="mb-3">
<label asp-for="Name"
class="form-label">
</label>
<input asp-for="Name"
class="form-control" />
<span asp-validation-for="Name"
class="text-danger">
</span>
</div>
<div class="mb-3">
<label asp-for="Designation"
class="form-label">
</label>
<input asp-for="Designation"
class="form-control" />
<span asp-validation-for="Designation"
class="text-danger">
</span>
</div>
<div class="mb-3">
<label asp-for="Address"
class="form-label">
</label>
<textarea asp-for="Address"
class="form-control"
rows="3">
</textarea>
<span asp-validation-for="Address"
class="text-danger">
</span>
</div>
<button type="submit"
class="btn btn-warning">
Update
</button>
<a asp-action="Index"
class="btn btn-secondary">
Cancel
</a>
</form>
</div>
The hidden Id identifies which employee is being updated.
The RecordCreatedOn property is not included as an editable field because it is managed by the application.
Step 18: Run the Application
At this point, two applications are involved:
API Project
|
+-- Runs on API port
|
+-- Connects to SQL Server
MVC Project
|
+-- Runs on MVC port
|
+-- Sends HTTP requests to API
Start the Web API first and verify its URL.
Then start the MVC application.
Navigate to the Employee controller:
/Employee
The MVC application calls:
/api/Employee
and displays the employee records returned by the API.
Complete CRUD Flow
The complete application flow looks like this:
User
|
v
ASP.NET Core MVC
|
HttpClient
|
v
ASP.NET Core Web API
|
Dependency Injection
|
v
ApplicationDbContext
|
Entity Framework Core
|
v
SQL Server
Create
MVC Form
|
v
POST /api/Employee
|
v
EF Core
|
v
SQL Server
Read
MVC
|
v
GET /api/Employee
|
v
EF Core
|
v
SQL Server
|
v
JSON Response
|
v
MVC View
Update
Edit Form
|
v
PUT /api/Employee/{id}
|
v
EF Core
|
v
SQL Server
Delete
Delete Button
|
v
DELETE /api/Employee/{id}
|
v
EF Core
|
v
SQL Server
Important Improvements Over the Basic Implementation
There are several important design decisions in this implementation.
DTOs Protect System-Managed Properties
The create and update DTOs do not expose:
Id
RecordCreatedOn
The API controls these values.
This prevents clients from arbitrarily changing properties that should be managed by the server.
Dependency Injection
Both the API's ApplicationDbContext and the MVC application's HttpClient are provided through Dependency Injection.
This avoids manually creating dependencies inside controllers.
Asynchronous Database Operations
The API uses asynchronous EF Core methods such as:
await _context.Employees.ToListAsync();
and:
await _context.SaveChangesAsync();
This is appropriate for I/O-bound database operations in web applications.
Separate API and UI Responsibilities
The API handles:
Data access
Database operations
HTTP API responses
The MVC application handles:
HTML pages
Forms
User interaction
Calling the API
This separation makes the application easier to maintain and evolve.
Common Issues and Troubleshooting
API Port Does Not Match
If the MVC application cannot reach the API, check:
builder.Services.AddHttpClient("EmployeeAPI", client =>
{
client.BaseAddress =
new Uri("http://localhost:5112/");
});
The URL must match the actual API address.
Database Connection Failure
Verify:
SQL Server is running.
Database credentials are correct.
Database server name is correct.
The connection string matches the environment.
The migration has been applied.
Migration Command Not Found
Make sure the Entity Framework Core tooling is installed and that the project's EF Core package versions are compatible.
For CLI usage, the dotnet-ef tool may also be required.
MVC Displays No Employees
First test:
/api/Employee
directly.
If the API does not return data, troubleshoot the API/database first.
If the API works correctly, check the MVC HttpClient base URL and JSON deserialization.
Project Structure
A possible project structure is:
EmployeeCRUD.API
│
├── Controllers
│ └── EmployeeController.cs
│
├── Data
│ └── ApplicationDbContext.cs
│
├── Models
│ ├── Employee.cs
│ ├── EmployeeCreateDto.cs
│ └── EmployeeUpdateDto.cs
│
├── Migrations
│
├── Program.cs
└── appsettings.json
Core_Web
│
├── Controllers
│ └── EmployeeController.cs
│
├── Models
│ └── EmployeeViewModel.cs
│
├── Views
│ └── Employee
│ ├── Index.cshtml
│ ├── Create.cshtml
│ └── Edit.cshtml
│
└── Program.cs
Conclusion
In this tutorial, we built an Employee CRUD application using ASP.NET Core Web API, ASP.NET Core MVC, Entity Framework Core, and SQL Server.
The Web API handles database operations through Entity Framework Core, while the MVC application communicates with the API using HttpClient and displays the data through Razor Views.
The main implementation flow is:
ASP.NET Core MVC
|
v
HttpClient
|
v
ASP.NET Core Web API
|
v
Entity Framework Core
|
v
SQL Server
The application demonstrates how to implement Create, Read, Update, and Delete operations, use DTOs for API requests, validate input, configure Dependency Injection, perform EF Core migrations, and separate the UI from the API layer.
This structure provides a useful foundation that can later be extended with authentication, authorization, centralized exception handling, logging, pagination, search, repository or service layers, and additional business rules.

Join the conversation! Your thoughts help the community grow.