Background
An API endpoint that returns phone numbers. I would appreciate some feedback on this very simple service class that is part of the overall solution. Here is its Interface:
public interface IPhoneNumbersService
{
Task<IList<PhoneNumberViewModel>> GetAllPhoneNumbersAsync();
Task<IList<CustomerPhoneNumberViewModel>> GetCustomerPhoneNumbersAsync(int customerId);
}
And here is its implementation:
public class PhoneNumbersService : IPhoneNumbersService
{
private ZetaContext context;
private readonly ILogger logger;
public PhoneNumbersService(
ZetaContext context,
ILogger<PhoneNumbersService> logger)
{
this.context = context;
this.logger = logger;
}
public async Task<IList<PhoneNumberViewModel>> GetAllPhoneNumbersAsync()
{
List<PhoneNumberViewModel> phoneNumbers = await context
.PhoneNumbers
.AsNoTracking()
.Select(x => new PhoneNumberViewModel
{
Customer = $"{x.Customer.Title} {x.Customer.FirstName} {x.Customer.LastName}",
PhoneNumber = x.Value
})
.ToListAsync();
logger.LogTrace($"{phoneNumbers.Count} phone numbers were retrieved from the database.");
return phoneNumbers;
}
public async Task<IList<CustomerPhoneNumberViewModel>> GetCustomerPhoneNumbersAsync(int customerId)
{
Customer customer = await context
.Customers
.AsNoTracking()
.Include(x => x.PhoneNumbers)
.Where(x => x.Id == customerId)
.SingleOrDefaultAsync();
if (customer == null)
{
throw new CustomerNotFoundException(customerId);
}
List<CustomerPhoneNumberViewModel> customerPhoneNumbers = customer
.PhoneNumbers
.Select(x => new CustomerPhoneNumberViewModel
{
PhoneNumber = x.Value,
IsActive = x.IsActive,
CreatedAt = x.CreatedAt
})
.ToList();
return customerPhoneNumbers;
}
}
The Controller that calls the above service looks like this:
[ApiController]
public class PhoneNumbersController : ControllerBase
{
private readonly IPhoneNumbersService phoneNumbersService;
private readonly ILogger logger;
public PhoneNumbersController(
IPhoneNumbersService phoneNumbersService,
ILogger<PhoneNumbersController> logger)
{
this.phoneNumbersService = phoneNumbersService;
this.logger = logger;
}
[Route("api/numbers/")]
public async Task<IActionResult> GetAllPhoneNumbersAsync()
{
IList<PhoneNumberViewModel> phoneNumbers = await phoneNumbersService.GetAllPhoneNumbersAsync();
return Ok(phoneNumbers);
}
[Route("api/customers/{customerId}/numbers")]
public async Task<IActionResult> GetCustomerPhoneNumbers(int? customerId)
{
if (!customerId.HasValue || customerId.Value == 0)
{
throw new ArgumentNullException(nameof(customerId));
}
try
{
IList<CustomerPhoneNumberViewModel> customerPhoneNumbers = await phoneNumbersService.GetCustomerPhoneNumbersAsync(customerId.Value);
return Ok(customerPhoneNumbers);
}
catch (CustomerNotFoundException exception)
{
logger.LogError($"The customer with Id: {exception.CustomerId} could not be found.");
return NotFound();
}
}
}
One thing I am not sure about: Where should one convert the Entity PhoneNumber.cs to its ViewModel PhoneNumberViewModel.cs. Should this happen in the Service class like it is now (see .Select())? Or should this happen in the controller?
If in the controller, then that means my service method GetAllPhoneNumbersAsync() should return a list (or should it be IEnumerable or ICollection) of the entities to the controller. Is that bad? Some say that is leaking my entities into the controller, others say the service should be as re-usable as possible and shouldn't do any mapping.
For an Enterprise application, what more can you do to improve the above? Any tips and pointers would be greatly appreciated.