AttributeRouting icon indicating copy to clipboard operation
AttributeRouting copied to clipboard

Support for versioning

Open gregmac opened this issue 12 years ago • 20 comments

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.

  • Aside: I realize there are many ways to version URIs and some people opposed to putting the version in the URI (especially in terms of REST), but that conversation is not really relevant to this implementation. If you choose not to do versioning or do it via headers or content recognition, that is fine and you can still use AR and ignore this stuff (just like some people may ignore AR's support for localization and areas).

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()
}    
  • /1.0/a == a()
  • /1.1/a == a()
  • /1.2/a == a()
  • /1.1/b == b()
  • /1.2/b == b()
  • /1.0/c == c()
  • /1.0/d == d()
  • /1.1/d == d()
  • /1.2/d == d2()

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

  • /1.0/b is not a route (because the "b" method was only introduced in version 1.1)
  • /1.1/c and 1.2/c are not routes (because the d method was dropped after 1.0)
  • for version 1.2, a different method is being called for /1.2/d (with a new return type).
  • /1.3/b is not a route (because "1.3" was not supported in configuration)

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

gregmac avatar Jul 18 '12 17:07 gregmac

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

mccalltd avatar Aug 02 '12 05:08 mccalltd

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.

  • 0.0 version == because there is a minVersion of 1.0 defined on the class, specifying null MinVersion on a specific route makes it inherit the class value. The only way to make just that one method compatible with anything pre-1.0 was to use 0.0. I didn't really like this but it did work
  • The SemanticVersion class is from "the" NuGet.org project, as in http://nuget.codeplex.com/SourceControl/changeset/view/fd2b166aacf3#src%2fCore%2fSemanticVersion.cs. Reading my comment again now I see why that was not clear.

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:

  • We really don't want to make it possible to accidentally leak routes without versions. If this happens and people start using it, it's very hard to transition away from.
  • We want ease of use for the developer - avoid as much as possible the need to go touch everything to release a new version (and forget to pull things forward).
    • This includes removing an old version -- in my implementation you just remove it from the config, and regardless of how routes are defined it is not available. I think this is important to ensure users are keeping up as you deprecate old versions.
  • We talked about a version-per-action but that gets hairy quickly in terms of documentation and ease-of-use for the user (eg, in the same code your "current" paths could be GET /v1/actionA, POST /v15/actionA, GET /v6/actionB)
  • We wanted to avoid duplicate code (eg, specifying several routes for each specific version on each action)

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.

gregmac avatar Aug 02 '12 06:08 gregmac

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 avatar Aug 08 '12 22:08 mccalltd

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

  • I have some niggles with the naming scheme you present to the user. RouteVersioned is a very confusing term for me because of the past tense; and MinVer/MaxVer I'd like to see presented to the end-user as MinVersion and MaxVersion instead.
  • You're right: that MinVer = "0.0" syntax is clunky. Maybe do: NoVersion/AnyVersion = true to override?
  • How does this work with outbound route gen? Not sure that it applies too much, as it looks more like an API use case.
  • And how about inbound routing? Do you have a constraint in there somewhere?
  • I think this makes a lot more sense for Web API than MVC, but it wouldn't be a problem to support both, once Web API support is fixed (and that's in the works, Microsoft is aware of AR + Web API and wants them to play nice).
  • One more thing: Are you using it right now in a project you're working on?

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

Cheers!

mccalltd avatar Aug 08 '12 23:08 mccalltd

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.

  • I guess I was thinking of "versioned" as an attribute (versioned as opposed to non-versioned) as opposed to a past-tense verb. Open to suggestions
  • Yeah again just the prototypical nature here. I added the SemanticVersion-typed MinVersion properties first, then realized they're a pain to use in the attribute syntax and added string overrides to make it easier. Those can be flipped or the strongly typed ones can be hidden. NoVersion/AnyVersion is ambiguous, there's still no way to override and specify "everything up to x" if you have a class-level version specified, but probably something like `[GET('/a', MinVersion="any")]" would be more obvious and natural
  • I didn't do anything specific for outbound -- it is an API case and so that doesn't apply
  • I'm not sure what you're asking for re inbound? (Also disclaimer: at this point I am mostly on the "knows enough to be dangerous" level with ASP.NET routing so I may be missing something obvious)
  • Yes, I'm using it in a REST backend of a UI we've been developing on for a few months (though versioning was just added a couple days before I sent this patch in). We're getting ready to start having customers use the API (as a beta) as well.

gregmac avatar Aug 09 '12 00:08 gregmac

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 [email protected] 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.

  • I guess I was thinking of "versioned" as an attribute (versioned as opposed to non-versioned) as opposed to a past-tense verb. Open to suggestions

  • Yeah again just the prototypical nature here. I added the SemanticVersion-typed MinVersion properties first, then realized they're a pain to use in the attribute syntax and added string overrides to make it easier. Those can be flipped or the strongly typed ones can be hidden. NoVersion/AnyVersion is ambiguous, there's still no way to override and specify "everything up to x" if you have a class-level version specified, but probably something like `[GET('/a', MinVersion="any")]" would be more obvious and natural

  • I didn't do anything specific for outbound -- it is an API case and so that doesn't apply

  • I'm not sure what you're asking for re inbound? (Also disclaimer: at this point I am mostly on the "knows enough to be dangerous" level with ASP.NET routing so I may be missing something obvious)

  • Yes, I'm using it in a REST backend of a UI we've been developing on for a few months (though versioning was just added a couple days before I sent this patch in). We're getting ready to start having customers use the API (as a beta) as well.

    — Reply to this email directly or view it on GitHubhttps://github.com/mccalltd/AttributeRouting/issues/91#issuecomment-7602145.

mccalltd avatar Aug 09 '12 00:08 mccalltd

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
}

sksk571 avatar Aug 16 '12 15:08 sksk571

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.

mccalltd avatar Aug 16 '12 15:08 mccalltd

Thanks Tim,

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

sksk571 avatar Aug 16 '12 19:08 sksk571

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!

vyrotek avatar Sep 15 '12 04:09 vyrotek

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.

mccalltd avatar Sep 15 '12 05:09 mccalltd

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 [email protected] Date: Fri, 14 Sep 2012 22:16:57 To: mccalltd/[email protected] Reply-To: mccalltd/AttributeRouting [email protected] Cc: Greg [email protected] 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

gregmac avatar Sep 15 '12 05:09 gregmac

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!

mccalltd avatar Sep 15 '12 05:09 mccalltd

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()):

  • If we release a new version without making any API changes, then we just simply add it to the AddVersions() call. Nothing else is done, all existing API methods are (by default) available in the new version as well
  • When we add a new method, we simply tag it with MinVer= so eg, [GET("/users", MinVer="3.3.0")]

gregmac avatar Nov 02 '12 22:11 gregmac

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 avatar Dec 07 '12 05:12 mccalltd

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

  • Define supported versions via the configuration objects.
  • Add RouteVersioned attribute at controller level to set default version for all routes therein.
  • Add properties to route attributes for defining min/max supported versions.

Internally

  • Include SemVar helper
  • Determine the supported versions
  • Generate a route for each version

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

  • Rename RouteVersionedAttribute to RouteVersionAttribute
  • Specify versions like you do with nuget: http://docs.nuget.org/docs/reference/versioning. This would eliminate the min/max properties from the attributes and allow a simpler Version property. The intellisense for such properties should describe how to specify the version.
  • I'd like to know why you chose to create a route per version rather than use a route constraint. Not that I have a strong feeling on the final impl, just curious.
  • Specs/tests ensuring behavior works as expected.
  • Ensure that this is supported with both MVC and Web API bits.

Let's get this in.

mccalltd avatar Feb 17 '13 20:02 mccalltd

@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:

  • If you use a constraint, you have to pass it to your action method and handle it there. This means a couple things:
    • You end up with unwieldy switch()-style logic in your action code
    • You can't change the return type from one version to another, which is one of the main benefits of versioning to begin with
  • Using a high-level object means API help (such as API Explorer or Swagger) can be aware of the versions
    • Perhaps it could be with a VersionConstraint, but you'd have to evaluate each constraint against each available version, and there are the other drawbacks of constraints above
  • There is potential that this could also be used in the future for other styles of versioning, such as using a HTTP header (instead of route), and this could be configurable.

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.

gregmac avatar Feb 24 '13 07:02 gregmac

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?

kamranayub avatar Feb 26 '13 19:02 kamranayub

@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.

gregmac avatar Feb 28 '13 04:02 gregmac

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

kamranayub avatar Feb 28 '13 21:02 kamranayub