CORS in ASP.NET Core Web API

CORS in ASP.NET Core Web API

In this article, I will discuss CORS (Cross-Origin Resource Sharing) in ASP.NET Core Web API Applications. Please read our previous article discussing implementing JWT Authentication in an ASP.NET Core Web API Application. 

Modern web applications often need to communicate securely across different domains, for example, a front-end application hosted on one server and a back-end API on another server. By default, browsers enforce the Same-Origin Policy (SOP) to prevent malicious cross-site requests, which can sometimes block legitimate interactions between your own client and server apps.

Cross-Origin Resource Sharing (CORS) is a standard mechanism that allows servers to specify who can access their resources and how. In this post, we will explore what Same-Origin Policy (SOP) and CORS are, how CORS works under the hood, and how to configure it in ASP.NET Core.

What is the Same-Origin Policy (SOP)?

The Same-Origin Policy (SOP) is a fundamental security mechanism enforced by all modern web browsers to protect users and their data. In other words, SOP stops a website you visit from accessing data using JavaScript or AJAX calls from another website in your browser without permission.

An origin is defined by the combination of three elements in a URL:

  • Protocol: HTTP or HTTPS
  • Domain: The website’s domain name (e.g., example.com)
  • Port: The port number used for the connection (e.g., 80 for HTTP, 443 for HTTPS)

If any of these (protocol, domain, or port) are different, then they are considered different origins. For example, https://example.com:443 is a different origin from http://example.com:80 because the protocol and port differ.

How Same-Origin Policy (SOP) Works?

