dotnet / aspnet-api-versioning

Provides a set of libraries which add service API versioning to ASP.NET Web API, OData with ASP.NET Web API, and ASP.NET Core.
MIT License
3.03k stars 702 forks source link

Question: centralizing versions for easier updating/maintenance #986

Closed nlersber closed 1 year ago

nlersber commented 1 year ago

In what way can versioning be centralized to a single location in a project? Currently, we have set up each of our controllers in our web API to have the versions defined and each endpoint is annotated with the versions that can be mapped to those methods.

    [ApiVersion("1.0")]
    public class ExampleController : QueryApiControllerBase
{
    [HttpGet]
    [Route("EndpointMethod")]
    [Route("v{version:apiVersion}/EndpointMethod")]
    [MapToApiVersion("1.0")]
    public async Task<IHttpActionResult> EndpointMethod()
    {
        return Foo();
    }
}

With a very simple setup in the HttpConfiguration as well:

this.AddApiVersioning(options =>
    {
         options.AssumeDefaultVersionWhenUnspecified = true;
         options.DefaultApiVersion = new ApiVersion(2, 0);
    });

Since our project is fairly large with a lot of controllers, doing a version update requires a huge effort to add the new version to every single controller and endpoint, even though all of em are virtually the same and only a couple of endpoints might deviate from the norm. Is there a way to centralize this and define all versions by default, unless it is specifically set up to only allow specific versions? So far I have tried creating a wrapper for [ApiVersion] to allow for inheritance and put this on my QueryApiControllerBase, but that resulted in no version:apiVersion being available and thus multiple methods with the same route besides the version being incompatible with our Swagger setup.

Due to 99% of our API being compatible with all possible versions, it is an incredible hassle to add annotations to everything. Can this be centralized in a some way?

commonsensesoftware commented 1 year ago

There are several ways that API versions can be centrally managed.

As you've noticed, the built-in attributes can be used in various ways or extended. [ApiVersion] is not inherited by design. If A has 1.0 and B : A with 2.0, this would be a problem because B is now 1.0 and 2.0, which leads to an ambiguous configuration. The ApiVersionsBaseAttribute is the place you can start for a baseline. API Versioning does not care specifically about any of the built-in attributes. It only cares about IApiVersionProvider. You are free to create whatever attributes you like as long as they implement the interface. Some have used this approach to implement a range concept.

The more straight forward approach is to use conventions. For example,

this.AddApiVersioning(options =>
{
    options.Conventions.Controller<ExampleController>().HasApiVersion(1.0);
    options.Conventions.Controller(typeof(Example2Controller)).HasApiVersion(1.0);
});

Conventions can be applied in all of the same ways that attributes can. They can be applied at the controller level and at the action level. There are strongly-typed forms that you could break up into other methods and there are also loosely-typed forms that you could use, potentially with information an external source such as configuration or a database.

You can also write custom conventions that implement IControllerConvention. There is only one such built-in convention: VersionByNamespaceConvention. This convention applies an ApiVersion to a controller based on its defined .NET namespace according to the documented rules. For example:

this.AddApiVersioning(options =>
{
    options.Conventions.Add(new VersionByNamespaceConvention());
});
namespace My.Api.V1.Controllers
{
    public class ExampleController : QueryApiControllerBase
    {
        [HttpGet]
        [Route("EndpointMethod")]
        [Route("v{version:apiVersion}/EndpointMethod")]
        public async Task<IHttpActionResult> EndpointMethod() => Ok(await Foo());
    }
}

ExampleController will now have 1.0 as its version by convention because V1 is in its namespace. Since C# uses the source folder structure as the default name of namespaces, this make adding and removing versions very simply. You only need to copy, paste, and rename a folder for a new version or just delete an old one.

All of these approaches are mutually inclusive so you can mix and match them to achieve your desired result.


Asides:

nlersber commented 1 year ago

Follow-up question since it follows from the centralized versioning: how does this work with swagger now that there is no longer an ApiVersion tag present in the controller itself? Our NSwag implementation seems to not find the ApiVersion for the v{version:apiVersion} part of our route and it ends up with something like sampleController/v/endpointName. From what I could find, using the Api Explorer seems to be a solution to this, but that seems only available for the ASPNetCore.Mvc package, while we use the Microsoft.Web.Http.Versioning package.

commonsensesoftware commented 1 year ago

First things first. If you are still using Microsoft.AspNet.WebApi.Versioning, you should stop and migrate as soon as possible. That package has been supplanted by Asp.Versioning.WebApi for several major versions now. There is more history and background in #807 and #808 if you're interested.

The old package for the ASP.NET Web API API Explorer extensions was Microsoft.AspNet.WebApi.Versioning.ApiExplorer. You should now be using Asp.Versioning.WebApi.ApiExplorer. The old and new packages use the same setup of:

var apiExplorer = configuration.AddVersionedApiExplorer(
    options =>
    {
        options.GroupNameFormat = "'v'VVV";
        options.SubstituteApiVersionInUrl = true;
    } );

Since you're versioning by URL segment, this will remove the API version route parameter and substitute the appropriate literal into the route template. The GroupNameFormat will control the name given to the group, which is based on formatting the API version. The Swagger UI will show 1.0 as v1, 2.0 as v2, and so on. This is only for grouping and visualization, which has nothing to do with the format used when substituting the API version in the route template. That format can be controlled via SubstitutionFormat, but there is rarely ever a need to do so. The default configuration should suffice.

The ASP.NET Web API OpenAPI with Swashbuckle example shows how to put all of the pieces together. It's not exactly the same as NSwag, but it should be pretty close. I honestly don't know NSwag that well and I only really know Swashbuckle from creating examples (which predated NSwag). There is nothing specific about Swashbuckle or NSwag in the API Versioning API Explorer extensions. There are definitely other NSwag users out there using the API Explorer extensions.

commonsensesoftware commented 1 year ago

This thread has gone idle. I presume you were eventually able to get things working. For final clarity, you want to use:

Feel free to reply or re-open the issue if you're still stuck.

ajbeaven commented 1 month ago

Thanks for your very detailed comments in this issue @commonsensesoftware. I came across this issue after getting stuck wondering why there was no version being specified in all of my derived controllers.

As you've noticed, the built-in attributes can be used in various ways or extended. [ApiVersion] is not inherited by design. If A has 1.0 and B : A with 2.0, this would be a problem because B is now 1.0 and 2.0, which leads to an ambiguous configuration.

I assume the issue is whether B in your example above, is both version 1.0 and 2.0 or only 2.0? How is this different to the ambiguity of decorating a controller multiple times with [ApiVersion], or decorating both the controller and a method? My default assumption is that attributes applied on controllers tend to be inherited (this is the default for attributes after all), so I think there's unavoidable confusion in this case đŸ˜…. I'm definitely coming in to this fresh though.

For others that hit this, here's an attribute that is inherited:

/// <summary>
/// Represents the metadata that describes the <see cref="ApiVersion">versions</see> associated with an API. Unlike
/// <see cref="ApiVersionAttribute"/>, this attribute is inherited.
/// </summary>
[AttributeUsage( AttributeTargets.Class, AllowMultiple = true, Inherited = true /* this is the key difference */ )]

internal class ApiVersionInheritableAttribute : ApiVersionsBaseAttribute, IApiVersionProvider
{
    private ApiVersionProviderOptions _options = ApiVersionProviderOptions.None;

    /// <summary>
    /// Initializes a new instance of the <see cref="ApiVersionInheritableAttribute"/> class.
    /// </summary>
    /// <param name="version">The API version string.</param>
    public ApiVersionInheritableAttribute(double version) : base(version)
    {
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="ApiVersionInheritableAttribute"/> class.
    /// </summary>
    /// <param name="version">A numeric API version.</param>
    /// <param name="status">The status associated with the API version, if any.</param>
    public ApiVersionInheritableAttribute(double version, string? status = default) : base(new ApiVersion(version, status))
    {
    }

    ApiVersionProviderOptions IApiVersionProvider.Options => _options;

    /// <summary>
    /// Gets or sets a value indicating whether the specified set of API versions are deprecated.
    /// </summary>
    /// <value>True if the specified set of API versions are deprecated; otherwise, false.
    /// The default value is <c>false</c>.</value>
    public bool Deprecated
    {
        get => ( _options & ApiVersionProviderOptions.Deprecated ) == ApiVersionProviderOptions.Deprecated;
        set
        {
            if ( value )
            {
                _options |= ApiVersionProviderOptions.Deprecated;
            }
            else
            {
                _options &= ~ApiVersionProviderOptions.Deprecated;
            }
        }
    }

    /// <inheritdoc />
    public override int GetHashCode() => HashCode.Combine( base.GetHashCode(), Deprecated );

    protected ApiVersionInheritableAttribute(ApiVersion version) 
        : base(version)
    {
    }

    protected ApiVersionInheritableAttribute(ApiVersion version, params ApiVersion[] otherVersions) 
        : base(version, otherVersions)
    {
    }

    protected ApiVersionInheritableAttribute(double version, params double[] otherVersions) 
        : base(version, otherVersions)
    {
    }

    protected ApiVersionInheritableAttribute(string version) 
        : base(version)
    {
    }

    protected ApiVersionInheritableAttribute(string version, params string[] otherVersions) 
        : base(version, otherVersions)
    {
    }

    protected ApiVersionInheritableAttribute(IApiVersionParser parser, string version) 
        : base(parser, version)
    {
    }

    protected ApiVersionInheritableAttribute(IApiVersionParser parser, string version, params string[] otherVersions) 
        : base(parser, version, otherVersions)
    {
    }
}