HTTP Methods in ASP.NET Core Web API

HTTP Methods in ASP.NET Core Web API

In this article, I will discuss How to Implement Different HTTP Methods in ASP.NET Core Web API Applications with Examples. Please read our previous article discussing Automapper in ASP.NET Core Web API with Examples. At the end of this article, you will understand the following pointers:

  1. What are HTTP Methods?
  2. Types of HTTP Methods.
  3. What are Safe and Unsafe HTTP Methods?
  4. What are Idempotent Methods in ASP.NET Core Web API?
  5. Real-time Example to Understand ASP.NET Core Web API with HTTP Methods
  6. Best Practices to Use HTTP Methods in ASP.NET Core Web API

What are HTTP Methods?

HTTP (Hypertext Transfer Protocol) methods are standardized request methods used in HTTP to specify the desired action performed on a specific resource in a web application. They are key components of RESTful APIs, allowing clients and servers to interact and manipulate resources effectively. These methods tell the web server what operation should be executed for a particular resource, such as retrieving data, creating new entries, updating resources, or deleting them.

Types of HTTP Methods:

The primary or most commonly used HTTP methods used in web applications and APIs include:

HTTP GET Method:

The HTTP GET Method retrieves data from the web server without modifying the resource. It is safe to call multiple times without changing the server state. For example, if you request a list of all users from a server, the server responds with the user data without modifying any existing information. We need to decorate the action method with the HttpGet attribute in ASP.NET Core Web API.

HTTP POST Method:

The HTTP POST method sends data to the server to create a new resource. It usually includes data as part of the request body, with the type specified by the Content-Type HTTP header. A common use case for POST is submitting a registration form to create a new user in the system. Each POST request creates a distinct user entry. We need to decorate the action method with the HttpPost attribute in ASP.NET Core Web API.

HTTP PUT Method:

The HTTP PUT method replaces all the current representations of the target resource with the request payload. It typically updates an existing resource. If the resource does not exist, the server may create a new resource at the specified URI (depending on the server implementation). For example, it may update a user’s profile information by replacing all existing details with the new data provided. We need to decorate the action method with the HttpPut attribute in ASP.NET Core Web API.

HTTP PATCH Method:

The HTTP PATCH method allows partial modification of a resource. It sends only the changes to be applied rather than the entire resource. Unlike PUT, it doesn’t replace the whole resource; instead, it only updates specific fields. For example, it updates just the email of a user profile without affecting other attributes. We need to decorate the action method with the HttpPatch attribute in ASP.NET Core Web API.

HTTP DELETE Method:

The HTTP DELETE method removes a specified resource from the server, such as a user account, from the database. We need to decorate the action method with the HttpDelete attribute in ASP.NET Core Web API.

HTTP HEAD Method:

Like GET, the HTTP HEAD method requests the resource’s headers without the response body. It retrieves the headers of a resource. It is commonly used to check metadata, such as content type and length, before making a full GET request. For example, check the size of a large file before downloading it without downloading the content. We need to decorate the action method with the HttpHead attribute in ASP.NET Core Web API.

HTTP OPTIONS Method:

The HTTP OPTIONS method describes the communication options for the target resource. It allows clients to determine which HTTP methods the server supports for a specific resource. It is commonly used in Cross-Origin Resource Sharing (CORS) preflight requests to check what methods and headers are allowed. For example, determining if the server supports the PUT method for a particular endpoint before attempting to update a resource. We need to decorate the action method with the HttpOtions attribute in ASP.NET Core Web API.

What are Safe and Unsafe HTTP Methods?

HTTP methods are categorized into two types based on their effect on server state: Safe Methods and Unsafe Methods.

Safe HTTP Methods:

Safe methods are those methods that do not alter (add, modify, or delete) the state of the server. They are typically read-only and have no side effects on the server or the data. They can be called multiple times without causing any side effects. The safe HTTP methods include:

  • GET: Retrieves data from the server without modifying it.
  • HEAD: Retrieves metadata about a resource without fetching the entire resource.
  • OPTIONS: Describe communication options for the resource.
