1

I have inherited a legacy WebAPI system which currently uses underscores in the routing pattern to denote versions. For example /api/1_0/account, /api/1_1/account etc.

I am attempting to update the auto-generated documentation to use Swagger, however using explicit routing with ApiVersion attributes which contain underscores leads to an exception. For example, this works fine:

[ApiVersion("1")]

However this throws an exception:

[ApiVersion("1_0")] // < note '_0' here
[RoutePrefix("api/{version:apiVersion}/account")]
public class AccountController : ApiBaseController
{
  // actions...
}

The exception is:

FormatException: The specified API version status '_1' is invalid.
System.InvalidOperationException: 'Failed to compare two elements in the array.'
at System.Collections.Generic.ArraySortHelper`1.Sort(T[] keys, Int32 index, Int32 length, IComparer`1 comparer)
at System.Array.Sort[T](T[] array, Int32 index, Int32 length, IComparer`1 comparer)
at System.Collections.Generic.List`1.Sort(Int32 index, Int32 count, IComparer`1 comparer)
at Microsoft.Web.Http.Dispatcher.ApiVersionControllerSelector.InitializeControllerInfoCache()
at System.Lazy`1.CreateValue()
at System.Lazy`1.LazyInitValue()
at System.Lazy`1.get_Value()
at Microsoft.Web.Http.Dispatcher.ApiVersionControllerSelector.GetControllerMapping()
at System.Web.Http.Routing.AttributeRoutingMapper.AddRouteEntries(SubRouteCollection collector, HttpConfiguration configuration, IInlineConstraintResolver constraintResolver, IDirectRouteProvider directRouteProvider)
at System.Web.Http.Routing.AttributeRoutingMapper.<>c__DisplayClass1_1.b__1()
at System.Web.Http.Routing.RouteCollectionRoute.EnsureInitialized(Func`1 initializer)
at System.Web.Http.Routing.AttributeRoutingMapper.<>c__DisplayClass1_0.b__0(HttpConfiguration config)
at System.Web.Http.HttpConfiguration.EnsureInitialized()
at ProjectName.Startup.Configuration(IAppBuilder app) in E:\ProjectPath\Foo.cs:line 25

The issue is obvious, but how can I include the underscore in the version attribute value? The problem is confusing as I am assuming that the innards of the class are (at some point) parsing the value to an integer, yet the attribute itself accepts a string...? So why would that be?

Vadim Kotov
  • 8,084
  • 8
  • 48
  • 62
Rory McCrossan
  • 331,213
  • 40
  • 305
  • 339
  • 1
    Assuming the `ApiVersion` attribute is from the `Microsoft.AspNet.WebApi.Versioning` package, I think you are [limited to using a `.` as separator](https://github.com/Microsoft/aspnet-api-versioning/wiki/Version-Format) – DavidG Apr 18 '18 at 16:41
  • https://github.com/Microsoft/aspnet-api-versioning/blob/master/src/Common/ApiVersion.cs#L25 – Dan Wilson Apr 18 '18 at 16:44
  • Are you set on using `ApiVersion` attributes? _ I do not use that to document with swagger, see a live example here: http://swagger-net-test-multiapiversions.azurewebsites.net/swagger/ui/index – Helder Sepulveda Apr 20 '18 at 14:27
  • As several have pointed out, the format does not allow for underscores or custom formats. In addition, the format is more complex than just `[.]`, though that's the form you are using. The **ApiVersion** is a formal type. The reason it's a string in the attribute is a limitation with .NET attributes. The attribute's constructor calls `ApiVersion.Parse` on the string value. – Chris Martinez Apr 21 '18 at 18:28

3 Answers3

3

Some additional information as to why this won't work. The Microsoft.AspNet.WebApi.Versioning package follows semantic versioning rules which require the separator between major and minor parts to be a period. See the rules for this package.

With some hacking around, it's possible to get the API versioning package to parse an underscore. This is very basic code and possibly not production ready, but should give you a direction to go. First thing you need is a custom route constraint (essentially ripping off the default one):

public class CustomApiVersionRouteConstraint : IHttpRouteConstraint
{
    public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, 
        IDictionary<string, object> values, HttpRouteDirection routeDirection)
    {
        if (string.IsNullOrEmpty(parameterName))
        {
            return false;
        }

        var properties = request.ApiVersionProperties();
        var versionString = "";

        if (values.TryGetValue(parameterName, out object value))
        {
            //This is the real 'magic' here, just replacing the underscore with a period
            versionString = ((string) value).Replace('_', '.');

            properties.RawApiVersion = versionString;
        }
        else
        {
            return false;
        }


        if (ApiVersion.TryParse(versionString, out var requestedVersion))
        {
            properties.ApiVersion = requestedVersion;
            return true;
        }

        return false;
    }
}

And make sure Web API is using the new constraint:

var constraintResolver = new DefaultInlineConstraintResolver()
{
    ConstraintMap =
    {
        ["apiVersion"] = typeof( CustomApiVersionRouteConstraint )
    }
};

config.MapHttpAttributeRoutes(constraintResolver);
DavidG
  • 113,891
  • 12
  • 217
  • 223
  • Thank you for this. Your code makes perfect sense, however it appears that the constraint is executed too late in the pipeline as I still have the same error. A breakpoint in the contraint itself is never hit. – Rory McCrossan Apr 19 '18 at 10:59
  • That's odd, it obviously works fine in my code here. Make sure you don't have another call to `MapHttpAttributeRoutes` perhaps? – DavidG Apr 19 '18 at 11:03
  • Ok - I'll investigate some more. I'll mark this as the correct answer as it was what I suspected was required. Must be something odd going on in this (rather old) project. Thanks again. – Rory McCrossan Apr 19 '18 at 15:08
  • An alternate variation of this solution would be to use the _Decorator_ pattern. Effectively, just create a new instance of **ApiVersionRouteConstraint**, do the string manipulation, and the call `ApiVersionRouteConstraint.Match`. Aside from being less code, it will keep you from having to know or maintain the inner workings of how the API version is extracted by the route constraint. I never considered this as a valid extension point, but there is clearly a case. There is a workable solution, but I think I'll add this scenario to the list of enhancements for the next release. – Chris Martinez Apr 21 '18 at 18:36
1

The ApiVersion class has a ParsePattern which defines the format of the version string.

const string ParsePattern = @"^(\d{4}-\d{2}-\d{2})?\.?(\d{0,9})\.?(\d{0,9})\.?-?(.*)$";

The pattern does not allow underscores. Supplying a version which does not match the expected pattern results in a FormatException.

Source: https://github.com/Microsoft/aspnet-api-versioning/blob/master/src/Common/ApiVersion.cs#L25

The ASP.NET API Version Format document provides more information (provided by @DavidG).

Dan Wilson
  • 3,937
  • 2
  • 17
  • 27
0

I see that all answers are focusing on the question in your title...

But your problem could be on your approach, you mentioned:

...attempting to update the auto-generated documentation to use Swagger, however using explicit routing with ApiVersion attributes which contain underscores leads to an exception.

Maybe simplify the attributes, only use the RoutePrefix, something like this:

[RoutePrefix("api/1_0/account")]
public class AccountController : ApiController

Bamm, problem solved...
All that remains is to configure the MultipleApiVersions in your SwaggerConfig, easy right?
Need an example look here: MultiApiVersions/Swagger_Test/App_Start/SwaggerConfig.cs

And here is a what the documentation looks like:
http://swagger-net-test-multiapiversions.azurewebsites.net/swagger/ui/index

Helder Sepulveda
  • 15,500
  • 4
  • 29
  • 56
  • Simple, but clever solution. Unfortunately, this probably won't actually solve the problem. Since the versioning is being done by a URL segment, API Versioning has no way to know or capture the API version from an incoming request. The provided route constraint is the thing that captures this value in the request pipeline. It's possible to make this approach work, but only if you also explicitly set the corresponding API version for these routes: `request.ApiVersioningProperties().ApiVersion = new ApiVersion(1,0);` – Chris Martinez Apr 21 '18 at 18:19
  • Hey @ChrisMartinez not sure why it won't work... He said "a WebAPI using underscores in the routing pattern to denote versions", that's the same I have on my example and it works fine. – Helder Sepulveda Apr 21 '18 at 22:32
  • The question is in the context of the [ASP.NET API Versioning](https://github.com/Microsoft/aspnet-api-versioning) stack. Perhaps that wasn't obvious. The route will match, but it will not have an API version. The segment `1_0` is not a valid API version. So while you can make the route template match, API versioning won't match the route because there is no corresponding API version value it can extract/parse. There are a few ways this problem could be solved: custom route constraint, message handler, url re-writing, etc. A route constraint is probably the simplest. – Chris Martinez Apr 23 '18 at 02:31