When a web page is loaded from an origin (say https://example.com:443), SOP prevents the JavaScript code (AJAX calls) on that page from making requests to a different origin (e.g., https://anotherdomain.com or even the same domain with a different port like https://example.com:8080). This restriction protects users by preventing one site from accessing sensitive data on another site without permission using JavaScript or jQuery AJAX calls.

When a web page attempts to make an AJAX request to a different origin, browsers enforce SOP to determine whether the request should be allowed. Let’s understand the scenarios with examples. For a better understanding, please refer to the following diagram.

How Same-Origin Policy (SOP) Works?

Let us understand the above scenarios:

Same-Origin Request
  • Client URL (web page): https://example.com/client
  • Server URL (API): https://example.com/api
  • Result: The AJAX request succeeds because both URLs share the same origin, same protocol (https), domain (example.com), and port (default 443). No special permissions are needed. CORS is not required here because the request is within the same origin.
Cross-Origin Request without CORS Enabled
  • Client URL (web page): https://myclient.com
  • Server URL (API): https://example.com/api
  • Result: The browser blocks the AJAX request because the origins (myclient.com vs. example.com) are different. The server has not specified that it allows requests from https://myclient.com, so the browser enforces the SOP and stops the request for security.
Cross-Origin Request with CORS Enabled
  • Client URL (web page): https://myclient.com
  • Server URL (API): https://example.com/api (with CORS enabled for myclient.com)
  • Result: The AJAX request succeeds because the server (example.com) explicitly permits requests from myclient.com by enabling CORS (Cross-Origin Resource Sharing). This tells the browser it’s safe to allow the request despite the different origins.
What is CORS?

Cross-Origin Resource Sharing (CORS) is a protocol that relaxes the Same-Origin Policy under controlled conditions. It allows servers to specify which external origins (web applications) are permitted to access their resources using JavaScript or jQuery via Web Browsers. This is essential for modern web applications, where APIs and frontends often reside on different domains or ports.

The server includes specific HTTP headers in its responses indicating which origins, HTTP methods, and request headers are allowed. These headers guide the browser to either allow or block cross-origin requests. The following are the CORS headers:

  • Access-Control-Allow-Origin: Specifies allowed origins (e.g., https://myclient.com or * for all origins).
  • Access-Control-Allow-Methods: Lists HTTP methods allowed (e.g., GET, POST, PUT).
  • Access-Control-Allow-Headers: Lists allowed custom headers that the client can send.
  • Access-Control-Max-Age: Specifies the maximum time the browser can cache the preflight response.

By setting these headers on the server, CORS enables secure cross-origin requests while still protecting against unauthorized access. These headers instruct the browser on how to handle the cross-origin request.

How Does Cross-Origin Resource Sharing (CORS) Work?

When a client Web Application sends an AJAX request to a Web API hosted on a different origin, the process involves several steps that ensure the request is safe and adheres to the server’s CORS policy. For a better understanding, please have a look at the image below:

How Does Cross-Origin Resource Sharing (CORS) Work?

Step 1: Preflight Request (Initial Check)

Before sending the actual request, the browser sends a preflight request using the HTTP OPTIONS method to the target server. This preflight request includes information about the intended HTTP method (e.g., GET, POST) and headers (including custom headers) of the actual request.

  • Purpose: This request asks the server whether the actual request is safe to send, including details about the HTTP method and headers the client intends to use.
  • Server Response: The server evaluates the preflight request against its CORS policy and responds with headers like Access-Control-Allow-Origin, Access-Control-Allow-Methods, and Access-Control-Allow-Headers to indicate permission.
  • If Allowed: The browser proceeds to send the actual request.
  • If Denied: The browser blocks the request.
Step 2: Actual Request
  • After a positive preflight response, the browser sends the real AJAX request (e.g., GET, POST) to the server.
  • The server processes this request and returns the response (like JSON data or XML data).
Step 3: Caching Preflight Results
  • To improve performance, browsers cache the server’s response to the preflight request for a time defined by the Access-Control-Max-Age header.
  • During this cache period, subsequent requests with the same method and headers skip the preflight step and send the actual request directly.
Second Request (Cached Preflight Result)
  • Browser Checks Preflight Result Cache: For subsequent requests to the same server with the same method and headers, the browser first checks its preflight result cache. If the preflight result is still valid (i.e., not expired), the browser skips sending another preflight request.
  • Browser Sends the Actual Request Directly: Since the preflight result is already cached, the browser sends the actual request to the server directly. The server processes the request and sends back the response.
Example to Understand CORS in ASP.NET Core:

As we have already discussed, Cross-Origin Resource Sharing (CORS) is a security feature implemented by browsers to restrict web pages from making requests to a different domain (origin) than the one that served the web page. In modern web applications, especially when building client-server architectures, CORS is a common challenge.

  • Server: An ASP.NET Core Web API application exposing CRUD endpoints for a User entity.
  • Client: An ASP.NET Core MVC application acting as a front-end, making AJAX calls to the Web API.

As these applications run on different origins (different ports), browsers will enforce CORS policies, potentially blocking client AJAX requests unless the server explicitly allows cross-origin calls. We will build a Single Page Application (SPA)-style user management dashboard as shown in the image below, where the front-end (MVC app) interacts with the back-end (Web API) using RESTful AJAX calls.

CORS in ASP.NET Core Web API

Creating the ASP.NET Core Web API Application (Web API)

This ASP.NET Core Web API application will expose endpoints for performing CRUD (Create, Read, Update, Delete) operations on a User Entity. First, create a new ASP.NET Core Web API project named UserServiceAPI. Once you create the project, please install the Entity Framework Core packages by executing the following commands in the Package Manager Console:

  • Install-Package Microsoft.EntityFrameworkCore.SqlServer
  • Install-Package Microsoft.EntityFrameworkCore.Tools
Create the User Model

First, create a folder named Models within the project’s root directory. In the Models folder, create a new class file named User.cs and copy and paste the following code. This will be our User Entity on which we will perform the CRUD operations.

namespace UserServiceAPI.Models
{
    public class User
    {
        public int Id { get; set; }
        public string FullName { get; set; } = null!;
        public string Email { get; set; } = null!;
        public string Position { get; set; } = null!;
    }
}
UserDbContext Class

Create a class file named UserDbContext.cs in the Models folder and then copy and paste the following code.

using Microsoft.EntityFrameworkCore;
namespace UserServiceAPI.Models
{
    public class UserDbContext : DbContext
    {
        public UserDbContext(DbContextOptions<UserDbContext> options) : base(options) { }
       
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            // Seeding dummy data
            modelBuilder.Entity<User>().HasData(
                new User { Id = 1, FullName = "John Doe", Email = "john.doe@example.com", Position = "Developer" },
                new User { Id = 2, FullName = "Jane Smith", Email = "jane.smith@example.com", Position = "Manager" },
                new User { Id = 3, FullName = "Sara Taylor", Email = "sara.taylor@example.com", Position = "Manager" },
                new User { Id = 4, FullName = "Pam Jordon", Email = "pam.jordon@example.com", Position = "Developer" }
            );
        }

        public DbSet<User> Users { get; set; } = null!;
    }
}
User API Controller

In the Controllers folder, create a new API Empty controller named UserController, and copy and paste the following code. The following controller exposes endpoints for client applications to perform CRUD Operations on the User entity.

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using UserServiceAPI.Models;
namespace UserServiceAPI.Controllers
{
    [Route("api/[controller]/[action]")]
    [ApiController]
    public class UserController : ControllerBase
    {
        private readonly UserDbContext _context;

        public UserController(UserDbContext context)
        {
            _context = context;
        }


        [HttpGet]
        public async Task<ActionResult<List<User>>> GetAllUsers()
        {
           var users = await _context.Users.AsNoTracking().ToListAsync();
            return Ok(users);
        }

        [HttpGet("{id}")]
        public async Task<ActionResult<User>> GetUserById(int id)
        {
            var user = await _context.Users.AsNoTracking().FirstOrDefaultAsync(usr => usr.Id == id);
            if (user == null)
            {
                return NotFound();
            }
            return Ok(user);
        }

        [HttpPost]
        public async Task<ActionResult> AddUser(User user)
        {
            await _context.Users.AddAsync(user);
            await _context.SaveChangesAsync();
            return CreatedAtAction(nameof(GetUserById), new { id = user.Id }, user);
        }

        [HttpPut("{id}")]
        public async Task<ActionResult> UpdateUser(int id, User user)
        {
            if (id != user.Id)
            {
                return BadRequest();
            }

            _context.Users.Update(user);
            await _context.SaveChangesAsync();

            return NoContent();
        }

        [HttpDelete("{id}")]
        public async Task<ActionResult> DeleteUser(int id)
        {
            var user = await _context.Users.FindAsync(id);
            if (user != null)
            {
                _context.Users.Remove(user);
                await _context.SaveChangesAsync();
            }

            return NoContent();
        }
    }
}
Modify appsettings.json

Ensure that you add a connection string to your appsettings.json file. So, please modify the appsettings.json file as follows.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "EFCoreDBConnection": "Server=LAPTOP-6P5NK25R\\SQLSERVER2022DEV;Database=UsersDB;Trusted_Connection=True;TrustServerCertificate=True;"
  }
}
Modify the Program Class:

Please modify the Program class as follows:

using Microsoft.EntityFrameworkCore;
using UserServiceAPI.Models;
namespace UserServiceAPI
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // Add controllers and configure JSON options
            builder.Services.AddControllers()
               .AddJsonOptions(options =>
               {
                   options.JsonSerializerOptions.PropertyNamingPolicy = null;
               });

            // Add Swagger
            builder.Services.AddEndpointsApiExplorer();
            builder.Services.AddSwaggerGen();

            // Configure EF Core DbContext
            builder.Services.AddDbContext<UserDbContext>(options =>
            {
                options.UseSqlServer(builder.Configuration.GetConnectionString("EFCoreDBConnection"));
            });

            var app = builder.Build();

            // Enable Swagger in Development
            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 Package Manager Console and execute the Add-Migration and Update-Database commands as follows.

CORS (Cross-Origin Resource Sharing) in ASP.NET Core Web API Applications

With this, our UsersDB Database with Users tables should be created as shown in the image below:

CORS (Cross-Origin Resource Sharing) in ASP.NET Core Web API

Test the API

At this stage, you can test the API using tools like Postman, Fiddler, or Swagger to ensure that the CRUD operations work as expected. We have not yet enabled CORS configuration, so that cross-origin requests will be blocked. Please note the port number where your ASP.NET Core Web API project is running. In my case, the application is running on port 7152.

Creating the ASP.NET Core MVC Application (Client Application)

This ASP.NET Core MVC application attempts to perform CRUD operations on the User entity via AJAX requests to the Web API, which results in CORS issues since the Web API does not allow cross-origin requests. So, create a new ASP.NET Core Project using the Model-View-Controller template and give the project name UserClientApp.

Create the User Model

In the Models folder, create a new class file named User.cs (same as in API) and then copy and paste the following code.

namespace UserClientApp.Models
{
    public class User
    {
        public int Id { get; set; }
        public string FullName { get; set; } = null!;
        public string Email { get; set; } = null!;
        public string Position { get; set; } = null!;
    }
}
Modify Home Controller

By default, the project is created with HomeController. So, please modify the HomeController as follows. When we make HTTP requests in the Controller (server-to-server), CORS does not apply. CORS is enforced by browsers for AJAX requests made from client-side code (JavaScript), not server-side code.

using Microsoft.AspNetCore.Mvc;
using UserClientApp.Models;
using System.Text.Json;

