Skip to content
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

.NET core 3 route failure with separate projects #14952

Closed
jjxtra opened this issue Oct 12, 2019 · 30 comments
Closed

.NET core 3 route failure with separate projects #14952

jjxtra opened this issue Oct 12, 2019 · 30 comments
Labels
area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates question

Comments

@jjxtra
Copy link

jjxtra commented Oct 12, 2019

Describe the bug

Unable to use routing in when creating host and using controllers from separate projects. When using everything from one project, routes work fine.

To Reproduce

Steps to reproduce the behavior:

  • See reproducible project with instructions in comment down below.

Expected behavior

Routes, views, razor, etc. should work regardless of which project the host builder, views and controllers are in (as they do in .NET core 2.2).

@davidfowl
Copy link
Member

Can you provide a running project?

@jjxtra
Copy link
Author

jjxtra commented Oct 12, 2019

Working on it

@jjxtra
Copy link
Author

jjxtra commented Oct 12, 2019

For some reason, .net core 2 tests can find controllers in other assemblies. This seems to have broken in .net core 3. For now my hacky work-around is this (new code added for assembly part that was not needed in .net core 2):

IMvcBuilder mvcBuilder = services.AddMvc((options) =>
{
    options.EnableEndpointRouting = true;
});
Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (Assembly assembly in assemblies)
{
    try
    {
        if (assembly.GetTypes().Any(t => t.IsSubclassOf(typeof(Controller))))
        {
            mvcBuilder.AddApplicationPart(assembly);
        }
    }
    catch
    {
        // bugs in unit test framework throw exceptions for weird assemblies like intellitrace
    }
}
mvcBuilder.SetCompatibilityVersion(CompatibilityVersion.Latest).AddJsonOptions(options =>
{
    options.JsonSerializerOptions.IgnoreNullValues = true;
});

@jjxtra
Copy link
Author

jjxtra commented Oct 12, 2019

With this solution unfortunately now my razor views cannot find any dlls, including system etc, even with preserve compilation context set in the csproj...

@davidfowl
Copy link
Member

Still waiting for a repro.

@jjxtra
Copy link
Author

jjxtra commented Oct 12, 2019

Still trying to strip my very large project down into the smallest reproducible case, Will post back if I can get it.

@jjxtra
Copy link
Author

jjxtra commented Oct 13, 2019

I can reproduce it on my machine here regularly but not able as of yet to upload to a repo. Is it possible to do a screen share?

@davidfowl
Copy link
Member

Did you try to reproduce it in a new project?

@jjxtra
Copy link
Author

jjxtra commented Oct 13, 2019

Yes, so far it can find the controller in a different assembly but I have not tried razor yet, just raw json. Copying the code over to my project and suddenly I get 404 errors with the same controller. Very bizzare.

public static async Task Main(string[] args)
{
    using (var host = CreateHostBuilder(args).Build())
    {
        await host.RunAsync();
    }
}

public static IHostBuilder CreateHostBuilder(params string[] args)
{
    return Host.CreateDefaultBuilder(args).ConfigureLogging((logBuilder) =>
    {
        logBuilder.AddConsole().SetMinimumLevel(LogLevel.Trace);
    })
    .ConfigureWebHostDefaults(webBuilder =>
    {
        webBuilder.Configure((ctx, app) =>
        {
            if (ctx.HostingEnvironment.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseAuthorization();
            app.UseRouting();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapRazorPages();
                endpoints.MapControllers();
                endpoints.MapControllerRoute("default", "/{action}/{id?}", new { action = "Index", controller = "Home" });
            });
        })
        .ConfigureServices((serviceCollection) =>
        {
            serviceCollection.AddAuthorization();
            serviceCollection.AddRazorPages();
        })
        .ConfigureAppConfiguration((ctx, configBuilder) =>
        {
            configBuilder.Build();
        });
    });
}

@jjxtra
Copy link
Author

jjxtra commented Oct 13, 2019

In my project, I hit same controller Index method, get this errors:

info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/1.1 GET http://localhost:5000/
dbug: Microsoft.AspNetCore.HostFiltering.HostFilteringMiddleware[0]
      Wildcard detected, all requests with hosts will be allowed.
trce: Microsoft.AspNetCore.HostFiltering.HostFilteringMiddleware[2]
      All hosts are allowed.
dbug: Microsoft.AspNetCore.Mvc.Razor.Compilation.DefaultViewCompiler[4]
      Initializing Razor view compiler with no compiled views.
