SDammann.WebApi.Versioning icon indicating copy to clipboard operation
SDammann.WebApi.Versioning copied to clipboard

Attribute routing

Open Sebazzz opened this issue 11 years ago • 6 comments

See issue: https://github.com/Sebazzz/SDammann.WebApi.Versioning/issues/25

Sebazzz avatar Dec 02 '14 16:12 Sebazzz

I'm unable to get the versioning working with attribute routing. The code ends with following exception:

The given key was not present in the dictionary.
System.Collections.Generic.KeyNotFoundException

at System.Collections.Generic.Dictionary`2.get_Item(TKey key) 
at System.Web.Http.Controllers.ApiControllerActionSelector.ActionSelectorCacheItem.FindActionMatchRequiredRouteAndQueryParameters(IEnumerable`1 candidatesFound) 
at System.Web.Http.Controllers.ApiControllerActionSelector.ActionSelectorCacheItem.FindMatchingActions(HttpControllerContext controllerContext, Boolean ignoreVerbs) 
at System.Web.Http.Controllers.ApiControllerActionSelector.ActionSelectorCacheItem.SelectAction(HttpControllerContext controllerContext) 
at System.Web.Http.Controllers.ApiControllerActionSelector.SelectAction(HttpControllerContext controllerContext) 
at System.Web.Http.ApiController.ExecuteAsync(HttpControllerContext controllerContext, CancellationToken cancellationToken) 
at System.Web.Http.Dispatcher.HttpControllerDispatcher.<SendAsync>d__1.MoveNext()

Please find a sample project here: WebApiVersioning.zip

pecanw avatar Oct 13 '16 07:10 pecanw

Thanks for the clear repro. I will take a look.

Quick dump of whát is happening here:

We're crashing in Web API itself. In the dictionary lookup:

            private List<CandidateActionWithParams> FindActionMatchRequiredRouteAndQueryParameters(IEnumerable<CandidateActionWithParams> candidatesFound)
            {
                List<CandidateActionWithParams> matches = new List<CandidateActionWithParams>();

                foreach (var candidate in candidatesFound)
                {
                    HttpActionDescriptor descriptor = candidate.ActionDescriptor;
                    if (IsSubset(_actionParameterNames[descriptor], candidate.CombinedParameterNames))
                    {
                        matches.Add(candidate);
                    }
                }

                return matches;
            }

Dictionary contents (1) opposed to requested key(2):

System.Linq.Enumerable.First(this.Keys)
{System.Web.Http.Controllers.ReflectedHttpActionDescriptor}
    ActionBinding: {System.Web.Http.Controllers.HttpActionBinding}
    ActionName: "Test"
    Configuration: {System.Web.Http.HttpConfiguration}
    ControllerDescriptor: {System.Web.Http.Controllers.HttpControllerDescriptor}
    MethodInfo: {System.String Test()}
    ParameterInfos: {System.Reflection.ParameterInfo[0]}
    Properties: Count = 1
    ResultConverter: {System.Web.Http.Controllers.ValueResultConverter<string>}
    ReturnType: {Name = "String" FullName = "System.String"}
    SupportedHttpMethods: Count = 1
    _actionBinding: {System.Web.Http.Controllers.HttpActionBinding}
    _actionExecutor: ThreadSafetyMode=ExecutionAndPublication, IsValueCreated=false, IsValueFaulted=false, Value=null
    _actionName: "Test"
    _attributeCache: {object[2]}
    _configuration: {System.Web.Http.HttpConfiguration}
    _controllerDescriptor: {System.Web.Http.Controllers.HttpControllerDescriptor}
    _converter: {System.Web.Http.Controllers.ValueResultConverter<string>}
    _declaredOnlyAttributeCache: {object[2]}
    _filterGrouping: null
    _filterPipeline: ThreadSafetyMode=ExecutionAndPublication, IsValueCreated=false, IsValueFaulted=false, Value=null
    _filterPipelineForGrouping: null
    _methodInfo: {System.String Test()}
    _parameterInfos: {System.Reflection.ParameterInfo[0]}
    _parameters: ThreadSafetyMode=ExecutionAndPublication, IsValueCreated=true, IsValueFaulted=false, Value={System.Collections.ObjectModel.Collection<System.Web.Http.Controllers.HttpParameterDescriptor>}
    _properties: Count = 1
    _returnType: {Name = "String" FullName = "System.String"}
    _supportedHttpMethods (System.Web.Http.Controllers.HttpActionDescriptor): Count = 0
    _supportedHttpMethods: Count = 1
