Understanding & Configuring CORS in ASP.NET Core

Understanding & Configuring CORS in ASP.NET Core

 If you’ve ever tried calling your ASP.NET Core API from a frontend app (like React, Angular, or Vue) and ran into the dreaded:

Access to fetch at 'https://api.example.com' from origin 'http://localhost:3000' 
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present...

…then you’ve met CORS.

But don’t worry — in this article, we’ll break down what CORS is, why it matters, and how to configure it in ASP.NET Core (9+) with practical examples

🧐 What is CORS?

CORS (Cross-Origin Resource Sharing) is a browser security feature that controls how web apps running at one origin (domain, port, or protocol) can request resources from another origin.

  • Same-origin request ✅ Allowed (e.g., http://example.com → http://example.com)

  • Cross-origin request ❌ Blocked by default (e.g., http://localhost:3000 → https://api.example.com)

CORS is enforced only by browsers, not by tools like Postman or curl.

⚡ Enabling CORS in ASP.NET Core

ASP.NET Core has built-in support for CORS. You just need to:

  1. Register CORS policies in Program.cs

  2. Apply those policies globally or to specific endpoints

🛠 Step 1: Add CORS Services

Open your Program.cs (or Startup.cs in older projects):

var builder = WebApplication.CreateBuilder(args);

// Register CORS policy
builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowLocalhost3000", policy =>
    {
        policy.WithOrigins("http://localhost:3000") // frontend app
              .AllowAnyHeader()
              .AllowAnyMethod();
    });

    // Example: Open for all origins (⚠️ not recommended for production)
    options.AddPolicy("AllowAll", policy =>
    {
        policy.AllowAnyOrigin()
              .AllowAnyHeader()
              .AllowAnyMethod();
    });
});

builder.Services.AddControllers();

var app = builder.Build();

🛠 Step 2: Apply CORS Middleware

In the request pipeline:

app.UseRouting();

// Apply globally
app.UseCors("AllowLocalhost3000");

// Or selectively per controller/action
// [EnableCors("AllowLocalhost3000")]
// [DisableCors] // To disable at specific endpoints

app.MapControllers();

app.Run();

🎯 Example: Per-Controller CORS

Instead of applying CORS globally, you can decorate controllers with [EnableCors]:

[ApiController]
[Route("api/[controller]")]
[EnableCors("AllowLocalhost3000")]
public class WeatherController : ControllerBase
{
    [HttpGet]
    public IActionResult GetWeather() => Ok(new { Temp = "28°C", Condition = "Sunny" });
}

Or disable CORS for a specific action:

[HttpGet("internal")]
[DisableCors]
public IActionResult InternalEndpoint() => Ok("No CORS here");

🎯 Example: With Minimal Endpoints

app.MapGet("/weatherforecast", () => 
{
    var forecast = GetWeatherForecasts();
    return forecast;
})
.RequireCors("CorsPolicy");

🕵️ Understanding Preflight Requests (OPTIONS)

When you send certain cross-origin requests (like POST with custom headers or PUT/DELETE), the browser doesn’t go straight to your API. Instead, it first sends a preflight request (an OPTIONS call) to check if the server allows it.

Example preflight headers from the browser:

OPTIONS /api/weather HTTP/1.1
Origin: http://localhost:3000
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization

If your ASP.NET Core API responds with the right CORS headers (Access-Control-Allow-OriginAccess-Control-Allow-Methods, etc.), the browser proceeds with the actual request.

⏱ Optimizing Preflight with SetPreflightMaxAge

By default, browsers may send preflight requests very frequently, which can slow down your app.

ASP.NET Core lets you control how long the preflight response can be cached on the client (browser) using SetPreflightMaxAge.

builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowLocalhost3000", policy =>
    {
        policy.WithOrigins("http://localhost:3000")
              .AllowAnyHeader()
              .AllowAnyMethod()
              .SetPreflightMaxAge(TimeSpan.FromHours(1)); // cache preflight for 1 hour
    });
});

This tells the browser:
👉 “You can reuse the preflight result for 1 hour before asking again.”

⚠️ Note: Some browsers cap the maximum allowed cache duration (e.g., Chrome caps at 2 hours).

✅ With this, your API becomes faster because browsers won’t constantly re-check CORS rules for every cross-origin call.

🔒 Best Practices for CORS in Production

✅ Allow only trusted origins (avoid AllowAnyOrigin() in production).
✅ Enable HTTPS everywhere.
✅ Use different policies for devstaging, and production.
✅ Log and monitor CORS rejections if debugging tricky issues.

🖼 CORS Workflow Diagram

Here’s a simplified flow:

Browser (React, Angular, etc.)
         │
         │  (OPTIONS preflight request)
         ▼
ASP.NET Core API ───> CORS Policy ───> Allow or Block
  • Simple request → Sent directly (GET, POST without custom headers)

  • Preflight request → Browser sends OPTIONS request first, checks headers, then proceeds

Comments

Popular posts from this blog

Performance Optimization in Sitecore

Strategies for Migrating to Sitecore from legacy or upgrading from older Sitecore

Azure Event Grid Sample code