9

I'm trying to create a single file asp.net core 5 web app. Goal is to have a single .exe file, run the Kestrel server by executing this exe file and load the page in the browser.

I created an ASP.NET Core 5 template app in VS 2019. Then using cli I run this command:

dotnet publish -c Release -r win-x64 /p:PublishSingleFile=true \n 
/p:IncludeNativeLibrariesForSelfExtract=true --self-contained true

This generates an exe file, which when I copy elsewhere, runs without a problem. But when I browse the page, none of the static files are loaded:

enter image description here

What would be the proper way of generating a single file asp.net core app, so it loads static content ?

EDIT

As requested, putting here the screenshot of the output after the publish

enter image description here

EDIT 2

To get a reproducible project:

Visual Studio 2019 -> New Solution -> ASP.NET Core Web App with the configuration below

enter image description here

EDIT 3

Thanks to the answer by @JHBonarius, I changed the Program.cs to set ContentRoot to a temp folder where wwwroot content is getting extracted.

public class Program
    {
        public static void Main(string[] args)
        {
            var path = Path.Combine(Path.GetTempPath(), ".net", typeof(Program).Assembly.GetName().Name);

            var directory = 
                Directory
                    .GetDirectories(path)
                    .Select(path => new DirectoryInfo(path))
                    .OrderByDescending(di => di.LastWriteTime)
                    .First();

            CreateHostBuilder(args)
                .UseContentRoot(directory.FullName)
                .Build()
                .Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)            
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }

While I appreciate that this seems like a hack (I couldn't find any official documentation for this path), I wanted to have some working code.

With these changes page now loads static resources, not all of them though.

This is the content of wwwroot folder in the solution explorer

enter image description here

And this is the content of extracted wwwroot folder on the temp path

enter image description here enter image description here

As can be seen js/css folders are missing altogether as well as jquery-validation & jquery-validation-unobtrusive folders.

Any clue what's going on ?

I created a github repo with latest changes.

15
  • Does it work if you publish the app without PublishSingleFile option? Commented Nov 10, 2021 at 20:00
  • Can you show the screenshot about your single .exe file ? I mean I want to see your content in your website. Commented Nov 11, 2021 at 8:06
  • And you also can try to set ItemGroup to contain static files when deploy. Commented Nov 11, 2021 at 8:07
  • @JasonPan see edited post Commented Nov 11, 2021 at 11:38
  • @AndrewSilver no, in that case I don't have a single file. Commented Nov 11, 2021 at 11:41

4 Answers 4

7
+200

I tried to reproduce your problem on new asp net core empty project and it works fine.

Perhaps Startup is missing some configuration.

Here's what I did.

csproj

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>
</Project>

Startup.cs

public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            app.UseDefaultFiles();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseEndpoints(endpoints => {});
        }
    }

wwwroot

Similar to your scenario, there is also bootstrap.

wwwroot content

index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>TEST</title>
    <link href="css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <script src="js/bootstrap.min.js"></script>
</body>
</html>

-- Update 29/11/2021

My expectation was that .exe will be a self-contained bundle and upon execution it would extract static files and be able to resolve references to it. That doesn't seem to be the case.

Well, then you'd have to mark everything in wwwroot as embedded resource

<PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
   <GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
</PropertyGroup>

<ItemGroup>
    <Compile Remove="wwwroot\**" />
    <Content Remove="wwwroot\**" />
     <EmbeddedResource Include="wwwroot\**\*">
      <Link>wwwroot\%(RecursiveDir)%(Filename)%(Extension)</Link>
      <LogicalName>wwwroot\%(RecursiveDir)%(Filename)%(Extension) 
      </LogicalName>
    </EmbeddedResource>
    <None Remove="wwwroot\**" />
</ItemGroup>

And perform file extraction in Configure(IApplicationBuilder, IWebHostEnvironment) method

var basePath = AppDomain.CurrentDomain.BaseDirectory;
var wwwrootPath = Path.Combine(basePath, "wwwroot");