Unsafe HTTP Methods:

The Unsafe HTTP Methods can modify the server’s state by creating, updating, or deleting resources. They should be used with caution because they may change or delete data. The unsafe HTTP methods include:

  • POST: Sends data to the server to create a new resource, changing server state.
  • PUT: Replaces the resource entirely, changing the server state.
  • PATCH: Partially updates the resource, changing the server state.
  • DELETE: Removes a resource from the server, altering the server state.
What are Idempotent Methods in ASP.NET Core Web API

In the context of ASP.NET Core Web API, an Idempotent Method is an HTTP method that, when called multiple times, produces the same outcome as making a single call. In other words, no matter how many times the same request is sent, the result remains the same without causing any additional side effects beyond the first request. The following are the Idempotent Methods:

  • GET: Repeated GET requests return the same data without side effects as long as the resource hasn’t been altered by other means. For example, requesting user details multiple times will consistently return the same information unless another operation modifies the user data.
  • PUT: Repeating a PUT request with the same data results in the same resource state. For example, updating a product’s price multiple times with the same value leaves the price unchanged after the first update.
  • DELETE: Once the resource is deleted, subsequent DELETE requests to the same resource have no effect because the resource no longer exists. For example, attempting to delete a file that has already been deleted does not change the server’s state further.
  • HEAD: Similar to GET, but retrieves only headers, making it idempotent. For example, checking the metadata of a document multiple times without affecting the document itself.
  • OPTIONS: Repeatedly querying the communication options does not change server settings. For example, continuously verifying supported methods for a resource without modifying its configuration.
  • PATCH: It depends. If the PATCH operation applies the same change each time, it can be idempotent. However, operations that accumulate changes (like incrementing a value) are not idempotent. For example, setting a user’s status to “active” is idempotent, whereas incrementing a user’s login count is not.
Non-Idempotent Method in ASP.NET Core Web API:

In the context of ASP.NET Core Web API, a Non-Idempotent HTTP method is an HTTP method that, when called multiple times, produces different results with each request. Making the same request more than once can have additional side effects or cause a change in the server’s state that is different from the initial request.

  • POST: This method is not idempotent because sending the same POST request multiple times typically creates multiple new resources or changes the state in different ways. For example, submitting a form multiple times may create multiple records in the database.
Idempotency vs Safety

Idempotency vs. Safety in ASP.NET Core Web API

  • Safety refers to the method’s ability to be called without modifying the server’s state. Safe methods (e.g., GET, HEAD, OPTIONS) do not change data.
  • Idempotency refers to the ability to make multiple identical requests with the same effect as a single request (e.g., PUT, DELETE).

For example:

  • GET is both safe and idempotent.
  • PUT and DELETE are not safe but are idempotent.
  • POST is neither safe nor idempotent because it creates new resources whenever it is called.

Key Differences:

  • A method can be safe and idempotent (e.g., the GET method will retrieve data without modifying the server state, and repeated calls have the same outcome).
  • A method can be unsafe but idempotent (e.g., DELETE modifies the server state, but repeated deletes have the same effect).
  • A method can be non-idempotent and unsafe (e.g., POST may create multiple resources with repeated requests).

Example to Understand ASP.NET Core Web API with HTTP Methods

Let’s create a real-time example using ASP.NET Core Web API and Entity Framework Core to demonstrate the practical use cases of each HTTP (GET, POST, PUT, PATCH, DELETE, HEAD, and OPTIONS) method. We will build a simple Product Management system where we can perform CRUD (Create, Read, Update, Delete) operations on product data.

Project Structure Overview
  • Setting Up the Project: ASP.NET Core Web API with required Packages
  • Models: Defines the Product entity.
  • Data: Configures the ProductDbContext for Entity Framework Core.
  • Controllers: Implements the ProductsController with actions for each HTTP method.
  • Program.cs: Sets up the ASP.NET Core application.
Setting Up the Project and Installing Entity Framework Core

