Hire a web Developer and Designer to upgrade and boost your online presence with cutting edge Technologies

Saturday, August 27, 2022

ASP.NET Core Web API – How to Handle Get Request

 The Get Request is something we can’t miss while creating API because it is the most common request. So, it is very important to learn more about handling that kind of request.

In the previous post, we have created a repository pattern for collecting the data from the database.

Now, it is time to use that repository for business logic.

We are going to keep all the database logic inside the repository classes. Controllers will be responsible for handling requests, model validation, and returning responses to the frontend part of the application.

By doing so, our controllers won’t be overwhelmed with the code thus making the code easier to read and maintain as well.


So, let’s start.Controllers and Routing in WEB API

To create a controller, right-click on the Controllers folder inside the main project and Add/Controller. Then from the menu choose API Controller - Empty and name it OwnerController.cs:

using Microsoft.AspNetCore.Mvc;
namespace AccountOwnerServer.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class OwnerController : ControllerBase
{
}
}

Every web API controller class inherits from the ControllerBase abstract class that provides all the necessary behavior for the derived class.

Also, above the controller class we can see this part of the code:

[Route("api/[controller]")]

This represents the routing and we will talk a little bit about the routing inside Web APIs.

Web API routing routes the incoming HTTP requests to the particular action method inside the Web API controller.

There are two types of routings:

  1. Convention based routing and
  2. Attribute routing

Convention-based routing is called that way because it establishes a convention for the URL paths. The first part makes the mapping for the controller name, the second part makes the mapping for the action method, and the third part is used for the optional parameter. We can configure this type of routing in the Startup class in the Configure method:

Attribute routing uses the attributes to map the routes directly to the action methods inside the controller. Usually, we place the base route above the controller class, as you can notice in our Web API controller class. Similarly, for the specific action methods, we create their routes right above them.

GetAllOwners GET Request in .NET Core

Let’s start.

First, let’s change the base route from: [Route("api/[controller]")] to: [Route("api/owner")]. Even though the first route will work just fine, with the second example we are more specific to show that this routing should point to the OwnerController.

Now it is time to create the first action method to return all the owners from the database.

In the IOwnerRepository interface create a definition for theGetAllOwners method :

public interface IOwnerRepository
{
IEnumerable<Owner> GetAllOwners();
}

Then implement that interface inside the OwnerRepository class:

namespace Repository
{
public class OwnerRepository : RepositoryBase<Owner>, IOwnerRepository
{
public OwnerRepository(RepositoryContext repositoryContext)
:base(repositoryContext)
{
}
public IEnumerable<Owner> GetAllOwners()
{
return FindAll()
.OrderBy(ow => ow.Name)
.ToList();
}
}
}

Finally, we need to return all the owners by using the GetAllOwners method inside the Web API action.

The purpose of the action methods, inside Web API controllers, is not only to return the results. It is the main purpose, but not the only one. You need to pay attention to the status codes of your Web API responses as well. Additionally, you’ll have to decorate your actions with the HTTP attributes which will mark the type of the HTTP request to that action.

You can read more on HTTP and find some HTTP request examples in part 1 of our HTTP series.

Finally, let’s modify the OwnerController:

using Contracts;
using Microsoft.AspNetCore.Mvc;
using System;
namespace AccountOwnerServer.Controllers
{
[Route("api/owner")]
[ApiController]
public class OwnerController : ControllerBase
{
private ILoggerManager _logger;
private IRepositoryWrapper _repository;
public OwnerController(ILoggerManager logger, IRepositoryWrapper repository)
{
_logger = logger;
_repository = repository;
}
[HttpGet]
public IActionResult GetAllOwners()
{
try
{
var owners = _repository.Owner.GetAllOwners();
_logger.LogInfo($"Returned all owners from database.");
return Ok(owners);
}
catch (Exception ex)
{
_logger.LogError($"Something went wrong inside GetAllOwners action: {ex.Message}");
return StatusCode(500, "Internal server error");
}
}
}
}

Let us explain this code a bit.

First of all, we inject the logger and repository services inside the constructor. Then by decorating the GetAllOwners action with [HttpGet] attribute, we are mapping this action to the GET request. Finally, we use both injected parameters to log the messages and to get the data from the repository class.

The IActionResult interface supports using a variety of methods, which return not only the result but the status codes as well. In this situation, the OK method returns all the owners and also the status code 200 which stands for OK. If an exception occurs, we will return the internal server error with the status code 500.

You can read more about status codes by reading The HTTP series – References.

One more thing. In this series, we are not using the service layer because we didn’t want to make things more complicated for this small project. But if you want to use it in your projects, which we strongly recommend, please read our Onion Architecture article to see how it should be done.

Because there is no route attribute right above the action, the route for the action GetAllOwners will be api/owner (http://localhost:5000/api/owner).

Code Permissions and Testing the Result

We would like to point out one more thing inside GetAllOwners action. Right now, if you look at the repository structure, its classes inherit from the abstract RepositoryBase<T> class and also from its own interface which then inherits from the IRepositoryBase<T> interface. With this hierarchy in place, by typing _repository.Owner. you are able to call the custom method from the OwnerRepository class and also all of the methods from the abstract RepositoryBase<T> class.

If you want to avoid that type of behavior and to allow actions inside the controller to call only methods from the repository user classes, all you need to do is to remove IRepositoryBase<T> inheritance from IOwnerRepository. Consequently, only repository user classes will be able to call generic methods from RepositoryBase<T> class. Likewise, action methods communicate only with repository user classes.

It is all up to you, how you want to organize your code and permissions.

To check the result, we are going to use the Postman tool to send requests towards the application.

Also, you can learn more about how to consume web API programmatically using C# by reading A few great ways to consume restful api in c#.

Let’s start the application, start the Postman and create a request:


Excellent, everything is working as planned.

As you can see, we return all the data from the database with this action. Of course, you can add paging to this action and optimize it by returning only the part of the data.

Before we continue, we would like to show you one more thing. If you look at the model classes, you’ll notice that all properties have the same name as the columns they are mapped to. But you can have the property with a different name than the column it points to, and still to map each other. To achieve that you need to use attribute [Column]

So, let’s do something like that.

We are going to change the property names from AccountId and OwnerId to just Id in the Owner and Account classes. Also, we are going to add the [Column] property which will map the Id property to the right column in the database:

[Table("Account")]
public class Account
{
[Column("AccountId")]
public Guid Id { get; set; }
[Required(ErrorMessage = "Date created is required")]
public DateTime DateCreated { get; set; }
[Required(ErrorMessage = "Account type is required")]
public string AccountType { get; set; }
[Required(ErrorMessage = "Owner Id is required")]
public Guid OwnerId { get; set; }
[ForeignKey(nameof(Owner))]
public Guid OwnerId { get; set; }
public Owner Owner { get; set; }
}

[Table("Owner")]
public class Owner
{
[Column("OwnerId")]
public Guid Id { get; set; }
[Required(ErrorMessage = "Name is required")]
[StringLength(60, ErrorMessage = "Name can't be longer than 60 characters")]
public string Name { get; set; }
[Required(ErrorMessage = "Date of birth is required")]
public DateTime DateOfBirth { get; set; }
[Required(ErrorMessage = "Address is required")]
[StringLength(100, ErrorMessage = "Address can not be loner then 100 characters")]
public string Address { get; set; }
public ICollection<Account> Accounts { get; set; }
}

Now let’s continue.

Using DTO and AutoMapper

DTO or Data Transfer Object serves the purpose to transfer data from the server to the client. That is exactly what are we going to use it for.

If we take a look at the GetAllOwners action, we can see that we use the model Owner class to fetch the data from the database (_repository.Owner.GetAllOwners()returns a list of  Owner objects) and also to return that result to the client. And that is not a good practice. A much better practice is to have a model class to fetch the data from the database and to have a DTO class to return that result to the client. The DTO object could be exactly the same as the model object but still, it is much better to use DTO objects because if something changes in the database the model class must change but that doesn’t mean that the client wants changed results. Thus the DTO object will not change.

Having that said, let’s create a new folder DataTransferObjects in the Entities project and let’s create OwnerDto class inside:

public class OwnerDto
{
public Guid Id { get; set; }
public string? Name { get; set; }
public DateTime DateOfBirth { get; set; }
public string? Address { get; set; }
}

As you can see, we don’t have the Accounts property, because we don’t want to show that information to the client right now.

Now, all we would have to do is to map a returned list of owners from the database to the list of ownerDto. But, doing that manually is a boring job and if we have twenty or even more properties in our DTO class, it would be time-consuming as well. Luckily for us, there is a great tool that could help us a lot in the mapping process. Yes, it is AutoMapper.

Working with AutoMapper

AutoMapper is a library that helps us map different objects. To install it, we have to type this command in the Package Manager Console window:

PM> Install-Package AutoMapper.Extensions.Microsoft.DependencyInjection

After the installation, we have to register it in the Program class:

builder.Services.AddAutoMapper(typeof(Program));

Now, we have to create a mapping profile class to tell AutoMapper how to execute mapping actions. So, let’s create a new class MappingProfile in the main project and modify it:

public class MappingProfile : Profile
{
public MappingProfile()
{
CreateMap<Owner, OwnerDto>();
}
}

Finally, we can modify the OwnerController:

public class OwnerController : ControllerBase
{
private ILoggerManager _logger;
private IRepositoryWrapper _repository;
private IMapper _mapper;
public OwnerController(ILoggerManager logger, IRepositoryWrapper repository, IMapper mapper)
{
_logger = logger;
_repository = repository;
_mapper = mapper;
}
[HttpGet]
public IActionResult GetAllOwners()
{
try
{
var owners = _repository.Owner.GetAllOwners();
_logger.LogInfo($"Returned all owners from database.");
var ownersResult = _mapper.Map<IEnumerable<OwnerDto>>(owners);
return Ok(ownersResult);
}
catch (Exception ex)
{
_logger.LogError($"Something went wrong inside GetAllOwners action: {ex.Message}");
return StatusCode(500, "Internal server error");
}
}
}

We can send the same request from Postman and we are going to get the same result (without accounts), but now, with much better implementation. AutoMapper has great capabilities and you can learn more by reading Getting Started With AutoMapper in ASP.NET Core.

GetOwnerById GET Request in .NET Core

To continue, let’s modify the IOwnerRepository interface:

public interface IOwnerRepository
{
IEnumerable<Owner> GetAllOwners();
Owner GetOwnerById(Guid ownerId);
}

Then, let’s implement the interface in the OwnerRepository.cs:

public Owner GetOwnerById(Guid ownerId)
{
return FindByCondition(owner => owner.Id.Equals(ownerId))
.FirstOrDefault();
}

Finally, let’s change the OwnerController:

[HttpGet("{id}")]
public IActionResult GetOwnerById(Guid id)
{
try
{
var owner = _repository.Owner.GetOwnerById(id);
if (owner is null)
{
_logger.LogError($"Owner with id: {id}, hasn't been found in db.");
return NotFound();
}
else
{
_logger.LogInfo($"Returned owner with id: {id}");
var ownerResult = _mapper.Map<OwnerDto>(owner);
return Ok(ownerResult);
}
}
catch (Exception ex)
{
_logger.LogError($"Something went wrong inside GetOwnerById action: {ex.Message}");
return StatusCode(500, "Internal server error");
}
}

We are going to use the Postman to send valid and invalid requests to check the results:

Invalid request:

Owner Details Request

Let’s continue by creating a logic to return the owner object with its account details.

First, we need to create the AccountDto class:

public class AccountDto
{
public Guid Id { get; set; }
public DateTime DateCreated { get; set; }
public string? AccountType { get; set; }
}

Then, we have to modify our OwnerDto class that will help us return the owner with all related accounts to it. If you want you can create an additional DTO class with name OwnerWithAccountsDto, but for the sake of simplicity, we are going to modify the existing DTO class:

public class OwnerDto
{
public Guid Id { get; set; }
public string Name { get; set; }
public DateTime DateOfBirth { get; set; }
public string Address { get; set; }
public IEnumerable<AccountDto>? Accounts { get; set; }
}

Notice the property Accounts which will bind all the accounts related to the certain owner. 

Let’s modify the interface accordingly:

public interface IOwnerRepository
{
IEnumerable<Owner> GetAllOwners();
Owner GetOwnerById(Guid ownerId);
Owner GetOwnerWithDetails(Guid ownerId);
}

Also, let’s modify the repository class:

public Owner GetOwnerWithDetails(Guid ownerId)
{
return FindByCondition(owner => owner.Id.Equals(ownerId))
.Include(ac => ac.Accounts)
.FirstOrDefault();
}

We are using the Include method to include all the accounts related to the current owner.

We have to add a new mapping rule in the MappingProfile class:

public MappingProfile()
{
CreateMap<Owner, OwnerDto>();
CreateMap<Account, AccountDto>();
}

Finally, let’s modify the controller:

[HttpGet("{id}/account")]
public IActionResult GetOwnerWithDetails(Guid id)
{
try
{
var owner = _repository.Owner.GetOwnerWithDetails(id);
if (owner == null)
{
_logger.LogError($"Owner with id: {id}, hasn't been found in db.");
return NotFound();
}
else
{
_logger.LogInfo($"Returned owner with details for id: {id}");
var ownerResult = _mapper.Map<OwnerDto>(owner);
return Ok(ownerResult);
}
}
catch (Exception ex)
{
_logger.LogError($"Something went wrong inside GetOwnerWithDetails action: {ex.Message}");
return StatusCode(500, "Internal server error");
}
}

Result:

We have created these actions that use Repository Pattern logic synchronously but it could be done asynchronously as well. If you want to learn how to do that you can visit Implementing Async Repository in .NET Core. Although we strongly recommend finishing all the parts from this series for an easier understanding of the project’s business logic.

Conclusion

Requests using GET should only retrieve the data from the database, and all the actions inside the OwnerController class are written like that.

By reading this post you’ve  learned:

  • How to work with a controller class
  • What is routing and how to use it
  • How to handle GET requests in a web API
  • The way to use DTOs while handling requests

Thank you all for reading and I hope you found something useful in it.

No comments:

Post a Comment