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