Skip to content

Problem with non-API routes. Any way to filter out controllers? #399

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
hugoqribeiro opened this issue Nov 12, 2018 · 13 comments
Closed

Problem with non-API routes. Any way to filter out controllers? #399

hugoqribeiro opened this issue Nov 12, 2018 · 13 comments

Comments

@hugoqribeiro
Copy link

Hello.

I have a host that runs a set of normal MVC routes (with views) and MVC API routes. These are segmented this way:

/{route} -> views
/api/v{version:apiVersion}/{route} -> API

If I have a route like this:

[GET] /myroute

And for some reason, I call it with a POST I get the following error, instead of the expected 404:

The HTTP resource that matches the request URI 'http://localhost:5000/myroute' does not support HTTP method 'POST'.

The problem here is that the "versioning middleware" is kicking in and I haven't found a way to tell it to completly ignore the MVC controllers (or a specific one for that matter).

Is there any way to achieve this?

Thanks in advance.

@commonsensesoftware
Copy link
Collaborator

That's correct. API Versioning doesn't make any concessions about controller types. In ASP.NET Core, a controller is a controller. API versioning does not allow no API version. The closest you can get is assuming a default (or initial) version or be API version-neutral, which means you accept every API version.

There are at least 3 ways you can deal with this:

  1. Use MapWhen to only apply API versioning to the API routes
  2. If you're using ASP.NET Core 2.1+ and API Versioning Beta 2+, you can use the ApiControllerAttribute to apply API Behaviors.
  3. Mark MVC controllers as API version-neutral
    a. You can do this imperatively per controller
    b. You can do this with conventions in your setup
    c. You can use a base class (shown below)
[ApiVersionNeutral]
public abstract class UIController : Controller
{
    protected UIController() { }
}

public class HomeController : UIController
{
}

@hugoqribeiro
Copy link
Author

Thank you for your answer.

I am using ASP.NET Core 2.0 (can't upgrade to 2.1 for now) and API Version 2.3.0 so I suppose the only way I can deal with the problem is with MapWhen.

But I'm not sure how to do it since I'm using AddApiVersioning like this:

services.AddApiVersioning(
            (options) =>
            {
                options.DefaultApiVersion = new ApiVersion(Metadata.ApiVersions.DefaultVersion.Major, Metadata.ApiVersions.DefaultVersion.Minor);
                options.AssumeDefaultVersionWhenUnspecified = true;
                options.ReportApiVersions = true;
            });

How can I manipulate the registration of the ApiVersioning middleware?

@commonsensesoftware
Copy link
Collaborator

API versioning doesn't add middleware, it (currently) adds a router. Starting in 3.0, it will technically add middleware, but that is only to inject the API versioning feature, which has little-to-no bearing on the execution pipeline. If you want customize the routes, you'll need to remap it yourself as is shown in #194.

The simplest way to achieve your goal is probably to apply a convention to all your UI controllers. This will work if all your UI controllers inherit from Controller and all your APIs inherit from ControllerBase.

var mvcBuilder = services.AddMvc();
services.AddApiVersioning( options =>
{
  options.AssumeDefaultVersionWhenUnspecified = true;
  options.ReportApiVersions = true;

  var controllerType = typeof( Controller );

  foreach ( var parts in mvcBuilder.PartManager.ApplicationParts )
  {
    foreach ( var part in parts.OfType<IApplicationPartTypeProvider>() )
    {
      foreach ( var type in part.Types )
      {
        if ( controllerType.IsAssignableFrom( type ) )
        {
          options.Conventions.Controller( type ).IsApiVersionNeutral();
        }
      }
    }
  }
} );

There were some issues with delayed evaluation when configuring the API versioning options prior to 3.0. It's possible that all of the controller types may not have yet been discovered (I didn't test this). If that turns out to be the case, you'll just need to provide your own mechanism for enumerating the UI controller type or migrate to 3.0 (Beta 2+). Enumerating all the types in a namespace is pretty straight forward.

API Versioning 3.0 currently still only requires ASP.NET Core 2.0

@hugoqribeiro
Copy link
Author

Am I wrong or the ApiVersionNeutral attribute has no bearing in my problem?

I see that my UI controller is being treated as version neutral (by debuging the source code) and still the mentioned error ("does not support HTTP method POST") is raised. As there is a route for GET...

If I'm not wrong, I understand from your answer that there is no way to work around this except to use version 3 and use the "hack" from issue #194.

Right?

@commonsensesoftware
Copy link
Collaborator

Oh ... I think I understand what's happening. In the context of REST, the general expectation is that an unsupported HTTP method on the server returns 405 (Method Not Supported) back to the client. This behavior is somewhat conflated with API versioning. When a route could match, but doesn't due to an unmatched HTTP method, you'll get 405. If the requested HTTP method matches, but the rest of the route doesn't, you'll receive 404 (or 400). This behavior doesn't matter if you're API version-neutral. You can alter the behavior in the options using a custom IErrorResponseProvider.

I hope that helps clear things up. I'm happy to answer more questions or add additional clarifications.