key
{System.Web.Http.Controllers.ReflectedHttpActionDescriptor}
    ActionBinding: {System.Web.Http.Controllers.HttpActionBinding}
    ActionName: "Test"
    Configuration: {System.Web.Http.HttpConfiguration}
    ControllerDescriptor: {System.Web.Http.Controllers.HttpControllerDescriptor}
    MethodInfo: {System.String Test()}
    ParameterInfos: {System.Reflection.ParameterInfo[0]}
    Properties: Count = 1
    ResultConverter: {System.Web.Http.Controllers.ValueResultConverter<string>}
    ReturnType: {Name = "String" FullName = "System.String"}
    SupportedHttpMethods: Count = 1
    _actionBinding: {System.Web.Http.Controllers.HttpActionBinding}
    _actionExecutor: ThreadSafetyMode=ExecutionAndPublication, IsValueCreated=false, IsValueFaulted=false, Value=null
    _actionName: "Test"
    _attributeCache: {object[2]}
    _configuration: {System.Web.Http.HttpConfiguration}
    _controllerDescriptor: {System.Web.Http.Controllers.HttpControllerDescriptor}
    _converter: {System.Web.Http.Controllers.ValueResultConverter<string>}
    _declaredOnlyAttributeCache: {object[2]}
    _filterGrouping: null
    _filterPipeline: ThreadSafetyMode=ExecutionAndPublication, IsValueCreated=false, IsValueFaulted=false, Value=null
    _filterPipelineForGrouping: null
    _methodInfo: {System.String Test()}
    _parameterInfos: {System.Reflection.ParameterInfo[0]}
    _parameters: ThreadSafetyMode=ExecutionAndPublication, IsValueCreated=true, IsValueFaulted=false, Value={System.Collections.ObjectModel.Collection<System.Web.Http.Controllers.HttpParameterDescriptor>}
    _properties: Count = 1
    _returnType: {Name = "String" FullName = "System.String"}
    _supportedHttpMethods (System.Web.Http.Controllers.HttpActionDescriptor): Count = 0
    _supportedHttpMethods: Count = 1

Sebazzz avatar Oct 13 '16 17:10 Sebazzz

Equality is determined based on the method info.

Input (1) vs existing in dictionary (2):

