We've been talking quite a lot recently about
Web API, and so I thought this week we'd look at the idea of Web API versioning. So if you've
already published your API, you've got customers who are using it, how we can then modify that not
by making any changes to the existing API, but by producing a new version so that those clients who
want to use the new version can use it, those who want to stick with the older version can stick with
it as well. So let's have a look at what we've got for
that. And what we're going to do here is use
the application that we were using over the last few videos, when we're demonstrating Swagger
and things like that. So it's pretty simple. What we've got is a SQLite database, we've got
an entity called BookReview that just has an Id, a Title and a Rating. We've got a load of
those reviews already in the database. We've got Entity Framework and a DbContext connecting
it all up. So just look at the earlier videos for the details of that. But then
we've got our
BookReviewsController. And so this is where we've got a number of endpoints for manipulating
this data. So we can get all the reviews. We can get an individual review by Id. And we can do, for
example, have a summary of the average rating for each one of the titles across those reviews. And
we've got posts and puts and things like that. And if we just run that up, we can see that it's all
working. And so we get Swagger showing us what's going on, we can do something like look
at all the
reviews. So if we just try that out, then we get some JSON back showing us all the reviews we've
got. So more than one review for each title. And then that's what feeds into the Summary down the
bottom here. So if we try that one out, it will run through them all, and then just give us the
average rating for all of the reviews for that particular book. So we can see that Dr No has an
average of 3.5. So that's our current API. And so then we may make a decision that we want to mo
dify
that. Now there's a number of ways you can modify it; the simplest one is just to be adding a new
piece of functionality. But you might also have the idea that you're going to remove functionality
- say we're no longer able to post reviews on this interface, something like that. You might also
want to modify existing functionality. So add a new parameter to one of those endpoints. And
indeed, you might want to make a big change, like changing the entire model. So our model
that we've
got here, our BookRreview is defined with Id, Title and Rating - if you added a new
property in there, that would mean that pretty much all of the endpoints were going to change.
And so that would be quite a big modification to your API. And we want to do that in accordance
with things like the Open Closed Principle and the Interface Segregation Principle. So we
don't want to be modifying the existing API; we want to leave that for those people who want
to use it and then add a new one, so
that people who do want to move forward with the new features
can do so. And the new feature we're going to add is simply - rather like the summary we've got
here, which gives us the average rating for all of the titles - we're going to have a summary that
takes the title as a parameter and just gives us back the average one of those. So it's going to
be an additional new endpoint. You could argue, well, we could just add that into the existing API
- it's not going to break anything, becaus
e people aren't going to be using it unless they know about
it. But that actually would still be a breach of the Interface Segregation Principle, in that it
would mean - if you remember the definition of Interface Segregation Principle - that interfaces
should not have methods that clients don't use. And that would happen in this case. Much better
if we produce a new interface with a new feature, and then those clients that want it can
migrate to it, those that don't can stick where they ar
e. So that's what we're going to
do. So let's go in there and let's do this in a very simple way to start with. Let's just add
a new controller, and we're going to call this ‘BookReviewsV2’ - version two – ‘Controller’.
And then I'm going to make a slight change, because I've adopted here the convention we're
not going to have ‘api/’ on there. So if we look at our original one, we've just got Route to
the [controller]. So the same sort of thing here. Then what we need to do is we need to ge
t
hold of a couple of things. We need to get hold of the repository, so we'll have a ‘private
readonly IReviewRepository’ so that we can get all the data. We'll call it ‘_ReviewRepository’.
But then additionally, we also need to get hold of the other controller, because we don't
want to be repeating ourselves in terms of the functionality that's already there. So we're going
to have a ‘BooksReviewController’, call it that, then we're going to have a constructor that will
inject the reposit
ory. So we'll pop that in there and then just move that back up to where it was.
Then in the constructor, as well as having the repository injected, we're also going to create
the other BookReviewsController and pass it the repository there. So that means we can make use
of that for anything which is the same in both the old controller and the new one. And then
for the most part we're just going to delegate over. So actually, let's go in here and let's
grab all of those endpoints and just p
aste them into the new controller. But we're not going to
use those implementations, because that would be code duplication. So what we then want to do
is simply delegate each one of those to the existing controller. So in here, I'm just going
to say ‘return _BookReviewsController.Get()’. And then we don't need to worry about anything
else. And similarly, we'll do that in the get by Id. So return and then ‘_BookReviewsController.’ And it
works out for us. Similarly, in the summary. And all
the way through, we can
just put those in there. So for the puts and for the delete. So that's got all the
same functionality now in our second controller. Then we want to add some new functionality. So
remember, we're going to do this as a summary for a particular title. So let's base it on what
we've got there. So this is now going to be called ‘summaryfortitle’ - change the name of the method.
So that's also ‘SummaryForTitle’. And that's going to take as a parameter the title, so we'll
just put that there on the route and also in the method. And then what we're going to do is we're
going to say ‘var reviews =’ and then this one, we directly access the repository because we
can't get this from the other controller. And we'll say ‘.AllReviews.Where’ and then it's worked out
for us where ‘review => review.Title ...’. So that's going to give us all the reviews matching
this particular title. We've then got to worry about the fact that actually there could
be nothing for that
title, if it's an invalid title. So we're then going to say ‘if’ and it's
given us ‘reviews.Any()’, but we actually want ‘!reviews.Any()’. And if there aren't any,
we're simply going to return a ‘NotFound’. And that means, of course, we've
got to remember, in our metadata, we've got to announce that we're returning
a NotFound. So we can put that in there and pick the NotFound. And then if we have
got some reviews, then we're going to say ‘var summary =’ and we're going to have a ‘new
BookR
eview’ and that's going to have a ‘Title’ which is simply ‘title’ - the parameter that’s
come in. But then it's going to have a ‘Rating’, which is all of those ‘reviews’ for this title,
and then ‘Average’ and then ‘r => r.Rating’. So the rating is the average of all those ratings. And
then we can simply return on ‘Ok’ of ‘summary’. A couple of other things we have to do, though.
Because we're now just returning a single item, we're going to change the return type,
and also change the type s
pecified in the ‘ProducesResponseType’. And so that should
be that all working. So if we now run that up, we can see that Swagger is now giving us two
completely separate controllers. So we've got two different API's. We've got the original one
that just has the original methods. And then we've got the new one that has all of the original
methods, plus the additional ‘summaryfortitle’ that we gave it there. So if we just check
that out, we can see that the get all works in exactly the same
way, so that just delegated
through to the original controller. Whereas the summaryfortitle - that's the new one. So if we
try that one out, and we go for something like ‘Goldfinger’ and execute that, then you can see
we're just getting the average for Goldfinger. And indeed, you don't have to use Swagger, obviously.
If we just take that URL and paste that in there directly, will still get the same result done
directly. So we've kind of done it. But we haven't really done actual versioning,
because all we've
got here is two separate APIs that happen to have similar names. And we've effectively used just
a naming convention to indicate that we've got BookReviewsV2 and the original one - that we might
have changed the name to V1 or just stick with BookReviews. But we can do better than that in
terms of having a much clearer definition of the versioning. So that's what we're going to do here.
So the first thing we're going to do is we're actually going to make it so that both of
these
controllers have the same name - they're both just called ‘BookReviewsController’ - we don't put the
version in the name. Now obviously, that's going to cause a problem because you can't have two
classes with the same name in the same namespace. But we're going to fix that. We're going to put
in separate folders for each of these. So let's put in a new folder that we’ll call ‘V1’ and
then another new folder that we’ll call 'V2'. And then let's drag those controllers in there.
And sa
y OK for that. Notice the nice thing it does: ‘Adjust namespaces for moved files?’.
So remember, that's now going to be in the namespace ‘Controllers.V1’. So it put that
one in for us. Same sort of thing with V2, drag that in there. Click OK on both of those. And
so that one is now in the V2 namespace. Next thing we're going to do though, is we're going to change
the name here. So let's rename that to just being ‘BookReviewsController’ exactly the same
name, but that is now valid because th
ey're in different namespaces – V1 and V2.
But now we've got a bit of a problem, because that's now going to be recursive. So this
one - the one that we're delegating to - we have to call ‘V1.BookReviewsController’, and the same
down there. So that's now what we're delegating to. So that's now given them the same name, put
them in separate folders, and it should still be working. If we run that one up however, then
we can see we're getting a problem. And it's not just a problem with Swagger
- this is not going
to work at all. So if we now go to a different tab and paste in that URL we had before, get rid
of that V2, because it's now just going to be ‘BookReviews’. So ‘/summaryfortitle/Goldfinger’
- that one actually worked. So that's not a problem. The problem is, if we just went
for something like ‘BookReviews/Summary’, that gives us a problem because it can't tell
which one we're supposed to be working with. So the routing failed because there are two things
with exactly t
he same route. So now we need to make some changes to that as well to get that
to work. So what we now need to do is actually introduce a NuGet package that is going to help
us out with it. So if we go in here, and just do a search on ‘MVC.Versioning’, that gives us the
package to give us the versioning information. So let's install that and then the next thing
we need to do is configure that. So we'll go into our Program and in here we're going to say
‘builder.Services.’ and then ‘AddApiVe
rsioning’. So that's been introduced with that new
package. And that takes some ‘options’. So we’ll put in those options, and the options
we can go for - we'll talk about the details of these, maybe, in a later video - but the
simple ones we're going to go for, we're going to say ‘options.DefaultApiVersion’ and then we're
going to say ‘= new ApiVersion’ and let's get rid of the namespace there and just put it in as a
using. And then this takes two parameters, which are the major version and
the minor version. So
that's the degree of control we’ve got: major and minor. So it's going to be version ‘1.0’. So we're
defaulting it back to the original, because that means that anybody who was working on the original
will not have their functionality changed, they'll stick with what they've got. Let's also do a few
other things. So we're going to say ‘options.’ and then ‘AssumeDefaultedVersionWhenUnspecified’.
So again, that's kind of related to same thing, that it means that anybody
who is unaware of the
change and doesn't include a version - they're just going to get the one that they want. And
then we're also going to say ‘options.’ and then ‘ReportApiVersion’ and set it ‘true’. And that
means that when a request is made to this API, we're going to get sent back in the response
header information to say what versions are actually available, so that anybody using
it can see that there may be a newer version for them. Then we need to make some changes to
the controll
ers themselves. So we'll go to the original one - the version one - and on there I'm
going to firstly add the version information. So we're going to put another attribute which is
going to be ‘[ApiVersion]’. And this is going to be version ‘1.0’, and so that's specifying
the version there. And then also, we're going to have to specify what the route is going to be
including the version. So I'm going to take that existing route and then we're going to change
that. So we're going to say ‘v’ f
or version, and then we're going to have curly braces and
we're going to say ‘{version:apiVersion}’. So that means that it's going to automatically
insert the version number after the ‘v’ there. So this is actually going to be ‘v1/BookReviews’. We
could have hard coded that, we could have used a different convention other than just having a ‘v’
there, but that's the simplest way to do it. So it'll take basically the ‘1.0’ from there and it
will put it in there. So that's where we're going t
o go. You'll notice though, we leave the original
route in there. So both of these routes will take us to this version one controller, and that,
again, is for backwards compatibility. So anybody who was accessing it by the old route doesn't have
to upgrade, that will just stay the same. We're then going to take that and we're going to put it
into the version two with some slight changes. So we're going to change that obviously to ‘2.0’.
Leave that exactly the same, that's fine, But then we'
re going to remove that one because
we're not defaulting to 2.0, we're defaulting to 1.0. And so those are the changes that we
need to make that. And if we run that one up, we're still failing on the Swagger, but the rest
of it should be working okay. So if we go to ‘localhost:…/BookReviews’ then that, remember,
is doing the get, and it's doing the get on the version one controller, because that was the
default. We can demonstrate that if we just go into Visual Studio, go to the version
on
e controller and go to the get for that. And then just run that again, we can
see that’s what we're hitting there. Similarly, if I were to change that to
‘/V1/BookReviews’ then we hit the same one again, because although we could go with the default,
we can also go with the name. But if I were to put in another breakpoint on the get for V2
– which, remember, is just delegating to V1, but it's going through our new controller -
then in that case, if I change that to V2, then we hit the V2 co
ntroller, which then
delegates through to the V1 controller and that then gives us a result in exactly the same way.
You could also actually put in ‘V2.0’ there and it will still do the same thing. So if you've got
‘0’ as your minor version then you don't have to put it in, but you can. So we can do that ‘V2.0’.
But what we can also do as well is we can now, on this one, do ‘/summaryfortitle/Goldfinger’, and
that's working as well. So our version two has the ‘summaryfortitle’, which obvious
ly our version one
is not going to have. And that's the whole point, that we've separated these two out. So we have
now got our two different versions - version one, version two of this particular API - with the
addition or whatever changes we wanted to make. And while we're here, we can just take a
quick look using the DevTools at what was coming in there. So if I refresh that, there we
can see Goldfinger. If I click on that and look at the headers and just scroll down a little
there, you
can see in the Response headers, we've got that API supported versions ‘1.0’ and
‘2.0’, which is what I configured it to do in the Program when we have that ‘ReportApiVersions’.
So we've got all of that working, all of that information being reported, but what we haven't
got - and we're going to see this in the next video - we haven't got Swagger working with this,
because there's a fair bit more work to put that together. So next time, we're going to look at how
to make Swagger work, and
also look at a few other details of what we can do with the versioning. So
I hope you found that helpful. If you did, do like, do subscribe, and I'll see you next time to
look at how we can make that work with Swagger.
Comments
Do your API vary much, or once published are they stable? Let me know. Source code available at: https://github.com/JasperKent/API-Versioning Remember to subscribe at http://www.youtube.com/channel/UCqWQzlUDdllnLmtgfSgYTCA?sub_confirmation=1 And if you liked the video, click the 👍.
Clearly explained and engaging, thanks so much!
Nicely done and well explained! (:
Another best tutorial thanks!
If v2 of a controller action takes in a model with extra/modified properties, do I create a new model? And should I create a new repository class to handle v2 controller actions?
Interested in the next parts because those are the different ones. I am just curious about the decision to go straight to 2.0. Basically the summaryfortitle endpoint is a new Feature. That would mean for every Feature you raise the major Version. What if you Just add a property to an existing Model. That could be handled tolerant (by ignoring it) within the api consuming Client. Imo that would be a raise of the minor Version of the model itself.