Skip to content

ASP.NET Core 2.0: Versioning causes routes to return 404 when UseMvc is behind a MapWhen #194

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
joshmackey opened this issue Sep 28, 2017 · 15 comments

Comments

@joshmackey
Copy link

After testing awhile, it looks like that if I place the UseMvc call behind a MapWhen, then all Mvc routes return a 404 with no errors.

If I comment out the MapWhen and place a UseMvc instead, the routes work.

Now, if I use MapWhen but comment out the AddApiVersioning, routes also work.

@rushfive
Copy link

I'm having this same issue on a NET Core 2.0 app.

Our API is using json rpc AND REST, so we currently use a .Map('/api') inside startup's Configure method to map requests to /api to use mvc.

Any ideas on how to resolve this?

@commonsensesoftware
Copy link
Collaborator

The registration order can definitely matter. As it currently stands, the API versioning route handler injects itself into the pipeline as the last handler. This must be the case to ensure proper behavior. Unfortunately, it's possible for the selection process to be hit multiple times. To ensure all possible candidates have been considered (which was a different issue in 1.0.x), API versioning needs to go last. I've had some discussions with the ASP.NET team on how to address this, but nothing is likely to change in until ASP.NET Core 2.1+.

Do either of you have a repro or configuration that you can share? There was a similar issue with JavaScript SPA handlers a few months back that we were able to get working. I suspect that the setup should be similar.

@joshmackey
Copy link
Author

joshmackey commented Sep 29, 2017

VersioningBugTest.zip

I've attached a simple project that demonstrates the issue. Comment out the MapWhen and just use UseMvc() and it works. Swap the comments and use MapWhen instead and it 404s. You won't even get the constraint warning if you miss using the version query parameter.

@commonsensesoftware
Copy link
Collaborator

Great! Thanks. I'll have a look.

@commonsensesoftware
Copy link
Collaborator

OK - I made a few minor tweaks and things work the way I expect. Hopefully, it's the way you expect. First, let me say that I do not believe that you can make the route templates overlap with this configuration. If that's what you were hoping for, it may not be possible. It will certainly take a lot more investigation if that's the case and I can't promise the results will be positive.

Here's what I changed:

  • Add the api/ prefix to your one controller
  • Enable UseMvc
  • Enable MapWhen with a real configuration

Looks like this:

[ApiVersion("1.0")]
[Route("api/[controller]")]
public class TestController : Controller { /* omitted */ }

// ... in Startup.cs

app.UseMvc();
app.MapWhen(
    c => !c.Request.Path.Value.StartsWith("/api"),
    b => b.Run(async c => await c.Response.WriteAsync("This is not MVC!")));

Here's the results:

Request URL Response
api/test 400 - no API version specified
api/test?api-version=1.0 200 - "This is a test, please ignore."
api/test?api-version=2.0 400 - no such API version
foo 200 - "This is not MVC!"
test 200 - "This is not MVC!"

Is this not what you were hoping to achieve? This setup is closely similar to what I documented in the known limitations for ASP.NET Core Routing, but perhaps you hadn't seen that. Let me know if I'm missing something here. Thanks.

@joshmackey
Copy link
Author

It looks like you have Mvc as the default and the non-Mvc as the fallback.

Due to some requirements out of my control, I have to prevent some calls from going to Mvc in the first place based on conditions that do not involve the path. To simplify my requirements, just think of a custom header that must exist and be a specific value before it should route to Mvc. If this header says "Mvc", then Mvc will run, if it says "other" than another chain will be run. If it is neither, or doesn't exist, then the default should be a 404. If this header does not exist or is not equal to Mvc, then Mvc should not run even if some of the url's would match.

That is why I had the UseMvc call inside the MapWhen.

tldr; I have two different http webservices running in asp.net core and I need to be able to route them differently based on business logic. Either or, not both. Having a fallback as your example shows would not meet my requirements.

@commonsensesoftware
Copy link
Collaborator

I see. That makes more sense. For my own edification, is there some reason why you wouldn't just create your own custom middleware to handle this scenario in put it in front of MVC in the pipeline?

It seems like your middleware would make a simple decision based on the header:

  • If the header is present
    • Map to custom handler and/or route path
    • Short-circuit the rest of the pipeline
    • The result is 2xx or 404
  • If the header is not present:
    • Allow continuation to the pipeline as normal
    • MVC does its thing with API versioning
    • The result is 2xx, 400, 405, or 404

I'm working off the knowledge in your posts and example. I didn't see anything like this in the repro. I'm not sure exactly how complex your scenario is, but I suspect that MapWhen could still work if it's based on filtering by header and placed before UseMvc (should you not want to create your own middleware). The decision is yours, but I would think that something like:

