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:
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
EDIT 2
To get a reproducible project:
Visual Studio 2019 -> New Solution -> ASP.NET Core Web App with the configuration below
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
And this is the content of extracted wwwroot
folder on the temp path
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.
Static files, such as HTML, CSS, images, and JavaScript, are assets an ASP.NET Core app serves directly to clients by default.
Static files are stored within the project's web root directory. The default directory is {content root}/wwwroot , but it can be changed with the UseWebRoot method. For more information, see Content root and Web root.
To serve static files from an ASP.NET Core app, you must configure static files middleware. With static files middleware configured, an ASP.NET Core app will serve all files located in a certain folder (typically /wwwroot).
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.
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\**\*" />
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With