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.