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

How to avoid duplicate code in controller for multiply version? #987

Unanswered
SuyliuMS asked this question in Q&A
Discussion options

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!

You must be logged in to vote

Replies: 1 comment 2 replies

Comment options

"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.

You must be logged in to vote
2 replies
Comment options

[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

Comment options

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.

image

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Category
Q&A
Labels
None yet

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