-
Notifications
You must be signed in to change notification settings - Fork 714
-
Hi, team,
If a method in controller is the same for version 1.0 and version 2.0, how to avoid duplicate code for different version? Currently, I use 2 methods with same logic to implement API versioning.
// Current controllers
[Route("api/alias/odata/virtualEndpoint")]
public class GetHelloWorldController : ODataController
{
// GET https://localhost:44382/api/alias/odata/virtualEndpoint/getHelloWorld
[HttpGet("getHelloWorld")]
[ApiVersion(1.0)]
[EnableQuery]
[AllowAnonymous]
public async Task<List<string>> GetHelloWorld()
{
List<string> result = new List<string>();
result.Add("Hello World");
return result;
}
// GET https://localhost:44382/api/alias/odata/virtualEndpoint/getHelloWorld
[HttpGet("getHelloWorld")]
[ApiVersion(2.0)]
[EnableQuery]
[AllowAnonymous]
public async Task<List<string>> GetHelloWorld2() // Same logic with GetHelloWorld()
{
List<string> result = new List<string>();
result.Add("Hello World");
return result;
}
}
Configuration:
public class VirtualEndpointConfiguration : IModelConfiguration
{
public void Apply(ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix)
{
dynamic type;
if (apiVersion.MajorVersion == 1)
{
type = builder.Singleton<VirtualEndpoint>("virtualEndpoint");
}
else
{
type = builder.Singleton<VirtualEndpoint2>("virtualEndpoint");
}
type.EntityType.Function("getHelloWorld").ReturnsCollection<string>();
}
}
This can't work, but this is kind of what I want:
[ApiVersion(1.0)]
[ApiVersion(2.0)]
[Route("api/alias/odata/virtualEndpoint")]
public class GetHelloWorldController : ODataController
{
// GET https://localhost:44382/api/alias/odata/virtualEndpoint/getHelloWorld
[HttpGet("getHelloWorld")]
[EnableQuery]
[AllowAnonymous]
public async Task<List<string>> GetHelloWorld()
{
List<string> result = new List<string>();
result.Add("Hello World");
return result;
}
}
I saw this discussion How do you use versioning with OData attribute routing? · dotnet/aspnet-api-versioning · Discussion #961 (github.com) has a workaround for version in url segment, but my project requires the version number to be included in the parameters.
However this can't work:
[Route("api/alias/odata/virtualEndpoint")]
public class GetHelloWorldController : ODataController
{
// GET https://localhost:44382/api/alias/odata/virtualEndpoint/getHelloWorld
[HttpGet("getHelloWorld?api-version={version:apiVersion}")]
[ApiVersion(1.0)]
[ApiVersion(2.0)]
[EnableQuery]
[AllowAnonymous]
public async Task<List<string>> GetHelloWorld()
{
List<string> result = new List<string>();
result.Add("Hello World");
return result;
}
}
Appreciate for your help!
Beta Was this translation helpful? Give feedback.
All reactions
Replies: 1 comment 2 replies
-
"This can't work, but this is kind of what I want:"
What makes you think that can't work? That would be perfect fine. The code is an implementation detail. There's no reason why two or more API versions cannot map to the same implementation. That behavior is actually part of the intended design. Applying an API version at the controller level implicitly applies all of those versions to the action level as well.
In your first example, you applied the API versions directly at the action level. This is a supported scenario, but it's - honestly - kind of strange. An API typically is a collection of endpoints; for example, the Order or Customer API usually doesn't consist of one endpoint. A controller logically represents the API and all the endpoints that it consists of. When you use controllers, applying API versions at the controller level is usually what you want. If you interleave multiple versions with different implementations, you can use [MapToApiVersion]
to indicate which actions satisfy which API versions. Mapping an API version is not the same thing as declaring an API version.
Beta Was this translation helpful? Give feedback.
All reactions
-
[HttpGet("getHelloWorld?api-version={version:apiVersion}")]
The route seems can't be mapped, it will throw this exception:
Microsoft.AspNetCore.Routing.Patterns.RoutePatternException
HResult=0x80131500
Message=The literal section 'getHelloWorld?api-version=' is invalid. Literal sections cannot contain the '?' character.
Source=Microsoft.AspNetCore.Routing
StackTrace:
at Microsoft.AspNetCore.Routing.Patterns.RoutePatternParser.Parse(String pattern)
at Microsoft.AspNetCore.Routing.Patterns.RoutePatternFactory.Parse(String pattern)
at Microsoft.AspNetCore.Mvc.Routing.ActionEndpointFactory.AddEndpoints(List`1 endpoints, HashSet`1 routeNames, ActionDescriptor action, IReadOnlyList`1 routes, IReadOnlyList`1 conventions, Boolean createInertEndpoints)
at Microsoft.AspNetCore.Mvc.Routing.ControllerActionEndpointDataSource.CreateEndpoints(IReadOnlyList`1 actions, IReadOnlyList`1 conventions)
at Microsoft.AspNetCore.Mvc.Routing.ActionEndpointDataSourceBase.UpdateEndpoints()
at Microsoft.AspNetCore.Mvc.Routing.ActionEndpointDataSourceBase.Initialize()
at Microsoft.AspNetCore.Mvc.Routing.ActionEndpointDataSourceBase.get_Endpoints()
at Microsoft.AspNetCore.Routing.CompositeEndpointDataSource.<>c.<HandleChange>b__16_0(EndpointDataSource d)
at System.Linq.Enumerable.SelectManySingleSelectorIterator`2.ToArray()
at System.Linq.Enumerable.ToArray[TSource](IEnumerable`1 source)
at Microsoft.AspNetCore.Routing.CompositeEndpointDataSource.HandleChange()
at Microsoft.AspNetCore.Routing.CompositeEndpointDataSource.OnDataSourcesChanged(Object sender, NotifyCollectionChangedEventArgs e)
at System.Collections.ObjectModel.ObservableCollection`1.OnCollectionChanged(NotifyCollectionChangedEventArgs e)
at System.Collections.ObjectModel.ObservableCollection`1.InsertItem(Int32 index, T item)
at System.Collections.ObjectModel.Collection`1.Add(T item)
at Microsoft.AspNetCore.Builder.EndpointRoutingApplicationBuilderExtensions.UseEndpoints(IApplicationBuilder builder, Action`1 configure)
at Microsoft.Management.Services.CloudPC.Api.StartupBase.Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider) in D:\CMD\Infra\CMD-Redist-Common\src\Libraries\Api\StartupBase.cs:line 102
at System.RuntimeMethodHandle.InvokeMethod(Object target, Span`1& arguments, Signature sig, Boolean constructor, Boolean wrapExceptions)
at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
at Microsoft.AspNetCore.Hosting.ConfigureBuilder.Invoke(Object instance, IApplicationBuilder builder)
at Microsoft.AspNetCore.Hosting.ConfigureBuilder.<>c__DisplayClass4_0.<Build>b__0(IApplicationBuilder builder)
at Microsoft.AspNetCore.Hosting.GenericWebHostBuilder.<>c__DisplayClass15_0.<UseStartup>b__1(IApplicationBuilder app)
at Microsoft.Management.Services.CloudPC.ResilientClientLibrary.StartupOptionsValidation`1.<>c__DisplayClass0_0.<Configure>b__0(IApplicationBuilder builder) in D:\CMD\Infra\CMD-Redist-Common\src\Libraries\ResilientClientLibrary\StartupOptionsValidation.cs:line 25
at Microsoft.AspNetCore.Mvc.Filters.MiddlewareFilterBuilderStartupFilter.<>c__DisplayClass0_0.<Configure>g__MiddlewareFilterBuilder|0(IApplicationBuilder builder)
at Microsoft.AspNetCore.HostFilteringStartupFilter.<>c__DisplayClass0_0.<Configure>b__0(IApplicationBuilder app)
at Microsoft.AspNetCore.Hosting.GenericWebHostService.<StartAsync>d__37.MoveNext()
If I add version attribute to controller, like:
[ApiVersion(1.0)]
[ApiVersion(2.0)]
[Route("api/alias/odata/virtualEndpoint")]
public class GetHelloWorldController : ODataController
{
// GET https://localhost:44382/api/alias/odata/virtualEndpoint/getHelloWorld
[HttpGet("getHelloWorld")]
[EnableQuery]
[AllowAnonymous]
public async Task<List<string>> GetHelloWorld()
{
List<string> result = new List<string>();
result.Add("Hello World");
return result;
}
}
The URL path will be wrong, it should be "api/alias/odata/virtualEndpoint/getHelloWorld":
image
Beta Was this translation helpful? Give feedback.
All reactions
-
Query parameters are not part of the route because they are not part of the path (e.g. the identifier); therefore, you do not include them in templates. This is handled by the IApiVersionReader
, which can be configured via ApiVersioningOptions.ApiVersionReader
. The QueryStringApiVersionReader
handles reading from the query string and is one of the defaults. The default query string parameter name is api-version
, but it can be changed to one or more alternatives in the QueryStringApiVersionReader
constructor.
It would appear you are using the UseODataRouteDebug
middleware. That will not show the api-version
query string parameter; however, it is there and enforced. You can see an example of a proper end-to-end working scenario in the Basic OData Example.
When you request api/People?api-version=1.0
, you'll receive:
{ "@odata.context": "api/$metadata#People", "value": [ { "id": 1, "firstName": "Bill", "lastName": "Mei" } ] }
but when you request api/People?api-version=2.0
, you'll receive:
{ "@odata.context": "api/$metadata#People", "value": [ { "id": 1, "firstName": "Bill", "lastName": "Mei", "email": "bill.mei@somewhere.com" } ] }
This is because both versions go to the same controller and action, but have differently configured EDMs that change what is sent over the wire.
If you need or want the API version in your action method, the simplest is approach is to declare it as a parameter:
public Task<List<string>> GetHelloWorld(ApiVersion version)
and model binding will provide it. You can also retrieve it via HttpContext.GetRequestedApiVersion()
, but note that it can be null
depending on when it's requested. The value will never be null
by the time an action is invoked.
Beta Was this translation helpful? Give feedback.