@hugoqribeiro
Copy link
Author

hugoqribeiro commented Nov 20, 2018

Well, the error response provider allows me to change the status code (the message, etc.) but that is not what I want.

What I want is for versioning to simply ignore routes that are not /api/* and do nothing. Because those other routes are not an API.

I see that this component doesn't support that. It doesn't make sense to me.
It should at least provide some sort of extensibility point to allow us to customize those kind of behaviors.

Maybe in the future...?

Thanks for trying to help me.

@hugoqribeiro hugoqribeiro changed the title Problem with MVC and MVP API. Any way to filter out controllers? Problem with non-API routes. Any way to filter out controllers? Nov 20, 2018
@commonsensesoftware
Copy link
Collaborator

Gotcha. Unfortunately, there isn't a particularly easy way to do that. ASP.NET Core doesn't intrinsically have a way to disambiguate UI and API controllers so there's not a whole lot for versioning to draw upon. Using one of previously mentioned methods is usually how it's addressed by devs that are hosting UIs and versioned APIs together. ASP.NET 2.1 Core introduced the concept of API Behaviors via the [ApiController] attribute. API versioning 3.0 introduces options.UseApiBehavior = true to honor that. Arguably, there isn't a scenario where you do not want that, but it could break existing behaviors when upgrading. I'll consider whether this should be the default for the final release.

I'm always open to new ideas to make things easier. A key design point, however, has been that you don't need to learn anything new about routing. Your scenario is achievable, it's just not as straight forward as one might like.

@figuerres
Copy link

one thought: split the project in two, one for mvc stuff the other for api stuff , make them child applications if you use iis.
then they are fully separate and you might only put versioning packages on the api project. then mvc works w/o versioning issues.

@commonsensesoftware
Copy link
Collaborator

It seems like this issue is resolved.

I mulled it over some more and the behavioral change to require [ApiController] for controller filtering doesn't really make sense. Using API Behaviors is not a requirement for API versioning and results in an unexpected change on upgrade. However, using [ApiController] with options.UseApiBehavior = true in API versioning 3.0+ should achieve the results you want with minimal effort.

@commonsensesoftware
Copy link
Collaborator

I thought I would quickly mention that I've changed the behavior in 3.1. ApiVersioningOptions.UseApiBehavior now defaults to true. To be considered an API controller, a ControllerModel must match a known IApiControllerSpecification, which is a new interface I've introduced.

The built-in specifications are:

  • ApiBehaviorSpecificiation - matches controllers decorated with [ApiController]
  • ODataControllerSpecification - matches controllers that use OData routing

If these do not satisfy your particular needs, you can roll you own specification and register it with the other services. For example,

public class MyApiControllerSpec : IApiControllerSpecification
{
    // consider all controllers that inherit from Controller to be a UI controller
    readonly Type UIControllerType = typeof( Controller ).GetTypeInfo();

    public bool IsSatisfiedBy( ControllerModel controller ) =>
        !UIControllerType.IsAssignableFrom( controller.ControllerType )
}

And register like:

services.TryAddEnumerable( ServiceDescriptor.Transient<IApiControllerSpecification, MyApiControllerSpec>() );

This prevents you having to muck with route configurations and so on. I'll add a section in the wiki about this. In the meantime, this should provide some information for those spelunking the issues. Thanks.

@webbes
Copy link

webbes commented Nov 12, 2019

Still does not work. I have an area with controllers that are decorated with the ApiController attribute and an rea where controllers are not decorated with the attribute.

I even specifically set the option
ApiVersioningOptions.UseApiBehavior to true

ApiVersioning is still not leaving my controllers without the attribute alone.

I then went as far as specifying and register an IApiControllerSpecification myself:
public class ApiControllerSpecification : IApiControllerSpecification
{
public bool IsSatisfiedBy(ControllerModel controller)
{
if (controller == null) throw new ArgumentNullException(nameof(controller));

        var controllerType = controller.ControllerType;

        var isApiControllerAttributeDefined = Attribute.IsDefined(controllerType, typeof(ApiControllerAttribute));
        return isApiControllerAttributeDefined;
    }
}

It is being called and obviously returns FALSE
STILL
ApiVersioning wont leave my controller alone.

Now what am I suppose to do?

=========================================================
UPDATE

I found out, that if I have a routing error of some kind (name/method) it will always result in an ApiVersioning error being returned. Either missing api or not supported etc... This caused a lot of confusion. In my case the error was a simple typo in the route and had nothing to do with versioning!

Kind regards

@fabiomaulo
Copy link

ApiVersioning 3.1.6 same problem but...
Two versions of API and some views about the documentation of the API.
ApiController used on real API controllers.
Everything works fine until I use the [Route("something")] attribute on a view.

@commonsensesoftware
Copy link
Collaborator

@fabiomaulo , make sure:

  1. ApiVersioningOptions.UseApiBehavior = true
  2. [ApiController] is only applied to API controllers
  3. Double-check your routes to make sure you aren't running into the same problem as @webbes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants