dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.2k stars 9.95k forks source link

<base href="/" /> or base-tag alternative for Blazor MVC integration. #43191

Open Varin6 opened 2 years ago

Varin6 commented 2 years ago

Is there an existing issue for this?

Is your feature request related to a problem? Please describe the problem.

Issue related to those issues, yet I think since those were reported, this is still a problem for a lot of programmers.

#14246 #24631

I am integrating a few components into an existing, large, multitenant MVC application - .NET6. Applications frontend is views/partial views, I am injecting the Blazor components using

@(await Html.RenderComponentAsync<MyComponent>()

and everything works fine as long as it's on the main page. As soon as I try adding components anywhere else, the _blazor/initializers can't be found as blazor.js tries to look for them in the current path rather than the base path of the app.

Adding <base href="/" /> fixes this issue, but then it ruins a lot of other things in the existing MVC app, including any anchor tabs/bootstrap tabs, if the blazor component exists on that view.

The choice I have right now is to either find an alternative for a base-tag or somehow fix the issues caused by the base-tag in the massive MVC app. From a business perspective, we wouldn't want to put so much work because base-tag is mandatory and there are no other ways of making it work. I feel like the integration side of Blazor is bit neglected, and businesses often first try new technology they want to implement before they decide to write an app fully using it. This stops the propagation of this tech.

Describe the solution you'd like

Currently I solved it (so far no errors in the console and everything works fine) with those two additions to my code:

In _Layout.cshtml I disable the autostart and add this config:


<script src="~/_framework/blazor.server.js" autostart="false"></script>
<script>
Blazor.start({
       configureSignalR: function (builder) {
            builder.withUrl("/_blazor", {

           });
        }
    });
</script>

This doesn't seems to solve the _blazor/initializers path so in Program.cs I had to add a rewrite:

var rewriteOptions = new RewriteOptions();
    rewriteOptions.AddRewrite("_blazor/initializers", "/_blazor/initializers", skipRemainingRules: true);
    app.UseRewriter(rewriteOptions);

This works so far, not sure if I will need to add more rewrites when some other errors appear, I can't seem to find any more information about how to solve it, while finding a lot of topics about this same issue I am currently having, all related to integration of Blazor into existing MVC projects.

It could simply be just an elegant solution of passing one parameter as an option, for those cases where base-tag is not desired.

Additional context

No response

javiercn commented 2 years ago

@Varin6 thanks for contacting us.

I think there might be some issues in your configuration or I am missing something. The initializers are fetched relative to the document. Here is where we load them.

If you are serving your app from /subpath then the base path needs to be set to /subpath and the blazor hub must also be served from /subpath. There is an overload of MapBlazorHub that takes in a path, which essentially accepts a route pattern.

There are also features in routing that give you more flexibility in how/when you can map the path of the Blazor hub.

Varin6 commented 2 years ago

I have a _Layout.cshtml which is used for all the views in the Admin area of the app. This is where the blazor.js is called and initialized. If I am on a home page, the _blazor/initializers are fetched fine, but if I navigate to for example:

https://localhost:7013/Admin/HealthcareProfessionals/Edit/1

then it fails to fetch it, as it tries to look for initializers here: image

This works fine if I add the base-tag to _Layout.cshtml, but that then messes up a lot of relative links in the rest of the MVC app. This is also what the rewrite rule above fixes, in order to avoid using base-tag. But with just the rewrite rule, there are still errors as SignalR is using wrong path, hence I manually change the signalr config as shown in the original post.

My route config in program.cs:


app.UseEndpoints(endpoints =>
    {

        endpoints.MapControllerRoute("areas", "{area:exists}/{controller=Home}/{action=Index}/{id?}");
        endpoints.MapControllerRoute("default", "{controller=PageContainers}/{action=Index}/{slug=index}");
        endpoints.MapControllerRoute("api", "api/{controller=Home}/{action=Index}/{id?}");
        endpoints.MapControllerRoute(
           name: "slugRouting",
           pattern: "{slug}",
           constraints: new { slug = "^[a-zA-Z]*$" },
           defaults: new { controller = "PageContainers", action = "Index", slug = "" });

        endpoints.MapFallback(context =>
        {
            context.Response.Redirect("/Error/PageNotFound"); //404
            return Task.CompletedTask;
        });

        endpoints.MapBlazorHub();

    });

the MapFallback seems to trigger when blazor tries to connect when I am not on a main view of the app. The above configuration is needed for the rest of the MVC app, not sure what config options I have here to achieve no problems with <base href=""/> tag, or to avoid using it - which is probably the best option in this case, as base-tag ruins links in the rest of the app.

javiercn commented 2 years ago

@Varin6 thanks for the additional details.

Here are some thoughts:

There are two types of links that you can have in a document:

Blazor applications, like other types of applications (angular, react, vue, and any other SPA framework) rely on the document base to resolve document relative links. This is not a SPA specific decission, but a fundamental building block of how the web works.

Offering a separate concept would mean that we would need to replicate the browser behaviors in every situation where the document base is used, and there would be cases in which we could not even make this work. Concretely, this affects:

This makes it very expensive/complex if not straight impossible to control everywhere, and as new functionality is added to the web, we are in a never ending catch-up race.

With that in mind, the way to think about it is as follows. There are three sources of links in your page:

If you are rendering a Blazor application from different documents (/Admin/B/C/, /Admin/D/E/, and so on) you have to be aware that if you do nothing, the base path will be different when the app renders in each document, and consequently the resources will be fetched from different urls.

There are two approaches you can follow for this:

The first option is more complicated and is not likely what you are looking for as it would make navigations different on each document. For example, if you were to have a page /Something/Else in Blazor, when rendered under /Admin/B/C/ it would be /Admin/B/C/Something/Else and when rendered under /Admin/D/E/ it would be /Admin/B/C/Something/Else.

For the second option, you would set the base in the document and map the server endpoints to paths under the base. To achieve this, we need to look at the options we have.

Some servers have the concept of Virtual paths or Virtual folders which allow them to host multiple sites under the same origin. ASP.NET Core is no different and IIS and Kestrel both have this concept. You can map the hub inside a forked pipeline using a snippet like this:

app.Map("/base/path/", subapp => {
  // You might need to use subapp.UsePathBase("/base/path/"); here
  subapp.UseRouting();
  subapp.UseEndpoints(endpoints => endpoints.MapBlazorHub());
})

An alternative is to pass a path to MapBlazorHub directly. For example like this:

endpoints.MapBlazorHub("base/path");

The benefit of doing it this way is that you can map patterns, like "{tenant}" and not just concrete paths.

In addition to that, if this is a Blazor webassembly application, you need to adjust the path of UseBlazorFrameworkFiles (which also receives an optional path prefix) and UseStaticFiles.

Finally, in the case of webassembly, you need to make sure that the files are in the correct location on disk. For that you need to make sure that StaticWebAssetBasePath is set to match whatever path you want to serve the files from.

Coming back to the first option, where you map the files dynamically, routing offers IDynamicEndpoint and MatcherPolicy which in combination can be used to make a decision at runtime of what endpoint should handle which request, and can be the basis for implementing a completely dynamic solution.

With all this said, the best way to go about this is:

There are some subset of things that I think we might be willing to entertain:

However, this will likely mean that document relative navigations in your app will not work correctly as they are dependent on the base path of the document.

We are definitely not willing to reinvent the base concept and reimplement the browser behavior in a different way.

Varin6 commented 2 years ago

@javiercn

Thank you very much for this very descriptive reply. I digested it, looked again at my code and I have to come back to you with apologies. It turns out there was an obscure JS hidden in the dungeon of a code, which was rewriting anchors. Base-tag introduction affected the results of that code, as it affected the basepath that JS uses to rewrite URL in the browser, which meant that suddenly when trying to navigate to anchors produced a wrong URL in the browser. Any other JS that rewrites URLs might be affected.

Before posting here, I tested it on another codebase to make sure it's not an isolated issue, turns out that other project I used to test it was based on our "base" code which contains that JS.

Either way - it is still an "issue" when integrating Blazor into existing MVC, as the introduction of base-tag can affect the operation of the existing project in ways hard to predict. I can also see how this can just be labeled as "existing code issue" and nothing to do with Blazor config itself. I now have removed the code I posted above:

_Layout.cshtml

<script src="~/_framework/blazor.server.js" autostart="false"></script>
<script>
Blazor.start({
       configureSignalR: function (builder) {
            builder.withUrl("/_blazor", {

           });
        }
    });
</script>

and this hack in program.cs :


var rewriteOptions = new RewriteOptions();
    rewriteOptions.AddRewrite("_blazor/initializers", "/_blazor/initializers", skipRemainingRules: true);
    app.UseRewriter(rewriteOptions);

and I have added base-tag, and all works fine after I rewrote the JS that was the culprit.

Nevertheless, the hack above works, I am not knowledgeable enough to know whether it literally does what base-tag would do or whether it can yield unexpected issues. It worked flawlessly in my case until I figured the base-tag issue out.

Thank you again for your informative reply, I have learned new things.

javiercn commented 2 years ago

@Varin6 no problem, this topic is complicated, it has come up several times and was due a more detailed write up that explained how things worked and why they work in that way.

We might do some work in .NET 8.0 to make some of these scenarios easier.

Varin6 commented 2 years ago

Thank you for the info. If you are interested what was causing the issue, this is the bit of code that rewrites url in the browser and allows to navigate to specific bootstrap tabs and open them when the address is entered in the URL bar:


/*------------------------
 *  Location Hash Reload
 * -----------------------*/
function locationHashReload() {
    // show active tab on reload

    if (location.hash !== '') {       
        let $target = $('a[href="' + location.hash + '"]');
        if ($target.length) {
            $target.tab('show');
        }
    }

    // remember the hash in the URL without jumping
    $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
        if (history.pushState) {
            history.pushState(null, null, '#' + $(e.target).attr('href').substr(1));
        } else {
            location.hash = '#' + $(e.target).attr('href').substr(1);
        }
        // Fix scrollable datatables
        $.fn.dataTable.tables({visible: true, api: true}).columns.adjust();
    });
}

