Upgrade to Pro — share decks privately, control downloads, hide ads and more …

An Opinionated, Maintainable API Code Architecture for ASP.NET Core

An Opinionated, Maintainable API Code Architecture for ASP.NET Core

Spencer Schneidenbach

November 26, 2020
Tweet

More Decks by Spencer Schneidenbach

Other Decks in Technology

Transcript

  1. Introducing the Employee public class Employee { public int Id

    { get; set; } [Required] public string FirstName { get; set; } [Required] public string LastName { get; set; } public DateTime DateOfBirth { get; set; } public DateTime DateOfHire { get; set; } public string SocialSecurityNumber { get; set; } }
  2. The Employee Object • Is part of payroll software •

    Contains sensitive data (social security number)
  3. That’s better public class Employee { public int Id {

    get; set; } [Required] public string FirstName { get; set; } [Required] public string LastName { get; set; } public DateTime DateOfBirth { get; set; } public DateTime DateOfHire { get; set; } public string RodneCislo { get; set; } }
  4. The Employee Object • Is part of payroll software •

    Contains sensitive data (social security number) rodné číslo
  5. Controller is a one man army • Route the request

    • Validate the request • Run service to execute request • Return data
  6. public class Employee { public int Id { get; set;

    } [Required] public string FirstName { get; set; } [Required] public string LastName { get; set; } public DateTime DateOfBirth { get; set; } public DateTime DateOfHire { get; set; } public string RodneCislo { get; set; } }
  7. public class Employee { public int Id { get; set;

    } [Required] public string FirstName { get; set; } [Required] public string LastName { get; set; } public DateTime DateOfBirth { get; set; } public DateTime DateOfHire { get; set; } public string RodneCislo { get; set; } }
  8. public class Employee { public int Id { get; set;

    } [Required] public string FirstName { get; set; } [Required] public string LastName { get; set; } public DateTime DateOfBirth { get; set; } public DateTime DateOfHire { get; set; } public string RodneCislo { get; set; } }
  9. Create (POST) public class CreateEmployeeRequest { public int Id {

    get; set; } [Required] public string FirstName { get; set; } [Required] public string LastName { get; set; } public DateTime DateOfBirth { get; set; } public DateTime DateOfHire { get; set; } public string RodneCislo { get; set; } }
  10. Update (PUT) public class UpdateEmployeeRequest { public int Id {

    get; set; } [Required] public string FirstName { get; set; } [Required] public string LastName { get; set; } public DateTime DateOfBirth { get; set; } public DateTime DateOfHire { get; set; } }
  11. Validation public class CreateEmployeeRequest { public int Id { get;

    set; } [Required] public string FirstName { get; set; } [Required] public string LastName { get; set; } public DateTime DateOfBirth { get; set; } public DateTime DateOfHire { get; set; } public string RodneCislo { get; set; } }
  12. public class CreateEmployeeRequest { [Required] public string FirstName { get;

    set; } [Required] public string LastName { get; set; } }
  13. public class EmployeeCreateValidator : AbstractValidator<EmployeeCreateRequest> { public EmployeeValidator() { RuleFor(e

    => e.FirstName).NotEmpty() .WithMessage("First name is required.") RuleFor(e => e.LastName).NotEmpty() .WithMessage("Last name is required.") } }
  14. public class EmployeeCreateValidator : AbstractValidator<EmployeeCreateRequest> { public EmployeeValidator() { RuleFor(e

    => e.FirstName).NotEmpty() .WithMessage("First name is required.") RuleFor(e => e.LastName).NotEmpty() .WithMessage("Last name is required.") } } public class EmployeeCreateRequest { public string FirstName { get; set; } public string LastName { get; set; } }
  15. [Test] public void EmployeeNameIsRequired() { var request = new EmployeeCreateRequest();

    //no props var validator = new EmployeeCreateValidator(); var result = validator.Validate(request); var firstNameMissing = result.Any(r => r.PropertyName == "FirstName"); var lastNameMissing = result.Any(r => r.PropertyName == "LastName"); Assert.That(firstNameMissing, Is.EqualTo(true)); Assert.That(lastNameMissing, Is.EqualTo(true)); } Test Independently
  16. Dependencies public class EmployeeDeleteValidator : AbstractValidator<EmployeeDeleteRequest> { public ApplicationDbContext Context

    { get; } public EmployeeValidator(ApplicationDbContext context) { Context = context; RuleFor(e => e.Id).Must(ExistInDatabase) .WithMessage("ID does not exist.") } public void ExistInDatabase(EmployeeDeleteRequest request) { return Context.Employee.Find(request.Id) != null; } }
  17. What We’ve Accomplished • Separated requests from the entity •

    Separated validation from entity and requests
  18. public class EmployeeCreateRequest : IRequest<int> { public string FirstName {

    get; set; } public string LastName { get; set; } }
  19. public class EmployeeCreateHandler : IRequestHandler<EmployeeCreateRequest, int> { public EmployeeCreateHandler(ApplicationDbContext context)

    { ... } public async Task<int> Handle(EmployeeCreateRequest request) { var newEmployee = new Employee { FirstName = request.FirstName, LastName = request.LastName }; _context.Employee.Add(newEmployee); await _context.SaveChangesAsync(); return newEmployee.Id; } }
  20. public class EmployeeCreateHandler : IRequestHandler<EmployeeCreateRequest, int> { public EmployeeCreateHandler(ApplicationDbContext context)

    { ... } public async Task<int> Handle(EmployeeCreateRequest request) { var newEmployee = new Employee { FirstName = request.FirstName, LastName = request.LastName }; _context.Employee.Add(newEmployee); await _context.SaveChangesAsync(); return newEmployee.Id; } }
  21. public class EmployeeCreateHandler : IRequestHandler<EmployeeCreateRequest, int> { public EmployeeCreateHandler(ApplicationDbContext context)

    { ... } public async Task<int> Handle(EmployeeCreateRequest request) { var newEmployee = new Employee { FirstName = request.FirstName, LastName = request.LastName }; _context.Employee.Add(newEmployee); await _context.SaveChangesAsync(); return newEmployee.Id; } }
  22. public class EmployeeCreateHandler : IRequestHandler<EmployeeCreateRequest, int> { public EmployeeCreateHandler(ApplicationDbContext context)

    { ... } public async Task<int> Handle(EmployeeCreateRequest request) { var newEmployee = new Employee { FirstName = request.FirstName, LastName = request.LastName }; _context.Employee.Add(newEmployee); await _context.SaveChangesAsync(); return newEmployee.Id; } }
  23. public class EmployeeCreateHandler : IRequestHandler<EmployeeCreateRequest, int> { public EmployeeCreateHandler(ApplicationDbContext context)

    { ... } public async Task<int> Handle(EmployeeCreateRequest request) { var newEmployee = new Employee { FirstName = request.FirstName, LastName = request.LastName }; _context.Employee.Add(newEmployee); await _context.SaveChangesAsync(); return newEmployee.Id; } }
  24. public class EmployeeCreateHandler : IRequestHandler<EmployeeCreateRequest, int> { public EmployeeCreateHandler( ApplicationDbContext

    context, IMapper mapper) { ... } public async Task<int> Handle(EmployeeCreateRequest request) { var newEmployee = _mapper.Map<Employee>(request); _context.Employee.Add(newEmployee); await _context.SaveChangesAsync(); return newEmployee.Id; } }
  25. public class EmployeeCreateHandler : IRequestHandler<EmployeeCreateRequest, int> { public EmployeeCreateHandler( ApplicationDbContext

    context, IMapper mapper) { ... } public async Task<int> Handle(EmployeeCreateRequest request) { var newEmployee = _mapper.Map<Employee>(request); _context.Employee.Add(newEmployee); await _context.SaveChangesAsync(); return newEmployee.Id; } }
  26. AutoMapper • Great for simple CRUD APIs/view models • Anything

    more complex, I usually just write mapper manually
  27. Putting it all together • Dependency injection handles dependencies •

    FluentValidation handles validation • MediatR handles request/responses • AutoMapper handles mapping • Controller will handle HTTP requests
  28. public async Task<IActionResult> Post([FromBody] EmployeeCreateRequest request) { if (!ModelState.IsValid) {

    return BadRequest(ModelState); } var newId = await Mediator.Send(request); return CreatedAtAction("GetEmployee", new { id = employee.Id }); }
  29. public async Task<IActionResult> Post([FromBody] EmployeeCreateRequest request) { if (!ModelState.IsValid) {

    return BadRequest(ModelState); } var newId = await Mediator.Send(request); return CreatedAtAction("GetEmployee", new { id = employee.Id }); }
  30. public async Task<IActionResult> Post([FromBody] EmployeeCreateRequest request) { if (!ModelState.IsValid) {

    return BadRequest(ModelState); } var newId = await Mediator.Send(request); return CreatedAtAction("GetEmployee", new { id = employee.Id }); }
  31. public async Task<IActionResult> Post([FromBody] EmployeeCreateRequest request) { if (!ModelState.IsValid) {

    return BadRequest(ModelState); } var newId = await Mediator.Send(request); return CreatedAtAction("GetEmployee", new { id = employee.Id }); }
  32. public class EmployeeUpdateHandler : IRequestHandler<EmployeeUpdateRequest, IActionResult> { public EmployeeUpdateHandler( ApplicationDbContext

    context IMapper mapper) { ... } public async Task<IActionResult> Handle(EmployeeUpdateRequest request) { var employee = await _context.FindAsync(request.Id); if (employee == null) { return new NotFoundResult(); } _mapper.Map(request, employee); await _context.SaveChangesAsync(); return new OkObjectResult(employee); } }
  33. public class EmployeeUpdateHandler : IRequestHandler<EmployeeUpdateRequest, IActionResult> { public EmployeeUpdateHandler( ApplicationDbContext

    context IMapper mapper) { ... } public async Task<IActionResult> Handle(EmployeeUpdateRequest request) { var employee = await _context.FindAsync(request.Id); if (employee == null) { return new NotFoundResult(); } _mapper.Map(request, employee); await _context.SaveChangesAsync(); return new OkObjectResult(employee); } }
  34. public class EmployeeUpdateHandler : IRequestHandler<EmployeeUpdateRequest, IActionResult> { public EmployeeUpdateHandler( ApplicationDbContext

    context IMapper mapper) { ... } public async Task<IActionResult> Handle(EmployeeUpdateRequest request) { var employee = await _context.FindAsync(request.Id); if (employee == null) { return new NotFoundResult(); } _mapper.Map(request, employee); await _context.SaveChangesAsync(); return new OkObjectResult(employee); } }
  35. public class EmployeeUpdateHandler : IRequestHandler<EmployeeUpdateRequest, Func<Controller, IActionResult>> { public EmployeeUpdateHandler(

    ApplicationDbContext context IMapper mapper) { ... } public async Task<Func<Controller, IActionResult>> Handle(EmployeeUpdateRequest request) { var employee = await _context.FindAsync(request.Id); if (employee == null) { return controller => controller.NotFound(); } _mapper.Map(request, employee); await _context.SaveChangesAsync(); return controller => controller.Ok(employee); } }
  36. public class EmployeeUpdateHandler : IRequestHandler<EmployeeUpdateRequest, Func<Controller, IActionResult>> { public EmployeeUpdateHandler(

    ApplicationDbContext context IMapper mapper) { ... } public async Task<Func<Controller, IActionResult>> Handle(EmployeeUpdateRequest request) { var employee = await _context.FindAsync(request.Id); if (employee == null) { return controller => controller.NotFound(); } _mapper.Map(request, employee); await _context.SaveChangesAsync(); return controller => controller.Ok(employee); } }
  37. public class EmployeeUpdateHandler : IRequestHandler<EmployeeUpdateRequest, Func<Controller, IActionResult>> { public EmployeeUpdateHandler(

    ApplicationDbContext context IMapper mapper) { ... } public async Task<Func<Controller, IActionResult>> Handle(EmployeeUpdateRequest request) { var employee = await _context.FindAsync(request.Id); if (employee == null) { return controller => controller.NotFound(); } _mapper.Map(request, employee); await _context.SaveChangesAsync(); return controller => controller.Ok(employee); } }
  38. public class EmployeeUpdateHandler : IRequestHandler<EmployeeUpdateRequest, Func<Controller, IActionResult>> { public EmployeeUpdateHandler(

    ApplicationDbContext context IMapper mapper) { ... } public async Task<Func<Controller, IActionResult>> Handle(EmployeeUpdateRequest request) { var employee = await _context.FindAsync(request.Id); if (employee == null) { return controller => controller.NotFound(); } _mapper.Map(request, employee); await _context.SaveChangesAsync(); return controller => controller.Ok(employee); } }
  39. public class EmployeeUpdateHandler : IRequestHandler<EmployeeUpdateRequest, Func<Controller, IActionResult>> { public EmployeeUpdateHandler(

    ApplicationDbContext context IMapper mapper) { ... } public async Task<Func<Controller, IActionResult>> Handle(EmployeeUpdateRequest request) { var employee = await _context.FindAsync(request.Id); if (employee == null) { return controller => controller.NotFound(); } _mapper.Map(request, employee); await _context.SaveChangesAsync(); return controller => controller.View("~/Employees/ViewEmployee.cshtml", employee); }
  40. [Route("api/Employees")] public class EmployeeController : BaseController { [HttpGet] public Task<IActionResult>

    Get() => Handle(new GetEmployeesRequest()); [HttpGet("{id}")] public Task<IActionResult> Get(int id) => Handle(new GetEmployeeRequest(id)); [HttpPost] public Task<IActionResult> Post(CreateEmployeeRequest request) => Handle(request); [HttpPut("{id}")] public Task<IActionResult> Put(UpdateEmployeeRequest request) => Handle(request); }
  41. protected async Task<IActionResult> Handle(object request) { if (!ModelState.IsValid) { return

    BadRequest(ModelState); } var func = await _mediator.Send(request); return func(this); }