CORS in .NET Core and classic ASP.NET solve the same browser problem, but they feel very different once you actually ship APIs with them.
If you’ve worked on both stacks, you’ve probably noticed the split right away:
- ASP.NET Core gives you a clean, policy-based CORS system built into the middleware pipeline.
- Classic ASP.NET usually feels more fragmented. Depending on whether you’re in Web API, MVC, IIS, or some mix of all three, CORS can be straightforward or weirdly annoying.
That difference matters because CORS bugs are rarely “the browser is wrong.” Usually the server emitted the wrong headers, emitted them in the wrong order, or skipped them on preflight requests.
Quick refresher: what CORS is actually doing
CORS tells the browser whether JavaScript from one origin can read responses from another origin.
If your frontend runs on:
https://app.example.com
and your API runs on:
https://api.example.com
the browser treats that as cross-origin. Your API has to send headers like:
Access-Control-Allow-Origin: https://app.example.com
Sometimes more headers are needed for methods, credentials, or custom headers.
A real-world example: api.github.com sends:
access-control-allow-origin: *
access-control-expose-headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
That’s a good reminder that CORS is not just Allow-Origin. Sometimes clients need access to response headers too.
ASP.NET Core CORS: the better developer experience
I’m going to be blunt: if you’re on ASP.NET Core, CORS is usually nicer.
You define policies centrally and apply them globally or per endpoint. That keeps things predictable.
Typical ASP.NET Core setup
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options =>
{
options.AddPolicy("FrontendPolicy", policy =>
{
policy
.WithOrigins("https://app.example.com")
.WithMethods("GET", "POST", "PUT", "DELETE")
.WithHeaders("Content-Type", "Authorization")
.WithExposedHeaders("ETag", "Link", "X-RateLimit-Remaining")
.AllowCredentials();
});
});
builder.Services.AddControllers();
var app = builder.Build();
app.UseRouting();
app.UseCors("FrontendPolicy");
app.UseAuthorization();
app.MapControllers();
app.Run();
The biggest win here is that CORS is part of the request pipeline. Once you understand middleware ordering, behavior becomes much easier to reason about.
Pros of CORS in ASP.NET Core
1. Policy-based configuration is clean
Policies are reusable and readable. You can create one for public APIs and another for admin apps.
options.AddPolicy("PublicApi", policy =>
{
policy
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
options.AddPolicy("AdminUi", policy =>
{
policy
.WithOrigins("https://admin.example.com")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
});
That’s much better than scattering CORS behavior across attributes, web.config, and custom handlers.
2. Per-endpoint control is practical
You can apply policies to controllers or actions:
[EnableCors("AdminUi")]
[ApiController]
[Route("api/[controller]")]
public class ReportsController : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok();
}
That’s useful when one API surface is public and another needs stricter browser access.
See the official docs for details: ASP.NET Core CORS documentation
3. Better support for modern APIs
Minimal APIs, endpoint routing, hosted services, reverse proxy deployments — ASP.NET Core fits modern architectures better. CORS plays nicely with that model.
Cons of CORS in ASP.NET Core
1. Middleware order still trips people up
If UseCors is in the wrong place, you get confusing behavior.
A common broken setup:
app.UseAuthorization();
app.UseCors("FrontendPolicy");
That can cause preflight or actual requests to fail in ways that look random. Put CORS in the right place in the pipeline.
2. Credentials and wildcard origins are incompatible
This is a browser rule, not a .NET quirk, but people hit it constantly:
policy
.AllowAnyOrigin()
.AllowCredentials(); // invalid combination
If you need cookies or auth credentials, specify exact origins.
3. Overly broad policies are easy to create
AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod() is tempting in dev and dangerous in prod if the API returns sensitive data.
Classic ASP.NET CORS: workable, but less elegant
Classic ASP.NET is a different story. There are multiple ways to do CORS depending on your app type:
- ASP.NET Web API
- ASP.NET MVC
- IIS configuration
- custom HTTP modules/handlers
That flexibility sounds nice until you inherit an older app and realize no two environments behave the same way.
Typical Web API 2 setup
For Web API 2, you usually enable CORS in configuration:
using System.Web.Http;
using System.Web.Http.Cors;
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
var cors = new EnableCorsAttribute(
"https://app.example.com",
"Content-Type, Authorization",
"GET, POST, PUT, DELETE");
cors.SupportsCredentials = true;
config.EnableCors(cors);
config.MapHttpAttributeRoutes();
}
}
You can also apply it per controller:
[EnableCors(origins: "https://app.example.com", headers: "*", methods: "*")]
public class ValuesController : ApiController
{
public IHttpActionResult Get()
{
return Ok(new { message = "ok" });
}
}
Official docs: ASP.NET Web API CORS documentation
Pros of CORS in classic ASP.NET
1. It can be added without rewriting the app
If you’re maintaining a legacy Web API app, you can enable CORS with relatively small changes.
That matters for teams stuck on older enterprise systems.
2. Attribute-based configuration is simple for small apps
For a couple of controllers, [EnableCors] is straightforward enough.
3. IIS can sometimes help
In some environments, ops teams are already comfortable managing headers at the IIS layer.
Cons of CORS in classic ASP.NET
1. Configuration gets fragmented fast
This is the biggest downside.
You can end up asking:
- Is CORS handled in Web API config?
- Is IIS injecting headers?
- Is a proxy adding or overwriting them?
- Are OPTIONS requests even reaching the app?
That’s not a fun debugging session.
2. Preflight handling is more fragile
Browsers send OPTIONS preflight requests for many cross-origin requests. In older ASP.NET setups, those requests can get blocked or mishandled before your API logic runs.
I’ve seen this happen with IIS verb restrictions, auth modules, and custom routing.
3. Mixed MVC + Web API apps are messy
Classic ASP.NET apps often mix technologies. CORS may work for Web API controllers and fail for MVC endpoints, or vice versa.
4. Less consistent than Core
You can absolutely make it work, but it doesn’t feel as unified or predictable as ASP.NET Core.
Side-by-side comparison
ASP.NET Core
Pros
- Built-in middleware model
- Centralized policy configuration
- Good per-endpoint control
- Better fit for modern APIs
- Easier to test and reason about
Cons
- Middleware order matters
- Easy to accidentally over-permit access
- Credentials rules still confuse teams
Classic ASP.NET
Pros
- Fine for legacy apps
- Can be enabled incrementally
- Attribute-based setup is okay for small APIs
Cons
- Fragmented configuration
- Preflight requests fail more often in practice
- Harder to debug across IIS/app boundaries
- Less ergonomic overall
Exposing headers: the part people forget
A lot of teams configure Access-Control-Allow-Origin and stop there. Then frontend code can’t read response headers they expected.
If your browser app needs things like pagination or rate limit metadata, expose them explicitly.
ASP.NET Core example
builder.Services.AddCors(options =>
{
options.AddPolicy("ApiPolicy", policy =>
{
policy
.WithOrigins("https://app.example.com")
.AllowAnyMethod()
.AllowAnyHeader()
.WithExposedHeaders(
"ETag",
"Link",
"Retry-After",
"X-RateLimit-Limit",
"X-RateLimit-Remaining",
"X-RateLimit-Reset");
});
});
That pattern mirrors what public APIs often do. GitHub’s exposed header list is a solid real-world example of how much useful metadata can live in headers.
My practical recommendation
If you’re building anything new, use ASP.NET Core and keep CORS policy centralized.
If you’re maintaining classic ASP.NET, avoid layering multiple CORS mechanisms on top of each other. Pick one place to manage it when possible. If IIS, app code, and a reverse proxy all try to “help,” you usually get duplicate or conflicting headers.
A few rules I stick to:
- Never use
AllowAnyOriginon authenticated endpoints - Be explicit about allowed origins in production
- Test preflight requests, not just simple GETs
- Expose only the headers frontend code actually needs
- Verify behavior in the browser, not just Postman
That last one matters because Postman doesn’t enforce CORS. Browsers do.
If you’re also tightening your overall header strategy, CORS is only one piece. CSP, HSTS, and related headers matter too. For that broader topic, see CSP Guide.
For official Microsoft references, use:
My short version: ASP.NET Core gives you a cleaner, safer CORS story. Classic ASP.NET can do the job, but you’ll spend more time fighting hosting and framework edges.