if (!Directory.Exists(wwwrootPath))
{
    var assembly = typeof(Startup).Assembly;
    Directory.CreateDirectory(wwwrootPath);

    var resourcePaths = assembly.GetManifestResourceNames()
        .Where(rnn => rnn.Contains("wwwroot"))
        .ToList();

    foreach (var resourcePath in resourcePaths)
    {
        var fileName = resourcePath;
        var filePath = Path.Combine(basePath, fileName);
        var fileInfo = new System.IO.FileInfo(filePath);
        fileInfo.Directory.Create();
        using var stream = File.Create(filePath);
        using var resourceStream = assembly.GetManifestResourceStream(resourcePath);

        resourceStream.CopyTo(stream);
    }
};

If you don't like this approach you might consider EmbeddedFileProvider

Startup.cs:Configure

app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new EmbeddedFileProvider(
        assembly: typeof(Startup).Assembly, 
        baseNamespace: "TestApp.wwwroot"),
});

app.UseEndpoints(endpoints =>
{
    endpoints.MapGet("/", async http => { http.Response.Redirect("/index.html"); });
});

csproj

<EmbeddedResource Include="wwwroot\**\*" />
Sign up to request clarification or add additional context in comments.

6 Comments

cześć Józef, do you have wwwroot folder in your output (like can be seen in my edit) ? can you copy your exe file elsewhere (only exe file) and try to run the app ? If I run the exe file from the output folder, because wwwroot folder contains all the static files, it runs fine (because it can resolved references to static files). My expectation was that .exe will be a self-contained bundle and upon execution it would extract static files and be able to resolve references to it. That doesn't seem to be the case.
Updated answer. Regardless which option you choose you need to set embedded resource build action for wwwroot folder and perform some tricks in Startup.cs:Configure method.
EmbeddedFileProvider" didn't work, that's the cleanest option imo. As for the first approach - that didn't work either, wwwroot` gets embedded (I don't see it in the output folder anymore), but copying exe file elsewhere and running the server still can't resolve references to static files.
I'm surprised EmbeddedFileProvider does not work. Are you sure you specified the right base namespace and configuration in csproj? That really matters. The last resort would be to create custom IFileProvider, implement it similarly to what EmbeddedFileProvider does and put additional logs when it tries to retrieve resource.
Ive seen your github repo and you don't have to do much to enable EmbeddedFileProvider. Its one config in csproj and right base namespace SingleFileApp.wwwroot
|
3

I found the solution here

Add the following to the csproj

<ItemGroup>
    <Content Update="wwwroot\**" ExcludeFromSingleFile="false">
        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </Content>
</ItemGroup>

Because what happens, is that the .exe contains an archive, which is extracted to a temporary directory.

However, this only works for .NET Core (3.1). They changed the behavior in .NET 5, where most dependencies are now directly loaded from the .exe or memory. So the content is now considered an external dependency, and the content path is set to the excuting directory, instead of the temporary directory.

The solution I mention will still include the wwwroot directory in the .exe and extract it to the temporary path, but since the content path is not pointing there, the content is not found.

You can change the content path using .UseWebRoot("[path]"). However, I haven't found a way to get the path of the temp directory.

edit: there's a whole other option: put the wwwroot files in a zip file, add the zip file as Embedded Resource, and serve it using a static file provider.

2 Comments

Thanks for this ! See my edited post, this helped a lot.
Upvoted. Much simpler solution than what I proposed. If .net version doesnt matter ( I'm conscious about .net core 3.1 end of support which ends roughly in one years time ) I would use it.
0

I might reproduced you issue.

I published the project, and open a command line at an irrelevant folder, open the exe with full path. Then the static resource are not loaded.

You can try to double click the exe, check if it works.

To fixed the issue:

Add Directory.SetCurrentDirectory(AppDomain.CurrentDomain.BaseDirectory); in the Program.cs

1 Comment

Tried it, didn't work. Be more specific. Note: there shouldn't be any "wwwroot" directory in the directory where the .exe file is!
0

According to requests all static files were requested without wwwroot path so either configure your html & js and add wwwroot in path or explicitly map wwwroot content like this:

app.UseStaticFiles(new StaticFileOptions()
{
    FileProvider = new PhysicalFileProvider( Path.Combine(Directory.GetCurrentDirectory(), @"wwwroot")),
    RequestPath = new PathString("/wwwroot")  // OR use "/" to map to base URI
});

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.