Back to: ASP.NET Core Web API Tutorials
3XX HTTP Status Codes in ASP.NET Core Web API
In this article, I will explain 3XX HTTP Status Codes in ASP.NET Core Web API Application with examples. Please read our previous article discussing 2XX HTTP Status Codes in ASP.NET Core Web API Applications.
301 Moved Permanently HTTP Status Code:
This status code is used when the requested resource has been permanently moved to a new URL. Any future requests should use the new URL. For a better understanding, please consider the following scenario.
- The system originally exposed products under the endpoint /api/catalog/products.
- Due to a major restructuring or version upgrade (e.g., from V1 to V2), the new permanent endpoint is /api/inventory/products.
- To avoid breaking old client applications, we want to permanently redirect old calls from /api/catalog/products to /api/inventory/products.
How Do We Return 301 HTTP Status Code in ASP.NET Core Web API?
You can use the RedirectPermanent(…) method to return a 301 Status Code and redirect clients to the new URL. This method sets the response status to 301 Moved Permanently and includes the Location header with the target URL. For example, return RedirectPermanent(“/new-resource”);
302 Found (Previously “Moved Temporarily”) HTTP Status Code:
This status code indicates that the requested resource has been temporarily moved to a different URL. However, the client should continue to use the original URL for future requests. That means this status code is useful when you temporarily redirect a client to a different resource and you plan to return the original resource soon. For a better understanding, please consider the following scenario.
- During certain marketing campaigns or flash sales, the system wants to temporarily redirect users from a normal endpoint to a special promotional endpoint.
- For instance, when a flash sale is active, requests to /api/inventory/products might temporarily redirect to a flash sale endpoint /api/inventory/flash-sale.
- This ensures that users are served special flash-sale data only during the campaign, but the original endpoint remains valid after the promotion ends.
Another example is A/B Testing:
We will demonstrate an endpoint (/api/products/abtest) that splits requests 50/50.
- Variant A: Immediately returns products matching certain filter criteria (e.g., electronics with Discount > 0 and Rating >= 4).
- Variant B: A 302 redirect to “/api/products/abtest-variant-b“, which returns a different filter set (e.g., home appliances with Discount >= 20 or brand “KitchenPro”
How Do We Return 302 HTTP Status Code in ASP.NET Core Web API?
You can use the Redirect(…) method to return a 302 Found (Temporary Redirect) status code. It is used to redirect the client to another URL temporarily. For example, return Redirect(“/temporary-target”);
304 Not Modified HTTP Status Code:
This status code is used for caching purposes. It means the resource hasn’t been modified since the last request, and there’s no need to resend it to the client. It helps save bandwidth by allowing clients to use their cached copy of the resource if it hasn’t changed. It’s used when the server determines that the resource hasn’t changed since the client’s last request, and the client can use its cached version. For a better understanding, please consider the following scenario:
- When a client fetches the details of a product (e.g., GET /api/inventory/products/1), the server includes an ETag (based on a LastModified field).
- On subsequent requests for the same product, the client sends If-None-Match: <etag>. If the product has not changed, the server returns 304 Not Modified, saving bandwidth and time.
This is a common real-time scenario for caching in RESTful APIs, improving performance by avoiding sending the same data repeatedly. When building APIs that serve static data that doesn’t change frequently like configuration data or static datasets, servers can implement conditional requests that respond with 304 status codes if the data hasn’t changed.
How Do We Return 304 HTTP Status Code in ASP.NET Core Web API?
You can manually set the response status code to 304 to indicate that the resource hasn’t changed. Typically, you need to check for conditions like If-Modified-Since or If-None-Match headers to determine whether the content is modified, then return 304 if it is not. For example, return StatusCode(304);
Example to Understand 3XX HTTP Status Codes in ASP.NET Core Web API
Let us understand 3XX HTTP Status Codes with an ASP.NET Core Web API application using Entity Framework (EF) Core Code-First approach. Based on the use cases we have discussed, this application demonstrates the usage of 3XX HTTP status codes (301, 302, and 304).
Creating a New ASP.NET Core Web API Project:
First, create a new ASP.NET Core Web API project named HTTPStatusCodeDemo. In our application, we will use Entity Framework Core and SQL Server database. So, to integrate EF Core with SQL Server, we need to install the following NuGet packages in our project.
- Microsoft.EntityFrameworkCore.SqlServer
- Microsoft.EntityFrameworkCore.Tools
So, please open the Package Manager Console in Visual Studio and then execute the following commands to install these packages:
- Install-Package Microsoft.EntityFrameworkCore.SqlServer
- Install-Package Microsoft.EntityFrameworkCore.Tools
Define the Product Model:
First, create a folder named Models in the project root directory and then create a class file named Product.cs within the Models folder and then copy and paste the following code. This model represents a product in the e-commerce system.
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace HTTPStatusCodeDemo.Models { public class Product { [Key] public int Id { get; set; } //Product Name [Required] public string Name { get; set; } //Helps differentiate products (e.g., "Electronics", "HomeAppliances", "Accessories", etc.). [Required] public string Category { get; set; } //Optional Description about the Product public string Description { get; set; } //Represents cost of the product [Range(0, double.MaxValue)] [Column(TypeName ="decimal(18, 2)")] public decimal Price { get; set; } // Discount percentage (e.g., 0 to 50). [Range(0, 100)] public double Discount { get; set; } // The brand of the product (e.g., 'Apple', 'Sony', 'KitchenPro'). public string Brand { get; set; } // Average rating on a scale of 1 to 5 (integers or decimals). [Range(1, 5)] public double Rating { get; set; } //Current stock level. [Range(0, int.MaxValue)] public int StockQuantity { get; set; } // Tracks when the product was last modified. public DateTime LastModified { get; set; } } }
Configure the Application Db Context
Next, create a new folder named Data in the project root directory. Add a new class file named ApplicationDbContext.cs within the Data folder and copy and paste the following code.
using HTTPStatusCodeDemo.Models; using Microsoft.EntityFrameworkCore; namespace HTTPStatusCodeDemo.Data { public class ApplicationDbContext : DbContext { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } public DbSet<Product> Products { get; set; } // Seed initial data protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Product>().HasData( new Product { Id = 1, Name = "Gaming Laptop", Category = "Electronics", Description = "High-end gaming laptop with RGB keyboard", Price = 1200.00M, Discount = 10, // 10% Brand = "VoltX", Rating = 4.5, StockQuantity = 10, LastModified = new DateTime(2025, 1, 20, 0, 0, 0, DateTimeKind.Utc) }, new Product { Id = 2, Name = "Smartphone", Category = "Electronics", Description = "Flagship phone with great camera", Price = 800.00M, Discount = 5, // 5% Brand = "CelPro", Rating = 4.0, StockQuantity = 50, LastModified = new DateTime(2025, 1, 20, 0, 0, 0, DateTimeKind.Utc) }, new Product { Id = 3, Name = "Coffee Maker", Category = "HomeAppliances", Description = "Automatic coffee machine", Price = 60.00M, Discount = 0, // No discount Brand = "KitchenPro", Rating = 3.5, StockQuantity = 100, LastModified = new DateTime(2025, 1, 20, 0, 0, 0, DateTimeKind.Utc) }, new Product { Id = 4, Name = "Vacuum Cleaner", Category = "HomeAppliances", Description = "Bagless vacuum with HEPA filter", Price = 150.00M, Discount = 20, // 20% Brand = "DustAway", Rating = 4.2, StockQuantity = 25, LastModified = new DateTime(2025, 1, 20, 0, 0, 0, DateTimeKind.Utc) }, new Product { Id = 5, Name = "Noise-Cancelling Headphones", Category = "Accessories", Description = "Over-ear headphones with active noise cancellation", Price = 200.00M, Discount = 0, // No discount Brand = "EchoSound", Rating = 4.7, StockQuantity = 30, LastModified = new DateTime(2025, 1, 20, 0, 0, 0, DateTimeKind.Utc) }, new Product { Id = 6, Name = "Wireless Mouse", Category = "Accessories", Description = "Ergonomic design with 2.4GHz wireless connection", Price = 25.00M, Discount = 10, // 10% Brand = "ClickPro", Rating = 4.0, StockQuantity = 150, LastModified = new DateTime(2025, 1, 20, 0, 0, 0, DateTimeKind.Utc) }, new Product { Id = 7, Name = "Fitness Tracker", Category = "Accessories", Description = "Water-resistant tracker with heart-rate monitor", Price = 99.99M, Discount = 15, // 15% Brand = "FitLife", Rating = 4.1, StockQuantity = 40, LastModified = new DateTime(2025, 1, 20, 0, 0, 0, DateTimeKind.Utc) }, new Product { Id = 8, Name = "Blender", Category = "HomeAppliances", Description = "Multi-speed blender for smoothies and soups", Price = 45.50M, Discount = 0, Brand = "KitchenPro", Rating = 3.9, StockQuantity = 60, LastModified = new DateTime(2025, 1, 20, 0, 0, 0, DateTimeKind.Utc) } ); } } }
Configure the Connection String
Add the SQL Server connection string to the appsettings.json file. So, please modify the appsettings.json file as follows.
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", "ConnectionStrings": { "DefaultConnection": "Server=LAPTOP-6P5NK25R\\SQLSERVER2022DEV;Database=ECommerceDB;Trusted_Connection=True;TrustServerCertificate=True;" } }
Register Services in Program.cs
Please modify the Program class as follows. Registers ApplicationDbContext with the dependency injection container using the SQL Server connection string from appsettings.json.
using HTTPStatusCodeDemo.Data; using Microsoft.EntityFrameworkCore; namespace HTTPStatusCodeDemo { public class Program { public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers() .AddJsonOptions(options => { // This will use the property names as defined in the C# model options.JsonSerializerOptions.PropertyNamingPolicy = null; }); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); // Configure DbContext with SQL Server builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); 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(); } } }
Database Migration
Next, we need to generate the Migration and update the database schema. So, open the Package Manager Console and Execute the Add-Migration and Update-Database commands as follows.
With this, our Database with Products table should be created as shown in the image below:
Implement the Products API Controller
Create an API Empty Controller named ProductsController within the Controllers folder and then copy and paste the following code. This API Controller handles product-related API endpoints demonstrating 301, 302, and 304 status codes.
using HTTPStatusCodeDemo.Data; using HTTPStatusCodeDemo.Models; using Microsoft.AspNetCore.Mvc; using System.Net; namespace HTTPStatusCodeDemo.Controllers { [ApiController] [Route("api/[controller]")] public class ProductsController : ControllerBase { private readonly ApplicationDbContext _context; // Simulate a scenario where we have a permanent endpoint /api/inventory/products // but some old clients are still calling /api/catalog/products private static bool _isOldEndpointActive = true; // Simulate a scenario where a flash sale is active private static bool _isFlashSaleActive = false; public ProductsController(ApplicationDbContext context) { _context = context; } // 301 Moved Permanently - Old Catalog Endpoint // Suppose older clients call GET /api/catalog/products // But the new permanent endpoint is /api/inventory/products // We will do a 301 redirect from the old route to the new route. // Old Endpoint [HttpGet("/api/catalog/products")] public IActionResult GetOldCatalogProducts() { if (_isOldEndpointActive) { // We have decided to permanently move to /api/inventory/products. // Return 301 to instruct clients to always use the new endpoint. return RedirectPermanent("/api/inventory/products"); } // If we somehow no longer consider it "moved," we can directly return data, // but typically 301 is permanent. We'll keep the code for demonstration. var products = _context.Products.ToList(); return Ok(products); } // New Endpoint [HttpGet("/api/inventory/products")] public IActionResult GetInventoryProducts() { try { // Return the product list at the new permanent location var products = _context.Products.ToList(); return Ok(products); } catch (Exception ex) { return StatusCode(500, "Server error: " + ex.Message); } } // 302 Found - Flash Sale or Temporary Redirect // We normally get products using /api/inventory/products, // but if a flash sale is active, we want to temporarily redirect // to /api/inventory/flash-sale for special deals. [HttpGet("check-flash-sale")] public IActionResult CheckFlashSale() { try { if (_isFlashSaleActive) { // 302 Found => temporary redirect // Tells the client to retrieve flash sale data from a different endpoint return Redirect("/api/inventory/flash-sale"); } // If flash sale is NOT active, just return normal products var products = _context.Products.ToList(); return Ok(products); } catch (Exception ex) { return StatusCode(500, "Server error: " + ex.Message); } } // Flash sale endpoint [HttpGet("/api/inventory/flash-sale")] public IActionResult GetFlashSaleProducts() { try { // Return discounted product data var products = _context.Products .Select(p => new { p.Id, p.Name, p.Description, DiscountPrice = "20% OFF", p.LastModified }).ToList(); return Ok(products); } catch (Exception ex) { return StatusCode(500, "Server error: " + ex.Message); } } // Endpoint to toggle flash sale mode for demonstration [HttpPost("toggle-flash-sale")] public IActionResult ToggleFlashSale(bool activate) { _isFlashSaleActive = activate; return Ok(new { FlashSaleActive = _isFlashSaleActive }); } // A/B Testing with extra properties (Description, Discount) // GET /api/products/abtest // Splits 50/50: // -- Variant A: Filter on "Electronics" that have a discount AND rating >= 4 // -- Variant B: 302 redirect => returns "HomeAppliances" with discount >= 20 // OR brand "KitchenPro" (for demonstration) [HttpGet("abtest")] public IActionResult ABTest() { // Randomly pick which variant var random = new Random(); bool goToVariantB = random.NextDouble() >= 0.5; if (goToVariantB) { // Return 302 => redirect to /api/products/abtest-variant-b return Redirect("/api/products/abtest-variant-b"); } else { // Variant A: Filter for Electronics that have discount > 0 // and rating >= 4, sort by discount descending for fun var variantAProducts = _context.Products .Where(p => p.Category == "Electronics" && p.Discount > 0 && p.Rating >= 4) .OrderByDescending(p => p.Discount) .ToList(); return Ok(new { Message = "Variant A: Electronics with discount > 0 and rating >= 4", Products = variantAProducts }); } } [HttpGet("abtest-variant-b")] public IActionResult ABTestVariantB() { // Variant B: Return any HomeAppliances with discount >= 20 // OR brand "KitchenPro" (for demonstration). // Sorted by rating descending. var variantBProducts = _context.Products .Where(p => (p.Category == "HomeAppliances" && p.Discount >= 20) || p.Brand == "KitchenPro") .OrderByDescending(p => p.Rating) .ToList(); return Ok(new { Message = "Variant B: HomeAppliances (discount >= 20) OR brand 'KitchenPro'", Products = variantBProducts }); } // 304 Not Modified - ETag / If-None-Match // Demonstrates caching. If the client has an up-to-date ETag, // we return 304 Not Modified, saving bandwidth. [HttpGet("{id}")] public async Task<IActionResult> GetProduct(int id) { try { var product = await _context.Products.FindAsync(id); if (product == null) return NotFound(); // Create an ETag from the LastModified timestamp var etag = product.LastModified.Ticks.ToString(); // Check for 'If-None-Match' from client if (Request.Headers.TryGetValue("If-None-Match", out var clientEtag)) { if (clientEtag == etag) { // No changes => 304 Not Modified return StatusCode((int)HttpStatusCode.NotModified); } } // Set ETag in the response Response.Headers["ETag"] = etag; return Ok(product); } catch (Exception ex) { return StatusCode(500, "Server error: " + ex.Message); } } // Standard CRUD Operations // Create product [HttpPost] public async Task<IActionResult> CreateProduct([FromBody] Product newProduct) { try { newProduct.LastModified = DateTime.UtcNow; _context.Products.Add(newProduct); await _context.SaveChangesAsync(); return CreatedAtAction(nameof(GetProduct), new { id = newProduct.Id }, newProduct); } catch (Exception ex) { return StatusCode(500, $"Server error: {ex.Message}"); } } [HttpPut("{id}")] public async Task<IActionResult> UpdateProduct(int id, [FromBody] Product updatedProduct) { if (id != updatedProduct.Id) return BadRequest("Product ID mismatch."); var product = await _context.Products.FindAsync(id); if (product == null) return NotFound(); product.Name = updatedProduct.Name; product.Category = updatedProduct.Category; product.Description = updatedProduct.Description; product.Price = updatedProduct.Price; product.Discount = updatedProduct.Discount; product.Brand = updatedProduct.Brand; product.Rating = updatedProduct.Rating; product.StockQuantity = updatedProduct.StockQuantity; product.LastModified = DateTime.UtcNow; await _context.SaveChangesAsync(); return NoContent(); } [HttpDelete("{id}")] public async Task<IActionResult> DeleteProduct(int id) { var product = await _context.Products.FindAsync(id); if (product == null) return NotFound(); _context.Products.Remove(product); await _context.SaveChangesAsync(); return NoContent(); } } }
How This Demonstrates Real-Time 3XX Use Cases
301 (Moved Permanently)
In real applications, URLs are sometimes reorganized for API versioning, domain changes, or SEO improvements. By sending 301 Moved Permanently from the old endpoint (/api/catalog/products) to the new endpoint (/api/inventory/products), clients know the resource has a permanent new location. Search engines also update their indexes accordingly.
Testing:
- GET https://localhost:<port>/api/catalog/products
- You should see a 301 Response redirecting you to /api/inventory/products.
- In a browser or Postman, you will notice the location header set to the new URL.
302 (Found) – Temporary Redirect
This is handy for dynamic or time-based conditions like maintenance pages, flash sales, A/B testing, or marketing campaigns. When the flash sale is active, the code issues a 302 to redirect from /api/inventory/products (or a check endpoint) to /api/inventory/flash-sale. This preserves the original URL for normal usage once the campaign/maintenance ends.
Testing Flash Sale:
- First activate the flash sale: POST https://localhost:<port>/api/products/toggle-flash-sale?activate=true
- Then request: GET https://localhost:<port>/api/products/check-flash-sale
- You will receive a 302 redirect to /api/inventory/flash-sale.
Check the A/B Testing
- GET https://localhost:<port>/api/products/abtest multiple times.
- Roughly half of the calls return a JSON object with “Variant A” data.
- The other half return a 302 redirect to “/api/products/abtest-variant-b”, which then returns “Variant B” data.
304 (Not Modified)
When clients re-fetch a product (e.g., GET /api/inventory/products/1), they send the last ETag they received in the If-None-Match header. If the product is unchanged (LastModified is the same as the ETag), the server can respond with 304 Not Modified, telling the client to use its cached copy. This reduces bandwidth and improves performance – a very common real-time scenario for API caching.
Testing:
- GET https://localhost:<port>/api/products/1
Observe the ETag in the response header (e.g., ETag: 638100960000000000). Make another request adding If-None-Match: <ETagValue>:
- GET https://localhost:<port>/api/products/1
- If-None-Match: 638100960000000000
You will get a 304 Not Modified if the product’s LastModified field is unchanged.
What is the If-None-Match Header?
The If-None-Match HTTP header is part of the HTTP protocol’s conditional request mechanism. It’s primarily used in conjunction with ETags (Entity Tags) to determine whether a resource has changed since the client last accessed it. Let us understand how it works.
Server Response with ETag:
When a client requests a resource (e.g., a product in your API), the server responds with the resource data and includes an ETag header. This ETag is a unique identifier representing the current version of the resource.
Client Subsequent Request with If-None-Match:
On subsequent requests for the same resource, the client includes the If-None-Match header with the previously received ETag value.
Server Validates ETag:
The server compares the ETag provided in If-None-Match with the current ETag of the resource. If they match (meaning the resource hasn’t changed), the server responds with a 304 Not Modified status, instructing the client to use its cached version.
HTTP/1.1 304 Not Modified
If they don’t match, the server returns the updated resource with a new ETag.
Note: Browsers handle the If-None-Match header automatically as part of their caching mechanisms.
Summary of 3XX HTTP Status Codes in ASP.NET Core Web API:
3XX HTTP status codes play a crucial role in web communication by managing resource redirection and caching. Implementing these codes properly enhances user experience, optimizes performance, and maintains SEO integrity.
- 301: Permanent endpoint relocations, commonly for API versioning or SEO.
- 302: Temporary conditions like flash sales, maintenance, or A/B tests.
- 304: Client-side caching with ETags for bandwidth and performance optimization.
In the next article, I will discuss 4XX HTTP Status Codes in ASP.NET Core Web API applications with Examples. In this article, I explain 3XX HTTP Status Codes in ASP.NET Core Web API application with multiple Examples. I hope you enjoy this article on “3XX HTTP Status Codes in ASP.NET Core Web API”.