dbug: Microsoft.AspNetCore.Routing.Matching.DfaMatcher[1000]
      No candidates found for the request path '/'
dbug: Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware[2]
      Request did not match any endpoints
dbug: Microsoft.AspNetCore.Server.Kestrel[9]
      Connection id "0HLQFJ9TFKQ6A" completed keep alive response.
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished in 27.218ms 404

@jjxtra
Copy link
Author

jjxtra commented Oct 13, 2019

I have a repro project. It is attached. Nice and small.

Download --> NetCore3RouteTestFail.zip

Instructions:

  • Extract the zip file to somewhere on your machine. Please use VS 2019 with latest .NET core 3.0 SDK.
  • Load NetCore3RouteTestFail.sln
  • Note the controller is in a separate project (Controllers)
  • Set NetCore3RouteTestFail.csproj as start up project
  • Run and visit http://localhost:5000/WeatherForecast
  • You should see json
  • Stop the running console app, leave browser open. Now uncomment line 21 and comment out line 22 in Program.cs in the NetCore3RouteTestFail project.
  • Run again. Refresh the browser using ctrl-F5 to bypass any cache. Notice the 404 error.
  • The method to build and run the host has been copied exactly into a separate project (HostCreator) using line 21 in Program.cs.
  • The unit test project has the same symtom. You can run tests and they pass. Then uncomment line 22 and comment out line 23 in UnitTest1.cs (RouteTestFail project) and run tests again. They fail with a 404.
  • Please note that I follow the same pattern (putting host builder in separate project, controllers in yet another project) in .NET core 2.2 and it works fine in both tests and applications, only fails in .NET core 3.0.
  • I can get the routes to work if I add AddApplicationPart and pass the assembly the controller is in, but this is only needed when the host builder code is not in the startup project. Weird. But the problem with this is, then any razor views do not get assembly references, even with preserve compilation context on.

@davidfowl

@jjxtra jjxtra changed the title .NET core 3 tests fail to match routes .NET core 3 route failure with separate projects Oct 13, 2019
@soeleman
Copy link

Try add UseSetting on CreateHostBuilder function.
CreateHostBuilder

On Program.Main we pass AssemblyName.
ProgramMain

And to make easy lunch the app, you can modify launchSettings.json (in Properties folder on project NetCore3RouteTestFail).
launchSettings_json

@javiercn
Copy link
Member

@jjxtra Thanks for contacting us and thanks for the repro.

I've looked at the project you sent us and I believe the reason you are not finding the controllers when using HostCreator is because it doesn't have a reference to the Controllers project.

The way discovery works in MVC is thought metadata stamped into the assembly at build time. When you try to start the host it looks for an assembly with the application name to find the assembly level attributes that tell it which assemblies to use for discovering controllers and other MVC primitives.

@soeleman answer will probably help here, as setting the application name should fix this issue. That said, if you are doing this for testing purposes I would highly recommend using https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-3.0 instead.

Let us know if that fixes your issue.

@javiercn javiercn added the area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates label Oct 14, 2019
@chassq
Copy link
Contributor

chassq commented Oct 14, 2019

Maybe this is related or not but if one tries to put a Page into an RCL the page cannot be routed too with the default startup.cs. Not sure if that is supported or not.

@javiercn
Copy link
Member

@chassq I don't think your issue is related.

If you think you've found an issue open a separate issue describing your scenario so that we can track it.

@jjxtra
Copy link
Author

jjxtra commented Oct 14, 2019

So has this changed from net core 2.2? I had the same pattern in 2.2 and it worked fine, no extra stuff needed.

@javiercn
Copy link
Member

@jjxtra This might have been a change in hosting as a result of the move to generic host.

@Tratcher Did we change something in how we set the app name from 2.2 to 3.0 with regards to UseStartup?

@Tratcher
Copy link
Member

Not intentionally, but there may be a subtle ordering difference or similar.

@jjxtra
Copy link
Author

jjxtra commented Oct 14, 2019

I don't know if it helps, but the legacy web host builder also has an identical problem in net core 3. It's possible that it and the new host builder share the same common code now.

@mkArtakMSFT
Copy link
Member

Thanks for contacting us. We believe that the question you've raised have been answered. If you still feel a need to continue the discussion, feel free to reopen it and add your comments.

@jjxtra
Copy link
Author

jjxtra commented Oct 14, 2019

I will attempt the suggested fix this evening and post back

@jjxtra
Copy link
Author

jjxtra commented Oct 15, 2019

This worked for the regular project, unfortunately the unit test project still fails. I am unable to re-open this issue.

