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/ )