What I should return from WebAPI ?

 

There are several approaches when returning code from WebAPI .  Let’s frame the problem: say we have a Person controller with 2 actions:

– a GET {id}  – that retrieves the Person with id

– a POST {Peron}  – that tries to verify the validity of the Person and then saves to database.

We will answer to some questions:

1.What we will return if the person with id does not exists ?

2. What happens with the validation about the Person  ?
Let’s see the Person class.

public class Person: IValidatableObject
{
    public int ID { get; set; }
    public string  Name { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        yield return new ValidationResult("not valid not matter what");
    }
}

 

First approach: Do nothing

What I mean is just return the Person , even if it is null.

1.1. The GET

[HttpGet("{id}")]
public Person GetPerson(int id)
{
var p = RetrieveFromDatabase(id);
return p;
}

The problem is that when we return null from an action, there are no response data from the HTTP call-  and the javascript should handle it .

 

1.2 The POST – no problem . [ApiController] should handle the validation and return BadRequest if not validate

 

Second approach:  Return standard HttpCodes

2.1 The GET


[HttpGet("{id}")]
public ActionResult<Person> GetPerson404(int id)
{
    var p = RetrieveFromDatabase(id);
    if (p == null)
        return NotFound($"{nameof(Person)} with {id} are not found");

    return p;

}

We intercept the null and return standard 404 HttpCode . In that case, I strongly suggest to add a message to the end-caller – to know that the reason is not that the http call was a problem, but the Person was not found

2.2 The POST – no problem . [ApiController] should handle the validation and return BadRequest if not validate

 

Third approach: Return OK with a custom ReplyData class

The ReplyData can look this way

public class ReplyData<T>
{
    public bool Success { get; set; }
    public string Message { get; set; }
    public T ReturnObject { get; set; }
}

2.1 The GET

This action is over simplified – the code just returns the ReplyData from the database.

[HttpGet("{id}")]
public ReplyData<Person> GetWithReply(int id) {

    return RetrieveWithReplyFromDatabase(id);
}

private ReplyData<Person> RetrieveWithReplyFromDatabase(int id)
{
    try
    {
        Person p = null;//retrieve somehow
        if (p == null)
        {
            var r = new ReplyData<Person>();
            r.Success = false;
            r.Message = "Cannot find person with id " + id;
            return r;
        }
        else
        {
            var r = new ReplyData<Person>();
            r.Success = true;
            r.ReturnObject = p;

            return r;
        }
    }
    catch(Exception ex)
    {
        var r = new ReplyData<Person>();
        r.Success = false;
        r.Message = ex.Message;
        return r;
    }
}

2.2 The POST

This is somehow complicated – the [ApiController] , if not valid, return a 400 BadRequest . So we should modify this with a custom middleware to return the same ReplyData

public class From400ValidationToReply : IMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
    try
    {
        var originalStream = context.Response.Body;
        var bufferStream = new MemoryStream();
        context.Response.Body = bufferStream;
        await next(context);
        bufferStream.Seek(0, SeekOrigin.Begin);
        if (context.Response.StatusCode != 400)
        {
            await bufferStream.CopyToAsync(originalStream);
            return;
        }
        var reader = new StreamReader(bufferStream);
        var response = await reader.ReadToEndAsync();
        if (!response.Contains("errors"))
        {
            await bufferStream.CopyToAsync(originalStream);
            return;
        }

        context.Response.StatusCode = 200;

        var r = new ReplyData<Person>();
        r.Success = false;
        r.Message=response;
        var text = JsonSerializer.Serialize(r);
        var bit = Encoding.UTF8.GetBytes(text);
        context.Response.ContentLength = bit.Length;
        await originalStream.WriteAsync(bit);

    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
        throw;
    }
}

And the problem is that will affect all the request pipeline….

Conclusion:
I would choose 2 . But … your choice… ( and , for an advanced discussion, please read https://blog.ploeh.dk/2013/05/01/rest-lesson-learned-avoid-hackable-urls/ )