Enable cross-origin requests in ASP.NET Web API 2

by Sachin Singh


Posted on Saturday, 13 March 2021

Tags: Enable cross-origin requests in ASP.NET Web API 2

In order to understand cross-origin request, first create a rest service which expose some data to the outside world and create a client application to consume the data that is being exposed by your service.

For demonstration purpose , I am going to create an employee service with Asp.Net web API 2, which will expose some employee data to the outside world, user will also be able to create employee on the server and to consume the service ,I will create a simple html page, where we will try to fetch the resource which will be exposed by web API service or create resource using JQuery AJAX.

Step 1.Create a database and name it EmployeeService and run the below script to generate required table.


  SET ANSI_NULLS ON
  GO
  SET QUOTED_IDENTIFIER ON
  GO
  SET ANSI_PADDING ON
  GO
  CREATE TABLE [dbo].[Employee](
	[Id] [int] IDENTITY(1,1) NOT NULL,
	[Name] [varchar](50) NULL,
	[DOB] [date] NULL,
	[Gender] [varchar](50) NULL,
	[Salary] [int] NULL,
  CONSTRAINT [PK_Employee] PRIMARY KEY CLUSTERED 
  (
	[Id] ASC
   )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, 
  ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
   ) ON [PRIMARY]

   GO

Step 2.Create a web api empty project and name it EmployeeService. and Create a web api 2 empty controller and name it employeecontroller.
  • Go to file and select new and then select project .

Create new project
Create new project
  • In the next window select Asp.net web application and give the solution a meaningful name like EmployeeService
select asp.net web application
Create new project step 2
  • Select Empty template and check the web api checkbox.
Empty Web API
Empty Web API template
  • Create a web api 2 controller and name it Employee
Add a controller
Add a controller to the controller folder
Add a web api controller
Empty web API 2 controller

Step 3.Generate entity model from database in the models folder of the project as shown below.
  • Right click on the models folder and select add then select new item

Add new Item
Add new item
  • Select Data tab and then select Ado.Net Entity data model , name the model as EmployeeServiceModel and click add.
Ado.Net Entity Data Model
Ado.Net Entity Data Model
  • In the next window select EF Designer from database and click next
EF Designer
EF designer
  • In the next window select or add a new connection, fill the server name, and select employee service database.   • In the next window save the connection setting in web.config as EmployeeServiceDbContext
Data Connection
Data Connection
  • In the next window select EF version as EF v6.0
EF version
EF version
  • In the next window select the database object (table) and name the model namespace as EmployeeServiceModel
Database object
Database Object
  • And Click finish.

Step 3.Create three methods in the Employee Controller as shown below.


   // [EnableCors(origins: "*", headers: "*", methods: "*", exposedHeaders: "Location", SupportsCredentials = true)] 
  // do not uncomment this right now , we will uncomment it later
  public class EmployeeController : ApiController
    {

        EmployeeServiceDbContext db = new EmployeeServiceDbContext();

        public HttpResponseMessage Get()
        {
            try
            {
               
                var data = db.Employees.ToList();
                if (data != null)
                {
                    return Request.CreateResponse(HttpStatusCode.OK, data);
                }
                return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No result found");
            }
            catch
            {
                return new HttpResponseMessage(HttpStatusCode.InternalServerError);
            }
        }
 
        public HttpResponseMessage Get(int Id)
        {
       
        try
            {
                var data = db.Employees.Find(Id);
                if (data != null)
                {
                    return Request.CreateResponse(HttpStatusCode.OK, data);

                }
                return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No Data Found");
            }
            catch
            {
                return new HttpResponseMessage(HttpStatusCode.InternalServerError);
            }
        }
        
        public HttpResponseMessage Post([FromBody] Employee obj)
        {
            try
            {
                db.Employees.Add(obj);
                db.SaveChanges();
                var msg = Request.CreateResponse(HttpStatusCode.Created, obj);
                msg.Headers.Location = new Uri(Request.RequestUri + obj.Id.ToString());
                return msg;
            }
            catch
            {
                return new HttpResponseMessage(HttpStatusCode.InternalServerError);
            }
        }   
    }
 

