Explain in detail how to write an efficient controller in ASP. NET Core

  • 2021-11-24 01:15:52
  • OfStack

By following best practices, you can write better controllers. So-called "thin" controllers (controllers with less code and fewer responsibilities) are easier to read and maintain. Moreover, once your controllers are thin, you may not need to test them too much. Instead, you can focus on testing business logic and data access code. Another advantage of a thin controller is that it is easier to maintain multiple versions of the controller.

This article discusses the bad habits of making controllers fat, and then explores ways to make controllers thinner and easier to manage. My list of best practices for writing controllers may not be comprehensive, but I have discussed the most important ones and provided the relevant source code where appropriate. In the next few sections, we'll look at what a fat controller is, why it's a bad smell of code, what a thin controller is, why it's beneficial, and how to make the controller thin, simple, testable, and manageable.

Delete a data access code from the controller

When writing a controller, you should adhere to the single responsibility principle, which means that the controller should have "one responsibility" or "there is only one reason to change". In other words, you want to minimize the reasons for changing the controller code. The following code shows a typical controller with data access logic.

Using a specific technology stack in the. NET ecosystem creates some confusion because there are many choices, such as which type of runtime should be used? In this article, we will try to make these points clear.


public class AuthorController : Controller
{
  private AuthorContext dataContext = new AuthorContext();
  public ActionResult Index(int authorId)
  {
    var authors = dataContext.Authors
      .OrderByDescending(x=>x.JoiningDate)
      .Where(x=>x.AuthorId == authorId)
      .ToList();
    return View(authors);
  }
}

Using data context instances to read data inside action violates the single 1 duty principle and fills your controller with code that shouldn't be there. In this example, we use an DataContext (assuming we use Entity Framework Core) to connect and process the data in the database.

Tomorrow if you decide to change your data access technology (for better performance or other reasons), you will also have to change your controller. For example, what if I want to use Dapper to connect to the underlying database? A better approach is to use the repository class to encapsulate the data access logic (although I don't like the repository pattern very much). Let's update AuthorController with the following code.


public class AuthorController : Controller
{
  private AuthorRepository authorRepository = new AuthorRepository();
  public ActionResult Index(int authorId)
  {
    var authors = authorRepository.GetAuthor(authorId);
    return View(authors);
  }
}

The controller now looks thinner. So is this the best way to write this controller? No. If your controller is accessing the data access component, it will do too much, thus violating the single 1 duty principle. The controller should not have data access logic or code that directly accesses the data access component. The following is an improved version of the AuthorController class.


public class AuthorController : Controller
{
  private AuthorService authorService = new AuthorService();
  public ActionResult Index(int authorId)
  {
    var authors = authorService.GetAuthor(authorId);
    return View(authors);
  }
}

The AuthorService class uses the AuthorRepository class to perform CRUD operations.


public class AuthorService
{
  private AuthorRepository authorRepository = new AuthorRepository();
  public Author GetAuthor (int authorId)
  {
    return authorRepository.GetAuthor(authorId);
  }
}

Avoid writing boilerplate code to map objects

You often need to map data transfer objects (DTO) and domain objects, and vice versa. Please refer to the code snippet given below, which shows the mapping logic inside the controller method.


public IActionResult GetAuthor(int authorId)
{
  var author = authorService.GetAuthor(authorId);
  var authorDTO = new AuthorDTO();
  authorDTO.AuthorId = author.AuthorId;
  authorDTO.FirstName = author.FirstName;
  authorDTO.LastName = author.LastName;
  authorDTO.JoiningDate = author.JoiningDate;
 }

You should not write such mapping logic in the controller, because it will inflate the controller and add extra responsibilities. If you want to write mapping logic, you can use object mapper tools like AutoMapper to avoid writing a lot of boilerplate code.

Finally, you should move the mapping logic to the service class created earlier. Notice how AutoMapper is used to map two incompatible types, Author and AuthorDTO.


public class AuthorService
{
  private AuthorRepository authorRepository = new AuthorRepository();
  public AuthorDTO GetAuthor (int authorId)
  {
    var author = authorRepository.GetAuthor(authorId);
    return Mapper.Map<AuthorDTO>(author);
  }
}

Avoid writing business logic code in the controller

Business logic or validation logic should not be written in the controller. The controller should only accept one request, then jump to the next action, and nothing else. All the business logic code should be moved to other classes (such as the AuthorService class we created earlier). There are several ways to set up a validator in the request pipeline without writing validation logic in the controller. This will make your controller unnecessarily bloated and make it responsible for tasks that it should not do.

Prefer dependency injection to composition

You should prefer to use dependency injection in the controller to manage dependencies. Dependency injection is a subset of the principle of inversion of control (IoC). It is used to remove internal dependencies by allowing dependencies injected from the outside.

By using dependency injection, you don't have to care about instantiation, initialization, etc. of objects. You can have a factory that returns an instance of the type you want, and then you can use that instance using constructor injection. The following code snippet shows how to use the constructor to inject an instance of type IAuthorService into AuthorController. (Assume that IAuthorService is an interface to the AuthorService class extension. )


public class AuthorController : Controller
{
  private IAuthorService authorService = new AuthorService();
  public AuthorController(IAuthorService authorService)
  {
    this.authorService = authorService;
  }
}

Use action filter to eliminate duplicate code

You can use the action filter in asp. net core to execute custom code at specific points in the request pipeline. For example, you can use the action filter to execute custom code before and after the action method is executed. Instead of writing validation logic in the controller, you can remove validation logic from the controller's action method and write it to the action filter. The following code snippet shows how to achieve this 1 point.


[ValidateModelState]
[HttpPost]
public ActionResult Create(AuthorRequest request)
{
  AuthorService authorService = new AuthorService();
  authorService.Save(request);
  return RedirectToAction("Home");
}

If you assign multiple responsibilities to one controller, there will be multiple reasons for the controller to change. Therefore, this violates the single 1 liability principle, which stipulates that a class should have only one reason to change.


Related articles: