Would the package support a versioning strategy where endpoints are automagically elevated to the latest version? #900
Replies: 2 comments 2 replies
-
This is a good question. You have the right idea. Most people want to implicitly move the version forward for the client, which is just wrong and honestly defeats the purpose of versioning. Unless I've misunderstood, what you are asking to achieve is what I call Symmetrical Versioning. Essentially, you want to provide a consistent, public API version surface for clients; regardless, of the server implementation. This type of solution is achievable with this library. You still have to be cautious when filling in versions because if People There are a number of ways this can be implemented. You can apply multiple API version metadata with attributes to a single controller (or endpoint). For example: [ApiVersion(1.0)]
[ApiVersion(1.1)]
[ApiVersion(2.0)]
[Route("v{version:apiVersion}/[controller"])
public class JobsController : ControllerBase { } This allows a single implementation to server API Versioning doesn't actually care about attributes and that is just one way the metadata can be applied. As you probably have observed (based on your question), this will work, but touching a bunch of controllers to maintain this can be tedious. This is a case where using the Conventions support is a better fit. Conventions can be explicit: builder.Services.AddApiVersioning().AddMvc( options =>
{
options.Conventions.Controller<JobsController>().HasApiVersion(1.0).HasApiVersion(1.1).HasApiVersion(2.0);
} ); This can still be tedious so you can apply a broader convention. The only built-in convention is the builder.Services.AddApiVersioning().AddMvc( options =>
{
options.Conventions.Add( new VersionByNamespaceConvention() );
} );
namespace Controllers
{
namespace V1
{
// 1.0 applied by convention
[Route("v{version:apiVersion}/[controller]")]
public class JobsController : ControllerBase { }
}
namespace V1_1
{
// 1.1 applied by convention
[Route("v{version:apiVersion}/[controller]")]
public class JobsController : ControllerBase { }
}
namespace V2
{
// 2.0 applied by convention
[Route("v{version:apiVersion}/[controller]")]
public class JobsController : ControllerBase { }
}
} This technique can work really well to keep things clean between versions. Namespaces are usually mapped to source code folders. A new version can be as simple as copying and pasting the baseline version you want, update the namespace, and change any necessary implementation. You are not restricted to using the built-in conventions. You can roll your own too. You only need to implement Since you want to fill in the additional versions to the highest implementation for each API, you probably need a custom Again, there are multiple solutions. A complete solution is a bit involved, but hopefully that gives you some ideas. Happy to answer any additional questions or give you some pointers. |
Beta Was this translation helpful? Give feedback.
-
This is mean to be the one-stop-shop for API versioning so I do what I can for community building. Starting in The only real challenge with this solution is that you need to collate all the possible API versions to fill in the blanks. This results in a 🐔 and 🥚 problem by letting API Versioning do all of the work. You can either let API Versioning calculate the metadata, collate the versions, and update the metadata or you can collate yourself and apply conventions before API Versioning runs. I was a bit curious what this would look like myself. I went with the second option. There's a few limitations and assumptions in the approach. This assumes you'll be using attributes. The solution simply applies additional API versions to the highest/current controller that isn't already defined elsewhere. This could be made to work with namespaces given a few minor modifications. using Asp.Versioning;
using Asp.Versioning.ApplicationModels;
using Asp.Versioning.Conventions;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.Extensions.Options;
public sealed class SymmetricalApiVersionProvider : IApplicationModelProvider
{
private readonly IApiControllerFilter controllerFilter;
private readonly IControllerNameConvention namingConvention;
private readonly IOptions<MvcApiVersioningOptions> options;
public SymmetricalApiVersionProvider(
IApiControllerFilter controllerFilter,
IControllerNameConvention namingConvention,
IOptions<MvcApiVersioningOptions> options )
{
this.controllerFilter = controllerFilter;
this.namingConvention = namingConvention;
this.options = options;
}
public int Order => -1; // run before API Versioning
public void OnProvidersExecuted( ApplicationModelProviderContext context ) { }
public void OnProvidersExecuting( ApplicationModelProviderContext context )
{
// exclude controller we're not interested in (ex: UI controllers)
var controllers = controllerFilter.Apply( context.Result.Controllers );
var supported = new SortedSet<ApiVersion>();
var deprecated = new SortedSet<ApiVersion>();
var apis = new Dictionary<string, Api>( capacity: controllers.Count, StringComparer.OrdinalIgnoreCase );
for ( var i = 0; i < controllers.Count; i++ )
{
// this assumes we are using attributes to explicitly declare versions
var controller = controllers[i];
var attributes = controller.Attributes;
var declared = new HashSet<ApiVersion>();
for ( var j = 0; j < attributes.Count; j++ )
{
// any valid attribute is allowed, but will typically be ApiVersionAttribute
if ( attributes[j] is not IApiVersionProvider provider )
{
continue;
}
SortedSet<ApiVersion> target;
// must be None or Deprecated to be considered a 'declared' (e.g. implemented) version,
// which is different from a mapped or 'advertised' version. deprecated versions are
// still functional so we want to consider them. if we didn't, then once a version was
// deprecated, it would sunset (e.g. 'disappear') and that would be bad.
switch ( provider.Options )
{
case ApiVersionProviderOptions.None:
target = supported;
break;
case ApiVersionProviderOptions.Deprecated:
target = deprecated;
break;
default:
continue;
}
for ( var k = 0; k < provider.Versions.Count; k++ )
{
var version = provider.Versions[k];
declared.Add( version );
target.Add( version );
}
}
if ( declared.Count == 0 )
{
continue;
}
// name is all we have to collate on so make sure we use the same convention
// that will be used elsewhere
var name = namingConvention.NormalizeName( controller.ControllerName );
if ( apis.TryGetValue( name, out var api ) )
{
// if a version declared by this api is greater than what's already been
// seen, then this is now the current api
if ( declared.Max() > api.Versions.Max() )
{
api.Current = controller;
}
}
else
{
apis.Add( name, api = new Api( controller ) );
}
api.Versions.UnionWith( declared );
}
var conventions = options.Value.Conventions;
// we'll only consider a version completely deprecated if it doesn't appear
// anywhere as supported
deprecated.ExceptWith( supported );
// apply a convention to the current controller for each api where a version
// from all defined versions is not already applied to another controller
foreach ( var api in apis.Values )
{
var convention = conventions.Controller( api.Current.ControllerType );
foreach ( var version in supported.Except( api.Versions ) )
{
convention.HasApiVersion( version );
}
foreach ( var version in deprecated.Except( api.Versions ) )
{
convention.HasDeprecatedApiVersion( version );
}
}
}
private sealed class Api
{
public Api( ControllerModel current ) => Current = current;
public SortedSet<ApiVersion> Versions { get; } = new();
public ControllerModel Current { get; set; }
}
} You would then register it with: builder.Services.AddTransient<IApplicationModelProvider, SymmetricalApiVersionProvider>(); You mentioned Swagger (e.g. OpenAPI). I took the OpenApi example and added the following controller: [ApiVersion( 1.0 )]
[Route( "[controller]" )]
public class ValuesController : ControllerBase
{
[HttpGet]
public IActionResult Get( ApiVersion version ) => Ok( new[] { version.ToString() } );
} Even though it's only defined for As previously mentioned, there are some inherent risks that you need to be cautious about, but I leave that in your capable hands. |
Beta Was this translation helpful? Give feedback.
-
After having setup aspnet-api-versioning for a project, I came up with an approach of which I'm unsure if it would help to use versioning, and if it's an actual feature or usage of the library that I missed, or if it would be better to create something from scratch.
So the approach is that everytime I add a new
ApiVersionAttribute
with a new version number, all existing endpoints will also be available under that same version number, for which the highest versioned one will be selected.So let's say I have
and I add
Now suddenly
/v1.1/persons
will be available as well, and point to the highest existing version, which is1.0
If I add
Now suddenly
/v1.2/jobs
will also be available, and point tov1.1
My thought is that this way, I can just release an entire api version update by creating just a single new endpoint. If this would work with swagger versioning, that would be another big plus (no idea how yet)
downside is that if I create a new version of an existing endpoint, but by mistake the new version number is lower than some existing version, it would actually change a previous version of the api. But I'm sure there's ways to fix it. For example, have the attribute force you to provide a timestamp. then on creating all the aliases, it can check if over time every new version has a higher number than the previous.
So, my questions are:
Thanks!
Beta Was this translation helpful? Give feedback.
All reactions