Skip to content

Commit 144ed88

Browse files
Return 400 instead of 404 for action that can match in at least one API version. Fixes #26 (#30)
1 parent 239ec61 commit 144ed88

File tree

5 files changed

+111
-7
lines changed

5 files changed

+111
-7
lines changed

src/Microsoft.AspNet.WebApi.Versioning/Controllers/ActionSelectorCacheItem.cs

+25-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
namespace Microsoft.Web.Http.Controllers
22
{
3+
using Dispatcher;
34
using Routing;
45
using System;
56
using System.Collections.Generic;
@@ -202,18 +203,34 @@ private HttpResponseMessage CreateSelectionError( HttpControllerContext controll
202203
{
203204
Contract.Ensures( Contract.Result<HttpResponseMessage>() != null );
204205

206+
if ( !controllerContext.ControllerDescriptor.GetApiVersionModel().IsApiVersionNeutral )
207+
{
208+
return CreateBadRequestResponse( controllerContext );
209+
}
210+
205211
var actionsFoundByParams = FindMatchingActions( controllerContext, ignoreVerbs: true );
206212

207213
if ( actionsFoundByParams.Count > 0 )
208214
{
209-
return Create405Response( controllerContext, actionsFoundByParams );
215+
return CreateMethodNotAllowedResponse( controllerContext, actionsFoundByParams );
210216
}
211217

212218
return CreateActionNotFoundResponse( controllerContext );
213219
}
214220

215221
[SuppressMessage( "Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Caller is responsible for disposing of response instance." )]
216-
private static HttpResponseMessage Create405Response( HttpControllerContext controllerContext, IEnumerable<HttpActionDescriptor> allowedCandidates )
222+
private static HttpResponseMessage CreateBadRequestResponse( HttpControllerContext controllerContext )
223+
{
224+
Contract.Requires( controllerContext != null );
225+
Contract.Ensures( Contract.Result<HttpResponseMessage>() != null );
226+
227+
var request = controllerContext.Request;
228+
var exceptionFactory = new HttpResponseExceptionFactory( request );
229+
return exceptionFactory.CreateBadRequestResponseForUnsupportedApiVersion( request.GetRequestedApiVersion() );
230+
}
231+
232+
[SuppressMessage( "Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Caller is responsible for disposing of response instance." )]
233+
private static HttpResponseMessage CreateMethodNotAllowedResponse( HttpControllerContext controllerContext, IEnumerable<HttpActionDescriptor> allowedCandidates )
217234
{
218235
Contract.Requires( controllerContext != null );
219236
Contract.Requires( allowedCandidates != null );
@@ -253,6 +270,11 @@ private HttpResponseMessage CreateActionNotFoundResponse( HttpControllerContext
253270
Contract.Requires( controllerContext != null );
254271
Contract.Ensures( Contract.Result<HttpResponseMessage>() != null );
255272

273+
if ( !controllerContext.ControllerDescriptor.GetApiVersionModel().IsApiVersionNeutral )
274+
{
275+
return CreateBadRequestResponse( controllerContext );
276+
}
277+
256278
var message = SR.ResourceNotFound.FormatDefault( controllerContext.Request.RequestUri );
257279
var messageDetail = SR.ApiControllerActionSelector_ActionNameNotFound.FormatDefault( controllerDescriptor.ControllerName, actionName );
258280
return controllerContext.Request.CreateErrorResponse( NotFound, message, messageDetail );
@@ -515,7 +537,7 @@ private static void FindActionsForVerbWorker( HttpMethod verb, CandidateAction[]
515537
}
516538
}
517539

518-
private static string CreateAmbiguousMatchList( IEnumerable<CandidateHttpActionDescriptor> ambiguousCandidates )
540+
internal static string CreateAmbiguousMatchList( IEnumerable<HttpActionDescriptor> ambiguousCandidates )
519541
{
520542
Contract.Requires( ambiguousCandidates != null );
521543
Contract.Ensures( Contract.Result<string>() != null );
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
namespace Microsoft.Web.Http.Controllers
2+
{
3+
using System.Collections;
4+
using System.Collections.Generic;
5+
using System.Diagnostics.Contracts;
6+
using System.Linq;
7+
using System.Web.Http.Controllers;
8+
9+
/// <content>
10+
/// Provides additional content for the <see cref="ApiVersionActionSelector"/> class.
11+
/// </content>
12+
public partial class ApiVersionActionSelector
13+
{
14+
private sealed class AggregatedActionMapping : ILookup<string, HttpActionDescriptor>
15+
{
16+
private readonly IReadOnlyList<ILookup<string, HttpActionDescriptor>> actionMappings;
17+
18+
internal AggregatedActionMapping( IReadOnlyList<ILookup<string, HttpActionDescriptor>> actionMappings )
19+
{
20+
Contract.Requires( actionMappings != null );
21+
this.actionMappings = actionMappings;
22+
}
23+
24+
public IEnumerable<HttpActionDescriptor> this[string key] => actionMappings.Where( am => am.Contains( key ) ).SelectMany( am => am[key] );
25+
26+
public int Count => actionMappings[0].Count;
27+
28+
public bool Contains( string key ) => actionMappings.Any( am => am.Contains( key ) );
29+
30+
public IEnumerator<IGrouping<string, HttpActionDescriptor>> GetEnumerator() => actionMappings[0].GetEnumerator();
31+
32+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
33+
}
34+
}
35+
}

src/Microsoft.AspNet.WebApi.Versioning/Controllers/ApiVersionActionSelector.cs

+42-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
namespace Microsoft.Web.Http.Controllers
22
{
3+
using Dispatcher;
4+
using System;
35
using System.Collections.Generic;
46
using System.Diagnostics.CodeAnalysis;
57
using System.Diagnostics.Contracts;
@@ -63,7 +65,14 @@ protected virtual HttpActionDescriptor SelectActionVersion( HttpControllerContex
6365
Arg.NotNull( controllerContext, nameof( controllerContext ) );
6466
Arg.NotNull( candidateActions, nameof( candidateActions ) );
6567

66-
var requestedVersion = controllerContext.Request.GetRequestedApiVersion();
68+
if ( candidateActions.Count == 0 )
69+
{
70+
return null;
71+
}
72+
73+
var request = controllerContext.Request;
74+
var requestedVersion = request.GetRequestedApiVersion();
75+
var exceptionFactory = new HttpResponseExceptionFactory( request );
6776

6877
if ( candidateActions.Count == 1 )
6978
{
@@ -93,14 +102,31 @@ protected virtual HttpActionDescriptor SelectActionVersion( HttpControllerContex
93102
switch ( explicitMatches.Count )
94103
{
95104
case 0:
96-
return implicitMatches.Count == 1 ? implicitMatches[0] : null;
105+
switch ( implicitMatches.Count )
106+
{
107+
case 0:
108+
break;
109+
case 1:
110+
return implicitMatches[0];
111+
default:
112+
throw CreateAmbiguousActionException( implicitMatches );
113+
}
114+
break;
97115
case 1:
98116
return explicitMatches[0];
117+
default:
118+
throw CreateAmbiguousActionException( explicitMatches );
99119
}
100120

101121
return null;
102122
}
103123

124+
private Exception CreateAmbiguousActionException( IEnumerable<HttpActionDescriptor> matches )
125+
{
126+
var ambiguityList = ActionSelectorCacheItem.CreateAmbiguousMatchList( matches );
127+
return new InvalidOperationException( SR.ApiControllerActionSelector_AmbiguousMatch.FormatDefault( ambiguityList ) );
128+
}
129+
104130
/// <summary>
105131
/// Selects and returns the action descriptor to invoke given the provided controller context.
106132
/// </summary>
@@ -129,7 +155,19 @@ public virtual ILookup<string, HttpActionDescriptor> GetActionMapping( HttpContr
129155
Contract.Ensures( Contract.Result<ILookup<string, HttpActionDescriptor>>() != null );
130156

131157
var internalSelector = GetInternalSelector( controllerDescriptor );
132-
return internalSelector.GetActionMapping();
158+
var actionMappings = new List<ILookup<string, HttpActionDescriptor>>();
159+
160+
actionMappings.Add( internalSelector.GetActionMapping() );
161+
162+
foreach ( var relatedControllerDescriptor in controllerDescriptor.GetRelatedCandidates() )
163+
{
164+
if ( relatedControllerDescriptor != controllerDescriptor )
165+
{
166+
actionMappings.Add( GetInternalSelector( relatedControllerDescriptor ).GetActionMapping() );
167+
}
168+
}
169+
170+
return actionMappings.Count == 1 ? actionMappings[0] : new AggregatedActionMapping( actionMappings );
133171
}
134172
}
135-
}
173+
}

src/Microsoft.AspNet.WebApi.Versioning/Dispatcher/ConventionRouteControllerSelector.cs

+1
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ private static HttpControllerDescriptor GetVersionedController( ApiVersionContro
139139
controller.SetApiVersionModel( aggregator.AllVersions );
140140
}
141141

142+
controller.SetRelatedCandidates( candidates );
142143
return controller;
143144
}
144145

src/Microsoft.AspNet.WebApi.Versioning/System.Web.Http/HttpControllerDescriptorExtensions.cs

+8
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Controllers;
55
using Diagnostics.CodeAnalysis;
66
using Diagnostics.Contracts;
7+
using Linq;
78
using Microsoft;
89
using Microsoft.Web.Http;
910
using Microsoft.Web.Http.Versioning;
@@ -16,6 +17,7 @@ public static class HttpControllerDescriptorExtensions
1617
private const string AttributeRoutedPropertyKey = "MS_IsAttributeRouted";
1718
private const string ApiVersionInfoKey = "MS_ApiVersionInfo";
1819
private const string ConventionsApiVersionInfoKey = "MS_ConventionsApiVersionInfo";
20+
private const string RelatedControllerCandidatesKey = "MS_RelatedControllerCandidates";
1921

2022
internal static bool IsAttributeRouted( this HttpControllerDescriptor controllerDescriptor )
2123
{
@@ -105,6 +107,12 @@ internal static void SetApiVersionModel( this HttpControllerDescriptor controlle
105107
internal static void SetConventionsApiVersionModel( this HttpControllerDescriptor controllerDescriptor, ApiVersionModel model ) =>
106108
controllerDescriptor.Properties.AddOrUpdate( ConventionsApiVersionInfoKey, model, ( key, currentModel ) => ( (ApiVersionModel) currentModel ).Aggregate( model ) );
107109

110+
internal static IEnumerable<HttpControllerDescriptor> GetRelatedCandidates( this HttpControllerDescriptor controllerDescriptor ) =>
111+
(IEnumerable<HttpControllerDescriptor>) controllerDescriptor.Properties.GetOrAdd( RelatedControllerCandidatesKey, key => Enumerable.Empty<HttpControllerDescriptor>() );
112+
113+
internal static void SetRelatedCandidates( this HttpControllerDescriptor controllerDescriptor, IEnumerable<HttpControllerDescriptor> value ) =>
114+
controllerDescriptor.Properties.AddOrUpdate( RelatedControllerCandidatesKey, value, ( key, oldValue ) => value );
115+
108116
/// <summary>
109117
/// Gets a value indicating whether the controller is API version neutral.
110118
/// </summary>

0 commit comments

Comments
 (0)