Thanks for visiting my blog! See more about me here: About Me
Before ASP.NET Core, our world was split between ASP.NET MVC and ASP.NET Web API. In ASP.NET Core that changes to a single model in ASP.NET MVC 6 for handling requests, whether they end up returning data or views.
There is a Web API Shim to bring over old controllers for use in ASP.NET Core. But for new projects (e.g. greenfield), I’d suggest writing your API controllers without the shim.
Short Review of View Controllers
In case you haven’t looked at View Controllers, Let’s look at a simple example from my WilderBlog project:
[Route("hwpod")]
public class PodcastController : Controller
{
private PodcastEpisodesProvider _podcastProvider;
public PodcastController(PodcastEpisodesProvider podcastProvider)
{
_podcastProvider = podcastProvider;
}
[HttpGet("")]
public IActionResult Index()
{
var episodes = _podcastProvider.Get();
return View(episodes);
}
What you can see here is that we’re using Route and verb attributes (e.g. HttpGet) to specify the routing for a specific action and that we’re returning a call to View to map a result to a view in the Views folder. This is straight out of how ASP.NET MVC has created controllers for many versions. The MVC 6 version is not much different.
But to create API controllers, we used to have a couple of options…either return data with the Json method:
[HttpGet("/api/episodes")]
public IActionResult GetEpisodes()
{
var episodes = _podcastProvider.Get();
return Json(episodes);
}
…or use ASP.NET Web API (with the ApiController base-class and the IHttpActionResult and helper methods for status codes):
[Route("/api/episodes")
public class PodcastController : ApiController
{
// ...
public IHttpActionResult Get()
{
var episodes = _podcastProvider.Get();
return Ok(episodes);
}
In ASP.NET MVC 6 these options still exist but they’re combined into a single codebase. Let’s build one and see how it works.
Writing an API Controller
Let’s create a new controller to expose the data as an API call for my Podcast episodes. First let’s create a controller class and derive from Controller. Yes the same class that View Controllers derive from:
public class EpisodeController : Controller
Next, Let’s add a Route attribute to specify the base URL for all calls on this controller:
[Route("/api/episodes")]
public class EpisodeController : Controller
{
Now let’s build an API method:
[Route("/api/episodes")
public class EpisodeController : Controller
{
// ...
public IActionResult Get()
{
var episodes = _podcastProvider.Get();
return Json(episodes);
}
You can see it returns an IActionResult which allows us to return a View or Json using the helper method. Last thing we need to use to get it to work is adding a verb attribute to know what this method is mapping to. In old ASP.NET Web API, the method being called “get” was sufficient to make this happen by convention but that doesn’t work any longer. You need the attribute to specify it, but it also serves as a space to put the route information. For example:
[HttpGet("")]
public IActionResult Get()
{
var episodes = _podcastProvider.Get();
return Json(episodes);
}
The empty string tells it that the route is *just* the same as the class-level Route attribute (e.g. /api/episodes). We could add parameters here too:
[HttpGet("{page:int}")]
public IActionResult Get(int page)
{
var episodes = _podcastProvider.Get();
return Json(episodes);
}
This works and is the old ASP.NET MVC way of writing APIs but I don’t like it. It removes me from handling status codes and error handling. It’s a little too brute force. Let’s re-write this using a more Web API way of doing things.
A Web API Approach
If you’ve built APIs with ASP.NET Web API, we can build on that experience. You can simply use the data-type as the return type and let the framework do the work for you:
[HttpGet("")]
public IEnumerable<PodcastEpisode> Get()
{
var episodes = _podcastProvider.Get();
return episodes;
}
This works but it makes it difficult to handle errors. I prefer to use the helper methods for status codes:
[HttpGet("")]
public IActionResult Get()
{
var episodes = _podcastProvider.Get();
return Ok(episodes);
}
Notice the “Ok” method. It specifies that you want to return a 200 status code and just pass in the data type to be serialized. The call is more descriptive too. Remember, not all success codes are 200 (like the Json result will do). For example, a successful post should return a Created status code:
[HttpPost("")]
public IActionResult Post(Episode episode)
{
var newEpisode = _podcastProvider.Append(episode);
return Created($"/api/episodes/{episode.EpisodeNumber}", newEpisode);
}
In this case, the Created method helps us by reminding us that Created needs the URL to the new object as well. But this still doesn’t solve the error handling case. Let’s look at a version that uses a try-catch block to be sure that nothing bad happened:
[HttpGet("{number:int}")]
public IActionResult Get(int number)
{
try
{
var data = LiveEpisodes
.FirstOrDefault(e => e.EpisodeNumber == number);
return Ok(data);
}
catch (Exception ex)
{
_logger.LogError("Failed to get episode from the API", ex);
return HttpBadRequest();
}
}
See in this example, we’re still using the Ok method to return the data, but if something bad happens, we can return a 400 error (Bad Request) so that callers of the API know it’s a failure. If you’re using an exception page and let the exception happen, this call might return a 200 (for the successful error HTML information) or a 302 (a redirect to that page). That makes it hard on people using the API to see if it was a bad request. Many of the common status codes have helper methods on Controller so that you can share what is really going on in an HTTP-way.
In general, I liked the convention based approach in ASP.NET Web API, but that’s the only thing we’re really losing in this new stack. The approach of staying closer to the HTTP stack instead of trying to hide real APIs in the code is a good thing. Take a look at what I’m doing for APIs in my WilderBlog project or you can also watch my Pluralsight course on ASP.NET Core as I discuss all of this at length in one of the modules of the course: