Introduction

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:

Step 1: Setting Up the ASP.NET Core Project

  1. Open Visual Studio.

  2. Select Create a new project.

  3. Search for ASP.NET Core Web API.

  4. Select the ASP.NET Core Web API template and click Next.

  5. Configure the project name and location.

  6. Select the required .NET version.

  7. Keep the default OpenAPI support enabled if you want to test the API through Swagger.

  8. 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:

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. Vikram Batra",
                Age = 24,
                Address = "Palampur, Himachal"
            },
            new Students
            {
                Id = 2,
                Name = "Capt. Manoj Kumar Pandey",
                Age = 24,
                Address = "Kashmir"
            }
        };

        [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. Vikram Batra",
    "age": 24,
    "address": "Palampur, Himachal"
  },
  {
    "id": 2,
    "name": "Capt. Manoj Kumar Pandey",
    "age": 24,
    "address": "Kashmir"
  }
]

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. Vikram Batra",
  "age": 24,
  "address": "Palampur, Himachal"
}

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": "Rahul Sharma",
  "age": 22,
  "address": "Delhi"
}

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": "Rahul Sharma",
  "age": 22,
  "address": "Delhi"
}

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": "Rahul Kumar Sharma",
  "age": 23,
  "address": "Noida"
}

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.