Converting our existing APIs to controllers

Let's start by converting our existing endpoints to controllers.

First, we need to define the controller class. We're going to take the extra step of creating a base controller class that will contain common functionality for all of our controllers.

🌶️🌶️🌶️ As I mentioned, I use this pattern a lot, and it's one of the first things I do when I create a new API project.

Add this controller to the base directory:

using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public abstract class BaseController : Controller
{
}

A couple of notes:

  • ApiController will apply to all of the controllers derived from BaseController, as will the [Route] attribute.
  • We make it abstract because it's an inherited class only.

We'll then create the EmployeesController inside of our Employees directory.

🌶️🌶️🌶️ Note that putting the controller in the Employees directory is different than the convention Microsoft does - they have controllers inside of a Controllers folder, DTO classes inside of a Models folder, and so on. That's known as "vertical slicing" and it's a popular convention in .NET development, but I prefer horizontal slicing where Employees stuff is grouped with Employees. My advice: do what you feel comfortable with!

public class EmployeesController : BaseController
{
    private readonly IRepository<Employee> _repository;
    private readonly IValidator<CreateEmployeeRequest> _createValidator;

    public EmployeesController(IRepository<Employee> repository, IValidator<CreateEmployeeRequest> createValidator)
    {
        _repository = repository;
        _createValidator = createValidator;
    }


    [HttpGet]
    public IActionResult GetAll()
    {
    }

    [HttpGet("{id}")]
    public IActionResult GetById(int id)
    {
    }

    [HttpPost]
    public IActionResult Create(CreateEmployeeRequest employee)
    {
    }

    [HttpPut("{id}")]
    public IActionResult Update(int id, UpdateEmployeeRequest employee)
    {
    }
}

So far, so good. Now that we have the rough structure of our controllers, we can pretty much copy/paste the logic from our existing endpoints into the controllers:

using FluentValidation;
using Microsoft.AspNetCore.Mvc;
using TheEmployeeAPI.Abstractions;

public class EmployeesController : BaseController
{
    private readonly IRepository<Employee> _repository;
    private readonly IValidator<CreateEmployeeRequest> _createValidator;

    public EmployeesController(IRepository<Employee> repository, IValidator<CreateEmployeeRequest> createValidator)
    {
        _repository = repository;
        _createValidator = createValidator;
    }

    [HttpGet]
    public IActionResult GetAllEmployees()
    {
        var employees = _repository.GetAll().Select(employee => new GetEmployeeResponse
        {
            FirstName = employee.FirstName,
            LastName = employee.LastName,
            Address1 = employee.Address1,
            Address2 = employee.Address2,
            City = employee.City,
            State = employee.State,
            ZipCode = employee.ZipCode,
            PhoneNumber = employee.PhoneNumber,
            Email = employee.Email
        });

        return Ok(employees);
    }

    [HttpGet("{id:int}")]
    public IActionResult GetEmployeeById(int id)
    {
        var employee = _repository.GetById(id);
        if (employee == null)
        {
            return NotFound();
        }

        var employeeResponse = new GetEmployeeResponse
        {
            FirstName = employee.FirstName,
            LastName = employee.LastName,
            Address1 = employee.Address1,
            Address2 = employee.Address2,
            City = employee.City,
            State = employee.State,
            ZipCode = employee.ZipCode,
            PhoneNumber = employee.PhoneNumber,
            Email = employee.Email
        };

        return Ok(employeeResponse);
    }

    [HttpPost]
    public async Task<IActionResult> CreateEmployee([FromBody] CreateEmployeeRequest employeeRequest)
    {
        var validationResults = await _createValidator.ValidateAsync(employeeRequest);
        if (!validationResults.IsValid)
        {
            return ValidationProblem(validationResults.ToModelStateDictionary());
        }

        var newEmployee = new Employee
        {
            FirstName = employeeRequest.FirstName!,
            LastName = employeeRequest.LastName!,
            SocialSecurityNumber = employeeRequest.SocialSecurityNumber,
            Address1 = employeeRequest.Address1,
            Address2 = employeeRequest.Address2,
            City = employeeRequest.City,
            State = employeeRequest.State,
            ZipCode = employeeRequest.ZipCode,
            PhoneNumber = employeeRequest.PhoneNumber,
            Email = employeeRequest.Email
        };

        _repository.Create(newEmployee);
        return CreatedAtAction(nameof(GetEmployeeById), new { id = newEmployee.Id }, newEmployee);
    }