Step 4.Add another project in the solution and name it as JqueryClient and click ok. And in the next window just select empty template and do not tick anything for core refrences.

Add new project to the solution
Add new project to the solution
Add Empty template
Add Empty template

Step 5.Add an Html page in the JqueryClient Project and Name it ClientOne.

Add a Html page
Add a Html page

Step 6.Add the Jquery reference in the Html page and paste the below code.


   <!DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>ClientOne</title>
        <link href="Content/bootstrap.min.css" rel="stylesheet" />
        <script src="JS/Jquery.js"></script>
        <script src="JS/bootstrap.min.js"></script>
    </head>
    <body>
        <div class="container">
            <table class="table table-bordered mt-5">
                <tr>
                    <td>Name</td>
                    <td><input type="text" name="name" value="" id="txtName" /></td>
                </tr>
                <tr>
                    <td>DOB</td>
                    <td><input type="date" name="dob" value="" id="txtDOB" /></td>
                </tr>
                <tr>
                    <td>Gender</td>
                    <td>
                        <input type="radio" name="Gender" value="Male" />Male
                        <input type="radio" name="Gender" value="Female" />Female
                    </td>
                </tr>
                <tr>
                    <td>Salary</td>
                    <td><input type="number" name="Salary" value="" id="txtSalary" /></td>
                </tr>
                <tr>
                    <td>
                        <button onclick="getEmployeeById()" class="btn btn-danger">Get Employee by Id</button>
                    </td>
                    <td>
                        <input type="number" name="Id" value="" id="Id" placeholder="Enter employee Id" />
                    </td>

                </tr>
                <tr>
                    <td>
                        <button onclick="getEmployee()" class="btn btn-primary">Get All Employee</button>
                        <button onclick="Refresh()" class="btn btn-primary">Refresh</button>
                    </td>
                    <td><button onclick="SaveEmployee()" class="btn btn-success">Add New</button></td>
                </tr>
            </table>

            <table class="table table-bordered" id="main" style="display:none">
                <thead>
                    <tr>
                        <th>Id</th>
                        <th>Name</th>
                        <th>DOB</th>
                        <th>Salary</th>
                        <th>Gender</th>
                    </tr>
                </thead>
                <tbody></tbody>
            </table>

        </div>

        <script>

            function SaveEmployee() {
                debugger;
                var model = {
                    Name: $("#txtName").val(),
                    Salary: $("#txtSalary").val(),
                    Gender: $(":input[name=Gender]:checked").val(),
                    DOB: $("#txtDOB").val(),


                }

                $.ajax({

                    "type": "Post",
                    "url": "http://localhost:1268/api/employee",
                    "dataType": "json",

                    "contentType": "application/json;charset=utf-8",
                    "data": JSON.stringify(model),

                    "dataTpe": "Json",

                    "cache": false,
                    "success": function (data, status, xhr) {
                        debugger;

                        alert(xhr.getResponseHeader('Location'));
                    },

                    "error": function (request, message, error) {
                        handleException(request, message, error);
                    }
                })

            }

            function getEmployeeById() {
                var id = $("#Id").val();
                debugger;
                $.ajax({
                    "type": "get",
                    "url": "http://localhost:1268/api/employee",
                    "data": { Id: id },
                    "dataTpe": "Json",
                    "cache": false,
                    "success": function (data) {
                        debugger;

                        $("#txtName").val(data.Name);
                        $("#txtSalary").val(data.Salary);
                        $(":input[name=Gender][value=" + data.Gender + "]").prop('checked', true);
                        var dob = CovertToDate(data.DOB);
                        $("#txtDOB").val(dob);
                    },

                    "error": function (request, message, error) {
                        handleException(request, message, error);
                    }
                })
            }

            function Refresh() {
                debugger;
                $("#txtName").val("");
                $("#txtSalary").val("");
                $(":input[name=Gender]").prop('checked', false);
                $("#txtDOB").val("");
                $("#Id").val("");
            }

            function handleException(request, message, error) {
                var msg = "";
                msg += "Code: " + request.status + "\n";
                msg += "Text: " + request.statusText + "\n";
                if (request.responseJSON != null) {
                    msg += "Message: " +
                           request.responseJSON.Message + "\n";
                }
                alert(msg);
            }

            function CovertToDate(date) {
                debugger;
                var dob;
                var d = new Date(date);
                var day = ("0" + d.getDate()).slice(-2);
                var month = ("0" + (d.getMonth() + 1)).slice(-2);
                dob = d.getFullYear() + "-" + (month) + "-" + (day);

                return dob;
            }


            function getEmployee() {
                debugger;
                $.ajax({
                    "type": "get",
                    "url": "http://localhost:1268/api/employee",
                    "dataTpe": "Json",
                    "cache": false,
                    "success": function (data) {

                        $(data).each(function (i, y) {
                            var trr = document.createElement("tr");
                            var dob = CovertToDate(y.DOB);
                            $(trr).append("<td>" + y.Id + "</td>").append("<td>" + y.Name + "</td>").append("<td>" + dob + 
         "</td>").append("<td>" + y.Salary + "</td>").append("<td>" + y.Gender + "</td>");
                            $("#main tbody").append(trr);
                        });
                        $("#main").show();

                    },
                })
            }

        </script>
    </body>
   </html>