app.UseLegacyRouting();
app.UseMvc();

Would be immensely clearer in intention and use for your application.

Uploading your entire application isn't practical and it is often difficult to strip things down for a repro, but if you're able to expand your current repro to include this new information, I'm sure we can get something working.

@joshmackey
Copy link
Author

Maybe I simplified too much, let me explain what I'm trying to do.

The application hosts two different web services. In the config, you can set the endpoints that will be used for each service. By default, http://:8080 and https://:8443 goes to our proprietary product, whereas http://:9090 and https://:9443 goes to Mvc. In the older version of the application, there was a http listener for each http service so they were separated at the http listener level. With aspnet core, there is a single listener that is configured to listen on all endpoints. I then used MapWhen to direct http:8080 & https:8443 to our product's middleware; and http:9090 & https:9443 to Mvc. As I understood, this created two separate pipelines. http:8080 and https:8443 cannot be passed to Mvc and vice-versa for the other endpoints.

Any ideas on how I can do this while still using api versioning?

@commonsensesoftware
Copy link
Collaborator

commonsensesoftware commented Oct 2, 2017

A few questions:

  1. Are you hosing in IIS or just bare bones Kestrel?
  2. Is there actual crossing between the applications?
    a. If no, you can host them separately
    b. If you're using IIS, you can also use host header mapping
    c. If yes, you could use 301/302 redirects as necessary

As far as I know, there the request handler pipeline is divorced from the HTTP listener. This means that you should be able to have a middleware that redirects to a pipeline based on the incoming request or even use MapWhen. It seems the decision is based on incoming port or host header though.

I'll see if I can't setup a sample application this way.

@joshmackey
Copy link
Author

  1. Barebones Kestrel. The plan is to be cross platform.
  2. No, no crossing. However, they have to be hosted in the same executable due to requirements.

Isn't MapWhen redirecting the pipeline? How is that different than what I'm doing? It'll still breaks api versioning.

@commonsensesoftware
Copy link
Collaborator

Thanks for clarifying. MapWhen doesn't redirect the pipeline, it just injects pseudo-middleware in the form of a simple RequestHandler.

The pipeline is a Chain of Responsibility so you can think of it like this:

// pseudocode
MapWhen(
  ( match, handler, next ) =>
  {
      if ( match( context.Request ) )
      {
           context.Handler = handler;
      }
     else
     {
          context.Handler = next;   
     }
  } );

I'll see if I can't emulate your scenario.

@commonsensesoftware
Copy link
Collaborator

There may be other solutions, but I have a solution that should work for you. Your scenario is rather unique. To make it work, it seems the best option is to not call AddApiVersioning directly and instead perform the setup yourself. I've linked to the source in the code and it's not terribly complex. By doing that, you can ensure that the IApiVersionRoutePolicy is only applied to the MVC path instead of all paths. In nearly all cases, this is not something you'd ever want to do. Again, your scenario is a little unique.

I setup the server like this:

public static IWebHost BuildWebHost(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
        .UseStartup<Startup>()
        .UseKestrel( options =>
        {
            options.Listen( IPAddress.Loopback, 8080 );
            options.Listen( IPAddress.Loopback, 9090 );
        } )
        .Build();

and then I configure the application like this:

public void Configure(
 IApplicationBuilder app,
 IHostingEnvironment env,
 IApiVersionRoutePolicy routePolicy )
{
    if ( env.IsDevelopment() )
    {
        app.UseDeveloperExceptionPage();
    }

    app.MapWhen(
        c => c.Request.Host.Port == 8080,
        a => a.UseMvc().UseRouter( b => b.Routes.Add( routePolicy ) ) );

    app.MapWhen(
        c => c.Request.Host.Port == 9090,
        b => b.Run( async c => await c.Response.WriteAsync( "This is not MVC!" ) ) );
}

This will produced the following results:

Request URL Response
http://localhost:8080/api/test 400 - no API version specified
http://localhost:8080/api/test?api-version=1.0 200 - "This is a test, please ignore."
http://localhost:8080/api/test?api-version=2.0 400 - no such API version
http://localhost:9090 200 - "This is not MVC!"
http://localhost:9090/api/test 200 - "This is not MVC!"
http://localhost:9090/api/test/api-version=1.0 200 - "This is not MVC!"

Hopefully this gives you the skeleton you need to build your application. I've attached the working revision that I used.
VersioningBugTest-Rev1.zip

@commonsensesoftware
Copy link
Collaborator

Did this solution work for you?

@joshmackey
Copy link
Author

Yes, appears to indeed work.

Sorry for not trying it earlier, I was re-tasked to something else.

@commonsensesoftware
Copy link
Collaborator

No problem. Glad it worked. Let me know if you need anything else.

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

3 participants