I don't see any good documentation on this usesetting with this key, it feels a little bit hacky. Trying it in my other big project also did not resolve the 404 error so I am still concerned that something is not working quite right in .net core 3.

@mkArtakMSFT

@jjxtra
Copy link
Author

jjxtra commented Oct 15, 2019

I see this link: https://github.com/dotnet/core/blob/master/release-notes/3.0/preview/3.0.0-preview-known-issues.md

I believe the issue "Referencing 3.0.0 MVC libraries don't work as intended: There are several issues with referencing a 3.0.0 MVC library" is not completely resolved, just from what I am seeing...

@jjxtra
Copy link
Author

jjxtra commented Oct 15, 2019

I can add application part for all assembly with typeof(Controller) and that allows api controllers to be found, but anything with razor still gives this, even with these project settings:

IMvcBuilder mvcBuilder = services.AddRazorPages().AddRazorRuntimeCompilation(options =>
{

}).AddMvcOptions((options) => options.EnableEndpointRouting = true);
foreach (Assembly a in IPBanExtensionMethods.GetAssembliesWithType(typeof(Controller)))
{
    mvcBuilder.AddApplicationPart(a);
}
<MvcRazorExcludeRefAssembliesFromPublish>false</MvcRazorExcludeRefAssembliesFromPublish>
<CopyRefAssembliesToPublishDirectory>true</CopyRefAssembliesToPublishDirectory>
<PreserveCompilationReferences>true</PreserveCompilationReferences>
<PreserveCompilationContext>true</PreserveCompilationContext>

image

@javiercn
Copy link
Member

I’ll take another look

@javiercn javiercn reopened this Oct 15, 2019
@javiercn
Copy link
Member

@jjxtra With the repro you provided I did the following:

  • Add a project reference from HostCreator to controllers
  • Change the test as follows:
    var host = HostCreator.HostBuilder.CreateHostBuilder().Build(); // broken
    //var host = NetCore3RouteTestFail.Program.CreateHostBuilder().Build(); // works
  • With that, starting up host creator (you need to add the line below) I can get to the API without problems.
    • CreateHostBuilder(args).Build().Start(); (This needs to be added to program main).
    • image

The other issue that you are seeing is likely caused by using a different app name, as it results in loading the wrong dependencycontext which is what razor uses to perform recompilation.

My original recommendation still stands, if you are doing this for testing purposes use WebApplicationFactory as it handles all these concerns.

I'm closing this issue as there's no more action to be taken here.

@jjxtra
Copy link
Author

jjxtra commented Oct 15, 2019

I was able to hack around the issue by doing the following. Posting it here for anyone else who comes across this issue in .NET core 3. I really hope it saves people the headache I went through.

You have to capture the web host environment before the service provider is built. You can do this in ConfigureAppConfiguration, .i.e.:

builder.ConfigureAppConfiguration((hostingContext, configurationBuilder) =>
{
    this.WebHostEnvironment = hostingContext.HostingEnvironment;
    // rest of your configuration code if any
});

Then, in ConfigureServices, set the application name, i.e.

webBuilder.ConfigureServices((ctx, serviceCollection) =>
{
    this.WebHostEnvironment.ApplicationName = Assembly.GetEntryAssembly().GetName().Name;
    // rest of your services configuration code if any
});

Unit tests unfortunately are still broken. Something does not wire up right when running under testhost and razor views cannot find any assemblies, despite using the appropriate csproj properties like preserve compilation context. I am still looking into a hack/workaround for that.

@alextof
Copy link

alextof commented Nov 21, 2019

@jjxtra , I'm trying to understand your resolution and trying to apply it to #15046 to be able to find a fix, but I still don't see how to relate them. Could you please give a bit more insights on how you was able to solve the issue?

@guibirow
Copy link

I can confirm the .UseSetting(WebHostDefaults.ApplicationKey, typeof(Startup).Assembly.GetName().Name) works like a charm.

Sample code:

WebHost.CreateDefaultBuilder()
                .UseStartup<TestStartup>()
                .UseSetting(WebHostDefaults.ApplicationKey, typeof(Startup).Assembly.GetName().Name)
                .Build()

@jjxtra
Copy link
Author

jjxtra commented Nov 28, 2019

The only issue remaining still is that razor does not work in unit tests, something to do with testhost I think, but not sure...

@ghost ghost locked as resolved and limited conversation to collaborators Dec 28, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates question
Projects
None yet
Development

No branches or pull requests

9 participants