Step 7. Make the ClientOne.html start page of the application.
  • Right click on the ClientOne.html and
  • Select set as start page.

ClientOne.html as Start Page
Start page of the Client App

Step 8. Configure the solution to run both the project (API and Client App) at the same time.
  • Right click on the solution (EmployeeService) and Go to properties.

Solution Properties
Solution properties

  • In the property window select multiple startup projects and start both projects as shown in figure.
Multiple startup projects
Multiple startup projects

Step 8.Run the application by pressing ctrl+f5 or start button on the menu bar. You will get the below output

Client App
Client App

Fill the Id field and click GetById button.
At this moment you will get error related to same origin policy which is logical and self explanatory , In real time the client app have different origin , here different origin means different domain name or port number or scheme(http or https) that is the reason I have created the client App as a different project so that you can take feel of real time project and face the problems that may occur in a live project.

Cors error
Cross origin request error

As you can see from the error , browser is not allowing ClientApp to access the resource ('http://localhost:1268/api/employee?Id=1) because the clientApp has the origin http://localhost:1274/ClientOne.html , both have different port number , which is violation of same origin policy.

It's time to understand what exactly this same origin policy is?

Browser security prevents a web page from making AJAX requests to another origin. This restriction is called the same-origin policy, which prevents a malicious site from reading sensitive data from another site.
Two URLs have the same origin if they have identical schemes, hosts, and ports.

Same origin policy
Same origin policy

In our case , the request to web api from html client application is a cross origin request because of different port numbers.

Cross origin request
Cross origin request

However, we want to let other sites like ClientOne in our example to call our web API. In order to let other sites to call our web API, we will have to bypass the same origin policy.
There are two techniques to achieve this.

Method I) JSONP

JSONP stands for JSON with Padding. JSONP is a method for sending JSON data using <script> tag. Requesting a resource from another domain causes problems, due to cross-domain policy. But Requesting an external script from another domain does not have this problem. JSONP uses this advantage, and request resource using the script tag instead of the XMLHttpRequest object.

JSONP is a clever rabbit, all he does is wraps the data in a function. So for example, if you have the following JSON object.


  {
    "Name" : "Sachin",
    "Gender"  : "Male",
    "DOB"    : "08/10/1995",
    "Salary": 10000
  }


JSONP will wrap the data in a function as shown below

  CallbackFunction({
  "Name" : "Sachin",
    "Gender"  : "Male",
    "DOB"    : "08/10/1995",
  "Salary": 10000
   })


As Browsers allow to consume JavaScript that is present in a different domain Since the data is wrapped in a JavaScript function, this can be consumed by a web page that is present in a different domain.

How to tell web API to return JSON formatted data.

we know, web API only have 4 inbuilt MediaTypeFormatters which are
  1. JsonMediaTypeFormatter.
  2. XMLMediaTypeFormatter.
  3. FormUrlEncodedMediaTypeFormatter and
  4. JqueryMvcFormUrlEncodedMediaTypeFormatter.
