Home > Back-end >  .NET Core WepApi Pattern for Bad Requests
.NET Core WepApi Pattern for Bad Requests

Time:10-07

I'm using .NET Core 5 Web API and I am trying to determine is there a better pattern than my current logic for handling 400 bad requests.

It is similar to this question - Best practice to return errors in ASP.NET Web API

Below is my code

    [HttpPost("register-customer")]
    [Produces("application/json")]
    [MapToApiVersion("1")]
    public async Task<ActionResult<IRegistrationStatus>> RegisterCustomer([FromBody] Customer customer)
    {
        try
        {
            if (customer == null)
                return StatusCode(400, "Request body is required");

            if (string.IsNullOrEmpty(customer.FirstName))
                return StatusCode(400, "First name is required");

            if (string.IsNullOrEmpty(customer.LastName))
                return StatusCode(400, "Last name is required");

            if (string.IsNullOrEmpty(customer.EmailAddress))
                return StatusCode(400, "Email address is required");

            if (customer.Roles == null || customer.Roles.Count == 0)
                return StatusCode(400, "At least one role is required");

            //call service to register
            return Ok(registrationStatus);
        }
        catch (Exception ex)
        {
            return StatusCode(500, ex.ToString());
        }
    }

All the code works fine but I am wondering is there a better pattern I could use that multiple if statements for each prop I want to check. I also have other APIs for RegisterEmployee for example and the employee class is a different model but it has some of the same fields that are getting checked for 400 - i,e FirstName, LastName - I dont really want to change the Models to sit behind say a Person Interface whcih I guess I could do and then both Employee and Customer would inherit from it - if I did it could be a breaking change for the consumers of my APIs

CodePudding user response:

You can simplify validation using:

  • ApiController attribute on your controller. It will force automatic HTTP 400 responses.
  • Using data annotations on members of the model dto class. Force FirstName to not be null:
    public class Customer
    {
        [Required]
        public string FirstName { get; set; }

        public string LastName { get; set; }
    }

Applying both techniques you can omit if checks.

CodePudding user response:

You should use DataAnnotations instead of checking every field one by one. And you can return the ModelState with 400.

One of the other solutions is using Fluent Validation. It's a package which can be found on Nuget Manager.

Api Controller:

        [HttpPost]
        public async Task<ApiResponse> Create([FromBody] BPRPEmailDto bprpEmail)
        {
            BPRPEmail _bPRPEmailDto = _mapper.Map<BPRPEmail>(bprpEmail);

            ValidationResult result = _bprpEmailValidator.Validate(_bPRPEmailDto);
            if (!result.IsValid)
            {
                foreach (var error in result.Errors)
                {
                    ModelState.AddModelError(error.ErrorCode, error.ErrorMessage);
                }
                throw new ApiProblemDetailsException(ModelState);
            }

            await _db.BPRPEmails.AddAsync(_bPRPEmailDto);
            await _db.SaveChangesAsync();
            return new ApiResponse("New record has been created in the database.", _mapper.Map<BPRPEmailDto>(_bPRPEmailDto), 201);
        }

What this code does? Controller gets an BPRPEmailDto then maps it to our model called BPRPEmail. After that it sends this object to Fluent Validation.

 public class BPRPEmailValidator : AbstractValidator<BPRPEmail>
    {
        private readonly AppDbContext _db;

        public BPRPEmailValidator(AppDbContext db)
        {
            _db = db;
            RuleFor(x => x.Id);
            RuleFor(x => x.Email).NotEmpty().Must((model, Email) => BeUniqueEmail(model, Email)).WithMessage("An email should be unique in the database");
            RuleFor(x => x.Email).EmailAddress().WithMessage("Please enter a valid email adress");
            RuleFor(x => x.BPResponsiblePersonId).NotEmpty().WithMessage("An Email's ResponsiblePersonID is required in the database");
            RuleFor(x => x.BPResponsiblePersonId)
              .Must(BeRegisteredResponsiblePerson).WithMessage("An Email's ResponsiblePersonID should be a reqistered ResponsiblePerson in the database");
        }


        private bool BeUniqueEmail(BPRPEmail sCRPEmail, string email)
        {
            var dbEmail = _db.BPRPEmails.Where(x => x.Email == email).FirstOrDefault();
            if (dbEmail == null)
                return true;
            return sCRPEmail.Id == dbEmail.Id;
        }
        private bool BeRegisteredResponsiblePerson(int id)
        {
            if (id == 0)
                return true;
            if (_db.BPResponsiblePeople.Any(x => x.Id == id))
                return true;
            return false;
        }
    }

If there are validation errors, we add these errors into ModelState and throw ApiProblemDetailsException. In frontend we catch this response

 try
        {
            BPRPEmailDto.ResponsiblePersonId = ResponsiblePersonId;
            var response = await BPResponsiblePeopleScreenUseCases.CreateEmailToBPRP(BPRPEmailDto);
            ShowNotification(new NotificationMessage { Severity = NotificationSeverity.Success, Summary = "Insert", Detail = $"Email with id {response.Id} inserted successfully.", Duration = 4000 });
            isCreate = false;
            await _bprpEmailGrid.Reload();
        }
        catch (Exception e)
        {
            JsonElement JsonErrors = JsonSerializer.Deserialize<dynamic>(e.Message);
            if (JsonErrors.GetProperty("validationErrors").EnumerateArray().Count() != 0)
            {
                foreach (var error in JsonErrors.GetProperty("validationErrors").EnumerateArray())
                {
                    ShowNotification(new NotificationMessage { Severity = NotificationSeverity.Error, Summary = error.GetProperty("name").ToString(), Detail = error.GetProperty("reason").ToString(), Duration = 4000 });
                }
            }
            else
            {
                ShowNotification(new NotificationMessage { Severity = NotificationSeverity.Error, Summary = "Error", Detail = JsonErrors.GetProperty("title").ToString(), Duration = 4000 });
            }
            await InsertRow(BPRPEmailDto);
        }

After we deserialize the request, inside a foreach loop we can show all the validation errors to user.

In my example I used AutoMapper, AutoWrapper and Fluent Validation. I suggest you to learn how to use AutoMapper and AutoWrapper, but for validation you can use DataAnnotations too.

  • Related