0

We have an API controller class that lists all supported versions with ApiVersion attribute. Versioning format used is, versiongroup.minor-status

[ApiController]
[ApiVersion("2020-11-01-preview")]
[ApiVersion("2020-11-01.1-preview")]
[ApiVersion("2021-11-01.2-preview")]
[ApiVersion("2021-11-01.3-preview")] 
[Route("/routeData")] 
public class TestController { }

Now as the list of ApiVersion attributes might keep growing, wanted to do a customize check, where (Suppose for a given controller we maintain a list of supported versions) for an incoming request with apiversion in query param, we could check if its a supported version for the given controller or not? Something like

[ApiController]
[ValidateApiVersion]
[Route("/routeData")] 
public class TestController { }

Want to know

  • if there already is a recommended way to achieve this?
  • We generate swagger for api documentation. And need to support that post the change as well.

I tried the following: Introducing

public sealed class ValidApiVersionsAttribute : Attribute, IActionConstraint
{
    public bool Accept(ActionConstraintContext context) {..}
}

startup has: services.AddApiVersioning();

For some reason my 'Accept' method never gets invoked.

  • Before providing a comprehensive answer, it's not entirely clear what you want/expect to achieve as an outcome. The API version will already be validated. If the API version is not supported, no requests will ever reach your controller. It would kind of defeat the value of API Versioning if you had to validate things yourself. Are you using the API Explorer extensions for Swagger/OpenAPI document generation or you just intend to? – Chris Martinez Sep 07 '21 at 17:15
  • Chris, my ask is - Current/default design makes one pile up the API version attributes to the controller. Is it possible to replace it with a single action attribute, whose handling is customized to check if given request version is supported or not. – Divya Malini Sep 08 '21 at 05:49

1 Answers1

1

An API version is nothing more than metadata. It is not an IActionConstraint nor a route constraint. The attributes provided out-of-the-box are extensible and customizable.

For example:

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class ApplicationApiVersionsAttribute : ApiVersionsBaseAttribute
{
  public ApplicationApiVersionsAttribute()
    : base(new[]
           {
             new(new DateTime(2020, 11, 1), "preview"),
             new(new DateTime(2020, 11, 1), 1, 0, "preview"),
             new(new DateTime(2020, 11, 1), 2, 0, "preview"),
             new(new DateTime(2020, 11, 1), 3, 0, "preview"),
           }) { }
}

Note that the base attribute constructor takes ApiVersion. Attribute data is limited to simple types such as string and numeric literals. Decorating a controller is limited to magic strings by the language, but the underlying types always use actual ApiVersion instances.

You can then decorator your controller as follows:

[ApiController]
[ApplicationApiVersions]
[Route("/routeData")] 
public class TestController : ControllerBase { }

Attributes are just one way that metadata can be exposed to API Versioning. While a few attributes are provided out-of-the-box for your convenience, what API Versioning really cares about is IApiVersionProvider, which those attributes implement. You can roll your own attributes from scratch if you prefer to as long as they implement IApiVersionProvider.

You can also use the conventions API so that you don't need attributes at all; for example:

services.AddApiVersioning(
  options =>
  {
     options.Conventions
            .Controller<TestController>()
            .HasApiVersion(2020, 11, 01, "preview")
            .HasApiVersion(2020, 11, 01, 1, 0, "preview")
            .HasApiVersion(2020, 11, 01, 2, 0, "preview")
            .HasApiVersion(2020, 11, 01, 3, 0, "preview");
  }

That's just scratching the surface. There are many ways that conventions can be applied. You can also roll your own conventions.

public class ApplicationApiVersionsConvention : IControllerConvention
{
  public bool Apply(
    IControllerConventionBuilder controller,
    ControllerModel controllerModel)
  {
    controller.HasApiVersion(2020, 11, 01, "preview")
              .HasApiVersion(2020, 11, 01, 1, 0, "preview")
              .HasApiVersion(2020, 11, 01, 2, 0, "preview")
              .HasApiVersion(2020, 11, 01, 3, 0, "preview");
  }
}

This would indiscriminately apply the specified API versions to all controllers. The controllerModel can be used for filtering. To configure your convention, you just add it to the setup:

services.AddApiVersioning(
  options => options.Conventions.Add(new ApplicationApiVersionsConvention());

Conventions can be very useful, but tend to be application-specific. The only provided convention is VersionByNamespaceConvention, which applies a single API version based on the .NET namespace that your controller resides in. That enables no attribution and one-line configuration.

A final aside. If you establish an API version policy - something like N-2, that will implicitly help in managing your code because you won't have a list of versions to support. For previews, I would consider limiting it to just one if possible. New previews generally don't need side-by-side support. You application needs may be different, but having a sound versioning policy ahead of time will make maintenance much easier.

I can revise or expand the answer if that doesn't fully cover your use case.

Chris Martinez
  • 3,185
  • 12
  • 28