so, obviously we will have to include JsonpMediaTypeFormatter in the Formatters collection of web API.
Below are the steps to include JsonpMediaTypeFormatter in the Formatters collection.

Step 1.Install Package WebApiContrib.Formatting.Jsonp
  • Go to tools-->Nuget Package Manager-->Package Manager Console.

Package Manager Console
Package Manager Console
  • Select your web api project as the default project in the package manager console and install the package as shown below.
Installing Package
Install package

step 2. create an object of JsonpMediaTypeFormatter and add it to the Formatters collection.
Go to App_Start folder and open WebApiConfig.cs class, and in the Register() Method include the below code.


  namespace EmployeeService
  {
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {

            var JsonpFormatter = new JsonpMediaTypeFormatter(config.Formatters.JsonFormatter);
            config.Formatters.Insert(0, JsonpFormatter);
            // Web API configuration and services

            // Web API routes
            config.MapHttpAttributeRoutes();
         
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }
    }
  }


Now , the client app needs to set the dataType option of Jquery Ajax as jsonp and Content-Type option as application/json.
After changing the dataType to Jsonp from Json , run the application and make a request to the web api.
You will successfully be able to make Get requests to web API, but Post request does not work with Jsonp which is obvious as it creates a <script> element to fetch data , so the requests can only be a Get request.

Method II) Cross Origin Resource Sharing (CORS)

Cors is a W3C standard that allows a server to relax the same-origin policy. Using CORS, a server in our case the web api ,can explicitly allow some cross-origin requests while rejecting others. CORS is safer and more flexible than earlier techniques such as JSONP.

Enable CORS in Asp.Net web API 2

Step 1.Install Package Microsoft.AspNet.WebApi.Cors
  • Go to tools-->Nuget Package Manager-->Package Manager Console.

Package Manager Console
Package Manager Console
  • Select your web api project as the default project in the package manager console and install the package as shown below.
Installing Cors Package
Install CORS package

step 2. Go to App_Start folder and open WebApiConfig.cs class, and in the Register() Method, comment out the Jsonp formatter and include the below code.


  namespace EmployeeService
  {
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {

            //var JsonpFormatter = new JsonpMediaTypeFormatter(config.Formatters.JsonFormatter);
            //config.Formatters.Insert(0, JsonpFormatter);
            // Web API configuration and services

            // Web API routes
            config.MapHttpAttributeRoutes();
            config.EnableCors(); 
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }
    }
   }

Step 3.UnComment the [EnableCors] attribute to the EmployeeController class. And change the dataType option of Ajax from Jsonp to Json in the ClientOne App.
At this moment run the application and everything should work properly.

Parameters of EnableCORS attribute
1. origins

The origins parameter of the [EnableCors] attribute specifies which origins are allowed to access the resource. The value is a comma-separated list of the allowed origins.


   [EnableCors(origins: "http://www.sharpencode.com,http://www.example.com",headers: "*", methods: "*")]

You can also use the wildcard value "*" to allow requests from any origins.

2. methods

The methods parameter of the [EnableCors] attribute specifies which HTTP methods are allowed to access the resource. To allow all methods, use the wildcard value "*".The following example allows only GET and POST requests.


   [EnableCors(origins: "*", headers: "*", methods: "get,post")]

3. headers

The headers parameter of the [EnableCors] attribute specifies which author request headers are allowed. To allow any headers, set headers to "*". To whitelist specific headers, set headers to a comma-separated list of the allowed headers.


    [EnableCors(origins: "*",headers: "accept,content-type,origin,x-mycustom-header", methods: "*")]

However, If you set headers to anything other than "*", you should include at least "accept", "content-type", and "origin", plus any custom headers that you want to support.

4. exposedHeaders

By default, the browser does not expose all of the response headers to the application. The response headers that are available by default are:
  • Cache-Control
  • Content-Language
  • Content-Type
  • Expires
  • Last-Modified
  • Pragma
The CORS spec calls these simple response headers. To make other headers available to the application, set the exposedHeaders parameter of [EnableCors]. In our example we have explicitly set the Location header to ExposeHeaders.


   [EnableCors(origins: "*", headers: "*", methods: "*", exposedHeaders: "Location")]

5.SupportsCredential