First, create a new ASP.NET Core Web API Project named ProductManagementAPI. Once you create the project, please install the Entity Framework Core Packages by executing the following command in the Package Manager Console. I am using SQL Server as the backend database and hence I am using SQL Server data provider for EF Core.

  • Install-Package Microsoft.EntityFrameworkCore.SqlServer
  • Install-Package Microsoft.EntityFrameworkCore.Tools
Defining the Product Model

First, create a folder named Models in the project root directory. Then, create a class file named Product.cs within the Models folder and then copy and paste the following code. This model represents the Product entity, which we will use to perform the database CRUD operations:

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ProductManagementAPI.Models
{
    public class Product
    {
        [Key]
        public int Id { get; set; }
        [Required]
        [StringLength(100)]
        public string Name { get; set; }
        [Required]
        [Column(TypeName ="decimal(18,2)")]
        public decimal Price { get; set; }
        public string? Description { get; set; }
    }
}
Create a DbContext Class

So, create a class file named ProductDbContext.cs within the Models folder and copy and paste the following code.

using Microsoft.EntityFrameworkCore;

namespace ProductManagementAPI.Models
{
    public class ProductDbContext : DbContext
    {
        public ProductDbContext(DbContextOptions<ProductDbContext> options) : base(options) { }
        
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Product>().HasData(
                new Product { Id = 1, Name = "Laptop", Price = 1000.00M, Description = "A powerful laptop" },
                new Product { Id = 2, Name = "Smartphone", Price = 500.00M, Description = "A modern smartphone" }
            );
        }

        public DbSet<Product> Products { get; set; }
    }
}
Configure the Database Connection

Next, please update the appsettings.json with the database connection string as follows:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "ProductDBConnection": "Server=LAPTOP-6P5NK25R\\SQLSERVER2022DEV;Database=ProductDB;Trusted_Connection=True;TrustServerCertificate=True;"
  }
}
Set Up Dependency Injection and Middleware:

Please modify the Program class as follows.

using ProductManagementAPI.Models;
using Microsoft.EntityFrameworkCore;

namespace ProductManagementAPI
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // Add services to the container.
            builder.Services.AddControllers()
                // Optionally, configure JSON options or other formatter settings
                .AddJsonOptions(options =>
                {
                    // Configure JSON serializer settings to keep the Original names in serialization and deserialization
                    options.JsonSerializerOptions.PropertyNamingPolicy = null;
                });

            // Register the ProductDbContext with dependency injection
            builder.Services.AddDbContext<ProductDbContext>(options =>
                options.UseSqlServer(builder.Configuration.GetConnectionString("ProductDBConnection")));

            // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
            builder.Services.AddEndpointsApiExplorer();
            builder.Services.AddSwaggerGen();

            var app = builder.Build();

            // Configure the HTTP request pipeline.
            if (app.Environment.IsDevelopment())
            {
                app.UseSwagger();
                app.UseSwaggerUI();
            }

            app.UseHttpsRedirection();

            app.UseAuthorization();


            app.MapControllers();

            app.Run();
        }
    }
}
Creating and Applying Database Migration:

Open the Package Manager Console and Execute the Add-Migration and Update-Database commands as follows to generate the Migration file and then apply the Migration file to create the ProductDB database and required Products table:

HTTP Methods in ASP.NET Core Web API

Once you execute the above commands and verify the database, you should see the ProductDB database with the required Products table, as shown in the image below.

HTTP Methods in ASP.NET Core Web API with Examples

Implementing the Products API Controller

Create a Products Controller to handle HTTP requests. So, create a new API Empty Controller named ProductsController within the Controllers folder and then copy and paste the following code:

using Microsoft.AspNetCore.Mvc;
using ProductManagementAPI.Models;
using Microsoft.EntityFrameworkCore;

