CREATING A CONTROLLER IN ASP.NET WEB API APPLICATION
For reading data from the database (GET method), writing data about a new product (POST), updating an existing product (PUT and PATCH) as well as deleting a product from the database, there are methods that should be created in the controller class. The controller should be created in a separate "Controllers" folder. Now let's create a controller called ProductController:
A controller is a class that must be derived from the ControllerBase class or its inherited Controller class. It is also important to set the [ApiController] attribute above the constructor as well as the [Route("[controller]")] attribute. The first attribute indicates that it is an api controller, and the second is a route where "[controller]" is replaced with a prefix in the controller name. In this case it is the word products, since the name of the controller is ProductsController. When a specific client application requests a service from the api web service, and in order to call this controller (ProductController) in the URL address field, type:
http://naziv_servera:port/products
If the application is on the local computer and the server is up on port 5001, the call will be:
https://localhost:5001/products
In this case, a GET request was sent, which will return a list of all products from the database, as can be seen in the following image:
Given that a connection to the database was created in the Startup class and added to the collection of available services, it was passed to the controller via the constructor and added to the _conn class field, which is actually an IDbConnection object. See below how to implement: GET, POST, DELETE, PUT and PATCH methods inside the controller.
Get requests within the API controller
The displayed method called GetAllProducts is an asynchronous method that is called by a client, web browser, other web application, mobile application or similar. and which retrieves a list of products from the database and returns an IEnumerable object as a return value, which is actually an array of Product class objects. In the case of asynchronous calls, as indicated by the words async and await in combination, tasks can be executed in parallel, thus speeding up the process. Read more about asynchronous methods at the following website: docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/
Within the method, an SQL query is created that should extract and return all records containing product data from the Product table. Via the connection object, the QueryAsync method is called, which uses the previously created query in the form of a string as a parameter. The method returns a Task and is an asynchronous method that should eventually return a list of IEnumerable<Product> products. The asList method converts a set of objects into a List<Product> list, and that list will be returned to the client, but only after all the data has been collected.
A get request that returns a specific product with the requested id
In order to get one, specific product from the database, for example with id=15, in the URL address field, type:
https://localhost:5001/products/15
It's also a get request but with a slightly different route that also requires the product id, so the method will look like:
The GetProduct method receives an id as a parameter and this is used to form a query string that will retrieve the product from the database. Here, the aforementioned query is passed to the asynchronous method QuerySingleOrDefaultAsync of the _conn connection object, which asynchronously returns a specific product from the database. And in this case, the return value is a Task with subtype ActionResult<Product>
Post request for entering new data into the database
A post request can be sent from another application from the same or different device, same or different domain and if we look at the current example the request would be:
https://localhost:5001/products/Create
This is actually a call to the POST method to execute and should be defined as follows:
The [HttpPost("Create")] attribute is required for the PostAsync method to respond to a POST request, and "Create" is the part of the URL that should be typed after the server call and the controller name prefix.
The method accepts an object of the Product class as a parameter and field values will be extracted from it to be inserted into the query string, which represents a query for entering a new row into the table within the database. Using this query and the _conn object (SqlConnection), a Sql command (SqlCommand) is created, where the mentioned objects are passed as parameters of the constructor of the SqlCommand class.
The Sql command will be executed asynchronously by executing the ExecuteAsync method, which will enter the product data into the database and return the Task object, which stores information about the number of rows inserted into the database.
The method accepts an object of the Product class as a parameter and field values will be extracted from it to be inserted into the query string, which represents a query for entering a new row into the table within the database. Using this query and the _conn object (SqlConnection), a Sql command (SqlCommand) is created, where the mentioned objects are passed as parameters of the constructor of the SqlCommand class.
The Sql command will be executed asynchronously by executing the ExecuteAsync method, which will enter the product data into the database and return the Task object, which stores information about the number of rows inserted into the database.
Modification of data on a specific product
In order to update data about a product that has its own identification number (id), a PUT call should be made, the URL of which should be:
https://localhost:5001/products/Update/5
Then the method that has the attribute [HttpPut] is called and as a parameter it accepts the id of the product as well as the data about the new product, which can be seen in the following image:
As with the previous methods, this one is executed asynchronously and returns Task<ActionResult> as a return value, where ActionResult enables the return of different statuses depending on whether the data has been successfully updated or not. After checking the number of updated rows returned, if this value is zero, NotFound() is called, an inherited method from the BaseController class that produces a Status404NotFound status in the response. Otherwise, if the rows in the database have been successfully updated, the NoContent() method will be called, returning Status204NoContent for the response.
Sql command that is formed using the well-known sql query for update and the _conn connection object, and executes as with the POST method by calling the ExecuteAsync method, which returns as a return value the number of updated rows in the database.
Sql command that is formed using the well-known sql query for update and the _conn connection object, and executes as with the POST method by calling the ExecuteAsync method, which returns as a return value the number of updated rows in the database.
Let it be e.g. running another application on the same computer, a web application that needs to display products or one product, create a new one, change or delete an existing one, by communicating through the web API server as an intermediary for working with the database. An example of a product change can be seen in the following image:
It can be seen that the application is running on the same localhost as the web API, but on a different port, in this case on port 5021, unlike the web API application which is on port 5001.
If eg for a quantity that was 1, change the value to eg. 3 and clicks on the edit button, a PUT request is created to the web API application where the url request is:
If eg for a quantity that was 1, change the value to eg. 3 and clicks on the edit button, a PUT request is created to the web API application where the url request is:
https://localhost:5001/products/Update/1013
This is a request to modify the product with id = 1013, which means that the PUT method of the web API will be called, which can be seen in the following image:
The image shows the method executed using the Debbuger in the VS Code tool. It can be noticed that the data about the product passed through the product parameter correspond to those that were seen on the edit form within the web application that sent the request to the web API server (localhost:5021).
After the update, the home page of this application looks like the following image:
After the update, the home page of this application looks like the following image:
It can be seen that the value of the Quantity property has been changed to 3.
Deleting a specific product from the database
In order to delete one product with a certain id, it is necessary to send a DELETE request:
https://localhost:5001/products/Delete/1013
A method whose [HttpDelete] attribute indicates that it is a method that responds to a DELETE request will be called. The method is shown in the following figure:
CRUD operations examples
1.Create (POST)
[HttpPost]
public IActionResult Create([FromBody] Product product)
{
if (ModelState.IsValid)
{
_context.Products.Add(product);
_context.SaveChanges();
return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
}
return BadRequest(ModelState);
}
public IActionResult Create([FromBody] Product product)
{
if (ModelState.IsValid)
{
_context.Products.Add(product);
_context.SaveChanges();
return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
}
return BadRequest(ModelState);
}
This method creates a new product. If the data is correct, it adds the product to the database and returns a 201 Created status.
2. Read (GET)
[HttpGet("{id}")]
public IActionResult GetById(int id)
{
var product = _context.Products.Find(id);
if (product == null)
return NotFound();
return Ok(product);
}
public IActionResult GetById(int id)
{
var product = _context.Products.Find(id);
if (product == null)
return NotFound();
return Ok(product);
}
This method creates a new product. If the data is correct, it adds the product to the database and returns a 201 Created status.
3. Update (PUT)
[HttpPut("{id}")]
public IActionResult Update(int id, [FromBody] Product updatedProduct)
{
var product = _context.Products.Find(id);
if (product == null)
return NotFound();
product.Name = updatedProduct.Name;
product.Price = updatedProduct.Price;
_context.SaveChanges();
return NoContent();
}
public IActionResult Update(int id, [FromBody] Product updatedProduct)
{
var product = _context.Products.Find(id);
if (product == null)
return NotFound();
product.Name = updatedProduct.Name;
product.Price = updatedProduct.Price;
_context.SaveChanges();
return NoContent();
}
Updates an existing product. If it doesn't exist, it returns 404 Not Found. Otherwise, it performs the update and returns 204 No Content.
4. Delete (DELETE)
[HttpDelete("{id}")]
public IActionResult Delete(int id)
{
var product = _context.Products.Find(id);
if (product == null)
return NotFound();
_context.Products.Remove(product);
_context.SaveChanges();
return NoContent();
}
public IActionResult Delete(int id)
{
var product = _context.Products.Find(id);
if (product == null)
return NotFound();
_context.Products.Remove(product);
_context.SaveChanges();
return NoContent();
}
Deletes a product by ID. If not found, returns 404 Not Found, and if deleted successfully, returns 204 No Content.
Model validation in ASP.NET Core using attributes such as [Required], [StringLength], and others:
Example of Model with Validation:
public class Product
{
[Required(ErrorMessage = "Naziv proizvoda je obavezan.")]
[StringLength(100, ErrorMessage = "Naziv ne može biti duži od 100 karaktera.")]
public string Name { get; set; }
[Range(0.01, 10000, ErrorMessage = "Cena mora biti između 0.01 i 10,000.")]
public decimal Price { get; set; }
}
{
[Required(ErrorMessage = "Naziv proizvoda je obavezan.")]
[StringLength(100, ErrorMessage = "Naziv ne može biti duži od 100 karaktera.")]
public string Name { get; set; }
[Range(0.01, 10000, ErrorMessage = "Cena mora biti između 0.01 i 10,000.")]
public decimal Price { get; set; }
}
- [Required]: This attribute ensures that the field is not empty.
- [StringLength]: Limits the string length.
- [Range]: Limits the value of numbers in a given range.
This validation is automatically applied when entering data into the controller, and ASP.NET Core generates appropriate error messages if the conditions are not met.
Authentication and authorization in Web API
In ASP.NET Core, simple authentication and authorization can be achieved using the [Authorize] attribute. For example:
[Authorize]
[HttpGet("{id}")]
public IActionResult GetSecureData(int id)
{
var data = _context.Data.Find(id);
if (data == null)
return NotFound();
return Ok(data);
}
[HttpGet("{id}")]
public IActionResult GetSecureData(int id)
{
var data = _context.Data.Find(id);
if (data == null)
return NotFound();
return Ok(data);
}
This attribute ensures that only authenticated users can access the method. You can also use [AllowAnonymous] to exclude certain actions from authentication.
Returning HTTP status codes
HTTP status codes help in communication between the server and the client. In ASP.NET Core Web API, they are easily used in controller actions.
Examples:
Examples:
- 201 Created: When the resource is successfully created, the CreatedAtAction is used:
return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
- 404 Not Found: Kada resurs nije pronađen, koristi se NotFound():
return NotFound();
- 204 No Content: Kada se operacija uspešno izvrši bez sadržaja za vraćanje:
return NoContent();