Aspnet-api-versioning: Namespace versioning not working

Created on 12 Sep 2017  路  4Comments  路  Source: microsoft/aspnet-api-versioning

Hi,
I tried to implement API versioning into my project, but I cannot get it to work.

If I call my method like this:
http://localhost:7291/api/Saved/GetNumberOfSavedWorkoutsForUser?api-version=2.0
I get this error:
Multiple types were found that match the controller named 'Saved'. This can happen if the route that services this request ('api/{controller}/{action}/{id}') found multiple controllers defined with the same name but differing namespaces, which is not supported.

On the other hand, if I call it like this:
http://localhost:7291/v2/Saved/GetNumberOfSavedWorkoutsForUser
Then I get error (404):
The resource you are looking for has been removed, had its name changed, or is temporarily unavailable.

I am not sure what I am doing wrong. Here is my code:

Startup.cs

public void Configuration(IAppBuilder app)
    {
        var configuration = new HttpConfiguration();
        var httpServer = new HttpServer(configuration);


        app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);

        // reporting api versions will return the headers "api-supported-versions" and "api-deprecated-versions"
        configuration.AddApiVersioning(o => 
        {
            o.AssumeDefaultVersionWhenUnspecified = true;
            o.ReportApiVersions = true; 
            o.DefaultApiVersion = ApiVersion.Default;
        });


        configuration.Routes.MapHttpRoute(
            "VersionedUrl",
            "v{apiVersion}/{controller}/{action}/{id}",
            defaults: null,
            constraints: new { apiVersion = new ApiVersionRouteConstraint() });

        configuration.Routes.MapHttpRoute(
            "VersionedQueryString",
            "api/{controller}/{action}/{id}",
            defaults: null);


        app.UseWebApi(httpServer);

        ConfigureAuth(app);
    }

Saved Controller (v1)

namespace Master.Infrastructure.Api.Controllers
{

    [Authorize]
    [RoutePrefix("api/Saved")]
    [ApiVersion("1.0")]
    public class SavedController : ApiController
    {

        private readonly IUserService _userService;

        public SavedController(IUserService userService)
        {
            _userService = userService;

        }


        [HttpGet]
        [ActionName("GetNumberOfSavedWorkouts")]
        public async Task<NumberOfSavedWorkouts> GetNumberOfSavedWorkouts()
        {
            var numOfSavedWorkouts = new NumberOfSavedWorkouts
            {
                CurrentNumberOfSavedWorkouts =
                    await _userService.GetNumberOfSavedWorkoutsForUserAsync(User.Identity.GetUserId())
            };

            return numOfSavedWorkouts;
        }

    }
}

Saved Controller (v2)

namespace Master.Infrastructure.Api.v2.Controllers
{
    [Authorize]
    [ApiVersion("2.0")]
    [RoutePrefix("v{version:apiVersion}/Saved")]
    public class SavedController : ApiController
    {

        private readonly ISavedWorkoutService _savedWorkoutService;


        public SavedController(ISavedWorkoutService savedWorkoutService)
        {       
            _savedWorkoutService = savedWorkoutService;
        }


        [ActionName("GetNumberOfSavedWorkoutsForUser")]
        public async Task<IHttpActionResult> GetNumberOfSavedWorkoutsForUser()
        {
            var cnt = await _savedWorkoutService.CountNumberOfSavedWorkoutsForUser(User.Identity.GetUserId());

            return Ok(cnt);
        }
    }
}

Could you please help me out ?

asp.net web api question

Most helpful comment

The AddApiVersioning method replaces the following services:

c# services.Replace( typeof( IHttpControllerSelector ), new ApiVersionControllerSelector( configuration, options ) ); services.Replace( typeof( IHttpActionSelector ), new ApiVersionActionSelector() );

If you were replacing them in your DI setup, it would cause an issue and you'd have to register them differently than using AddApiVersioning. It seems you are not doing that, so it's not a problem.

I didn't really need to see your whole DI setup, but now that you've shared it, I notice that you are using Ninject extensions that are dynamically registering IHttpModule instances. That means you are also using the legacy IIS model.

It would appear you took the code verbatim from the examples. All of the examples are based on the OWIN model, primarily because the examples between Web API and Core then look similar. While the OWIN support in Web API does have integration with IIS, it doesn't rely on other IHttpModule instances. Have you set a breakpoint in the Configuration(IAppBuilder builder) method? If I had to guess, it's not even being hit. This would example a few other things as well. You likely need to move this code into your HttpApplication class for the Global.asax and ditch the Startup.cs or wherever this setup exists.