This line:

history.pushState(null, null, '#' + $(e.target).attr('href').substr(1));

history.pushState() seems to work differently when is present. After adding base-tag, I had to add:

var baseUrl = location.protocol + '//' + location.host + location.pathname;

and change those two lines:

history.pushState(null, null, baseUrl + '#' + $(e.target).attr('href').substr(1));

location.hash = baseUrl +'#' + $(e.target).attr('href').substr(1);

This is probably an issue for those niche cases of established MVCs that have some JS scripts that use history.pushState() and maybe some other methods that base-tag could affect, and not knowing about this, they try to integrate Blazor. Saying that - if base-tag is required mandatory for other frameworks like Angular etc, then this is not isolated to Blazor at all.

mreyeros commented 2 years ago

I have a similar question as to how the base path should be setup when a blazor server app is served via an iframe. I currently have a need to serve up my blazor server application via an iframe in a legacy webforms application(s). When the blazor app is loaded in the iframe the base path is relative to the site that is hosting the iframe, not relative to the blazor server app running in the iframe. What modifications or adjustments should I make to the blazor server app in order for the js and css to be loaded relative to the actual blazor server app?

mkArtakMSFT commented 1 year ago

@javiercn can you please list the actionable items for us and what you think about those being in .NET 8 or backlog ?