namespace UserClientApp.Controllers
{
    public class HomeController : Controller
    {
        //The Index action method will load the list of users from the API
        //and pass it to the view on the initial load.
        public async Task<IActionResult> Index()
        {
            //Create an Instance of HttpClient
            HttpClient _httpClient = new HttpClient();

            //Set the base Address
            //Please replace the port on which your Web API Application is running
            _httpClient.BaseAddress = new Uri("https://localhost:7152/");

            //When the Page Load we want to display the List of User
            //Specify the endpoint which returns the list of Users, i.e., api/User/GetAllUsers
            //Here, we will not get any CORS issue, this is because of Server to Server call
            var response = await _httpClient.GetStringAsync("api/User/GetAllUsers");

            //Convert the Response which is in JSON format to List<User>
            var users = JsonSerializer.Deserialize<List<User>>(response);

            //Pass the List of Users to the View
            return View(users);
        }
    }
}
Modify the Index View

In the Views/Home folder, update the Index.cshtml view to include forms for creating, updating, and deleting users:

@model List<User>
@{
ViewData["Title"] = "User Management";
}
<h2 class="text-center my-4">👥 User Management</h2>
<div class="container">
<div class="row">
<div class="col-md-12">
<!-- Success and Error Messages -->
<div id="successMessage" class="alert alert-success alert-dismissible fade show" role="alert" style="display:none;">
<span id="successText"></span> 😊
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<div id="errorMessage" class="alert alert-danger alert-dismissible fade show" role="alert" style="display:none;">
<span id="errorText"></span> 😟
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<button id="loadUsers" class="btn btn-primary mb-3"><i class="bi bi-arrow-repeat"></i> 🔄 Refresh User List</button>
<button id="createUserBtn" class="btn btn-success mb-3"><i class="bi bi-person-plus"></i> ➕ Add New User</button>
<div id="createUserForm" class="mb-4 card card-body shadow-sm" style="display: none;">
<h4 class="card-title">🆕 Add New User</h4>
<div class="form-group mb-3">
<label for="createFullName">Full Name</label>
<input type="text" id="createFullName" class="form-control" placeholder="Enter full name" />
</div>
<div class="form-group mb-3">
<label for="createEmail">Email</label>
<input type="email" id="createEmail" class="form-control" placeholder="Enter email" />
</div>
<div class="form-group mb-3">
<label for="createPosition">Position</label>
<input type="text" id="createPosition" class="form-control" placeholder="Enter position" />
</div>
<button id="createUser" class="btn btn-success mt-2"><i class="bi bi-save"></i> 💾 Save User</button>
<button id="cancelCreate" class="btn btn-secondary mt-2"><i class="bi bi-x-circle"></i> ❌ Cancel</button>
</div>
<table class="table table-hover table-striped shadow-sm">
<thead class="table-dark">
<tr>
<th>🆔 ID</th>
<th>📛 Full Name</th>
<th>📧 Email</th>
<th>💼 Position</th>
<th>🔧 Actions</th>
</tr>
</thead>
<tbody id="userTableBody">
@foreach (var user in Model)
{
<tr>
<td>@user.Id</td>
<td>@user.FullName</td>
<td>@user.Email</td>
<td>@user.Position</td>
<td>
<button class="btn btn-info editUser" data-id="@user.Id"><i class="bi bi-pencil"></i> ✏️ Edit</button>
<button class="btn btn-danger deleteUser" data-id="@user.Id"><i class="bi bi-trash"></i> 🗑️ Delete</button>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
<!-- Bootstrap Modal for Editing User -->
<div class="modal fade" id="editUserModal" tabindex="-1" aria-labelledby="editUserModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editUserModalLabel">✏️ Edit User Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<input type="hidden" id="editUserId" />
<div class="form-group mb-3">
<label for="editFullName">Full Name</label>
<input type="text" id="editFullName" class="form-control" placeholder="Enter full name" />
</div>
<div class="form-group mb-3">
<label for="editEmail">Email</label>
<input type="email" id="editEmail" class="form-control" placeholder="Enter email" />
</div>
<div class="form-group mb-3">
<label for="editPosition">Position</label>
<input type="text" id="editPosition" class="form-control" placeholder="Enter position" />
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><i class="bi bi-x-circle"></i> ❌ Cancel</button>
<button type="button" class="btn btn-primary" id="updateUserBtn"><i class="bi bi-save"></i> 💾 Update User</button>
</div>
</div>
</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/js/bootstrap.min.js"></script>
<script>
$(document).ready(function () {
// Show Create User Form
$("#createUserBtn").click(function () {
$("#createUserForm").show();
});
// Cancel Create User
$("#cancelCreate").click(function () {
$("#createUserForm").hide();
});
// Load Users
$("#loadUsers").click(function () {
loadUsers();
});
// Function to Load Users
function loadUsers() {
$.ajax({
url: "https://localhost:7152/api/user/getallusers",
type: "GET",
success: function (data) {
var rows = "";
data.forEach(function (user) {
rows += "<tr><td>" + user.Id + "</td><td>" + user.FullName + "</td><td>" + user.Email + "</td><td>" + user.Position + "</td><td><button class='btn btn-info editUser' data-id='" + user.Id + "'><i class='bi bi-pencil'></i> ✏️ Edit</button><button class='btn btn-danger deleteUser' data-id='" + user.Id + "'><i class='bi bi-trash'></i> 🗑️ Delete</button></td></tr>";
});
$("#userTableBody").html(rows);
},
error: function (xhr, status, error) {
showErrorMessage("⚠️ Oops! An error occurred while loading the user list. Please try again.");
}
});
}
// Create User
$("#createUser").click(function () {
var user = {
FullName: $("#createFullName").val(),
Email: $("#createEmail").val(),
Position: $("#createPosition").val()
};
$.ajax({
url: "https://localhost:7152/api/user/adduser",
type: "POST",
contentType: "application/json",
data: JSON.stringify(user),
success: function () {
showSuccessMessage("🎉 New user created successfully!");
loadUsers(); // Reload the list of users
$("#createUserForm").hide();
},
error: function (xhr, status, error) {
showErrorMessage("⚠️ Oops! An error occurred while creating the user. Please try again.");
}
});
});
// Edit User
$(document).on("click", ".editUser", function () {
var userId = $(this).data("id");
$.ajax({
url: "https://localhost:7152/api/user/getuserbyid/" + userId,
type: "GET",
success: function (user) {
$("#editUserId").val(user.Id);
$("#editFullName").val(user.FullName);
$("#editEmail").val(user.Email);
$("#editPosition").val(user.Position);
$("#editUserModal").modal("show");
},
error: function (xhr, status, error) {
showErrorMessage("⚠️ Oops! An error occurred while loading the user details. Please try again.");
}
});
});
// Update User
$("#updateUserBtn").click(function () {
var user = {
Id: $("#editUserId").val(),
FullName: $("#editFullName").val(),
Email: $("#editEmail").val(),
Position: $("#editPosition").val()
};
$.ajax({
url: "https://localhost:7152/api/user/updateuser/" + user.Id,
type: "PUT",
contentType: "application/json",
data: JSON.stringify(user),
success: function () {
showSuccessMessage("🎉 User details updated successfully!");
$("#editUserModal").modal("hide");
loadUsers(); // Reload to fetch the updated list of users
},
error: function (xhr, status, error) {
showErrorMessage("⚠️ Oops! An error occurred while updating the user. Please try again.");
}
});
});
// Delete User
$(document).on("click", ".deleteUser", function () {
var userId = $(this).data("id");
if (confirm("❗ Are you sure you want to delete this user? This action cannot be undone.")) {
$.ajax({
url: "https://localhost:7152/api/user/deleteuser/" + userId,
type: "DELETE",
success: function () {
showSuccessMessage("🗑️ User deleted successfully.");
loadUsers(); // Reload to fetch the updated list of users
},
error: function (xhr, status, error) {
showErrorMessage("⚠️ Oops! An error occurred while deleting the user. Please try again.");
}
});
}
});
// Function to show success message
function showSuccessMessage(message) {
$("#successText").text(message);
$("#successMessage").show();
setTimeout(function () {
$("#successMessage").fadeOut("slow");
}, 8000);
}
// Function to show error message
function showErrorMessage(message) {
$("#errorText").text(message);
$("#errorMessage").show();
setTimeout(function () {
$("#errorMessage").fadeOut("slow");
}, 8000);
}
});
</script>
Explanation:
  • Create User Form: A form for creating new users is displayed when the “Add New User” button is clicked.
  • Edit User: The modal for editing a user is populated with the user’s data when the “Edit” button is clicked.
  • Update User: The update button sends a PUT request to the Web API to update the user’s details.
  • Delete User: The delete button sends a DELETE request to remove a user from the database.
Run Both Applications
  • Run the Web API (UserServiceAPI): The URL is: https://localhost:7082/.
  • Run the MVC Application (UserClientApp): The URL is: https://localhost:7196/.
Performing the Create, Update, and Delete Operations

When we attempt to create, update, or delete users using the ASP.NET Core MVC application, the browser will send AJAX requests to the ASP.NET Core Web API endpoints. Since the Web API is configured to block cross-origin requests (CORS), we will encounter CORS issues, and the requests will fail.

To understand this better, open the browser developer tool, open the Console tab, and then try to perform the Create, Update, and Delete operations by clicking on the respective buttons. Then you will see the CORS issue as shown in the image below:

Performing the Create, Update, and Delete Operations

Understanding the CORS Issue

In our example, the client application, i.e., ASP.NET Core MVC Application, is hosted at https://localhost:7102/, and the ASP.NET Core Web API application is hosted at https://localhost:7052/; hence, we have different origins based on the port number.

We are making AJAX requests from the MVC Client Application to the Web API Server Application, but the Web API application or Server is not configured to allow cross-origin requests. Let’s proceed and see how the browser and server handle this scenario. Let’s take a look at the Create User Post Request.

Step 1: Preflight Request

When we try to send a POST request to the server with a header (e.g., Content-Type: application/json), the browser first sends a preflight request (an HTTP OPTIONS Request) to the Web API server to check if the server allows this cross-origin request. The Preflight Request (OPTIONS) contains the following:

  • OPTIONS /api/user/adduser HTTP/1.1
  • Origin: https://localhost:7102
  • Access-Control-Request-Method: POST
  • Access-Control-Request-Headers: Content-Type
Explanation:
  • Origin: Specifies the origin of the request (https://localhost:7082). This is the client domain.
  • Access-Control-Request-Method: Indicates that the actual request will use the POST method.
  • Access-Control-Request-Headers: Lists any headers that will be used in the actual request (e.g., Content-Type).
Step 2: Preflight Response from Server

However, since the ASP.NET Core Web API server is not configured to allow cross-origin requests, so the web server will not include the necessary CORS headers in its response. Since the Web API server did not respond with the required CORS headers, the browser blocks the actual POST request, and the AJAX call fails. And you will see an error similar to the following in the browser’s console:

Access to XMLHttpRequest at ‘https://localhost:7152/api/user/adduser’ from origin ‘https://localhost:7102’ has been blocked by CORS policy: Response to preflight request doesn’t pass access control check: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.

How to Enable CORS in an ASP.NET Core Web API Application?

When developing applications where the client (front-end) and server (back-end API) are hosted on different domains or ports, browsers block AJAX requests for security reasons. This is known as the Same-Origin Policy. To allow your client application to communicate with your ASP.NET Core Web API, you must enable and configure CORS (Cross-Origin Resource Sharing) on your API server.

CORS Configuration Levels

We can configure CORS in ASP.NET Core at three different levels:

  • Globally (recommended for consistent policy across all endpoints)
  • Controller-level (for applying different policies per controller)
  • Action-level (for more control on specific endpoints)
Enabling CORS Globally (Recommended Approach)

Configuring CORS globally is the simplest and most common approach. This ensures the same policy applies to all controllers and endpoints. So, please modify the Program.cs file as follows:

using Microsoft.EntityFrameworkCore;
using UserServiceAPI.Models;
namespace UserServiceAPI
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add controllers and configure JSON options
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = null;
});
// Add Swagger
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Configure EF Core DbContext
builder.Services.AddDbContext<UserDbContext>(options =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("EFCoreDBConnection"));
});
// Configure CORS policy to allow requests from the client application
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAllOrigin",
builder =>
{
builder.AllowAnyOrigin()  // Allow all Origins
.AllowAnyHeader()  // Allow all headers (like Content-Type)
.AllowAnyMethod(); // Allow all HTTP methods (GET, POST, etc.)
});
});
var app = builder.Build();
// Enable Swagger in Development
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
// Enable the CORS middleware globally with the defined policy
app.UseCors("AllowAllOrigin");
app.UseAuthorization();
app.MapControllers();
app.Run();
}
}
}
Code Explanation
  • Adding the CORS service (AddCors) defines a policy named “AllowAllOrigin” that allows any origin, any HTTP method, and any headers.
  • Enabling the CORS middleware (UseCors) with the policy name applies this policy to every incoming request.

