jannikb
jannikb

Reputation: 524

How to embed a SPA into an ASP.NET Core library and serve it from a path

Szenario

I want to build an aspnetcore library/module that includes a small SPA frontend. I.e. the html/js/css files should come along with the dll. The SPA should be served from a specific path, i.e. /some-module (does not need to be configurable). The SPA consists of several html/js/css files, lets assume following files:

/my-module/index.html
/my-module/index.js
/my-module/index.css

These urls should just server the files. all other paths such as

/my-module/
/my-module/page-1
/my-module/page-2

should serve the index.html, so that client-side routing can handle the requests.

I have a partial solution, where the SPA files are served. However I don't know how to make all other subpaths i.e. /my-module/* (excluding the files) to return index.html.

Partial Solution using embedded files and UseFileServer:

Assuming the SPA build files are located in /my-library/web/build/. The following csproj embeds these files into the dll using GenerateEmbeddedFilesManifest and EmbeddedResource:

my-module.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netcoreapp2.2</TargetFramework>
    <GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
    <SpaRoot>web\</SpaRoot>
    <DefaultItemExcludes>$(DefaultItemExcludes);$(SpaRoot)node_modules\**</DefaultItemExcludes>
  </PropertyGroup>

  <ItemGroup>
    <Folder Include="web\" />
  </ItemGroup>

  <ItemGroup>
    <EmbeddedResource Include="web\build\**" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="2.2.0" />
    <PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="2.2.0" />
    <PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.2.0" />
  </ItemGroup>

</Project>

The following middleware configuration serves these embedded files using the ManifestEmbeddedFileProvider:

        public static void UseMyModuleUi(this IApplicationBuilder app)
        {
            app.UseFileServer(new FileServerOptions
            {
                RequestPath = "/my-module",
                FileProvider = new ManifestEmbeddedFileProvider(
                    assembly: Assembly.GetAssembly(typeof(ServiceExtensions)), "web/build")
            });
        }

Calling the UseMyModuleUi extension method in some aspnetcore application that references my module, this does indeed serve the files under /my-module/index.html /my-module/index.js /my-module/index.css

even /my-module/ serves my-module/index.html.

However all other paths under /my-module/ such as my-module/page-1 do not serve index.html.#

Alternatives I tried

I tried to use some combinations of UseSpaStaticFiles or UseStaticFiles and Map, however with no good results.

The project I am build is available here: https://github.com/jannikbuschke/ef-configuration-provider the ui project is the library that embedds a SPA. The sample project is the host application.

Upvotes: 2

Views: 1394

Answers (1)

user2900970
user2900970

Reputation: 761

The following should work for you:

    public static void UseMyModuleUi(this IApplicationBuilder app)
    {
        app.Map("/my-module", builder => 
        {
            var provider = new ManifestEmbeddedFileProvider(
                assembly: Assembly.GetAssembly(typeof(ServiceExtensions)), "web/build"); 
            builder.UseStaticFiles(new StaticFileOptions 
            {
                FileProvider = provider
            });
            builder.Run(async context =>
            {
                await context.Response.SendFileAsync(provider.GetFileInfo("index.html"));
            });
        });
    }
  1. Map defines a new branch that gets executed when the request starts with the given path and removes that path segment from the for this branch.
  2. The static file provider will match every file that exists in the ManifestEmbeddedFileProvider
  3. In order to serve the index.html for every non-matching request, Run defines a delegate that always returns the given file. Thats your so called catch-all for this branch.

Upvotes: 2

Related Questions