diff --git a/.editorconfig b/.editorconfig
index 4c2ddb8f..ecc12f91 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -13,11 +13,11 @@ indent_size = 4
# xml project files
[*.{csproj,vbproj,proj,projitems,shproj}]
-indent_size = 2
+indent_size = 1
# xml config files
[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]
-indent_size = 2
+indent_size = 1
# json files
[*.json]
diff --git a/build/common.props b/build/common.props
index f69d871b..faf2e28c 100644
--- a/build/common.props
+++ b/build/common.props
@@ -7,6 +7,10 @@
© $(Company). All rights reserved.
en
en-US
+
+
+
+ latest
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning/Abstractions/ActionDescriptorExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Versioning/Abstractions/ActionDescriptorExtensions.cs
index 23860b2c..154a096d 100644
--- a/src/Microsoft.AspNetCore.Mvc.Versioning/Abstractions/ActionDescriptorExtensions.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Versioning/Abstractions/ActionDescriptorExtensions.cs
@@ -2,8 +2,6 @@
{
using ApplicationModels;
using System;
- using System.Collections.Generic;
- using System.Diagnostics.Contracts;
using System.Linq;
using Versioning;
@@ -13,30 +11,6 @@
[CLSCompliant( false )]
public static class ActionDescriptorExtensions
{
- const string VersionPolicyIsAppliedKey = "MS_" + nameof( VersionPolicyIsApplied );
-
- static void VersionPolicyIsApplied( this ActionDescriptor action, bool value ) => action.Properties[VersionPolicyIsAppliedKey] = value;
-
- internal static bool VersionPolicyIsApplied( this ActionDescriptor action ) => action.Properties.GetOrDefault( VersionPolicyIsAppliedKey, false );
-
- internal static void AggregateAllVersions( this ActionDescriptor action, IEnumerable matchingActions )
- {
- Contract.Requires( action != null );
- Contract.Requires( matchingActions != null );
-
- if ( action.VersionPolicyIsApplied() )
- {
- return;
- }
-
- action.VersionPolicyIsApplied( true );
-
- var model = action.GetProperty();
- Contract.Assume( model != null );
-
- action.SetProperty( model.Aggregate( matchingActions.Select( a => a.GetProperty() ).Where( m => m != null ) ) );
- }
-
///
/// Returns a value indicating whether the provided action implicitly maps to the specified version.
///
diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning/Microsoft.Extensions.DependencyInjection/IServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Versioning/Microsoft.Extensions.DependencyInjection/IServiceCollectionExtensions.cs
index 93720977..dd6f9221 100644
--- a/src/Microsoft.AspNetCore.Mvc.Versioning/Microsoft.Extensions.DependencyInjection/IServiceCollectionExtensions.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Versioning/Microsoft.Extensions.DependencyInjection/IServiceCollectionExtensions.cs
@@ -7,6 +7,7 @@
using Extensions;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
+ using Microsoft.AspNetCore.Mvc.Abstractions;
using Options;
using System;
using System.Diagnostics.Contracts;
@@ -54,6 +55,7 @@ public static IServiceCollection AddApiVersioning( this IServiceCollection servi
if ( options.ReportApiVersions )
{
services.TryAddSingleton();
+ services.AddTransient();
}
else
{
diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning/Routing/DefaultApiVersionRoutePolicy.cs b/src/Microsoft.AspNetCore.Mvc.Versioning/Routing/DefaultApiVersionRoutePolicy.cs
index 4c8d96dc..9ba180b8 100644
--- a/src/Microsoft.AspNetCore.Mvc.Versioning/Routing/DefaultApiVersionRoutePolicy.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Versioning/Routing/DefaultApiVersionRoutePolicy.cs
@@ -164,9 +164,7 @@ protected virtual void OnSingleMatch( RouteContext context, ActionSelectionResul
Arg.NotNull( match, nameof( match ) );
var handler = new DefaultActionHandler( ActionInvokerFactory, ActionContextAccessor, selectionResult, match );
- var candidates = selectionResult.CandidateActions.SelectMany( kvp => kvp.Value );
- match.Action.AggregateAllVersions( candidates );
context.RouteData = match.RouteData;
context.Handler = handler.Invoke;
}
diff --git a/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ApiVersionActionSelector.cs b/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ApiVersionActionSelector.cs
index 706f603c..c2ea9edc 100644
--- a/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ApiVersionActionSelector.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Versioning/Versioning/ApiVersionActionSelector.cs
@@ -166,6 +166,7 @@ public virtual ActionDescriptor SelectBestCandidate( RouteContext context, IRead
}
else
{
+ AppendPossibleMatches( new[] { selectedAction }, context, selectionResult );
return selectedAction;
}
}
@@ -261,7 +262,7 @@ static ActionDescriptor SelectActionWithApiVersionPolicyApplied( IReadOnlyList
+ /// Represents an object that collates API versions per action.
+ ///
+ [CLSCompliant( false )]
+ public class ApiVersionCollator : IActionDescriptorProvider
+ {
+ ///
+ public int Order { get; protected set; }
+
+ ///
+ public virtual void OnProvidersExecuted( ActionDescriptorProviderContext context )
+ {
+ foreach ( var actions in GroupActionsByController( context.Results ) )
+ {
+ var collatedModel = CollateModel( actions );
+
+ foreach ( var action in actions )
+ {
+ var model = action.GetProperty();
+
+ if ( model != null )
+ {
+ action.SetProperty( model.Aggregate( collatedModel ) );
+ }
+ }
+ }
+ }
+
+ ///
+ public virtual void OnProvidersExecuting( ActionDescriptorProviderContext context )
+ {
+ }
+
+ ///
+ /// Resolves and returns the logical controller name for the specified action.
+ ///
+ /// The action to get the controller name from.
+ /// The logical name of the associated controller.
+ ///
+ ///
+ /// The logical controller name is used to collate actions together and aggregate API versions. The
+ /// default implementation uses the "controller" route parameter and falls back to the
+ /// property when available.
+ ///
+ ///
+ /// The default implementation will also trim trailing numbers in the controller name by convention. For example,
+ /// the type "Values2Controller" will have the controller name "Values2", which will be trimmed to just "Values".
+ /// This behavior can be changed by using the or overriding the default
+ /// implementation.
+ ///
+ ///
+ protected virtual string GetControllerName( ActionDescriptor action )
+ {
+ Arg.NotNull( action, nameof( action ) );
+
+ if ( !action.RouteValues.TryGetValue( "controller", out var key ) )
+ {
+ if ( action is ControllerActionDescriptor controllerAction )
+ {
+ key = controllerAction.ControllerName;
+ }
+ }
+
+ return TrimTrailingNumbers( key );
+ }
+
+ IEnumerable> GroupActionsByController( IEnumerable actions )
+ {
+ Contract.Requires( actions != null );
+ Contract.Ensures( Contract.Result>>() != null );
+
+ var groups = new Dictionary>( StringComparer.OrdinalIgnoreCase );
+
+ foreach ( var action in actions )
+ {
+ var key = GetControllerName( action );
+
+ if ( string.IsNullOrEmpty( key ) )
+ {
+ continue;
+ }
+
+ if ( !groups.TryGetValue( key, out var values ) )
+ {
+ groups.Add( key, values = new List() );
+ }
+
+ values.Add( action );
+ }
+
+ foreach ( var value in groups.Values )
+ {
+ yield return value;
+ }
+ }
+
+ static string TrimTrailingNumbers( string name )
+ {
+ if ( string.IsNullOrEmpty( name ) )
+ {
+ return name;
+ }
+
+ var last = name.Length - 1;
+
+ for ( var i = last; i >= 0; i-- )
+ {
+ if ( !char.IsNumber( name[i] ) )
+ {
+ if ( i < last )
+ {
+ return name.Substring( 0, i + 1 );
+ }
+
+ return name;
+ }
+ }
+
+ return name;
+ }
+
+ static ApiVersionModel CollateModel( IEnumerable actions ) =>
+ actions.Select( a => a.GetProperty() ).Where( m => m != null ).Aggregate();
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/Versioning/ApiVersionActionSelectorTest.cs b/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/Versioning/ApiVersionActionSelectorTest.cs
index a882bd43..fd593563 100644
--- a/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/Versioning/ApiVersionActionSelectorTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/Versioning/ApiVersionActionSelectorTest.cs
@@ -452,7 +452,7 @@ public async Task select_best_candidate_should_return_correct_controller_for_ver
var deprecated = new[] { new ApiVersion( 4, 0 ) };
var implemented = supported.Union( deprecated ).OrderBy( v => v ).ToArray();
- using ( var server = new WebServer() )
+ using ( var server = new WebServer( o => o.ReportApiVersions = true ) )
{
await server.Client.GetAsync( $"api/{versionSegment}/attributed" );
diff --git a/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/Versioning/ApiVersionCollatorTest.cs b/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/Versioning/ApiVersionCollatorTest.cs
new file mode 100644
index 00000000..9cb3700e
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Mvc.Versioning.Tests/Versioning/ApiVersionCollatorTest.cs
@@ -0,0 +1,140 @@
+namespace Microsoft.AspNetCore.Mvc.Versioning
+{
+ using FluentAssertions;
+ using Microsoft.AspNetCore.Mvc.Abstractions;
+ using Microsoft.AspNetCore.Mvc.Controllers;
+ using System.Collections.Generic;
+ using System.Linq;
+ using Xunit;
+
+ public class ApiVersionCollatorTest
+ {
+ [Theory]
+ [MemberData( nameof( ActionDescriptorProviderContexts ) )]
+ public void on_providers_executed_should_aggregate_api_version_models_by_controller( ActionDescriptorProviderContext context )
+ {
+ // arrange
+ var collator = new ApiVersionCollator();
+ var expected = new[] { new ApiVersion( 1, 0 ), new ApiVersion( 2, 0 ), new ApiVersion( 3, 0 ) };
+
+ // act
+ collator.OnProvidersExecuted( context );
+
+ // assert
+ var actions = context.Results.Where( a => a.GetProperty() != null );
+
+ actions.All( a => a.GetProperty().SupportedApiVersions.SequenceEqual( expected ) ).Should().BeTrue();
+ }
+
+ public static IEnumerable