Note: This global configuration is straightforward and effective when you want to allow your API to be accessible from any client.

Testing CORS Configuration
  • Run your Web API application.
  • Run your client application (e.g., an MVC app or SPA) that makes AJAX requests.
  • Requests should now succeed without CORS errors, as the server explicitly allows cross-origin requests.
What Happens When CORS Is Configured Properly?

Let us understand when CORS is enabled, what is happening behind the scenes during a cross-origin AJAX call

Step 1: Preflight Request (OPTIONS)

Before sending the actual POST request (or any request with custom headers or methods), the browser sends a special HTTP OPTIONS request, called a “preflight” request, to the server to verify permissions. The preflight request includes the following:

  • OPTIONS /api/user/adduser
  • Origin: https://localhost:7102
  • Access-Control-Request-Method: POST
  • Access-Control-Request-Headers: Content-Type

Here,

  • Origin: Client application’s URL.
  • Access-Control-Request-Method: The HTTP method for the actual request.
  • Access-Control-Request-Headers: Headers that will be sent with the actual request.
Step 2: Preflight Response from Server

The API responds with CORS headers indicating allowed origins, methods, and headers, and a 204 No Content HTTP status. If the API is configured to allow CORS, it responds with:

  • Access-Control-Allow-Origin: *
  • Access-Control-Allow-Methods: POST
  • Access-Control-Allow-Headers: Content-Type