namespace ProductManagementAPI.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ProductsController : ControllerBase
    {
        private readonly ProductDbContext _context;

        public ProductsController(ProductDbContext context)
        {
            _context = context;
        }

        // HTTP GET Method: Retrieve all products (Safe, Idempotent)
        // Safely retrieves resources without altering the server state. It's idempotent and safe.
        [HttpGet]
        public async Task<IActionResult> GetAllProducts()
        {
            try
            {
                var products = await _context.Products.ToListAsync();
                return Ok(products); // Returns 200 OK with the list of products
            }
            catch (Exception ex)
            {
                // Handles any unexpected errors
                return StatusCode(500, new { Message = "An error occurred while retrieving products.", Details = ex.Message });
            }
        }

        // HTTP GET Method: Retrieve product by ID (Safe, Idempotent)
        [HttpGet("{id}")]
        public async Task<IActionResult> GetProductById(int id)
        {
            try
            {
                var product = await _context.Products.FindAsync(id);

                if (product == null)
                    return NotFound(new { Message = $"Product with ID {id} not found." }); // Returns 404 Not Found if product does not exist

                return Ok(product); // Returns 200 OK with the product
            }
            catch (Exception ex)
            {
                // Handles any unexpected errors
                return StatusCode(500, new { Message = "An error occurred while retrieving the product.", Details = ex.Message });
            }
        }

        // HTTP POST Method: Create a new product (Unsafe, Non-Idempotent)
        // Creates new resources, which makes it unsafe and non-idempotent.
        [HttpPost]
        public async Task<IActionResult> CreateProduct([FromBody] Product product)
        {
            try
            {
                if (!ModelState.IsValid)
                    return BadRequest(ModelState); // Returns 400 Bad Request if validation fails

                await _context.Products.AddAsync(product);
                await _context.SaveChangesAsync();

                // Return a created response with the URI of the new product
                return CreatedAtAction(nameof(GetProductById), new { id = product.Id }, product); // Returns 201 Created
            }
            catch (Exception ex)
            {
                // Handles any unexpected errors
                return StatusCode(500, new { Message = "An error occurred while creating the product.", Details = ex.Message });
            }
        }

        // HTTP PUT Method: Update existing product by replacing it entirely (Unsafe, Idempotent)
        // Updates the entire resource, making it unsafe but idempotent.
        [HttpPut("{id}")]
        public async Task<IActionResult> UpdateProduct(int id, [FromBody] Product product)
        {
            try
            {
                if (id != product.Id)
                    return BadRequest("Product ID mismatch."); // Returns 400 Bad Request if IDs do not match

                var existingProduct = await _context.Products.FindAsync(id);
                if (existingProduct == null)
                    return NotFound(new { Message = $"Product with ID {id} not found." }); // Returns 404 Not Found if product does not exist

                // Update product properties
                existingProduct.Name = product.Name;
                existingProduct.Price = product.Price;
                existingProduct.Description = product.Description;

                await _context.SaveChangesAsync(); // Saves changes to the database

                return NoContent(); // Returns 204 No Content on success
            }
            catch (DbUpdateException ex)
            {
                //Handle database exception
                return StatusCode(500, new { Message = "An error occurred while updating the product.", Details = ex.InnerException?.Message ?? ex.Message });
            }
            catch (Exception ex)
            {
                // Handles any unexpected errors
                return StatusCode(500, new { Message = "An error occurred while updating the product.", Details = ex.Message });
            }
        }

        // HTTP PATCH Method: Partial update of an existing product (Unsafe, Idempotent)
        // Partially updates a resource, unsafe and potentially idempotent depending on how it’s used.
        [HttpPatch("{id}")]
        public async Task<IActionResult> UpdateProductPrice(int id, [FromBody] Product product)
        {
            try
            {
                if (id != product.Id)
                    return BadRequest("Product ID mismatch."); // Returns 400 Bad Request if IDs do not match

                var existingProduct = await _context.Products.FindAsync(id);
                if (existingProduct == null)
                    return NotFound(new { Message = $"Product with ID {id} not found." }); // Returns 404 Not Found if product does not exist

                // Only update the price
                existingProduct.Price = product.Price;

                await _context.SaveChangesAsync(); // Saves changes to the database

                return NoContent(); // Returns 204 No Content on success
            }
            catch (DbUpdateException ex)
            {
                //Handle database exception
                return StatusCode(500, new { Message = "An error occurred while updating the product.", Details = ex.InnerException?.Message ?? ex.Message });
            }
            catch (Exception ex)
            {
                // Handles any unexpected errors
                return StatusCode(500, new { Message = "An error occurred while updating the product price.", Details = ex.Message });
            }
        }

        // HTTP DELETE Method: Delete an existing product (Unsafe, Idempotent)
        // Removes a resource, unsafe but idempotent because once deleted, subsequent deletes have no effect.
        [HttpDelete("{id}")]
        public async Task<IActionResult> DeleteProduct(int id)
        {
            try
            {
                var product = await _context.Products.FindAsync(id);
                if (product == null)
                    return NotFound(new { Message = $"Product with ID {id} not found." }); // Returns 404 Not Found if product does not exist

                _context.Products.Remove(product);
                await _context.SaveChangesAsync(); // Saves changes to the database

                return NoContent(); // Returns 204 No Content on success
            }
            catch (DbUpdateException ex)
            {
                //Handle database exception
                return StatusCode(500, new { Message = "An error occurred while updating the product.", Details = ex.InnerException?.Message ?? ex.Message });
            }
            catch (Exception ex)
            {
                // Handles any unexpected errors
                return StatusCode(500, new { Message = "An error occurred while deleting the product.", Details = ex.Message });
            }
        }

        // HTTP HEAD Method: Retrieve metadata (headers) for a product without fetching the body
        // Retrieves only headers, safe and idempotent.
        [HttpHead("{id}")]
        public async Task<IActionResult> HeadProduct(int id)
        {
            try
            {
                var productExists = await _context.Products.AnyAsync(p => p.Id == id);
                if (!productExists)
                    return NotFound(); // Returns 404 Not Found if product does not exist

                // Add desired headers
                // Set Content-Type header to indicate the media type of the resource (JSON in this case)
                Response.Headers.Append("Content-Type", "application/json");

                // Set Content-Length header to indicate the size of the response body (though body is not returned in HEAD request)
                var contentLength = System.Text.Json.JsonSerializer.Serialize(productExists).Length;
                Response.Headers.Append("Content-Length", contentLength.ToString());

                // Add a custom header (e.g., "X-Custom-Header")
                Response.Headers.Append("X-Custom-Header", "CustomHeaderValue");

                // Return status 200 OK (with headers only)
                return Ok();
            }
            catch (Exception ex)
            {
                // Handles any unexpected errors
                return StatusCode(500, new { Message = "An error occurred while retrieving product metadata.", Details = ex.Message });
            }
        }

        // HTTP OPTIONS Method: List allowed methods for this controller (Safe)
        // Safe and idempotent, useful for preflight requests or discovering allowed methods.
        [HttpOptions]
        public IActionResult GetOptions()
        {
            try
            {
                // Returns the list of supported HTTP methods
                Response.Headers.Append("Allow", "GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD");
                return Ok(); // Returns 200 OK with Allow header
            }
            catch (Exception ex)
            {
                // Handles any unexpected errors
                return StatusCode(500, new { Message = "An error occurred while retrieving options.", Details = ex.Message });
            }
        }
    }
}

Now, run the application and test the functionalities, and it should work as expected.

Best Practices for Using HTTP Methods in ASP.NET Core Web API:

To ensure clarity, maintainability, and adherence to RESTful standards in ASP.NET Core Web API, please follow the below best practices when using HTTP methods:

  • GET: Use for safe, read-only operations that do not change the server’s state.
  • POST: Use to create new resources or submit data for processing.
  • PUT: Use to replace or update an existing resource completely.
  • PATCH: Use for partial updates to a resource when only a few fields need to be modified.
  • DELETE: Use to remove a resource from the server.
  • OPTIONS: Use to determine which HTTP methods are supported by the server.
  • HEAD: Use when you need only the metadata about a resource without downloading its content.

In the next article, I will discuss How to Implement the HTTP GET Method in the ASP.NET Core Web API Application with Examples. In this article, I explain HTTP Methods in ASP.NET Core Web API with Examples. I hope you enjoy this article, “HTTP Methods in ASP.NET Core Web API.”

Leave a Reply

Your email address will not be published. Required fields are marked *