NicolasDorier commented 1 year ago

If anybody has this issue, here is my hack:

// HACK: blazor server js hard code some path, making it works only on root path. This fix it.
// Workaround this bug https://github.com/dotnet/aspnetcore/issues/43191
var rewriteOptions = new RewriteOptions();
rewriteOptions.AddRewrite("_blazor/(negotiate|initializers|disconnect)$", "/_blazor/$1", skipRemainingRules: true);
rewriteOptions.AddRewrite("_blazor$", "/_blazor", skipRemainingRules: true);
app.UseRewriter(rewriteOptions);

This rewrite the paths used by blazor.server.js.

I also had to register static files manually due to a bug https://github.com/dotnet/aspnetcore/issues/19578

// HACK: Make blazor js available on: ~/_blazorfiles/_framework/blazor.server.js
// Workaround this bug https://github.com/dotnet/aspnetcore/issues/19578
app.UseStaticFiles(new StaticFileOptions()
{
    RequestPath = "/_blazorfiles",
    FileProvider = new ManifestEmbeddedFileProvider(typeof(ComponentServiceCollectionExtensions).Assembly),
    OnPrepareResponse = LongCache
});

With

private static void LongCache(Microsoft.AspNetCore.StaticFiles.StaticFileResponseContext ctx)
{
    // Cache static assets for one year, set asp-append-version="true" on references to update on change.
    // https://andrewlock.net/adding-cache-control-headers-to-static-files-in-asp-net-core/
    const int durationInSeconds = 60 * 60 * 24 * 365;
    ctx.Context.Response.Headers[HeaderNames.CacheControl] = "public,max-age=" + durationInSeconds;
}

