In this episode, we start transforming the current application into a Web API, so it can be used in the single page application we'll be developing.
For the walk-through you can check the next video, but if you prefer a quick read, skip to the written synthesis.
The playlist for the whole series is here.
Intro
So far we've been building a server side rendered MVC application. The goal of this project however, is to end up with a single page application that handles most of the UI needs. To achieve this, we will transition this MVC application into a Web API, so it and other components we build in the future can be used by the SPA (or other components that may require it).
Also, a note on me calling this a Web API or an HTTP API and not a REST API. I don't want to annoy anyone by calling it a REST API while not implementing it following all the requirements that implies, including, but not only, hypermedia. Let's say it's a RESTish API (😛), as it implements what most expect from it, but is not fully compliant to be called REST.
Remove unneeded bits
Since we're moving to a Web API, there are some things that we have no more need and can cleanup/adjust namely:
- Remove views
- Replace
AddMvc
withAddMvcCore
, adding only needed MVC features - Inherit from
ControllerBase
instead ofController
Regarding the first one, there's not much to say, just delete the folder with the views as we won't be needing them anymore. Regarding the others, let's get into more detail.
Replace AddMvc with AddMvcCore
AddMvc
registers in the DI container MVC's services, the ones it surely requires and some that it might. To do this it calls AddMvcCore
and adds some more things. We can avoid registering unneeded services by calling AddMvcCore
directly, plus any other MVC services we know we need.
Will this have an impact on performance? Probably not that much (didn't measure it though), but since we're at it, we remove some unused bits and take a better look at some of the available MVC features. Most of the time though, keep AddMvc
and don't worry about it 🙂
Let's take a look at the MvcServiceCollectionExtensions.cs
file on GitHub, where AddMvc
is defined.
// ...
public static class MvcServiceCollectionExtensions
{
/// <summary>
/// Adds MVC services to the specified <see cref="IServiceCollection" />.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection" /> to add services to.</param>
/// <returns>An <see cref="IMvcBuilder"/> that can be used to further configure the MVC services.</returns>
public static IMvcBuilder AddMvc(this IServiceCollection services)
{
// ...
var builder = services.AddMvcCore();
builder.AddApiExplorer();
builder.AddAuthorization();
AddDefaultFrameworkParts(builder.PartManager);
// Order added affects options setup order
// Default framework order
builder.AddFormatterMappings();
builder.AddViews();
builder.AddRazorViewEngine();
builder.AddRazorPages();
builder.AddCacheTagHelper();
// +1 order
builder.AddDataAnnotations(); // +1 order
// +10 order
builder.AddJsonFormatters();
builder.AddCors();
return new MvcBuilder(builder.Services, builder.PartManager);
}
private static void AddDefaultFrameworkParts(ApplicationPartManager partManager)
{
var mvcTagHelpersAssembly = typeof(InputTagHelper).GetTypeInfo().Assembly;
if (!partManager.ApplicationParts.OfType<AssemblyPart>().Any(p => p.Assembly == mvcTagHelpersAssembly))
{
partManager.ApplicationParts.Add(new FrameworkAssemblyPart(mvcTagHelpersAssembly));
}
var mvcRazorAssembly = typeof(UrlResolutionTagHelper).GetTypeInfo().Assembly;
if (!partManager.ApplicationParts.OfType<AssemblyPart>().Any(p => p.Assembly == mvcRazorAssembly))
{
partManager.ApplicationParts.Add(new FrameworkAssemblyPart(mvcRazorAssembly));
}
}
// ...
}
I cleaned up some parts of the file that aren't really needed for what we're looking for right now.
Looking at AddMvc
, we see that the first thing it does is calling AddMvcCore
, storing the returned IMvcBuilder
in a variable for further configuration, so that's something we'll also need to do. Now let's look at the other things that are configured using the builder.
- AddApiExplorer - used to expose information about the MVC application. It's useful, for instance, for creating Swagger documentation endpoints. For more info check out Andrew Lock's post. We'll use Swagger eventually, but since we aren't using it yet... out with it!
- AddAuthorization - adds the necessary authorization services that we'll certainly need in the future, but not right now, so... out!
- AddDefaultFrameworkParts - looking at
AddDefaultFrameworkParts
implementation belowAddMvc
, we can see it's registering services from Razor and TagHelpers assemblies, so we can also bypass this one. - AddFormatterMappings - I had to take a look at the implementation of this one to figure out what it does, and it basically registers a
FormatFilter
that checks the request's route data and query string for the presence of a format argument, to be used like theAccept
header is used. Another one that we're not going to need, so skip it. - AddViews - views... next!
- AddRazorViewEngine - more Razor stuff, also next!
- AddRazorPages - and more Razor... let's keep on skipping.
- AddCacheTagHelper - more TagHelpers, skip it.
- AddDataAnnotations - we're not using data annotations so far, and I'm also not expecting to use them in this API in the future (we'll use Fluent Validation), so we can also safely ignore it.
- AddJsonFormatters - now this we need, as our API will need to handle JSON.
- AddCors - I'm not expecting we'll be accessing the API from different domains, so we can safely ignore this one as well.
So, what do we end up with? In the Startup
class the services.AddMvc();
is replaced by services.AddRequiredMvcComponents();
, a new extension method we add to the ServiceCollectionExtensions
class we already created. In this file we add the following:
ServiceCollectionExtensions.cs
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddRequiredMvcComponents(this IServiceCollection services)
{
var mvcBuilder = services.AddMvcCore();
mvcBuilder.AddJsonFormatters();
return services;
}
// ...
}
Considering what we had, we only replaced the AddMvc
with AddMvcCore
and also added a call to AddJsonFormatters
. We'll get back to this method later, as we'll need some extra configurations for the changes we'll be making.
Inherit from ControllerBase instead of Controller
Our GroupsController
class so far inherited from Controller
. It can still inherit from Controller
, but since the only thing Controller
has that ControllerBase
doesn't (from which it inherits by the way) is support for views, we can skip the unnecessary extra bits.
Because of this change, since we are still calling the View
method, we'll get compilation errors. We'll get right to it.
Adjust endpoints
Now let's rework our GroupsController
into an API controller. In summary, it'll end up with methods with the signatures pretty much the same as the IGroupsService
, calling the service's methods and adding some extra "controller things" into the mix, namely, routing, HTTP methods and HTTP responses.
GroupsController.cs
[Route("groups")]
public class GroupsController : ControllerBase
{
private readonly IGroupsService _groupsService;
public GroupsController(IGroupsService groupsService)
{
_groupsService = groupsService;
}
[HttpGet]
[Route("")]
public async Task<IActionResult> GetAllAsync(CancellationToken ct)
{
var result = await _groupsService.GetAllAsync(ct);
return Ok(result.ToModel());
}
[HttpGet]
[Route("{id}")]
public async Task<IActionResult> GetByIdAsync(long id, CancellationToken ct)
{
var group = await _groupsService.GetByIdAsync(id, ct);
if (group == null)
{
return NotFound();
}
return Ok(group.ToModel());
}
[HttpPut]
[Route("{id}")]
public async Task<IActionResult> UpdateAsync(long id, GroupModel model, CancellationToken ct)
{
model.Id = id; //not needed when we move to MediatR
var group = await _groupsService.UpdateAsync(model.ToServiceModel(), ct);
return Ok(group.ToModel());
}
[HttpPut]
[HttpPost]
[Route("")]
public async Task<IActionResult> AddAsync(GroupModel model, CancellationToken ct)
{
model.Id = 0; //not needed when we move to MediatR
var group = await _groupsService.AddAsync(model.ToServiceModel(), ct);
return CreatedAtAction(nameof(GetByIdAsync), new { id = group.Id }, group.ToModel());
}
[HttpDelete]
[Route("{id}")]
public async Task<IActionResult> RemoveAsync(long id, CancellationToken ct)
{
await _groupsService.RemoveAsync(id, ct);
return NoContent();
}
}
I think it's really straightforward to understand what's going on here, but I'll try to point out some small details.
The read actions, including their routes remain basically the same, with some method name adjustments and returning the models directly instead of views (JSON or any other format serialization is handled by the framework).
The update action is bound to HTTP PUT, the usual in HTTP APIs, as we're expecting full replacement of the stored entity.
The add action is bound to both HTTP POST and PUT methods, differing from the update action by not expecting an id. This is of course dependent on this implementation, that generates an id automatically. Other implementations might allow for the client to provide an id, in which case we would probably replace these two actions with a single one that would add if the entity doesn't exist, update otherwise.
The add action returns an HTTP 201 Created status code, including the
Location
header to the url to fetch the added entity, but also including it in the response body.
On a final note in this section, notice that in the add and update methods I'm overriding the ids that come from the client. I shouldn't need to be doing this, but am doing to ensure the service doesn't get inconsistent/unexpected ids - defining an id on the add that should have no id yet (at least in the way we're doing it right now, allowing the id to be defined could also be valid) and a different id than what was put on the route in the case of the update.
The problem mainly stems from the fact we're reusing the same model for all operations. The best way to do it would be to use specific models for each, which we'll get to when we add MediatR to the project.
Use ApiController attribute
The API is mostly ready, but if we try to run it now, the read actions will work as expected but not the writing ones.
Lets make an attempt at adding a new group, by POSTing to http://localhost:5000/groups
the info { "name": "Test Group" }
. The response we get is the following:
{
"id": 1,
"name": null,
"rowVersion": "611"
}
The id
and the rowVersion
are correct, but not the name
. The former are good because they are generated server side, but the name is not getting to the action method. If we put a break point in the AddAsync
method we'll see that the model
argument is empty. Same is happening with UpdateAsync
.
The problem is that the actions don't know that the model
should be deserialized from the body. We can easily solve this by adding the [FromBody]
attribute to the model
argument.
This is acceptable, but we can do better. In ASP.NET Core 2.1 the ApiController
was introduced, allowing us to decorate a controller, making it follow some conventions that save us work, like inferring that complex objects come from the request body. This can be overridden of course, but it's a good default to avoid us some typing. You can read more about this attribute here.
To use this attribute we must go to our dependency injection MVC configuration and add the following:
ServiceCollectionExtensions.cs
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddRequiredMvcComponents(this IServiceCollection services)
{
var mvcBuilder = services.AddMvcCore();
mvcBuilder.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
// ...
}
// ...
}
Because using this attribute requires the use of new ASP.NET Core MVC bits from version 2.1 and up, which might cause breaking changes when compared to 2.0, we must explicitly tell it we want to use the new features. Read more about compatibility versions here.
I'm using compatibility version 2.2 because in this one we can use the ApiController
attribute as an assembly attribute that is applied automatically to all controllers, instead of having to decorate each of them.
In the Startup
class file I just added the attribute as [assembly: ApiController]
, and we now have a correctly working controller 🙂
Create an exception filter
Building on what we learned in past episodes, we can create an ExceptionFilter
. In this case we'll create a filter that returns an HTTP 409 Conflict status code when an update to a group fails due to it being outdated - an optimistic concurrency exception like we saw in the last episode. We can improve the filter later to handle more kinds of errors.
ApiExceptionFilter.cs
public class ApiExceptionFilter : IExceptionFilter
{
public void OnException(ExceptionContext context)
{
if (context.Exception is DbUpdateConcurrencyException)
{
context.Result =
new ConflictObjectResult(
new
{
Message = "The updated entity has changed, please refresh your current copy."
});
}
}
}
We can improve this in the future, by making the business layer abstract the exception that reaches the API to a more generic concurrency exception, instead of being tied to an Entity Framework specific exception. For now, it's more than good enough.
Now we go back to our ServiceCollectionExtensions
class and wrap-up the changes (for today) to MVC's configuration, by adding this filter to it.
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddRequiredMvcComponents(this IServiceCollection services)
{
services.AddTransient<ApiExceptionFilter>();
var mvcBuilder = services.AddMvcCore(options =>
{
options.Filters.AddService<ApiExceptionFilter>();
});
mvcBuilder.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
mvcBuilder.AddJsonFormatters();
return services;
}
// ...
}
Outro
That's about it for this post. We migrated our MVC application to a Web API - which is build on top of MVC anyway, but you get the point!
In future episodes we will build upon this API, which is still very simple but has served us well so far, and we were able to explore a lot of ASP.NET Core building blocks and features.
In the next episode though, we'll take a break from ASP.NET Core and start creating a single page application with Vue.js, which will be our PlayBall project frontend, and will use this API to implement its features.
Links in the post:
- Andrew Lock's Introduction to the ApiExplorer in ASP.NET Core
- Build web APIs with ASP.NET Core
- Compatibility version for ASP.NET Core MVC
The source code for this post is here.
If you want to see the changes from the previous code to the changes of the episode, don't forget you can take a look at the commits and pull requests in GitHub, for instance, you can see the PR for this episode here.
Feel free to ask any questions and don't hesitate to provide feedback.
Thanks for stopping by, cyaz!
Top comments (6)
Brilliant! Great article!
Thanks!
Wow! Thanks for the great content!
Thanks!
Watch out - after changing AddMVC to AddMVCCore - double check your API Debug logs.
Can you share some issues you encountered?