1+ namespace Microsoft . AspNetCore . Mvc . Routing
2+ {
3+ using Microsoft . AspNetCore . Http ;
4+ using Microsoft . AspNetCore . Mvc . Abstractions ;
5+ using Microsoft . AspNetCore . Mvc . Versioning ;
6+ using Microsoft . AspNetCore . Routing ;
7+ using Microsoft . AspNetCore . Routing . Matching ;
8+ using Microsoft . Extensions . Logging ;
9+ using Microsoft . Extensions . Options ;
10+ using System ;
11+ using System . Collections . Generic ;
12+ using System . Diagnostics . Contracts ;
13+ using System . Linq ;
14+ using System . Threading . Tasks ;
15+ using static Microsoft . AspNetCore . Mvc . Versioning . ApiVersionMapping ;
16+ using static Microsoft . AspNetCore . Mvc . Versioning . ErrorCodes ;
17+ using static System . Threading . Tasks . Task ;
18+
19+ /// <summary>
20+ /// Represents the <see cref="IEndpointSelectorPolicy">endpoint selector policy</see> for API versions.
21+ /// </summary>
22+ [ CLSCompliant ( false ) ]
23+ public sealed class ApiVersionMatcherPolicy : MatcherPolicy , IEndpointSelectorPolicy
24+ {
25+ readonly IOptions < ApiVersioningOptions > options ;
26+
27+ /// <summary>
28+ /// Initializes a new instance of the <see cref="ApiVersionMatcherPolicy"/> class.
29+ /// </summary>
30+ /// <param name="options">The <see cref="ApiVersioningOptions">options</see> associated with the action selector.</param>
31+ /// <param name="reportApiVersions">The <see cref="IReportApiVersions">object</see> used to report API versions.</param>
32+ /// <param name="loggerFactory">The <see cref="ILoggerFactory">factory</see> used to create <see cref="ILogger">loggers</see>.</param>
33+ public ApiVersionMatcherPolicy (
34+ IOptions < ApiVersioningOptions > options ,
35+ IReportApiVersions reportApiVersions ,
36+ ILoggerFactory loggerFactory )
37+ {
38+ Arg . NotNull ( options , nameof ( options ) ) ;
39+ Arg . NotNull ( reportApiVersions , nameof ( reportApiVersions ) ) ;
40+ Arg . NotNull ( loggerFactory , nameof ( loggerFactory ) ) ;
41+
42+ this . options = options ;
43+ ApiVersionReporter = reportApiVersions ;
44+ Logger = loggerFactory . CreateLogger ( GetType ( ) ) ;
45+ }
46+
47+ /// <inheritdoc />
48+ public override int Order => 0 ;
49+
50+ ApiVersioningOptions Options => options . Value ;
51+
52+ IApiVersionSelector ApiVersionSelector => Options . ApiVersionSelector ;
53+
54+ IReportApiVersions ApiVersionReporter { get ; }
55+
56+ ILogger Logger { get ; }
57+
58+ /// <inheritdoc />
59+ public bool AppliesToEndpoints ( IReadOnlyList < Endpoint > endpoints )
60+ {
61+ Arg . NotNull ( endpoints , nameof ( endpoints ) ) ;
62+
63+ for ( var i = 0 ; i < endpoints . Count ; i ++ )
64+ {
65+ var action = endpoints [ i ] . Metadata ? . GetMetadata < ActionDescriptor > ( ) ;
66+
67+ if ( action ? . GetProperty < ApiVersionModel > ( ) != null )
68+ {
69+ return true ;
70+ }
71+ }
72+
73+ return false ;
74+ }
75+
76+ /// <inheritdoc />
77+ public Task ApplyAsync ( HttpContext httpContext , EndpointSelectorContext context , CandidateSet candidates )
78+ {
79+ Arg . NotNull ( httpContext , nameof ( httpContext ) ) ;
80+ Arg . NotNull ( context , nameof ( context ) ) ;
81+ Arg . NotNull ( candidates , nameof ( candidates ) ) ;
82+
83+ if ( IsRequestedApiVersionAmbiguous ( httpContext , context , out var apiVersion ) )
84+ {
85+ return CompletedTask ;
86+ }
87+
88+ if ( apiVersion == null && Options . AssumeDefaultVersionWhenUnspecified )
89+ {
90+ apiVersion = TrySelectApiVersion ( httpContext , candidates ) ;
91+ httpContext . Features . Get < IApiVersioningFeature > ( ) . RequestedApiVersion = apiVersion ;
92+ }
93+
94+ var finalMatches = EvaluateApiVersion ( httpContext , candidates , apiVersion ) ;
95+
96+ if ( finalMatches . Count == 0 )
97+ {
98+ context . Endpoint = ClientError ( httpContext , candidates ) ;
99+ }
100+ else
101+ {
102+ for ( var i = 0 ; i < finalMatches . Count ; i ++ )
103+ {
104+ candidates . SetValidity ( finalMatches [ i ] . index , true ) ;
105+ }
106+ }
107+
108+ return CompletedTask ;
109+ }
110+
111+ static IReadOnlyList < ( int index , ActionDescriptor action ) > EvaluateApiVersion (
112+ HttpContext httpContext ,
113+ CandidateSet candidates ,
114+ ApiVersion apiVersion )
115+ {
116+ Contract . Requires ( httpContext != null ) ;
117+ Contract . Requires ( candidates != null ) ;
118+ Contract . Ensures ( Contract . Result < IReadOnlyList < ( int index , ActionDescriptor action ) > > ( ) != null ) ;
119+
120+ var bestMatches = new List < ( int index , ActionDescriptor action ) > ( ) ;
121+ var implicitMatches = new List < ( int , ActionDescriptor ) > ( ) ;
122+
123+ for ( var i = 0 ; i < candidates . Count ; i ++ )
124+ {
125+ if ( ! candidates . IsValidCandidate ( i ) )
126+ {
127+ continue ;
128+ }
129+
130+ ref var candidate = ref candidates [ i ] ;
131+ var action = candidate . Endpoint . Metadata ? . GetMetadata < ActionDescriptor > ( ) ;
132+
133+ if ( action == null )
134+ {
135+ candidates . SetValidity ( i , false ) ;
136+ continue ;
137+ }
138+
139+ switch ( action . MappingTo ( apiVersion ) )
140+ {
141+ case Explicit :
142+ bestMatches . Add ( ( i , action ) ) ;
143+ break ;
144+ case Implicit :
145+ implicitMatches . Add ( ( i , action ) ) ;
146+ break ;
147+ }
148+
149+ // perf: always make the candidate invalid so we only need to loop through the
150+ // final, best matches for any remaining, valid candidates
151+ candidates . SetValidity ( i , false ) ;
152+ }
153+
154+ switch ( bestMatches . Count )
155+ {
156+ case 0 :
157+ bestMatches . AddRange ( implicitMatches ) ;
158+ break ;
159+ case 1 :
160+ var model = bestMatches [ 0 ] . action . GetApiVersionModel ( ) ;
161+
162+ if ( model . IsApiVersionNeutral )
163+ {
164+ bestMatches . AddRange ( implicitMatches ) ;
165+ }
166+
167+ break ;
168+ }
169+
170+ return bestMatches . ToArray ( ) ;
171+ }
172+
173+ bool IsRequestedApiVersionAmbiguous ( HttpContext httpContext , EndpointSelectorContext context , out ApiVersion apiVersion )
174+ {
175+ Contract . Requires ( httpContext != null ) ;
176+ Contract . Requires ( context != null ) ;
177+
178+ try
179+ {
180+ apiVersion = httpContext . GetRequestedApiVersion ( ) ;
181+ }
182+ catch ( AmbiguousApiVersionException ex )
183+ {
184+ Logger . LogInformation ( ex . Message ) ;
185+ apiVersion = default ;
186+
187+ var handlerContext = new RequestHandlerContext ( Options . ErrorResponses )
188+ {
189+ Code = AmbiguousApiVersion ,
190+ Message = ex . Message ,
191+ } ;
192+
193+ context . Endpoint = new BadRequestHandler ( handlerContext ) ;
194+ return true ;
195+ }
196+
197+ return false ;
198+ }
199+
200+ ApiVersion TrySelectApiVersion ( HttpContext httpContext , CandidateSet candidates )
201+ {
202+ Contract . Requires ( httpContext != null ) ;
203+ Contract . Requires ( candidates != null ) ;
204+
205+ var models = new List < ApiVersionModel > ( ) ;
206+
207+ for ( var i = 0 ; i < candidates . Count ; i ++ )
208+ {
209+ ref var candidate = ref candidates [ i ] ;
210+ var model = candidate . Endpoint . Metadata ? . GetMetadata < ActionDescriptor > ( ) ? . GetApiVersionModel ( ) ;
211+
212+ if ( model != null )
213+ {
214+ models . Add ( model ) ;
215+ }
216+ }
217+
218+ return ApiVersionSelector . SelectVersion ( httpContext . Request , models . Aggregate ( ) ) ;
219+ }
220+
221+ RequestHandler ClientError ( HttpContext httpContext , CandidateSet candidateSet )
222+ {
223+ var candidates = new List < ActionDescriptor > ( candidateSet . Count ) ;
224+
225+ for ( var i = 0 ; i < candidateSet . Count ; i ++ )
226+ {
227+ ref var candidate = ref candidateSet [ i ] ;
228+ var action = candidate . Endpoint . Metadata ? . GetMetadata < ActionDescriptor > ( ) ;
229+
230+ if ( action != null )
231+ {
232+ candidates . Add ( action ) ;
233+ }
234+ }
235+
236+ var builder = new ClientErrorBuilder ( )
237+ {
238+ Options = Options ,
239+ ApiVersionReporter = ApiVersionReporter ,
240+ HttpContext = httpContext ,
241+ Candidates = candidates ,
242+ Logger = Logger ,
243+ } ;
244+
245+ return builder . Build ( ) ;
246+ }
247+ }
248+ }
0 commit comments