CRUD stands for Create, Read, Update, and Delete. These four operations form the foundation of many data-driven web applications. In this article, we will build a simple ASP.NET Core MVC application that performs CRUD operations for a Students entity. We will create the project, define the model, configure the application, create a controller, and test the CRUD endpoints.
The example uses an in-memory collection to keep the implementation simple and focused on understanding the CRUD flow. A SQL Server or another database can be introduced later when persistent data storage is required.

Prerequisites
Before we begin, ensure you have the following:
- Visual Studio 2022 or later.
- .NET SDK.
- Basic knowledge of C# and ASP.NET Core MVC.
- Basic understanding of HTTP methods such as GET, POST, PUT, and DELETE.
Step 1: Setting Up the ASP.NET Core Project
- Open Visual Studio.
- Select Create a new project.
- Search for ASP.NET Core Web API.
- Select the ASP.NET Core Web API template and click Next.
- Configure the project name and location.
- Select the required .NET version.
- Keep the default OpenAPI support enabled if you want to test the API through Swagger.
- Click Create.
For this example, the project is named:
CRUDOperationAPI
Although the original example refers to an MVC project, the supplied StudentsController inherits from ControllerBase and uses [ApiController], which makes it an ASP.NET Core Web API controller. Therefore, this implementation follows the Web API approach.
Step 2: Configure the Project
For a basic CRUD API, the default project configuration is sufficient.
A typical Program.cs file looks like this:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
Here:
- AddControllers() registers controller support.
- AddEndpointsApiExplorer() enables API endpoint discovery.
- AddSwaggerGen() generates OpenAPI documentation.
- UseSwagger() and UseSwaggerUI() provide a browser-based interface for testing the API.
- MapControllers() maps controller routes.
Step 3: Create the Student Model
Create a Models folder in the project.
Inside the folder, create a Students.cs class:
namespace CRUDOperationAPI.Models
{
public class Students
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public int Age { get; set; }
public string Address { get; set; } = string.Empty;
}
}
This class represents the data that our CRUD API will manage.
The properties are:
|
Property
|
Description
|
|
Id
|
Unique identifier for the student
|
|
Name
|
Student's name
|
|
Age
|
Student's age
|
|
Address
|
Student's address
|
Step 4: Create the Students Controller
Create a StudentsController.cs file inside the Controllers folder.
The controller will expose endpoints for all four CRUD operations.
using CRUDOperationAPI.Models;
using Microsoft.AspNetCore.Mvc;
namespace CRUDOperationAPI.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class StudentsController : ControllerBase
{
private static readonly List<Students> students = new()
{
new Students
{
Id = 1,
Name = "Capt. Peter",
Age = 24,
Address = "London"
},
new Students
{
Id = 2,
Name = "Capt. Scott",
Age = 24,
Address = "Leeds"
}
};
[HttpGet]
public ActionResult<List<Students>> GetAllStudents()
{
return Ok(students);
}
[HttpGet("{id}")]
public ActionResult<Students> GetStudentById(int id)
{
var student = students.FirstOrDefault(s => s.Id == id);
if (student == null)
{
return NotFound();
}
return Ok(student);
}
[HttpPost]
public ActionResult<Students> CreateStudent([FromBody] Students student)
{
student.Id = students.Count == 0
? 1
: students.Max(s => s.Id) + 1;
students.Add(student);
return CreatedAtAction(
nameof(GetStudentById),
new { id = student.Id },
student);
}
[HttpPut("{id}")]
public ActionResult UpdateStudent(
int id,
[FromBody] Students updatedStudent)
{
var student = students.FirstOrDefault(s => s.Id == id);
if (student == null)
{
return NotFound();
}
student.Name = updatedStudent.Name;
student.Age = updatedStudent.Age;
student.Address = updatedStudent.Address;
return NoContent();
}
[HttpDelete("{id}")]
public ActionResult DeleteStudent(int id)
{
var student = students.FirstOrDefault(s => s.Id == id);
if (student == null)
{
return NotFound();
}
students.Remove(student);
return NoContent();
}
}
}
The controller uses a static List<Students> as temporary storage. This means the data exists only while the application is running.
Step 5: Understand the CRUD Endpoints
The controller exposes five HTTP endpoints.
|
HTTP Method
|
Endpoint
|
Operation
|
|
GET
|
/api/students
|
Get all students
|
|
GET
|
/api/students/{id}
|
Get a student by ID
|
|
POST
|
/api/students
|
Create a student
|
|
PUT
|
/api/students/{id}
|
Update a student
|
|
DELETE
|
/api/students/{id}
|
Delete a student
|
These endpoints represent the standard CRUD workflow.
Step 6: Test the Read Operation
Run the application using F5 or the Start button in Visual Studio. If Swagger is enabled, the application will open the Swagger interface.
Expand:
GET /api/Students
Click Try it out, followed by Execute.
The response will contain the existing students:
[
{
"id": 1,
"name": "Capt. Tim",
"age": 24,
"address": "Liverpool"
},
{
"id": 2,
"name": "Capt. Scott",
"age": 24,
"address": "Leeds"
}
]
This demonstrates the Read operation.
Step 7: Get a Student by ID
To retrieve a specific student, use:
GET /api/Students/1
The API searches the collection for a matching ID.
var student = students.FirstOrDefault(s => s.Id == id);
if (student == null)
{
return NotFound();
}
return Ok(student);
For ID 1, the response is:
{
"id": 1,
"name": "Capt. Tim",
"age": 24,
"address": "Liverpool"
}
If the requested student does not exist, the API returns an HTTP 404 Not Found response.
Step 8: Create a New Student
To create a student, use:
POST /api/Students
Send the following JSON request body:
{
"name": "David",
"age": 22,
"address": "London"
}
The controller assigns a new ID and adds the student to the collection.
student.Id = students.Count == 0
? 1
: students.Max(s => s.Id) + 1;
students.Add(student);
The API returns 201 Created along with the newly created resource.
Example response:
{
"id": 3,
"name": "David",
"age": 22,
"address": "London"
}
This demonstrates the Create operation.
Step 9: Update a Student
To update an existing student, use:
PUT /api/Students/3
Send the updated data:
{
"name": "Peter",
"age": 23,
"address": "London"
}
The controller finds the student and updates its properties.
student.Name = updatedStudent.Name;
student.Age = updatedStudent.Age;
student.Address = updatedStudent.Address;
If the operation succeeds, the API returns:
204 No Content
This demonstrates the Update operation.
Step 10: Delete a Student
To delete a student, use:
DELETE /api/Students/3
The controller finds the student and removes it from the collection.
students.Remove(student);
If the deletion succeeds, the API returns:
204 No Content
If the student does not exist, the API returns:
404 Not Found
This demonstrates the Delete operation.
Step 11: Validate the CRUD Flow
The complete CRUD flow can now be tested as follows:
Create
|
v
POST /api/Students
|
v
Read
|
v
GET /api/Students
|
v
Update
|
v
PUT /api/Students/{id}
|
v
Read Updated Data
|
v
GET /api/Students/{id}
|
v
Delete
|
v
DELETE /api/Students/{id}
This provides a simple end-to-end test of the API.
Step 12: Customize the Controller
You may want to customize the controller logic according to your application's requirements.
For example, validation can be added before creating a student:
[HttpPost]
public ActionResult<Students> CreateStudent([FromBody] Students student)
{
if (string.IsNullOrWhiteSpace(student.Name))
{
return BadRequest("Student name is required.");
}
if (student.Age <= 0)
{
return BadRequest("Age must be greater than zero.");
}
student.Id = students.Count == 0
? 1
: students.Max(s => s.Id) + 1;
students.Add(student);
return CreatedAtAction(
nameof(GetStudentById),
new { id = student.Id },
student);
}
This prevents invalid data from being added to the collection.
For a production application, validation would generally be implemented using model validation attributes or a dedicated validation approach rather than keeping all validation logic directly inside controller actions.
Step 13: Move from In-Memory Data to SQL Server
The example uses an in-memory list so that the CRUD concepts can be demonstrated without introducing database configuration.
For a real application, the data would normally be stored in a database such as SQL Server.
A common architecture would be:
Client
|
v
ASP.NET Core Controller
|
v
Service Layer
|
v
Repository / Data Access
|
v
SQL Server
Entity Framework Core can be used to connect the ASP.NET Core application to SQL Server.
For example, the required packages can be installed using the .NET CLI:
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
The exact package versions should match the .NET and Entity Framework Core versions used by the project.
Once a database is introduced, the static list can be replaced with an DbContext and database queries.
Why the Original Package Configuration Needs Attention?
The original implementation listed packages such as:
Microsoft.ASPNetCore
Microsoft.SqlServer
Microsoft.Data.SqlClient
These are not the appropriate set of packages for the MVC/API CRUD implementation shown in the article.
If SQL Server is being used with Entity Framework Core, the application should use the appropriate Entity Framework Core SQL Server provider, such as:
Microsoft.EntityFrameworkCore.SqlServer
Microsoft.Data.SqlClient is the SQL Server ADO.NET provider and is useful when directly working with SQL Server through ADO.NET, but it does not replace the Entity Framework Core SQL Server provider.
MVC Views vs Web API Controllers
There is an important distinction between the two approaches.
An ASP.NET Core MVC application commonly uses:
Controller
|
v
Razor Views
|
v
HTML UI
A Web API application commonly uses:
Client
|
v
API Controller
|
v
JSON Response
The supplied StudentsController uses:
[ApiController]
public class StudentsController : ControllerBase
Therefore, it is a Web API controller rather than an MVC controller that returns Razor Views.
If the goal is to create an MVC application with Create, Edit, Details, and Delete pages, the controller would instead work with Razor Views and a database context.
Common Mistakes to Avoid
Mixing MVC and Web API Terminology
An MVC controller and an API controller serve different purposes. Make sure the project template, controller base class, routes, and expected output match the application's architecture.
Assuming In-Memory Data Is Persistent
The static list is only temporary storage. Restarting the application resets the data.
Not Validating Input
APIs should validate incoming data before storing or processing it.
Exposing Internal Exception Details
Production APIs should avoid returning sensitive exception information directly to clients.
Using Manual ID Generation in Production
The example generates IDs from the current collection because it uses an in-memory list. A database should normally be responsible for generating unique primary keys.
Conclusion
CRUD operations are fundamental to most data-driven applications. ASP.NET Core provides a straightforward way to implement Create, Read, Update, and Delete operations through HTTP endpoints. In this example, we created a students model and implemented GET, POST, PUT, and DELETE operations using an ASP.NET Core Web API controller. We also tested the endpoints and reviewed how the same application can be extended with SQL Server and Entity Framework Core for persistent data storage.
The example is intentionally simple so that the CRUD workflow is easy to understand. In a production application, the next steps would typically include database persistence, model validation, authentication and authorization, service and repository layers, logging, error handling, and automated testing.