How to emulate the way Kestrel and IIS handles a URI of "/" with Response substitution to support Blazor?

I’m trying to connect a Blazor (WASM) app in a browser to a ServiceStack host. In Blazor V0.9.0 and prior, if SS simply responded to the ‘/’ URI with a 302 redirect to /index.html, all was good. Starting with Blazor V3.0 Preview 4, that no longer works. Fiddler shows, in V30P4, that Blazor app sends uri ‘/’ to Kestrel, and kestrel returns status code 200, and the contents of the index.html file. I’m trying to figure out how to tell SS that when it gets a request of ‘/’, to return the contents of the file at wwwroot/index.html. I’m guessing it is some kind of request filter, but I’m still new to SS. Initial searches of the internet and this forum didn’t come back with any clear directions, hence this post. TIAA!

That’s the default behavior, can you share a small project on GitHub which shows the issue?

Blazor was just upgraded from V0.9.0 to to V3.0 Preview 4 (new versioning pattern). The GitHub repository below, master branch, works with V0.9.0

Documentation is quite a bit (3 months) out of date, but provides the basic directions to get Demo01 working.

In Demo01 there is a Blazor GUI project that runs on a browser, and a SS ConsoleApp project, and they communicate over HTTP. Both have a reference to the same CommonDTOs project. In the Console App, I have the lines

        // // change the default redirect path so that a request that does not match a route will redirect to /index.html
        this.Config.DefaultRedirectPath = "/index.html";

When the Blazor GUI V0.9.0 and earlier requests ‘/’ from SS, it gets back a 302, redirects, calls for index.html by name, gets its response/contents, and moves on. But the Blazor upgrade to V30P4 has a breaking change; It crashes ("can’t find route to /index.html is the Chrome console error) when it gets a 302 redirect to /index.html.

The branch named
DEV/WGH-UpgradeBlazor3.0Preview4andVS2019Preview-Issue24
will compile, link, and run with Blazor V30P4.

Under V30P4, If I use SS, the Demo1 fails as stated above. If I use the Visual Studio built-in host, Kestrel, then Kestrel, when it gets a request for ‘/’, doesn’t issue a 302; it just returns status code 200 and the contents of index.html.

I wanted to see how to configure SS for the same behavior. Since it seems that the behaviour I want is the default, I’ll remove the instruction for DefaultRedirectPath when I try testing again, probably tomorrow. Thanks for your help!

P.S. - I’d like to tag this post with both WASM and Blazor, if you wouldn’t mind creating those tags. I’ve been working towards an integration of Blazor and SS for about 6 months, and have a few more questions, mostly about SS on Mono and eventually, I hope, SS on Mono-WASM, that I’d like to ask and have grouped by those tags. My hoped-for goal is to eventually see the SS Clients running on browsers that support WASM, talking directly to a network of SS host-side apps.

I can’t get it to build, it always fails with:

Where is App from?

Also is there a specific reason why you’re using a .NET Framework Self Host instead of a .NET Core App? i.e. since Blazor requires .NET Core.

FYI you shouldn’t use an empty path mapping, if you don’t have a mapping you can just use:

new FileSystemVirtualFiles(path)

But instead of adding a new Virtual File System IMO you should change the WebRoot, e.g:

SetConfig(new HostConfig
{
    AllowFileExtensions = { "json" },
    WebHostPhysicalPath = Path.GetFullPath("../../../../GUI/bin/Debug/netstandard2.0/Publish/GUI/dist"),
    //DefaultRedirectPath = "/index.html",
});

You shouldn’t need a default redirect, if there’s an index.html for a directory request ServiceStack will automatically load the index.html.

To make the demo build, the development environment needs all the Blazor prerequisites, which change with every preview release. Instructions on how to configure the dev environment are in each release’s Release Notes. An announcement of all releases, with links to the individual release notes, is here: https://github.com/aspnet/Blazor/releases.
The release notes for V0.9.0 https://devblogs.microsoft.com/aspnet/blazor-0-9-0-experimental-release-now-available/
should be followed to install the SDK and blazor vsix extension for V0.9.0, which is the version for the demo’s master branch in GitHub.

Blazor solutions/projects come in two flavors; Server-Side Blazor (SSB) and Client-Side Blazor (CSB). A SSB solution creates both the client-side Blazor project and a serverside (host) project. A CSB solution creates only the client-side Blazor project. A CSB project does not care what technology is used to serve its pages, anything capable of service a static website will do. It is only the SSB project that creates a Net Core Host, to act as the static web server.

My main app is a Windows Service using SS to provide both REST endpoints, and serve the static files to the CSB Blazor GUI. So my demos are built to mimic this structure.

The main app can server multiple versions of the GUI, with different look-and-feel. I need (for example) a virtual path of /gui01/ to serve files from one physical root path, and /gui02/ to serve files from a different root path. Hence I selected Virtual File Systems instead of modifying the WWWRoot. I will eventually provide a demo illustrating multiple GUIs.

I have not had a chance to try the default behaviour in a demo yet, but I did try it in my main app. Removing Config.DefaultRedirectPath = "/index.html"; caused a request for / received by SS to produce a 302 response to /metadata
Then I added appHost.Config.EnableFeatures = Feature.All.Remove(Feature.Metadata); but I still get the 302 redirect to /metadata

I hope to be able to try removing Config.DefaultRedirectPath = "/index.html"; in the demo code later today.

After upgrading to SS V5.6, and also upgrading to .Net Core V3.0 Preview 8, I’m again having problems getting the code to deliver static files, specifically index.html found in a Virtual Filesystem. When using SS 5.5.1 (development, from MyGet), and .Net Core preview 7, the code would deliver static files from a Virtual Filesystem. But after the upgrade, requests to “/” return a 404, instead of the contents of index.html. In the earlier version, I did not need to set WWWRoot, although I did need to set the default redirect to “index.html”, and the code would deliver the index.html file found in the physicalpath I had mapped to the virtual path “”. But that fails with the current releases. I’ve created a GitHub repository that illustrates the issue. In this repository, I’ve done things a bit differently; I want to deliver static files from multiple places, to support multiple GUIs. I want to do this from a PlugIn. In this repository, The GUIServices PlugIn has a configuration text file that defines multiple mappings from a virtual root to a physicalpath. The PlugIn reads this configuration, and performs the mapping. But all attempts to get index.html from the physicalroots via the virtualpaths fail with a “404”. However, the logging shows that index.html IS found at the physical path, and debugging shows that the mappings are done as expected For what it’s worth, when I configure the webserver to deliver static files from a location outside of WWWroot, it works, but, that has to be done in the GenericHost builder before the code instantiates the SS app, and certainly before any PlugIns are loaded. I’m hoping for a solution that will let me configure multiple virtual filesystems from a SS plugin and deliver static files from those virtual filesystems. Could you help me understand what I’m missing? The public GitHub repository with the example code is at https://github.com/BillHertzing/StaticFileIssueWithSS. Both Master and Develop branches are in sync. Note that the ReadMe.md has notes on how to build, as well as notes on what I’d like to accomplish.

I don’t want to install a VS preview on my machine, so I’m trying to run it in VS Code. I’ve installed the latest .NET Core 3.0 preview but I can’t build it due to a missing Xamarin.Offline.config which doesn’t exist?

C:\Program Files\dotnet\sdk\3.0.100-preview8-013656\NuGet.targets(528,5): error : Failed to read NuGet.Config due to unauthorized access. Path: 'C:\Program Files (x86)\NuGet\Config\Xamarin.Offline.config'. [C:\Users\mythz\Downloads\StaticFileIssueWithSS-master\StaticFileIssueWithSS\Demo01\PlugIns\GUIServices\Shared\PlugIn.GUIServices.Shared.csproj]

Can you try getting your project to run in VS Code with the latest .NET Core preview so I can run it?

There should not be any dependency on anything Xamarin, I’ve never seen that error message when building with VS 2019…

Instructions for building Blazor Client-Side Application (CSB) for .net Core 3.0 Preview 8 with Visual Studio Code are here: https://docs.microsoft.com/en-us/aspnet/core/blazor/get-started?view=aspnetcore-3.0&tabs=visual-studio-code . In particular, those instructions call for a VS Code extension C# for Visual Studio Code (powered by OmniSharp).

I’ll try to build with Visual Studio Code later tonight, and will let you know how that turns out. Thanks!

I’ve managed to get past the build error by installing the latest VS .NET Update and I’m able to run it, I’ve had to make some changes to Program.cs to get it to run

It was failing this this check:

if (webHostBuilderName=="IntegratedIISInProcessWebHostBuilder") {
    // Create an IntegratedIISInProcess generic host builder
    Log.Debug("in Program.Main: create genericHostBuilder by calling static method CreateGenericHostHostingIntegratedIISInProcessWebHostBuilder");
    genericHostBuilder=CreateGenericHostHostingIntegratedIISInProcessWebHostBuilder();
} else if (webHostBuilderName=="KestrelAloneWebHostBuilder") {
    // Create an Kestrel only generic host builder
    Log.Debug("in Program.Main: create genericHostBuilder by calling static method CreateGenericHostHostingKestrelAloneBuilder");
    genericHostBuilder=CreateGenericHostHostingKestrelAloneBuilder();
} else {
    throw new InvalidDataException(InvalidWebHostBuilderStaticMethodNameExceptionMessage);
}

Which is different to your default so I’ve changed it to:

if (webHostBuilderName=="IntegratedIISInProcessWebHostBuilder" || webHostBuilderName==WebHostBuilderStringDefault) {
...
}

Which gets me past the error, I also had to manually publish each of the GUI projects so the folders you’re trying to add a mapping to exist. I can now get the server running without Exceptions, will look into it further.

Maybe my project is not fully configured, but it’s returning a 404 because you don’t have a default document on the Server’s configured WebRoot. When I added an index.html and set it to copy if newer, it will return the static .html file.

When I have a look at where you’ve added the mapping to, both GUI dist\ folders only contain these 2 folders:

dist\
    _content\
    _framework\

I’m assuming it’s meant to create a full website or something?

Great, I’m able to build using the CLI tools as well. Yes, the GUI projects have to be ‘published’, and, need to use a specific PublishProfile. I’ll be sure to add that to the build instructions in my demos.

When I use this CLI dotnet command to publish: PS C:\Dropbox\whertzing\GitHub\StaticFileIssueWithSS\demo01\GUI\GUI01> dotnet publish /p:PublishProfile=DebugFolderProfile.pubxml then the mapped path contains the following:

The publishing for a client-side Blazor project produces a number of intermediate folders with names very similar to the final folder (bin\Debug\netstandard2.0\publish\GUI\dist), but, the final folder after publishing has everything needed for the Blazor app to run.

The goal is to not serve any files unless the GUIServices plugin is loaded, which is why the demo does not have an index.html file at ContentRootPath

Oh, the DebugFolderProfile.pubxml file is found at StaticFileIssueWithSS\Demo01\GUI\GUI01\Properties\PublishProfiles

But you’ve only added File Mappings in the plugins, you need to add a default document in your Web Root path if you want the default / request to return something. E.g. you could add an index.html in your MemoryVirtualFile Source in one of your plugins implementing IPostInitPlugin:

public void AfterPluginsLoaded(IAppHost appHost)
{
    var memFs = appHost.VirtualFileSources.GetMemoryVirtualFiles();
    memFs.WriteFile("index.html", "<h1>Test Page</h1>");
}

Otherwise change Config.DefaultRedirectPath to redirect to one of your plugins mapped paths.

I’m trying to get the url /01/ to return the index.html found in GUI01\bin\Debug\netstandard2.0\publish\GUI\dist, and the url /02/ to return the different index.html found at GUI02\bin\Debug\netstandard2.0\publish\GUI\dist. The index.html files do already exist at the base of each FileSystemMapping. The example screenshot of the GUI01 publish above showsindex.html present in the base of the physicalpath used for mapping the virtual path /01/. Perhaps I need to add a global response filter that says “if the response status code is 404 and the request URL is for the base of a mapped file system (e…g. /01/), return the index.html file found at the base of the physicalpath for that mapped file system.”? I’ll see if I can figure out how to get what I need by writing such a filter next…
As always thank you very much for your exceptional customer support via this forum!

This is likely best achieved with a FallbackRoute.

Further experimentation shows that the static file delivery works as expected (including delivering index.html) if the code calls this.AddVirtualFileSources.Add(new FileSystemMapping("01", "C:\longphysicalpath")); from the overridden Configure method of the AppHostBase, but the same call to appHost.AddVirtualFileSources.Add(new FileSystemMapping("01", "C:\longphysicalpath")); fails to add a VirtualFileSource to the VirtualFileSources collection if the code is called from within a PlugIn’s Configure(IAppHost appHost)) or AfterPluginsLoaded(IAppHost appHost) methods. Is it possible to add a FileSystemMapping to the VirtualFileSources collection from within a PlugIn?

Have your Plugins implement IPreInitPlugin to add Virtual FIle Sources in Plugins:

public void BeforePluginsLoaded(IAppHost appHost)
{
    appHost.AddVirtualFileSources.Add(
        new FileSystemMapping("01", "C:\longphysicalpath")); 
}

Thank you! Now my code serves the multiple static files for multiple Blazor GUIs from the physicalpaths defined in a GUIMapping collection POCO loaded from the plugin’sAppSettings. All code for this is now in the BeforePluginsLoaded method of the GUIServices PlugIn.

1 Like