((System.Web.Http.Controllers.ReflectedHttpActionDescriptor)(object)(key))._methodInfo
{System.String Test()}
    Attributes: Public | HideBySig
    BindingFlags: Instance | Public
    CallingConvention: Standard | HasThis
    ContainsGenericParameters: false
    CustomAttributes: Count = 2
    DeclaringType: {Name = "TestController" FullName = "WebApiVersioning.Controllers.V1.TestController"}
    FullName: "WebApiVersioning.Controllers.V1.TestController.Test()"
    InvocationFlags: INVOCATION_FLAGS_INITIALIZED | INVOCATION_FLAGS_NEED_SECURITY
    IsAbstract: false
    IsAssembly: false
    IsConstructor: false
    IsDynamicallyInvokable: true
    IsFamily: false
    IsFamilyAndAssembly: false
    IsFamilyOrAssembly: false
    IsFinal: false
    IsGenericMethod: false
    IsGenericMethodDefinition: false
    IsHideBySig: true
    IsOverloaded: false
    IsPrivate: false
    IsPublic: true
    IsSecurityCritical: true
    IsSecuritySafeCritical: false
    IsSecurityTransparent: false
    IsSpecialName: false
    IsStatic: false
    IsVirtual: false
    MemberType: Method
    MetadataToken: 100663442
    MethodHandle: {System.RuntimeMethodHandle}
    MethodImplementationFlags: IL
    Module: {WebApiVersioning.dll}
    Name: "Test"
    ReflectedType: {Name = "TestController" FullName = "WebApiVersioning.Controllers.V1.TestController"}
    ReflectedTypeInternal: {Name = "TestController" FullName = "WebApiVersioning.Controllers.V1.TestController"}
    RemotingCache: {System.Runtime.Remoting.Metadata.RemotingMethodCachedData}
    ReturnParameter: {System.String }
    ReturnType: {Name = "String" FullName = "System.String"}
    ReturnTypeCustomAttributes: {System.String }
    Signature: {System.Signature}
    System.IRuntimeMethodInfo.Value: {System.RuntimeMethodHandleInternal}
    System.Runtime.InteropServices._MethodBase.IsAbstract: false
    System.Runtime.InteropServices._MethodBase.IsAssembly: false
    System.Runtime.InteropServices._MethodBase.IsConstructor: false
    System.Runtime.InteropServices._MethodBase.IsFamily: false
    System.Runtime.InteropServices._MethodBase.IsFamilyAndAssembly: false
    System.Runtime.InteropServices._MethodBase.IsFamilyOrAssembly: false
    System.Runtime.InteropServices._MethodBase.IsFinal: false
    System.Runtime.InteropServices._MethodBase.IsHideBySig: true
    System.Runtime.InteropServices._MethodBase.IsPrivate: false
    System.Runtime.InteropServices._MethodBase.IsPublic: true
    System.Runtime.InteropServices._MethodBase.IsSpecialName: false
    System.Runtime.InteropServices._MethodBase.IsStatic: false
    System.Runtime.InteropServices._MethodBase.IsVirtual: false
    m_bindingFlags: Instance | Public
    m_cachedData: {System.Runtime.Remoting.Metadata.RemotingMethodCachedData}
    m_declaringType: {Name = "TestController" FullName = "WebApiVersioning.Controllers.V1.TestController"}
    m_handle: {140714995937072}
    m_invocationFlags: INVOCATION_FLAGS_INITIALIZED | INVOCATION_FLAGS_NEED_SECURITY
    m_keepalive: null
    m_methodAttributes: Public | HideBySig
    m_name: "Test"
    m_parameters: {System.Reflection.ParameterInfo[0]}
    m_reflectedTypeCache: {System.RuntimeType.RuntimeTypeCache}
    m_returnParameter: {System.String }
    m_signature: {System.Signature}
    m_toString: "System.String Test()"
