0

I am still very much an amateur programmer trying to tune my skills and one habit which I read about is adding a consistent API response to ensure users are getting consistent results from my API. So I have created the following model:

public class ResponseDTO<T>
{
    public required bool Succeeded { get; set; }
    public required string Message { get; set; } = "";
    public required int StatusCode {get; set;}
    public string[]? Errors { get; set; }

    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public Pagination? Pagination {get; set;}
    public T Data { get; set; }
}

It was unclear if from best practices if the data is suppose to be a single result for T and not a List should I ignore the Pagination? by using the JsonIgnore attribute or should I always return pagination data.

My second issue is how to capture errors during some requests. For instance, for a document upload request I have the following controller:

    public async Task<IActionResult> AddRevisedCertificationAsync([FromForm] AddDocumentDTO addDocumentDTO)
    {
        try
        {
            var documentDTO = await _documentsService.AddRevisedCertificationAsync(addDocumentDTO);
            return Ok(new ResponseDTO<DocumentDTO>(){
                Succeeded = true,
                StatusCode = 200,
                Message = "The file have been successfully uploaded.",
                Data = documentDTO,
                Pagination = null});
        }
        catch(Exception ex)
        {
            var documentDTO = await _documentsService.AddRevisedCertificationAsync(addDocumentDTO);
            return Ok(new ResponseDTO<DocumentDTO>(){
                Succeeded = false,
                StatusCode = 400,
                Message = "The files were not uploaded due to a system error.",
                Data = null,
                Pagination = null});
        } 
    }

Should I be doing a try and catch statement in every controller to catch errors and return the result back as a status code despite the overall status code from the request would show 200 since I am returning with Ok()? Just feels weird. Also, say I know there are two possible exceptions. One the document didnt upload to our cloud storage which is box from some unknown error or the file should have been a pdf. Should I create if statements in my catch statement looking for thrown exception types? Like throw a 400 exception in my service layer back to my controller so the if statement in catch will indicate failed to upload since it need pdf and throw a 500 exception in the service layer if the document did not upload to box from a system error and again catch it in the if statement?

I am just struggling with how this should all work so I can implement it consistently through my application.

1 Answer 1

0

It was unclear if from best practices if the data is suppose to be a single result for T and not a List should I ignore the Pagination? by using the JsonIgnore attribute or should I always return pagination data.

To answer your this question:

If the Data property holds a single object not a list,it is batter to exclude the pagination an use JsonIgnore attribute is appropriate in this case.

My second issue is how to capture errors during some requests. For instance, for a document upload request I have the following controller:

To handle error in your code you could use the try-catch clock.

[ApiController]
[Route("[controller]")]
public class DocumentsController : ControllerBase
{
    [HttpPost]
    public async Task<IActionResult> AddRevisedCertificationAsync([FromForm] AddDocumentDTO addDocumentDTO)
    {
        if (!ModelState.IsValid)
        {
            var errors = ModelState.Values
                                    .SelectMany(v => v.Errors)
                                    .Select(e => e.ErrorMessage)
                                    .ToArray();

            return BadRequest(new ResponseDTO<DocumentDTO>()
            {
                Succeeded = false,
                StatusCode = 400,
                Message = "Validation errors occurred.",
                Errors = errors
            });
        }

        try
        {
            var documentDTO = await _documentsService.AddRevisedCertificationAsync(addDocumentDTO);
            return Ok(new ResponseDTO<DocumentDTO>()
            {
                Succeeded = true,
                StatusCode = 200,
                Message = "The file has been successfully uploaded.",
                Data = documentDTO
            });
        }
        catch (InvalidFileFormatException ex)
        {
            return BadRequest(new ResponseDTO<DocumentDTO>()
            {
                Succeeded = false,
                StatusCode = 400,
                Message = ex.Message,
                Errors = new[] { "The file must be a PDF." }
            });
        }
        catch (StorageException ex)
        {
            return StatusCode(500, new ResponseDTO<DocumentDTO>()
            {
                Succeeded = false,
                StatusCode = 500,
                Message = "Failed to upload the document to storage.",
                Errors = new[] { ex.Message }
            });
        }
        catch (Exception ex)
        {
            return StatusCode(500, new ResponseDTO<DocumentDTO>()
            {
                Succeeded = false,
                StatusCode = 500,
                Message = "An unexpected error occurred.",
                Errors = new[] { ex.Message }
            });
        }
    }
}

Throw specific exceptions in the service layer for known error scenarios and handle these in the controller to provide consistent API responses:

public class InvalidFileFormatException : Exception
{
    public InvalidFileFormatException(string message) : base(message) { }
}

public class StorageException : Exception
{
    public StorageException(string message) : base(message) { }
}

Example service layer method:

public async Task<DocumentDTO> AddRevisedCertificationAsync(AddDocumentDTO addDocumentDTO)
{
    if (addDocumentDTO.File == null || !IsPdf(addDocumentDTO.File))
    {
        throw new InvalidFileFormatException("The file must be a PDF.");
    }

    try
    {
        // Upload logic here
    }
    catch (Exception ex)
    {
        throw new StorageException("Failed to upload the document to Box.");
    }

    // Assume the upload was successful and return the DocumentDTO
}

You could refer this document for more detail:

Handle errors in ASP.NET Core web APIs

Sign up to request clarification or add additional context in comments.

8 Comments

Thank you for your answer. What about exceptions that are triggered by data annotations? They do not even make it to the service layer to use the response wrapper.
@Qiuzman could you try adding the [ApiController] that will help you validate the model and return a consistent response for validation errors. or you could try using the you can create a custom action filter. i have updated my sample code
@Qiuzman You're correct that with the ApiController attribute, model validation happens automatically before the action method is invoked.when applied to a controller, enables automatic model validation and results in a 400 Bad Request response if the model state is invalid. This built-in behavior simplifies the code and reduces the need to explicitly check ModelState.IsValid in each action method.By using the ApiController attribute, model validation is automatically handled before the action method is executed, resulting in a 400 Bad Request response if validation fails.
you could refer this two post that will help you understand more in detail : stackoverflow.com/a/66546105/11147346 and stackoverflow.com/a/53580845/11147346
@Qiuzman The ModelState in ASP.NET Core captures errors during both model binding and JSON deserialization. This means that if there are issues with deserializing a JSON payload to a model lets say type mismatches or invalid JSON, these errors will be included in the ModelState. The ApiController attribute simplifies handling these errors by automatically returning a 400 Bad Request response if the model state is invalid.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.