Based on your code and configuration I would expect:

| Request URL | Status Code |
| -------------- | ------------- |
| /api/saved/getNumberOfSavedWorkouts | 200 |
| /api/saved/getNumberOfSavedWorkouts?api-version=1.0 | 200 |
| /api/saved/getNumberOfSavedWorkouts?api-version=2.0 | 400 |
| /api/saved/getNumberOfSavedWorkoutsForUser | 400 |
| /api/saved/getNumberOfSavedWorkouts?api-version=1.0 | 400 |
| /api/saved/getNumberOfSavedWorkouts?api-version=2.0 | 200 |

This is because getNumberOfSavedWorkouts only supports 1.0 and getNumberOfSavedWorkoutsForUser only supports 2.0. However, since you allow implicit API versioning and the default API version is 1.0, getNumberOfSavedWorkouts is also accessible without specifying any API version.

I've attached a simple example application based on your setup that demonstrates things working. I hope that helps.
WorkoutRepro.zip

All 4 comments

There are a number of things happening here. Let me try to address each one:

  • You don't need to set the options o.DefaultApiVersion = ApiVersion.Default because that's already the default (unless you _really_ want to be explicit)
  • I strongly recommend that you do not use both convention-based and attribute-based routing in an application. I only show that in examples so that I don't have to have numerous samples. I may consider splitting up the example or provide more detailed comments indicating that you should choose _Option A_ or _Option B_.
  • I also don't recommend supporting multiple API versioning semantics. Again, I only show that in examples as a possibility since there are numerous, supported flavors. Choose one style and stick to it. The default and recommended method is using the query string. Unless you _really_ want to the support URL segments too, I'd remove the mapping.

    • It's worth noting that since you are allowing implicit versioning, there are limitations with the URL segment approach because you cannot have a default or empty segment inferred as an API version.

  • You are decorating your controllers with attribute-routing metadata (ex: RoutePrefix), but you have not configured or enabled attribute routing via configuration.MapAttributeRoutes(). If you only intend on using convention-based routes, then you should remove these attributes as they only add confusion.
  • Your defined route templates do not match your controllers. This partially explains why things are not matching. The template shows api/{controller}/{action}/{id}, but {id} is not optional and your controllers do not accept an id parameter. You probably want to make the route parameter optional.

The first error message is throwing me off a little. It almost seems like the API versioning services are not wired up somehow. It seems I'm not seeing the entire configuration. From the looks of it, some type of dependency injection is configured, but is not shown. I don't really need to see the DI mapping stuff, but are you modifying the IHttpControllerSelector or IHttpActionSelector services? These services generally do not need to be changed. The are replaced by AddApiVersioning. If you are adding your own custom, additional behaviors, then you should decorate ApiVersionControllerSelector and ApiVersionActionSelector in your container registration respectively.

Firstly thank you very much for your detailed answer, it is much appreciated.

I haven't completely understood what you mean

you should decorate ApiVersionControllerSelector and ApiVersionActionSelector in your container registration respectively.

Could you please explain further / give me some example ?

Also I wanted to point out this is only happening when calling controller placed in v2 namespace, if I call:
http://localhost:7291/api/Saved/GetNumberOfSavedWorkoutsForUser

Everything is fine.

I am using Ninject for DI, but as far as I can see I am not modifying IHttpControllerSelector / IHttpActionSelector.

Nevertheless, here is my DI code:

[assembly: WebActivatorEx.PreApplicationStartMethod(typeof(Master.Infrastructure.Api.App_Start.NinjectWebCommon), "Start")]
[assembly: WebActivatorEx.ApplicationShutdownMethodAttribute(typeof(Master.Infrastructure.Api.App_Start.NinjectWebCommon), "Stop")]

namespace Master.Infrastructure.Api.App_Start
{
    using Microsoft.Azure.NotificationHubs;
    using Microsoft.Web.Infrastructure.DynamicModuleHelper;
    using Ninject;
    using Ninject.Web.Common;
    using Services.PushNotificationService.Implementations;
    using Services.PushNotificationService.Interfaces;
    using Services.WorkoutPlanService.Implementations;
    using Services.WorkoutPlanService.Interfaces;
    using System;
    using System.Web;
    public static class NinjectWebCommon
    {
        private static readonly Bootstrapper bootstrapper = new Bootstrapper();