If you want your web API to accept any Authorization header value then set it to true.


   [EnableCors(origins: "*", headers: "*", methods: "*", exposedHeaders: "Location", SupportsCredentials = true)]

Scope Rules for [EnableCors]

You can enable CORS per action, per controller, or globally for all Web API controllers in your application.

Per Action

To enable CORS for a single action, set the [EnableCors] attribute on the action method. The following example enables CORS for the Get(int Id) method only.


    public class EmployeeController : ApiController
    {
        EmployeeServiceDbContext db = new EmployeeServiceDbContext();
      
        public HttpResponseMessage Get()
        {
            try
            {
               
                var data = db.Employees.ToList();
                if (data != null)
                {
                    return Request.CreateResponse(HttpStatusCode.OK, data);
                }
                return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No result found");
            }
            catch
            {
                return new HttpResponseMessage(HttpStatusCode.InternalServerError);
            }
        }

    [EnableCors(origins: "*", headers: "*", methods: "*", exposedHeaders: "Location", SupportsCredentials = true)]
        public HttpResponseMessage Get(int Id)
        {
            try
            {
                var data = db.Employees.Find(Id);
                if (data != null)
                {
                    return Request.CreateResponse(HttpStatusCode.OK, data);

                }
                return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No Data Found");
            }
            catch
            {
                return new HttpResponseMessage(HttpStatusCode.InternalServerError);
            }
        }
        
        public HttpResponseMessage Post([FromBody]Employee emp)
        {
            try
            {
                db.Employees.Add(emp);
                db.SaveChanges();
                var msg = Request.CreateResponse(HttpStatusCode.Created, emp);
                msg.Headers.Location = new Uri(Request.RequestUri +"/"+ emp.Id.ToString());
                return msg;
            }
            catch
            {
                return new HttpResponseMessage(HttpStatusCode.InternalServerError);
            }
        }
     
    }

Per Controller

If you set [EnableCors] on the controller class, it applies to all the actions on the controller. To disable CORS for an action, add the [DisableCors] attribute to the action. The following example enables CORS for every method except Get(int Id) method.


     [EnableCors(origins: "*", headers: "*", methods: "*", exposedHeaders: "Location", SupportsCredentials = true)]
     public class EmployeeController : ApiController
    {
        EmployeeServiceDbContext db = new EmployeeServiceDbContext();
      
        public HttpResponseMessage Get()
        {
            try
            {
               
                var data = db.Employees.ToList();
                if (data != null)
                {
                    return Request.CreateResponse(HttpStatusCode.OK, data);
                }
                return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No result found");
            }
            catch
            {
                return new HttpResponseMessage(HttpStatusCode.InternalServerError);
            }
        }


         [DisableCors]
        public HttpResponseMessage Get(int Id)
        {
            try
            {
                var data = db.Employees.Find(Id);
                if (data != null)
                {
                    return Request.CreateResponse(HttpStatusCode.OK, data);

                }
                return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No Data Found");
            }
            catch
            {
                return new HttpResponseMessage(HttpStatusCode.InternalServerError);
            }
        }
        
        public HttpResponseMessage Post([FromBody]Employee emp)
        {
            try
            {
                db.Employees.Add(emp);
                db.SaveChanges();
                var msg = Request.CreateResponse(HttpStatusCode.Created, emp);
                msg.Headers.Location = new Uri(Request.RequestUri +"/"+ emp.Id.ToString());
                return msg;
            }
            catch
            {
                return new HttpResponseMessage(HttpStatusCode.InternalServerError);
            }
        }   
    }

Globally

To enable CORS for all Web API controllers in your application, pass an EnableCorsAttribute instance to the EnableCors method , in the Register() method of webApiConfig.cs class.


  namespace EmployeeService
   {
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {

            //var JsonpFormatter = new JsonpMediaTypeFormatter(config.Formatters.JsonFormatter);
            //config.Formatters.Insert(0, JsonpFormatter);
            // Web API configuration and services

            // Web API routes
            config.MapHttpAttributeRoutes();

            var cors = new EnableCorsAttribute("*", "*", "*");
            config.EnableCors(cors); 


            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }
    }
   }