Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Custom ApiVersion, where to inject CustomApiVersionParser #988

Closed
TonicHess started this conversation in General
Discussion options

I'm looking to use a custom string format for the API Versioning in our service, and I'm stuck.

To get off the ground, I've extended the built in Version/VersionFormatProvider/VersionReader classes, and am just tagging some breakpoints in these classes to validate they are being called.

I'm starting with the ApiVersionParser... I can register it (and see it function... sometimes..) with the services.AddSingleton<IApiVersionParser, CustomApiVersionParser>(); command at the beginning of the ConfigureServices bootstrap... but it will ONLY fire with a valid and populated version parameter. If I specify any other version format string, it will not get called to parse the version format string. It seems the default version parser is still present somewhere in the call stack, validating the string before it gets to the provided Singleton...

You must be logged in to vote

Replies: 3 comments

Comment options

I've scoured back through the code to be sure, but there is only a single place where ApiVersionParser.Default is ever directly used in the code and that is as the default within ApiVersionsBaseAttribute. All other built-in attributes, such as ApiVersionAttribute, derive from this attribute using the default parser.

If you use any of the following:

[ApiVersion("1.0")]
[ApiVersion("1.0-preview.1")]
[ApiVersion("2023-04-01")]

that will use the default IApiVersionParser.

If you use any of these:

[ApiVersion(1.0)]
[ApiVersion(1.0, "preview.1")]

it won't because there is nothing to parse.

This should make sense because there is no way for an attribute to get access to the DI container. A static singleton would provide a way to access it, but that is an anti-pattern and you'd likely have to set one in the DI container and one as some static property or method. Furthermore, there could be scenarios where someone needs two different parsers for some reason which would then be impossible or very difficult.

That doesn't mean you're out of luck. There are still several options:

Option 1

Use conventions over attributes to assign the API versions. This doesn't require any parsing. The parsing behavior is due to a limitation in how attributes work. The ApiVersion and DateOnly types cannot be expressed as a literal in the attribute.

Option 2

Extend the existing attributes. All of the built-in attributes can be extended and have protected constructors that accept an IApiVersionParser. This is how ApiVersionParser.Default is used in the first place. You can even use the same attribute name in your own namespace to make the code churn minimal. For example:

namespace My.Code;
public sealed class ApiVersionAttribute : Asp.Versioning.ApiVersionAttribute
{
 private static readonly CustomApiVersionParser Parser = new();
 public ApiVersionAttribute( string version ) : base( Parser, version ) { }
}

The compiler takes precedence of your namespaces when resolving types, which means [ApiVersion("1.0")] will resolve to My.Code.ApiVersionAttribute instead of Asp.Versioning.ApiVersionAttribute. They are different attributes and types, but look identical. You're free to use different names of course.

Option 3

The final option would be to implement a fully custom solution that is similar to Option 2. API Versioning doesn't care about specific attributes. It only cares about IApiVersionProvider, which the built-in attributes implement. You can roll your own, custom attributes any way you like, with whatever names you like, as long as they implement that interface. In this scenario, I'd probably stick with Option 2 if you are using attributes and need a custom parser.


Hopefully that clears things up and provides some approaches to close the gap on implementing custom parsing.

You must be logged in to vote
0 replies
Comment options

This was great! It got me unblocked. Thank you for mentioning that Attributes are a bit out of the scope of the DI.

Working with Option 2 right now, as it seems to be moving me along. Digging through what the override methods all now need to be. O_O

You must be logged in to vote
0 replies
Comment options

@commonsensesoftware Thanks for the previous answers. I followed your Option 2, and I feel like I am almost there!

I am trying to create a CustomApiVersion that looks like this 12.20250626.1402.

In this case I have created a CustomApiVersionParser from some example code I found. This seems to work.

Then I used your Option 2 above to create my custom ApiVersion attribute.

using Asp.Versioning;
namespace My.Api.Versioning;
public sealed class ApiVersionLatestAttribute : ApiVersionAttribute
{
 private static readonly CustomApiVersionParser _parser = new();
 public ApiVersionLatestAttribute() : base(_parser, AppApiVersions.Latest.ToString()) { }
}
public sealed class ApiVersionPreviousAttribute : ApiVersionAttribute
{
 private static readonly CustomApiVersionParser _parser = new();
 public ApiVersionPreviousAttribute() : base(_parser, AppApiVersions.Previous.ToString()) { }
}

Then i used the same principles to create a custom MapToApiVersion attribute.

using Asp.Versioning;
namespace My.Api.Versioning;
public sealed class MapToLatestApiVersionAttribute : MapToApiVersionAttribute
{
 private static readonly CustomApiVersionParser _parser = new();
 public MapToLatestApiVersionAttribute() : base(_parser, AppApiVersions.Latest.ToString()) { }
}
public sealed class MapToPreviousApiVersionAttribute : MapToApiVersionAttribute
{
 private static readonly CustomApiVersionParser _parser = new();
 public MapToPreviousApiVersionAttribute() : base(_parser, AppApiVersions.Previous.ToString()) { }
}

Lastly, I am trying to do the following for my controller:

using My.Api.Versioning;
using Microsoft.AspNetCore.Mvc;
[ApiVersionLatest]
[ApiVersionPrevious]
[ApiController]
[Route("/api/misc/app-updates/test")]
public class AppUpdatesControllerTest : ControllerBase
{
 public AppUpdatesControllerTest()
 {
 }
 [MapToLatestApiVersion]
 [HttpGet("latest")]
 public ActionResult GetLatestAppUpdateVLatest()
 {
 return Ok("Latest version!");
 }
 [MapToPreviousApiVersion]
 [HttpGet("latest")]
 public ActionResult GetLatestAppUpdateVPrevious()
 {
 return Ok("Previous version!");
 }
}

Sadly this results in a Microsoft.AspNetCore.Routing.Matching.AmbiguousMatchException.

The request matched multiple endpoints. Matches: 
AppUpdatesControllerTest.GetLatestAppUpdateVPrevious
AppUpdatesControllerTest.GetLatestAppUpdateVLatest

What am I doing wrong? and is what I am trying to do even possible?

You must be logged in to vote
0 replies
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

AltStyle によって変換されたページ (->オリジナル) /