mccalltd / AttributeRouting

Define your routes using attributes on actions in ASP.NET MVC and Web API.
http://mccalltd.github.io/AttributeRouting/
MIT License
416 stars 89 forks source link

Support for versioning #91

Open gregmac opened 12 years ago

gregmac commented 12 years ago

I am considering adding support for versioning, but I wanted to see if there is upstream interest in this feature, or any feedback on my proposed implementation.

What I'm trying to do is version an API, but I think this is general enough that it makes sense to be part of AttributeRouting. The idea behind this is you can have your API follow semantic versioning and maintain both backwards and forwards compatibility as you release new versions. I also want to make it easy for developers to support old versions.

There would be a new RouteVersion attribute: RouteVersionAttribute(string minVersion, string maxVersion = null). These will be compared using semantic versioning (eg, "1.9.0" is older than "1.10.1-alpha"). If null is passed, it means unbounded.

You would first tell AR what "versions" you are currently supporting:

            routes.MapAttributeRoutes(config =>
            {
                config.AddVersions("1.0","1.1","1.2");
            }

Given:

[RouteVersioned]
public class MyController : Controller 
{
    [GET("a")]
    public void a()

    [GET("b", MinVer="1.1")] // supported in 1.1 and beyond
    public void b()

    [GET("c", MaxVer = "1.0")] // supported only up to 1.0
    public void c()

    [GET("d", MaxVer = "1.1")] // supported only up to 1.1
    public ReturnTypeD d()

    [GET("d", MinVer = "1.2)] // supported in 1.2 and beyond
    public ReturnTypeD2 d2()
}    

A route is defined for each known version according to the rules for each route. Note in my example that:

As a developer, if I make a version 1.3, all I need to do is update the configuration, and my methods are all automatically supported in 1.3 (unless I've explicitly defined a maxVersion). If I am removing or changing a specific route of course, I do need to go touch that one and adjust the version.

I know one reaction may be that "Gasp! I don't know if method b() is going to be supported forever!" but that is why the versions are defined in configuration. In this particular build, although there is no maxVersion for b(), AR only knows about 3 versions -- if I go to /1.3/b, I will get a 404. If I am building version 1.3 and want to remove b() from it, I go adjust the RouteVersion attribute on b() and add "1.3" to the configuration.

Similarly, it's easy to remove old versions. If I want to stop supporting version 1.0, I just have to remove that from the configuration. Hopefully I also go through and remember to remove c() but even if I don't, there would be no way to call it any more.


It's also possible to specify Min/MaxVer on the RouteVersioned attribute, in which case they are used unless explicitly overridden in the route attributes. One exception is if you specify eg, a MinVer in the RouteVersioned attribute, null does not override it.

[RouteVersioned(MinVer="1.0")]
public class MyController : Controller
{
    [GET("a", MinVer="0.0")]
    public void a()
}

Versions come after areas (if used) but before prefixes. In the future this may be configurable.

Controllers without [RouteVersioned] specified work exactly like normal controllers. RouteVersionedAttribute is inheritable so it can be defined in a base controller and thus applied everywhere.

The default, if no versions are specified, is min=null and max=null, which means that the route is available in all configured versions.


Any other feedback is welcome, but I intend to start working on this relatively soon. I'm also open to creating this as an optional add-on project but I think it would still require some extension points in core AR to support that scenario (and frankly, that may be way too complex -- and considering localization and areas and prefixes are built in, versioning makes sense to me as well).

mccalltd commented 12 years ago

Hi Greg. So I looked though the commits, and made some comments that I've since deleted cause I wanted to just respond here instead, so sorry for any ghosted github alerts.

So what is your use case? How is this helpful to you? The reason I ask -- to play devil's advocate -- is why not use route prefixes and custom contraints to do this? You could define a custom constraint that wrapped up your semantic version parsing logic, which you could then apply to a url segment. A bit more verbose, but then you could put the version anywhere in your URL that you want, at the beginning, after area name and prefix, between area name and prefix.... Here's an example:

[RoutePrefix("api")]
public class SampleController : ApiController
{
    // Any version will do here, so long as v is parsable as a semantic version
    [GET("{v:version}/a")]
    public string ActionA() {}

    // v >= 1.1
    [GET("{v:minVersion(1.1)}/b")]
    public string ActionB() {}

    // v >= 1.0 && <= 1.2
    [GET("{v:minVersion(1.0):maxVersion(1.2)}/c")]
    public string ActionC() {}

    // v <= 1.2
    [GET("{v:maxVersion(1.2)}/d")]
    public string ActionD() {}

    // v >= 1.3
    [GET("{v:minVersion(1.3)}/d")]
    public string ActionD2() {}
}

or maybe something like:

// This controller contains only v1.0 routes
[RoutePrefix("api/{v:version(1.0)")]
public class SampleV1Dot0Controller : ApiController
{
    [GET("a")]
    public string ActionA() {}

    [GET("b")]
    public string ActionB() {}
}

// This controller contains only v1.1 routes
[RoutePrefix("api/{v:version(1.1)")]
public class SampleV1Dot1Controller : ApiController
{
    [GET("a")]
    public string ActionA() {}

    [GET("b")]
    public string ActionB() {}
}

Will look more at the diff tomorrow. It's too late to be doing this :)

Cheers

gregmac commented 12 years ago

Ha, I was trying to figure out where the heck the comments were :) Some are interesting so it would be good to fix them up, however, also keep in mind this was more of a proof-of-concept and talking point than intended to be a finalized solution.


I talked with some colleagues quite a bit about the best way to do this, and we came up with a few principles that led to this implementation:

The use case is a public API, which will undergo changes as subsequent versions are released, but where you want to continue to provide support/compatibility for any released version for some amount of time or number of versions.

mccalltd commented 12 years ago

Sorry for the delay Greg. I have been S-L-A-M-M-E-D the past week, but plan on working on a few odds and ends this weekend.

mccalltd commented 12 years ago

Hey, as a prelude to getting into your branch in a few days, here are some comments:

I'll review things this weekend and give ya more feedback.

Cheers!

gregmac commented 12 years ago

No problem, I am actually going to be short on time too for the next few weeks but I'll see what I can do.

mccalltd commented 12 years ago

Well let's just keep chatting in this issue. Obviously it would help more if I read the code. I'll do that this weekend at the latest and get back to you. Good luck with your release. I like this idea and we'll work on getting it in.

On Aug 8, 2012, at 6:35 PM, Greg MacLellan notifications@github.com wrote:

No problem, I am actually going to be short on time too for the next few weeks but I'll see what I can do.

sksk571 commented 12 years ago

Actually, versioning can be done in existing terms, but it requires support for route constraints in RoutePrefix:

[RoutePrefix("api/v{version:range(1,3)}")]
public class MyController : ApiController
{
    // Legacy action available only in v1 and v2
    [GET("api/v{version:range(1,2)}/data", IsAbsoluteUrl = true)]
    public MyLegacyData GetLegacyData()
    { }

    // Available in v3
    [GET("data")]
    public MyData GetData()
    { }

    // Available in v1-3
    [GET("another-data")]
    public MyAnotherData GetAnotherData()
    { }

    // other actions
}
mccalltd commented 12 years ago

You cannot apply constraints to route prefixes? That's a bug. Sergey, could you open a new issue for that and I'll fix it later today?

On Aug 16, 2012, at 9:33 AM, Sergey Kovalenko wrote:

Actually, versioning can be done in existing terms, but it requires support for route constraints in RoutePrefix:

[RoutePrefix("api/v{version:range(1,3)}")] public class MyController : ApiController { // Legacy action available only in v1 and v2 [GET("api/v{version:range(1,2)}/data", IsAbsoluteUrl = true)] public MyLegacyData GetLegacyData() { }

// Available in v3
[GET("data")]
public MyData GetData()
{ }

// Available in v1-3
[GET("another-data")]
public MyAnotherData GetAnotherData()
{ }

// other actions

} — Reply to this email directly or view it on GitHub.

sksk571 commented 12 years ago

Thanks Tim,

Confirmed a bug here: https://github.com/mccalltd/AttributeRouting/issues/111

vyrotek commented 12 years ago

Hi everyone. I just had a quick question about the efforts being made to implement versioning. It seems that all these use cases assume that you want to version multiple actions inside a single type of controller. Is it possible to have AttributeRouting support versioning across controllers that are named the same?

For example, I have these two controller classes (note the same names):

/Controllers/API/V1/OrdersController.cs /Controllers/API/V2/OrdersController.cs

I essentially want to version an entire controller. Right now if you set up the controllers like I have above, even if you have different route attributes defined such as [RoutePrefix("API/V2")] you will get an MVC exception that says:

Multiple types were found that match the controller named 'Orders'. This can happen if the route that services this request ('api/v1/orders') found multiple controllers defined with the same name but differing namespaces, which is not supported.

I've started to work around this by implementing a custom controller selector. But, I was wondering if that is absolutely necessary or if there was a way to work around this? My other option seems to be to rename my controller classes to:

/Controllers/API/V1/V1OrdersController.cs /Controllers/API/V2/V2OrdersController.cs

Thoughts? Thanks to everyone for the work you've already put into this project already!

mccalltd commented 12 years ago

Yes, you cannot have multiple controllers with the same name in different namespaces. For now the workaround is to have uniquely named controllers, as you've surmised. As the names have nothing to do with the URLs you expose, I don't feel that it's too big a penalty. Plus naming your controllers like EntityVnController makes it pretty clear what they do, in my opinion.


[RoutePrefix("api/v1/Orders")]
public class OrdersV1Controller : ApiController { }

[RoutePrefix("api/v2/Orders")]
public class OrdersV2Controller : ApiController { }

Also, the versioning support has not been rolled into AR. I'm still not convinced that there need be explicit support for this, as the inline route constraints provide a flexible means of doing this already.

Feel free to suggest alternatives. But do note that the exception you get is due to the expectations of Web API. It is what it is.

gregmac commented 12 years ago

Tim: I do still think there is a case for this specifically around API Explorer and providing help, but I'll wait until AR works with web API before putting effort in.

For now, I am still using my web api-type project (nservicemvc) and am inspecting the route table to pull out the list of API methods per version, and from there generating api docs (using swagger-ui) based on route, method signature and .NET XML documentation.

My gut feeling is that pulling this stuff from route constraints alone would be incredibly more complex if not broken/useless (eg it could end up generating docs for versions that do not really exist).

So.. Leave on hold until web API is up, then I will port the API and docs I have now and either agree that route constraints are enough, or have an exact case why this is needed.

-----Original Message----- From: Tim McCall notifications@github.com Date: Fri, 14 Sep 2012 22:16:57 To: mccalltd/AttributeRoutingAttributeRouting@noreply.github.com Reply-To: mccalltd/AttributeRouting reply@reply.github.com Cc: Greg MacLellangithub@list.mtechsolutions.ca Subject: Re: [AttributeRouting] Support for versioning (#91)

Yes, you cannot have multiple controllers with the same name in different namespaces. For now the workaround is to have uniquely named controllers, as you've surmised. As the names have nothing to do with the URLs you expose, I don't feel that it's too big a penalty. Plus naming your controllers like EntityVnController makes it pretty clear what they do, in my opinion.


[RoutePrefix("api/v1/Orders")]
public class OrdersV1Controller : ApiController { }

[RoutePrefix("api/v2/Orders")]
public class OrdersV2Controller : ApiController { }

Also, the versioning support has not been rolled into AR. I'm still not convinced that there need be explicit support for this, as the inline route constraints provide a flexible means of doing this already.

Feel free to suggest alternatives. But do note that the exception you get is due to the expectations of Web API. It is what it is.


Reply to this email directly or view it on GitHub: https://github.com/mccalltd/AttributeRouting/issues/91#issuecomment-8581992

mccalltd commented 12 years ago

Yeah, the ApiExplorer/doc-generation could get hairy. And AR works with Web API with the caveats listed in #96. What in specific are you waiting for? Please comment in that issue so I can keep track.

Cheers!

gregmac commented 11 years ago

Sorry, I have been extremely busy and haven't had time to get back to this. I am not quite sure what I'm missing from WebAPI anymore, and I am experimentally trying to port my project over and get back the existing feature sets, but contribute back the upstream enhancements I've done.

As I mentioned, one of my primary use cases of having explicit versions listed is generating API help, which I am doing via Swagger-UI. I'm also trying to update Swagger.Net to support AttributeRouting (starting with https://github.com/miketrionfo/Swagger.Net/issues/14), and once that's done I'll come back to the versioning issue.

To give you an idea of my end goal, here's a screenshot of what I have now in my current project.

Swagger-UI showing AR + versioning

This is done with MVC 3, NServiceMVC (which was a project I started about a month before WebAPI was first released, with nearly identical use), AttributeRouting, and (slightly modified) Swagger-UI. As you change the version drop-down, it changes the API methods that are available, depending on the Version attribute value.

We number the API version according to the product release, and so there's two really nice features here (which you don't get using RoutePrefix or :range()):

mccalltd commented 11 years ago

This is going into a tentative v3.5 milestone. I need to take a look at what you've done, and hear what you've discovered with odd cases, etc.

mccalltd commented 11 years ago

Have you pulled the latest from master and merged? If you do so and write a few specs/tests to exercise what your expected behavior is we can fast track this. Looking over your pull request I think I get what you've done. But let me relay to you so you can correct me where I'm wrong.

New Public Interface

Internally

There are a couple of things I'd like to change/discuss before rolling things in:

Let's get this in.

gregmac commented 11 years ago

@mccalltd you got it exactly right.

I really like the idea of using nuget versions, I will definitely do that change.

Using route-per-version has a number of benefits in my mind:

To be honest, I'd love to just build this as a separate library, but the way the code is structured now and due to the complexity, I don't really see how it's possible (but am open to ideas).

Anyway I'll work on getting it updated to the current code.. once again looks like a lot has changed so I'll likely just have to reapply from current master.

kamranayub commented 11 years ago

Love this, +1

Was on my phone, so to expand:

I need to support versioning for my API (Web API) because I'll be consuming my own service from multiple clients (web + Metro + Phone). Versioning will ensure my devices can use old API versions until I can release updates for them but it lets me update the website as much as I can.

I just recently worked on a Windows Phone client for an API and they did versions per method. As a consumer, I actually liked this because most of the calls were against one version but a couple had some bugs that were fixed in a newer version. However, if they had forced to wholesale update to the new version, I would have been forced to redo my client object model. By versioning methods, I could cherry pick the updates I wanted to support.

Of course, it's probably complicated on their side... and I'd be interested in seeing how they keep different versions of their object model.

Versioning an API is one thing, but you also need to version the rest of your code (and even storage). I think this is definitely a big help.

RE: Web API support

Web API has native support for HTTP message versions, so you can access a request message version using Request.Version and set a version with Response.Version. Perhaps this would make it "easy" to add filter/constraints like you're doing and then compare against the incoming/outgoing versions?

gregmac commented 11 years ago

@kamranayub I haven't heard of that, but are you referring to this property? http://msdn.microsoft.com/en-us/library/system.net.http.httprequestmessage.version.aspx If so, I believe that's HTTP version, and thus you can't change it or you'll probably confuse the webserver (and any proxies/load balancers in front of it).

Even still, doing versioning in code has a major drawback in that you can't change the model type! My method allows you to change the model in a newer version, but continue to apply bug fixes to the older versions as well.

One of the other reasons I did this was to partially avoid version-per-method. Even without this patch, you can do:

[GET("v1/users")] 
public IEnumerable<User> Users_v1() 

[GET("v2/users")] 
public IEnumerable<User_v2> Users_v2() 

[GET("v3/users")] 
public IEnumerable<User_v3> Users_v3() 

[GET("v1/users/{id}")] 
public User Users(int id) 

The trouble is if I'm a new consumer to your API, I need to figure out what versions for each resource to use. /v3/users /v1/users/id, /v6/groups, /v4/roles, /v16/orders, /v11/products.... That's craziness, in fact, you might as well name all your methods just random GUIDs as it would be about as helpful. IMHO :)

If I'm new, I'd rather just know: Ok, current version is v16. That means I use /v16/users /v16/products, etc.

Now, you could also copy and paste if the version hasn't changed, so each resource is available on each version.

[GET("v1/users/{id}")] 
[GET("v2/users/{id}")] 
[GET("v3/users/{id}")] 
public User Users(int id) 

Two or three methods - not a big deal. 20 or 30 or 300? Shoot me now.

With my method, you get a single global version number without having to touch each method, but you can still change the response model type without losing benefits of strongly-typed WebAPI methods. It also separates the concern of versioning from your actual code, so you don't have to write crap like this:

[GET("v{version}/users")]
public object Users(int version) {
   if (version == 1) {
      return new Users_v1()...
   } else if (version == 2) {
      return new Users_v2()...
   } else if (version == 3) {
      return new Users_v3()...
   } 
}

Even if there was a way other than the URL to get a version number, there still needs to be a way to invoke a different method, in my opinion. This is why using a Routing handler like AR is a great place to implement versioning. I also happen to love how AR is a very natural, flexible and easy way to set up routes. Convention can be handy sometimes, but when you end up putting comments in like this:

// GET /users/{id}/comments
public User GetComments(int id) { } 

..you might as well give up and just use AR.

kamranayub commented 11 years ago

Good points all around, I hope there's a fleshed out sample so there's something to reference.

As far as versioning DTOs, with your method all I'd need to take care of is how to handle old versions of my code.

I know this is far away from web land, but I've been researching Sterling as a storage provider for Windows Phone. In the latest release, they implemented logic to handle migrations and since its an object oriented DB, I think the implementation would translate well to an API, where objects change over time and you need to handle that without creating a maintenance nightmare.

Here's the docs on how they support it:

http://sterling.codeplex.com/wikipage?title=Changes%20in%20Sterling%201.6%20-%20changing%20your%20classes&referringTitle=Documentation