With this I can just to <script src="~/_blazorfiles/_framework/blazor.server.js" asp-append-version="true"></script> without initializing anything after.

Integrating blazor server to existing project hasn't been smooth experience for now.

ghost commented 11 months ago

To learn more about what this message means, what to expect next, and how this issue will be handled you can read our Triage Process document. We're moving this issue to the .NET 9 Planning milestone for future evaluation / consideration. Because it's not immediately obvious what is causing this behavior, we would like to keep this around to collect more feedback, which can later help us determine how to handle this. We will re-evaluate this issue, during our next planning meeting(s). If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact work.

icnocop commented 9 months ago

https://github.com/dotnet/aspnetcore/issues/30316 is also related.

ghost commented 9 months ago

To learn more about what this message means, what to expect next, and how this issue will be handled you can read our Triage Process document. We're moving this issue to the .NET 9 Planning milestone for future evaluation / consideration. Because it's not immediately obvious what is causing this behavior, we would like to keep this around to collect more feedback, which can later help us determine how to handle this. We will re-evaluate this issue, during our next planning meeting(s). If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact work.

MackinnonBuck commented 8 months ago

One possible solution (discussed during triage) would be to emit a comment with the base path when debugging and log a warning if we detect that it was misconfigured.

xcITs-Xian commented 8 months ago

@Varin6 thanks for the additional details.

Here are some thoughts:

