-
Notifications
You must be signed in to change notification settings - Fork 716
-
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...
Beta Was this translation helpful? Give feedback.
All reactions
Replies: 3 comments
-
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.
Beta Was this translation helpful? Give feedback.
All reactions
-
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
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 1
-
@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?
Beta Was this translation helpful? Give feedback.