Here,

  • Access-Control-Allow-Origin: * allows requests from any origin.
  • Access-Control-Allow-Methods: POST allows only POST requests to this endpoint.
  • Access-Control-Allow-Headers: Content-Type permits the Content-Type header.

Note: The browser caches this preflight response, reducing the need to send preflight requests for subsequent similar requests within the cache lifetime. The cache duration is controlled by Access-Control-Max-Age if set.

Step 3: Actual AJAX Request

Since the preflight was successful:

  • The browser then sends the actual request (e.g., POST).
  • The API processes the request and returns a response (such as 201 Created for a new resource).
Observing Requests in Browser Network Tab

You should see:

  • An OPTIONS request (with a 204 status and CORS headers).
  • The actual POST (or GET/PUT/DELETE) request (with your response data).

For repeated requests (while the preflight result is cached), the browser may skip the preflight and go directly to the actual request.

How to Configure a Specific CORS Policy in ASP.NET Core Web API

In most real-world scenarios, you want to restrict cross-origin access to only trusted domains, necessary HTTP methods, and required headers for enhanced security. This provides better security by limiting who and how clients can interact with your API. You can accomplish this by explicitly defining a CORS policy in your Program.cs file.

Let us understand how to restrict CORS by Origin, Headers, and Methods. Please modify the AddCors method in your Program.cs file to define a named CORS policy with specific settings.