There are two types of links that you can have in a document:

  • Absolute links, which can contain scheme,host,port,path, etc or just / followed by the path (https://example.com/a/b/c or /a/b/c)
  • Relative links which contain just a path and do not start with / like b/c. These are resolved relative to the current document URL or the base tag if specified. One important aspect of this is that the presence of a trailing slash is significant to compute the base path. For example https://example.com/a has a base path https://example.com/ compared to https://example.com/a/ that has a base path of https://example.com/a.

Blazor applications, like other types of applications (angular, react, vue, and any other SPA framework) rely on the document base to resolve document relative links. This is not a SPA specific decission, but a fundamental building block of how the web works.

Offering a separate concept would mean that we would need to replicate the browser behaviors in every situation where the document base is used, and there would be cases in which we could not even make this work. Concretely, this affects:

  • Navigation to relative urls in links.
  • Resources (scripts, files) with relative urls.
  • fetch requests sent.

This makes it very expensive/complex if not straight impossible to control everywhere, and as new functionality is added to the web, we are in a never ending catch-up race.

With that in mind, the way to think about it is as follows. There are three sources of links in your page:

  • Tag hepers: Which always emit absolute links.
  • URLs manually written in the cshtml file, which if you are rendering inside different documents should always be absolute (otherwise you get the same errors you are getting).
  • URLs in .razor (that are typically relative, but are essentially also manually written).
  • URLs in scripts (like blazor.server.js) which are always document relative.

If you are rendering a Blazor application from different documents (/Admin/B/C/, /Admin/D/E/, and so on) you have to be aware that if you do nothing, the base path will be different when the app renders in each document, and consequently the resources will be fetched from different urls.

There are two approaches you can follow for this:

  • Map the resources dynamically using the document they were rendered on as the root.
  • Set a consistent base for the document and map the resources under that base path.

The first option is more complicated and is not likely what you are looking for as it would make navigations different on each document. For example, if you were to have a page /Something/Else in Blazor, when rendered under /Admin/B/C/ it would be /Admin/B/C/Something/Else and when rendered under /Admin/D/E/ it would be /Admin/B/C/Something/Else.

For the second option, you would set the base in the document and map the server endpoints to paths under the base. To achieve this, we need to look at the options we have.

Some servers have the concept of Virtual paths or Virtual folders which allow them to host multiple sites under the same origin. ASP.NET Core is no different and IIS and Kestrel both have this concept. You can map the hub inside a forked pipeline using a snippet like this:

app.Map("/base/path/", subapp => {
  // You might need to use subapp.UsePathBase("/base/path/"); here
  subapp.UseRouting();
  subapp.UseEndpoints(endpoints => endpoints.MapBlazorHub());
})

An alternative is to pass a path to MapBlazorHub directly. For example like this:

endpoints.MapBlazorHub("base/path");

The benefit of doing it this way is that you can map patterns, like "{tenant}" and not just concrete paths.

In addition to that, if this is a Blazor webassembly application, you need to adjust the path of UseBlazorFrameworkFiles (which also receives an optional path prefix) and UseStaticFiles.

Finally, in the case of webassembly, you need to make sure that the files are in the correct location on disk. For that you need to make sure that StaticWebAssetBasePath is set to match whatever path you want to serve the files from.

Coming back to the first option, where you map the files dynamically, routing offers IDynamicEndpoint and MatcherPolicy which in combination can be used to make a decision at runtime of what endpoint should handle which request, and can be the basis for implementing a completely dynamic solution.

With all this said, the best way to go about this is:

  • Map the hub to the root of the area(s) you care about.
  • Setup the static files middleware to serve files from those prefixes.
  • Set the base tag on the pages inside the area to refer to the area prefix.
  • Make urls either absolute or relative to the area.

There are some subset of things that I think we might be willing to entertain:

  • Possibility to configure absolute urls for the blazor endpoints in blazor.Server.js.
  • Relax the base path check when initializing the navigation manager to not break if the base is not a prefix of the current URL.

    • (This might not be needed at all, but is a source of confusion)

However, this will likely mean that document relative navigations in your app will not work correctly as they are dependent on the base path of the document.

We are definitely not willing to reinvent the base concept and reimplement the browser behavior in a different way.

Thank you for this great explanation, it explains everything very clearly.

But one big problem remains: You have to know at the time of programming (!) under which path the application will be installed. Unfortunately, this makes no sense at all for ISVs, as you never know how and where the application will be installed at the customer's site.

Is there any way (without specifying it in the code or elsewhere) that a Blazor Server App can be installed anywhere? So directly below an IIS website as root or any virtual directory of a website?

Never made a problem in the good old days of ASP and ASP.NET, should be solvable for Blazor as well.

Thanks for any advice that helps to be flexible here. (And please don't suggest throwing everything into a container as a solution, that's not a solution. The real world and customers work differently ... ;-))

purplepiranha commented 4 months ago

I followed this guide to add a Blazor control to an existing web app: https://learn.microsoft.com/en-us/aspnet/core/blazor/components/integration?view=aspnetcore-8.0

This broke a lot of functionality in the MVC app, including logging out and sorting of data grids.

I'd chosen to use a Blazor component in this way because it was going to save me a lot of time over writing the functionality in Javascript, and Microsoft said it was possible. Microsoft really need to make it clear in their documentation if doing something like this will break key functionality.

My question has to be why when using a single component on it's own a base path should have to be specified. Surely it should only be necessary when using the routing functionality of Blazor.

I couldn't get any of the workarounds above to work. Probably because I'm using a newer version. The following seems to work though for me.

My workaround

In layout.cshtml remove

<base href="~/" />

and update

<script src="_framework/blazor.web.js"></script>

to

<script src="~/_framework/blazor.web.js"></script>

Add to program.cs

var rewriteOptions = new RewriteOptions();
rewriteOptions.AddRewrite("_blazor(.*)", "/_blazor$1", skipRemainingRules: true);
app.UseRewriter(rewriteOptions);