UsersController

Create the UsersController

Add a new class UsersController derived from Controller. Decorate it with Authorize and with a Route: “api/[controller]“.

Add a constructor to inject the DatingRepository.

Add two methods to get all the users and a single user.

[Authorize]
[Route("api/[controller]")]
public class UsersController : Controller
{
    private readonly IDatingRepository _repo;
    public UsersController(IDatingRepository repo)
    {
        _repo = repo;
    }

    [HttpGet]
    public async Task<IActionResult> GetUsers()
    {
        var users = await _repo.GetUsers();

        return Ok(users);
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> GetUser(int id)
    {
        var user = await _repo.GetUser(id);

        return Ok(user);
    }
}

DTOs

Returning the internal model from an API call is plain wrong. So we map our User to a view model.

Create a new class DTOs/PhotoForUser with a subset of the Photo properties.

public class PhotoForUser
{
    public int Id { get; set; }
    public string Url { get; set; }
    public string Description { get; set; }
}

Create a new class DTOs/UserForList with a subset of the user properties.

public class UserForList
{
    public int Id { get; set; }
    public string Username { get; set; }
    public string Gender { get; set; }
    public int Age { get; set; }
    public string KnownAs { get; set; }
    public DateTime Created { get; set; }
    public DateTime LastActive { get; set; }
    public string City { get; set; }
    public string Country { get; set; }
    public string ProfilePhotoUrl { get; set; }
}

Create another user class DTOs/UserForDetail with more information.

public class UserForDetail
{
    public int Id { get; set; }
    public string Username { get; set; }
    public string Gender { get; set; }
    public int Age { get; set; }
    public string KnownAs { get; set; }
    public DateTime Created { get; set; }
    public DateTime LastActive { get; set; }
    public string Introduction { get; set; }
    public string LookingFor { get; set; }
    public string Interests { get; set; }
    public string City { get; set; }
    public string Country { get; set; }
    public string ProfilePhotoUrl { get; set; }
    public ICollection<PhotoForUser> Photos { get; set; }
}

Automapper

Using the nuget extension CTRL+SHIFT+p NuGet Package Manager: Add Package search AutoMapper.Extensions.Microsoft.DependencyInjection and add the latest version (now it is 4.0.1).

Vscode will ask to restore packages, click restore.

In Startup.ConfigureServices register the service for DI.

services.AddAutoMapper();

Now inject in the constructor of UserController.

public UsersController(IDatingRepository repo, IMapper mapper)
{
    _repo = repo;
    _mapper = mapper;
}

And then fix the two get methods to return view models.

[HttpGet]
public async Task<IActionResult> GetUsers()
{
    var users = await _repo.GetUsers();
    var usersVM = _mapper.Map<IEnumerable<UserForList>>(users);

    return Ok(usersVM);
}

[HttpGet("{id}")]
public async Task<IActionResult> GetUser(int id)
{
    var user = await _repo.GetUser(id);
    var userVM = _mapper.Map<UserForDetail>(user);

    return Ok(userVM);
}

Automapper mapping configuration

Create a new class Helpers/DateTimeExtensions.cs to calculate the Age.

public static class DataTimeExtensions
{
    public static int CalculateAge(this DateTime birthDate)
    {
        var age = DateTime.Today.Year - birthDate.Year;
        // Correct if the birthday for this year is in the future
        if(birthDate.AddYears(age) > DateTime.Today) age--;

        return age;
    }
}

Create a new class Helpers/AutoMapperProfiles.cs to store the mapping information.

Use the extension for the Age and a simple linq search for ProfilePhotoUrl.

public class AutoMapperProfiles : Profile
{
    public AutoMapperProfiles()
    {
        CreateMap<Photo, PhotoForUser>();

        CreateMap<User, UserForList>()
            .ForMember(dest => dest.Age, opt => opt.MapFrom(src => src.DateOfBirth.CalculateAge()))
            .ForMember(dest => dest.ProfilePhotoUrl, opt => opt.MapFrom(src => src.Photos.FirstOrDefault(x => x.IsMain).Url))
        ;

        CreateMap<User, UserForDetail>()
            .ForMember(dest => dest.Age, opt => opt.MapFrom(src => src.DateOfBirth.CalculateAge()))
            .ForMember(dest => dest.ProfilePhotoUrl, opt => opt.MapFrom(src => src.Photos.FirstOrDefault(x => x.IsMain).Url))
        ;
    }
}

Test

Test the user controller with Postman calling /api/users and /api/users/1 (or any other valid id).

Note

Initially i used a value object for Username but it was not a good idea to use complex artifacts in a simple application.

The lowercase conversions are moved inside the property setter of User

private string userName;
public string Username { get { return userName; } set { userName = value.ToLowerInvariant(); } }

And in AuthRepository

...
public async Task<User> Login(string username, string password)
{
    if(string.IsNullOrWhiteSpace(username)) return null;

    var user = await _context.Users
      .FirstOrDefaultAsync(x => x.Username == username.ToLowerInvariant());
...
public async Task<bool> UserExists(string username)
{
    if(string.IsNullOrWhiteSpace(username)) return false;

    return await _context.Users
      .AnyAsync(x => x.Username == username.ToLowerInvariant());
}
...