using Microsoft.EntityFrameworkCore;
using UserServiceAPI.Models;
namespace UserServiceAPI
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add controllers and configure JSON options
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = null;
});
// Add Swagger
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Configure EF Core DbContext
builder.Services.AddDbContext<UserDbContext>(options =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("EFCoreDBConnection"));
});
// Configure a specific CORS policy
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowSpecificOrigin", policy =>
{
policy
// Only allow these origins
.WithOrigins("https://localhost:7102", "https://example.com")
// Only allow these request headers
.WithHeaders("Content-Type", "Authorization", "Accept")
// Only allow these HTTP methods
.WithMethods("GET", "POST", "PUT", "DELETE");
});
});
var app = builder.Build();
// Enable Swagger in Development
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
// Apply the named CORS policy globally
app.UseCors("AllowSpecificOrigin");
app.UseAuthorization();
app.MapControllers();
app.Run();
}
}
}
What This Configuration Does
  • WithOrigins(…): Specifies the exact client URLs allowed to make requests. Requests from any other origins will be blocked.
  • WithHeaders(…): Lists the specific HTTP headers the API accepts from client requests.
  • WithMethods(…): Specifies which HTTP methods (GET, POST, PUT, DELETE, etc.) the API allows.
CORS Configuration Methods
  • AllowAnyOrigin(): Allows requests from any origin (domain, protocol, port)
  • AllowAnyMethod(): Allows all HTTP methods (GET, POST, PUT, DELETE, OPTIONS, etc.)
  • AllowAnyHeader(): Allows any HTTP headers (e.g., Content-Type, Authorization)
  • WithOrigins(…): Restricts to specific allowed origins (e.g., “https://example.com”)
  • WithMethods(…): Restricts to specific HTTP methods (e.g., “GET”, “POST”)
  • WithHeaders(…): Restricts to specific headers (e.g., “Content-Type”, “Authorization”)
CORS Configuration at Controller and Action Level in ASP.NET Core

In many real-world scenarios, different parts of your API may need to accept cross-origin requests from different client applications or enforce different rules. ASP.NET Core makes this easy by allowing you to apply different CORS policies at the controller or action level, instead of globally. ASP.NET Core allows you to do this by using the [EnableCors] attribute.

Define Multiple CORS Policies in the Program.cs

First, we need to define our CORS policies in the Program.cs file. Only call AddCors once, and add multiple policies inside it. So, please modify the Program class as follows:

using Microsoft.EntityFrameworkCore;
using UserServiceAPI.Models;
namespace UserServiceAPI
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add controllers and configure JSON options
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = null;
});
// Add Swagger
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Configure EF Core DbContext
builder.Services.AddDbContext<UserDbContext>(options =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("EFCoreDBConnection"));
});
// Define two distinct CORS policies
builder.Services.AddCors(options =>
{
// Policy for your first client
options.AddPolicy("AllowSpecificOrigin", policy =>
{
policy.WithOrigins("https://localhost:7102") // Allowed origin
.WithHeaders("Content-Type", "Authorization", "Any-Custom-Header", "Accept") // Allowed headers
.WithMethods("GET", "POST", "PUT", "DELETE"); // Allowed methods
});
// Policy for another client or use case
options.AddPolicy("AllowAnotherSpecificOrigin", policy =>
{
policy.WithOrigins("https://example.com") // Another allowed origin
.AllowAnyHeader()
.WithMethods("GET", "POST", "PUT", "DELETE");
});
});
var app = builder.Build();
// Enable Swagger in Development
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
// Do NOT apply CORS globally here if you want to use attribute-based policies
// app.UseCors();
app.UseAuthorization();
app.MapControllers();
app.Run();
}
}
}

Note: Since you want to apply CORS policies selectively via attributes, do not call app.UseCors() globally. Instead, CORS middleware will be applied when the attribute is detected.

Apply CORS at Controller and Action Method Level

Add the [EnableCors(“PolicyName”)] attribute at the top of the controller class to apply a CORS policy to all actions in that controller. Similarly, add [EnableCors(“PolicyName”)] only to specific actions when different endpoints in the same controller need different CORS rules. So, modify the UserController as follows:

using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using UserServiceAPI.Models;
namespace UserServiceAPI.Controllers
{
[Route("api/[controller]/[action]")]
[ApiController]
[EnableCors("AllowSpecificOrigin")] // Applies this CORS policy to all actions in this controller
public class UserController : ControllerBase
{
private readonly UserDbContext _context;
public UserController(UserDbContext context)
{
_context = context;
}
[HttpGet]
public async Task<ActionResult<List<User>>> GetAllUsers()
{
var users = await _context.Users.AsNoTracking().ToListAsync();
return Ok(users);
}
[EnableCors("AllowSpecificOrigin")]
[HttpGet("{id}")]
public async Task<ActionResult<User>> GetUserById(int id)
{
var user = await _context.Users.AsNoTracking().FirstOrDefaultAsync(usr => usr.Id == id);
if (user == null)
{
return NotFound();
}
return Ok(user);
}
[EnableCors("AllowSpecificOrigin")]
[HttpPost]
public async Task<ActionResult> AddUser(User user)
{
await _context.Users.AddAsync(user);
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(GetUserById), new { id = user.Id }, user);
}
[EnableCors("AllowAnotherSpecificOrigin")]
[HttpPut("{id}")]
public async Task<ActionResult> UpdateUser(int id, User user)
{
if (id != user.Id)
{
return BadRequest();
}
_context.Users.Update(user);
await _context.SaveChangesAsync();
return NoContent();
}
[DisableCors]
[HttpDelete("{id}")]
public async Task<ActionResult> DeleteUser(int id)
{
var user = await _context.Users.FindAsync(id);
if (user != null)
{
_context.Users.Remove(user);
await _context.SaveChangesAsync();
}
return NoContent();
}
}
}

Note: In certain cases, you might want to disable CORS for a specific action while it is enabled for others. To do so, you need to use the [DisableCors] attribute.

What happens if CORS is enabled at all three levels?

When CORS is enabled at the global, controller, and action levels in ASP.NET Core, the most specific (closest) policy takes precedence for each request.

  • Global CORS Policy (app.UseCors): The global CORS middleware applies to every request first. It sets the initial CORS policy for all incoming requests unless overridden by more specific settings.
  • Controller-Level CORS Policy ([EnableCors] on Controller): When a controller has the [EnableCors(“PolicyName”)] attribute, it overrides the global CORS policy for all actions within that controller.
  • Action-Level CORS Policy ([EnableCors] on Action): When an action method has [EnableCors(“PolicyName”)], it overrides both the global and controller-level policies for that specific action.

This means the action’s CORS policy is the most specific and takes the highest precedence.

Understanding and properly configuring CORS is essential for any ASP.NET Core developer building modern, distributed web applications. With the right CORS policies, you can enable secure cross-origin communication between your front-end and back-end services while protecting your API from unauthorized or malicious requests.

In the next article, I will discuss how to implement Token-Based Authentication using JWT in an ASP.NET Core Web API Application. In this article, I explain CORS (Cross-Origin Resource Sharing) in an ASP.NET Core Web API application with Examples. I hope you enjoy this article on CORS (Cross-Origin Resource Sharing) in an ASP.NET Core Web API.

Leave a Reply

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