    [HttpPut("{id}")]
    public IActionResult UpdateEmployee(int id, [FromBody] UpdateEmployeeRequest employeeRequest)
    {
        var existingEmployee = _repository.GetById(id);
        if (existingEmployee == null)
        {
            return NotFound();
        }

        existingEmployee.Address1 = employeeRequest.Address1;
        existingEmployee.Address2 = employeeRequest.Address2;
        existingEmployee.City = employeeRequest.City;
        existingEmployee.State = employeeRequest.State;
        existingEmployee.ZipCode = employeeRequest.ZipCode;
        existingEmployee.PhoneNumber = employeeRequest.PhoneNumber;
        existingEmployee.Email = employeeRequest.Email;

        _repository.Update(existingEmployee);
        return Ok(existingEmployee);
    }
}

Now, we'll delete our endpoints from our Program.cs file and run our tests.

🌶️🌶️🌶️ We're going to spend the rest of our time using controllers since that's most of what's out in the wild - you can always refer back to earlier versions of this project to see how things are done with minimal APIs!

If your tests are failing, you're in good company! We still gotta make ASP.NET Core aware that we're using controllers instead of minimal APIs:

app.MapControllers();

app.Run();

Alright, now rerun them. Wait a tick, the Create one is failing - looks like our validation problems aren't being constructed correctly.

{
  "FirstName": ["'First Name' must not be empty."]
}

We can use the ValidationProblem method to return a 400 response with the validation errors, except for one problem: it doesn't use IDictionary<string, string[]> for the errors, so it won't compile. Our previously used extension method is pretty lonely - let's just get rid of it and add a different one. Refactor your Extensions.cs to only include the ModelStateDictionary:

using System;
using FluentValidation.Results;
using Microsoft.AspNetCore.Mvc.ModelBinding;

namespace TheEmployeeAPI;

public static class Extensions
{

    public static ModelStateDictionary ToModelStateDictionary(this ValidationResult validationResult)
    {
        var modelState = new ModelStateDictionary();

        foreach (var error in validationResult.Errors)
        {
            modelState.AddModelError(error.PropertyName, error.ErrorMessage);
        }

        return modelState;
    }

}

NOW our tests are happily passing!

Alright, what ELSE could we do?

I'm not a fan of the validator being injected into the controller since it's only used by one endpoint.

We could add it to the Create method like so:

[HttpPost]
public async Task<IActionResult> CreateEmployee([FromBody] CreateEmployeeRequest employeeRequest, [FromServices] IValidator<CreateEmployeeRequest> validator)

The tests pass after making that change, but it's still more boilerplate than I'd like to have.

So let's use our base controller and add a ValidateAsync method:

protected async Task<ValidationResult> ValidateAsync<T>(T instance)
{
    var validator = HttpContext.RequestServices.GetService<IValidator<T>>();
    if (validator == null)
    {
        throw new ArgumentException($"No validator found for {typeof(T).Name}");
    }
    var validationContext = new ValidationContext<T>(instance);

    var result = await validator.ValidateAsync(validationContext);
    return result;
}

We can now call ValidateAsync from within our controller methods:

[HttpPost]
public async Task<IActionResult> CreateEmployee([FromBody] CreateEmployeeRequest employeeRequest)
{
    var validationResults = await ValidateAsync(employeeRequest);
}

What about all of that property copying stuff I see everywhere?

Oh, you mean this?

var employeeResponse = new GetEmployeeResponse
{
    FirstName = employee.FirstName,
    LastName = employee.LastName,
    Address1 = employee.Address1,
    Address2 = employee.Address2,
    City = employee.City,
    State = employee.State,
    ZipCode = employee.ZipCode,
    //etc...

Yeah, that doesn't look great, and it IS boilerplate. Could we eliminate it? We certainly could, with libraries like AutoMapper. I'm not going to cover it in this course, but know that it exists, and it's pretty easy to use!