((System.Web.Http.Controllers.ReflectedHttpActionDescriptor)(object)(System.Linq.Enumerable.First(this.Keys)))._methodInfo
{System.String Test()}
    Attributes: Public | HideBySig
    BindingFlags: Instance | Public
    CallingConvention: Standard | HasThis
    ContainsGenericParameters: false
    CustomAttributes: Count = 2
    DeclaringType: {Name = "TestController" FullName = "WebApiVersioning.Controllers.V1.TestController"}
    FullName: "WebApiVersioning.Controllers.V1.TestController.Test()"
    InvocationFlags: INVOCATION_FLAGS_INITIALIZED | INVOCATION_FLAGS_NEED_SECURITY
    IsAbstract: false
    IsAssembly: false
    IsConstructor: false
    IsDynamicallyInvokable: true
    IsFamily: false
    IsFamilyAndAssembly: false
    IsFamilyOrAssembly: false
    IsFinal: false
    IsGenericMethod: false
    IsGenericMethodDefinition: false
    IsHideBySig: true
    IsOverloaded: false
    IsPrivate: false
    IsPublic: true
    IsSecurityCritical: true
    IsSecuritySafeCritical: false
    IsSecurityTransparent: false
    IsSpecialName: false
    IsStatic: false
    IsVirtual: false
    MemberType: Method
    MetadataToken: 100663442
    MethodHandle: {System.RuntimeMethodHandle}
    MethodImplementationFlags: IL
    Module: {WebApiVersioning.dll}
    Name: "Test"
    ReflectedType: {Name = "TestController" FullName = "WebApiVersioning.Controllers.V1.TestController"}
    ReflectedTypeInternal: {Name = "TestController" FullName = "WebApiVersioning.Controllers.V1.TestController"}
    RemotingCache: {System.Runtime.Remoting.Metadata.RemotingMethodCachedData}
    ReturnParameter: {System.String }
    ReturnType: {Name = "String" FullName = "System.String"}
    ReturnTypeCustomAttributes: {System.String }
    Signature: {System.Signature}
    System.IRuntimeMethodInfo.Value: {System.RuntimeMethodHandleInternal}
    System.Runtime.InteropServices._MethodBase.IsAbstract: false
    System.Runtime.InteropServices._MethodBase.IsAssembly: false
    System.Runtime.InteropServices._MethodBase.IsConstructor: false
    System.Runtime.InteropServices._MethodBase.IsFamily: false
    System.Runtime.InteropServices._MethodBase.IsFamilyAndAssembly: false
    System.Runtime.InteropServices._MethodBase.IsFamilyOrAssembly: false
    System.Runtime.InteropServices._MethodBase.IsFinal: false
    System.Runtime.InteropServices._MethodBase.IsHideBySig: true
    System.Runtime.InteropServices._MethodBase.IsPrivate: false
    System.Runtime.InteropServices._MethodBase.IsPublic: true
    System.Runtime.InteropServices._MethodBase.IsSpecialName: false
    System.Runtime.InteropServices._MethodBase.IsStatic: false
    System.Runtime.InteropServices._MethodBase.IsVirtual: false
    m_bindingFlags: Instance | Public
    m_cachedData: {System.Runtime.Remoting.Metadata.RemotingMethodCachedData}
    m_declaringType: {Name = "TestController" FullName = "WebApiVersioning.Controllers.V1.TestController"}
    m_handle: {140714995937072}
    m_invocationFlags: INVOCATION_FLAGS_INITIALIZED | INVOCATION_FLAGS_NEED_SECURITY
    m_keepalive: null
    m_methodAttributes: Public | HideBySig
    m_name: "Test"
    m_parameters: {System.Reflection.ParameterInfo[0]}
    m_reflectedTypeCache: {System.RuntimeType.RuntimeTypeCache}
    m_returnParameter: {System.String }
    m_signature: {System.Signature}
    m_toString: "System.String Test()"

Since both are of type RuntimeMethodInfo, we just follow to reference source and find that apparently there is checked for reference equality.

The question is now: Why does reference equality not match?

Sebazzz avatar Oct 13 '16 17:10 Sebazzz

Never mind -- previous post was based on a wrong assumption.

Because both routes match, Web API tries to match both actions in the dictionary.

I suggest you either:

  • Don't use the {version} token at all in your direct route, so bypass this lib and route directly, e.g: api/v1/test and api/v2/test.
  • Try to use the 3.x beta version of this library

Sebazzz avatar Oct 13 '16 17:10 Sebazzz

Thank you for the suggestion. However I'm not able to make it run. If I don't use your library at all, the standard WebAPI is not able to deal with two controllers of the same class name in different namespaces. If I use the library (just without the {version} token) I get 404 - The API 'Test' doesn't exist".

As for the 3.x beta - I have already tried, but I was not able to make it running within the project I want to use it in. I can do a feew tests in the testing app and then try to integrate it again.

pecanw avatar Oct 19 '16 14:10 pecanw

That's correct. ASP.NET Web API is unable to handle controllers with the same name, resulting in 404. You need to prefix your controllers :(

Sebazzz avatar Oct 19 '16 15:10 Sebazzz