        /// <summary>
        /// Starts the application
        /// </summary>
        public static void Start()
        {
            DynamicModuleUtility.RegisterModule(typeof(OnePerRequestHttpModule));
            DynamicModuleUtility.RegisterModule(typeof(NinjectHttpModule));
            bootstrapper.Initialize(CreateKernel);
        }

        /// <summary>
        /// Stops the application.
        /// </summary>
        public static void Stop()
        {
            bootstrapper.ShutDown();
        }

        /// <summary>
        /// Creates the kernel that will manage your application.
        /// </summary>
        /// <returns>The created kernel.</returns>
        private static IKernel CreateKernel()
        {
            var kernel = new StandardKernel();
            try
            {
                System.Web.Http.GlobalConfiguration.Configuration.DependencyResolver = new Ninject.WebApi.DependencyResolver.NinjectDependencyResolver(kernel);
                kernel.Bind<Func<IKernel>>().ToMethod(ctx => () => new Bootstrapper().Kernel);
                kernel.Bind<IHttpModule>().To<HttpApplicationInitializationHttpModule>();

                RegisterServices(kernel);
                return kernel;
            }
            catch
            {
                kernel.Dispose();
                throw;
            }
        }
        /// <summary>
        /// Load your modules or register your services here!
        /// </summary>
        /// <param name="kernel">The kernel.</param>
        private static void RegisterServices(IKernel kernel)
        {

            kernel.Bind<MasterDatabaseContext>().ToSelf().InRequestScope();
            kernel.Bind<IDatabaseContextFactory>().ToMethod(c => DatabaseContextFactory.Instance).InSingletonScope();
            kernel.Bind<IUnitOfWork>().To<UnitOfWork>().InRequestScope();

            kernel.Bind(typeof(IMasterRepository<,>)).To(typeof(MasterRepository<,>));

            kernel.Bind<ISavedWorkoutsRepository>().To<SavedWorkoutRepository>();
            kernel.Bind<IUserRepository>().To<UserRepository>();

            kernel.Bind<IUserService>().To<UserService>();
            kernel.Bind<IBlogService>().To<BlogService>();

            //bunch of other similar code as abowe mapping custom IServices to Services

        }
    }
}

The AddApiVersioning method replaces the following services:

c# services.Replace( typeof( IHttpControllerSelector ), new ApiVersionControllerSelector( configuration, options ) ); services.Replace( typeof( IHttpActionSelector ), new ApiVersionActionSelector() );

If you were replacing them in your DI setup, it would cause an issue and you'd have to register them differently than using AddApiVersioning. It seems you are not doing that, so it's not a problem.

I didn't really need to see your whole DI setup, but now that you've shared it, I notice that you are using Ninject extensions that are dynamically registering IHttpModule instances. That means you are also using the legacy IIS model.

It would appear you took the code verbatim from the examples. All of the examples are based on the OWIN model, primarily because the examples between Web API and Core then look similar. While the OWIN support in Web API does have integration with IIS, it doesn't rely on other IHttpModule instances. Have you set a breakpoint in the Configuration(IAppBuilder builder) method? If I had to guess, it's not even being hit. This would example a few other things as well. You likely need to move this code into your HttpApplication class for the Global.asax and ditch the Startup.cs or wherever this setup exists.

Based on your code and configuration I would expect:

| Request URL | Status Code |
| -------------- | ------------- |
| /api/saved/getNumberOfSavedWorkouts | 200 |
| /api/saved/getNumberOfSavedWorkouts?api-version=1.0 | 200 |
| /api/saved/getNumberOfSavedWorkouts?api-version=2.0 | 400 |
| /api/saved/getNumberOfSavedWorkoutsForUser | 400 |
| /api/saved/getNumberOfSavedWorkouts?api-version=1.0 | 400 |
| /api/saved/getNumberOfSavedWorkouts?api-version=2.0 | 200 |

This is because getNumberOfSavedWorkouts only supports 1.0 and getNumberOfSavedWorkoutsForUser only supports 2.0. However, since you allow implicit API versioning and the default API version is 1.0, getNumberOfSavedWorkouts is also accessible without specifying any API version.

I've attached a simple example application based on your setup that demonstrates things working. I hope that helps.
WorkoutRepro.zip

Again, thank you very much, you have helped me a lot and I was able to fix everything following your samples!

Was this page helpful?
0 / 5 - 0 ratings