IIS ASP.NET 6 startup throws System.IO.DirectoryNotFoundException: D:\agent\_work\38\s\IdentityServer\wwwroot\ - asp.net-core

We are updating one of our applications, in this case IdentityServer, from .NET 5 to .NET 6. It is being hosted by IIS and deployed by Azure Devops Services. The issue we are seeing is that on our development environment the website fails to load but on our staging environment it runs just fine. The error we are seeing on development is
12:45:37.519|Fatal|1||Host terminated unexpectedly.||
System.IO.DirectoryNotFoundException: D:\agent\_work\38\s\IdentityServer\wwwroot\
at Microsoft.Extensions.FileProviders.PhysicalFileProvider..ctor(String root, ExclusionFilters filters)
at Microsoft.Extensions.FileProviders.PhysicalFileProvider..ctor(String root)
at Microsoft.AspNetCore.Hosting.StaticWebAssets.StaticWebAssetsLoader.<>c.<UseStaticWebAssetsCore>b__1_0(String contentRoot)
at Microsoft.AspNetCore.StaticWebAssets.ManifestStaticWebAssetFileProvider..ctor(StaticWebAssetManifest manifest, Func`2 fileProviderFactory)
at Microsoft.AspNetCore.Hosting.StaticWebAssets.StaticWebAssetsLoader.UseStaticWebAssetsCore(IWebHostEnvironment environment, Stream manifest)
at Microsoft.AspNetCore.Hosting.StaticWebAssets.StaticWebAssetsLoader.UseStaticWebAssets(IWebHostEnvironment environment, IConfiguration configuration)
at Microsoft.AspNetCore.WebHost.<>c.<ConfigureWebDefaults>b__9_0(WebHostBuilderContext ctx, IConfigurationBuilder cb)
at Microsoft.AspNetCore.Hosting.GenericWebHostBuilder.<>c__DisplayClass9_0.<ConfigureAppConfiguration>b__0(HostBuilderContext context, IConfigurationBuilder builder)
at Microsoft.Extensions.Hosting.HostBuilder.BuildAppConfiguration()
at Microsoft.Extensions.Hosting.HostBuilder.Build()
at IdentityServer.Program.Main(String[] args) in D:\agent\_work\38\s\IdentityServer\Program.cs:line 23
The path it reports, D:\agent\_work\38\s\IdentityServer\wwwroot\ is interesting because that path is the same as the path from the DevOps build machine. We don't see this error if we revert back to .NET 5 and we don't see the problem on our staging machine.
The Program.cs class is defined as
using System;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using NewRelic.LogEnrichers.Serilog;
using Serilog;
using Serilog.Events;
namespace IdentityServer
{
public class Program
{
public static int Main(string[] args)
{
try
{
CreateLogger();
Log.Information("Starting host...");
CreateHostBuilder(args).Build().Run();
return 0;
}
catch (Exception ex)
{
Log.Fatal(ex, "Host terminated unexpectedly.");
return 1;
}
finally
{
Log.CloseAndFlush();
}
}
public static void CreateLogger()
{
var configuration = GetConfiguration();
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.Enrich.FromLogContext() // allows logging middleware to inject output values
.Enrich.WithThreadId()
.Enrich.WithNewRelicLogsInContext()
.CreateLogger();
}
public static IHostBuilder CreateHostBuilder(string[] args)
{
var configuration = GetConfiguration();
return Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(
webBuilder =>
{
webBuilder.UseConfiguration(configuration);
webBuilder.UseSerilog();
webBuilder.UseIIS();
webBuilder.CaptureStartupErrors(true);
webBuilder.UseStartup<Startup>();
});
}
private static IConfiguration GetConfiguration()
{
var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
var builder = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.AddJsonFile($"appsettings.{environment}.json", true, true);
var configuration = builder.Build();
return configuration;
}
}
}
We do have other .NET 6 web applications running just fine on this instance of IIS. I was thinking that the problem might be in our release pipelines but they are identical in their task configurations between the environments. Tried looking for the directory path in the code or configuration but don't see it anywhere. Have tried manually setting the WebRoot and ContentRoot paths via .UseWebRoot("path to folder") and .UseContentRoot("path to folder") in the Program.cs but didn't see any change in the logs or the app starting.
Even updated the web.config file to have the exact path for executing the project dll in the aspNetCore element but still no change.
Update 10 Feb 2022
Added debug output to the startup to verify file and folder paths. Everything in the environment variables and execution file path look correct.
ASPNETCORE_IIS_PHYSICAL_PATH - C:\inetpub\webapps\IdentityServer\
Executable Path: C:\inetpub\webapps\IdentityServer\IdentityServer.dll

The problem ended up being how we were pushing our updates out to the servers from DevOps. Our pipelines were built to copy over files out of the Release directory of the build folder. One of the problems with this approach is that files not needed for a site to run but generated during a build are also copied to the release server. In this case, a new file which is generated in .NET 6, .staticwebassets.runtime.json, was getting copied to our servers.
The way .NET 6 seems to behave is that if the environment is set to Development then it will look for this file to figure out where the static web assets are located. If the file doesn't exist then it will assume the files are in a wwwroot sub-directory of the site. This makes sense for instances where you are running the project from your local Visual Studio. More details about this file are available in another SO post with links to the source code in GitHub. To fix our problem we changed our release pipeline to use the publish.zip file that is generated when you run the publish command on a solution. The archive only contains the files needed to run the site, so none of the extraneous files like .staticwebassets.runtime.json are included. We should have been doing this the whole time... lesson learned.
We now unzip the publish.zip file, apply any file transformations, then copy the unzipped files to the web server.

Related

appsettings.json file not in .net core console project

I understand that .net core has replaced the app.config file with appsetting.json. However this file seems to be added for ASP.net projects only. In fact it is not even available in the add items list.
I found this post that list packages needed to be added:
Microsoft.Extensions.Configuration
Microsoft.Extensions.Configuration.FileExtensions
Microsoft.Extensions.Configuration.Json
I added all these and it does give me the option of adding a json configuration file but still not the App Settings File which is only available under ASP.Net Core.
I am trying to understand why, doesn't a non web project need configuration and what is the recommended way to configure a .net core console application.
Thanks in advance.
Non-web project may or may not need configuration. But, as you noticed, Visual Studio doesn't scaffold console projects with appsettings.json. Obviously, you can add it to the project as json file. Once you have it, the challenge is to make use of it. I frequently use Configuration object and dependency injection in Entity Framework utilities.
For example,
public static class Program
{
private static IConfigurationRoot Configuration { get; set; }
public static void Main()
{
IConfigurationBuilder builder = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.AddEnvironmentVariables();
Configuration = builder.Build();
IServiceCollection services = new ServiceCollection();
services.AddDbContext<MyDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddScoped<IMyService, MyService>();
IServiceProvider provider = services.BuildServiceProvider();
IMyService myService = provider.GetService<IMyService>();
myService.SomeMethod();
}
public class TemporaryDbContextFactory : IDesignTimeDbContextFactory<MyDbContext>
{
public MyDbContext CreateDbContext(string[] args)
{
IConfigurationBuilder configBuilder = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.AddEnvironmentVariables();
IConfigurationRoot configuration = configBuilder.Build();
DbContextOptionsBuilder<MyDbContext> builder = new DbContextOptionsBuilder<MyDbContext>();
builder.UseSqlServer(configuration.GetConnectionString("DefaultConnection"));
return new MyDbContext(builder.Options);
}
}
}
This allows me to both run migrations and console-based utilities against DbContext. You don't specify what kind of configuration you are going to need - so this is just one example. But hopefully, you can adjust it to your needs.

Force the usage of a bin folder

I have a larger ASP.NET Core project which means that a lot of DLLs are included in the publish.
Since all of the DLLs are placed in the root folder it's cumbersome to navigate the folder structure (to mange configs etc) due to the sheer amount of files.
Is it possible to tell ASP.NET Core that it should load all assemblies from another folder (bin\)?
I would do it in opposite side. If your problem is just config files then relocate them into config folder and keep them there. As of now dotnet will publish your project + framework (if you use self contained flag).
You can configure aspnetcore to use files
config.AddJsonFile("config/appsettings.json", optional: false, reloadOnChange: false);
So then that folder will be on top and better accessible
Yes in root folder there still be web.config but in my project that file usually is same for all environments. But again it depends where you deploy, because if you deploy to non IIS environment then you don't even need it
Hi How about the Managed Extensibility Framework , It allows you load assemblies dynamically.
Use BuildManager to load assemblies dynamically,
string pluginPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "plugins");
foreach (string f in Directory.GetDirectories(pluginPath))
{
string binPath = Path.Combine(f, "bin");
if (Directory.Exists(binPath))
{
foreach (String file in Directory.GetFiles(binPath, "*.dll"))
{
Assembly a = Assembly.LoadFrom(file);
BuildManager.AddReferencedAssembly(a);
}
}
Resolve assemblies using below code,
protected virtual void Application_Start(object sender, EventArgs e)
{
//...
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
}
System.Reflection.Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
var currentAssemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (var assembly in currentAssemblies)
{
if (assembly.FullName == args.Name || assembly.GetName().Name == args.Name)
{
return assembly;
}
}
return null;
}

How to configure multiple ASPNETCORE_ENVIRONMENT on same machine?

I have ASP.NET core web application. I have configured the web application on our web server and set the ASPNETCORE_ENVIRONMENT variable to Development. I set this variable at machine level like shown in the picture below.
Now on the same machine i want to configured one more instance of same web application as Staging environment.
What are my options here to set ASPNETCORE_ENVIRONMENT at application level instead of machine level? so i can host multiple instances of the same application on the same machine?
You have a couple of options.
Run each app as a different user, and set the environment variable within that user profile. This gives you a nice added security bonus. You'll have to set the app pool to load the user profile.
Use IIS configuration
Start IIS manager
Choose configuration editor Pull down the section
combobox and choose system.webServer/aspNetCore
Pull down the from
combobox and choose Applicationhost.config
Click on the
environmentVariables element and click on the ... button hiding in
the second column, at the right.
Set your environment variables.
Exit out of the environment variables screen and then click Apply.
Restart the app pool/app.
Can you change the code parsing configuration running on the web server? That's what I would recommend doing. That would allow you to configure your environment more naturally in a Windows setting.
While the traditional way to configure the IHostingEnvironment.EnvironmentName variable is via the ASPNETCORE_ENVIRONMENT environment variable as you have done, you can change how ASP.NET Core parses its configuration such that you can set the variable via a command line argument.
To get into specifics...
By default, the Program.cs file emitted by the dotnet new -t web command looks something like the following:
public static void Main(string[] args) {
var host = new WebHostBuilder()
.UseKestrel()
.UseUrls("http://0.0.0.0:5000")
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.Build();
host.Run();
}
This makes ASP.NET Core use the default configuration processing (environment variables with a ASPNETCORE_ prefix) to determine the value of IHostingEnvironment.EnvironmentName, which you are using to configure how your application runs.
Fortunately, you can alter the way that ASP.NET Core parses configuration by utilizing the UseConfiguration() extension method on WebHostBuilder. Here's an example of using custom configuration with the default implementation:
public static void Main(string[] args) {
var configuration =
new ConfigurationBuilder()
.AddEnvironmentVariables("ASPNETCORE_")
.Build();
var host =
new WebHostBuilder()
.UseConfiguration(configuration)
.UseKestrel()
.UseUrls("http://0.0.0.0:5000")
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.Build();
host.Run();
}
From here, I would change it so it can use the command line in addition to the ASPNETCORE_ prefixed environment variables. This will allow you to easily run your application with whatever environment name you want, like so:
public static void Main(string[] args) {
var configuration =
new ConfigurationBuilder()
.AddEnvironmentVariables("ASPNETCORE_")
.AddCommandLine(args)
.Build();
var host =
new WebHostBuilder()
.UseConfiguration(configuration)
.UseKestrel()
.UseUrls("http://0.0.0.0:5000")
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.Build();
host.Run();
}
Then, when you start your dotnet core application with dotnet run, you can set the environment on the command line, like this:
dotnet run environment=development
dotnet run environment=staging
Now the ASPNETCORE_ENVIRONMENT environment variable will still be respected, but you can override it via the command line when you are doing local development. As a note, you will need to include the Microsoft.Extensions.Configuration.CommandLine nuget package to your project.json file if you have no already done so to get the AddCommandLine() extension method.

How do we set ContentRootPath and WebRootPath?

We're ending up with the following ContentRoot and WebRoot when we run our app from IIS.
ContentRoot: C:\MyApp\wwwroot
WebRoot: C:\MyApp\wwwroot\wwwroot
Here is how we are setting ContentRoot and WebRoot.
public class Startup
{
private readonly IHostingEnvironment _hostingEnv;
public Startup(IHostingEnvironment hostingEnv)
{
_hostingEnv = hostingEnv;
}
public void Configure(IApplicationBuilder app)
{
app.Run(context =>
{
// test output
context.Response.WriteAsync(_hostingEnv.ContentRootPath + "\r\n");
return context.Response.WriteAsync(_hostingEnv.WebRootPath + "\r\n");
});
}
public static void Main(string[] args)
{
var contentRoot = Directory.GetCurrentDirectory();
var webRoot = Path.Combine(contentRoot, "wwwroot");
var host = new WebHostBuilder()
.UseKestrel()
.UseIISPlatformHandlerUrl()
.UseContentRoot(contentRoot) // set content root
.UseWebRoot(webRoot) // set web root
.UseStartup<Startup>()
.Build();
host.Run();
}
}
From intellisense I see that...
ContentRootPath contains the application content files.
WebRootPath contains the web-servable content files.
How do we make the test output look instead like this:
ContentRoot: C:\MyApp\
WebRoot: C:\MyApp\wwwroot\
While RC2 documentation is still being prepared, here is what I learned while trying to deploy pre-RC2 app as Azure Web App:
There is no Visual Studio tooling yet, so the app must be published and deployed manually over FTP. For publishing, use: dotnet publish --configuration Release --output ./approot
If connected to Azure over FTP, you will probably see something similar to:
The "approot" folder can be replaced with the published one (the web.config is left in the approot).
The "approot" must be configured as a virtual application in Azure Portal (the default was site\wwwroot):
An the last thing to get static files served from the wwwroot folder, the Startup.cs file should be modified to include custom UseWebRoot call:
var currentDirectory = Directory.GetCurrentDirectory();
var host = new WebHostBuilder()
.UseKestrel()
.UseWebRoot(Path.Combine(currentDirectory, "..", "wwwroot"))
.UseDefaultHostingConfiguration(args)
.UseIISIntegration()
.UseStartup<Startup>()
.Build();
After these steps you should have ASPNET Core pre-RC2 web app running on Azure.
In RC2, if we put the web.config beside wwwroot and point IIS at the MyApp directory like this...
MyApp
web.config
wwwroot
...the code from the original question outputs this...
ContentRoot: C:\MyApp\
WebRoot: C:\MyApp\wwwroot\

ASP.NET Core ignores ASPNET_ENV and Hosting:Environment

No matter when or where I set either ASPNET_ENV or Hosting:Environment the startup code will always enter
//This method is invoked when ASPNET_ENV is 'Production'
//The allowed values are Development,Staging and Production
public void ConfigureProduction(IApplicationBuilder app, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(minLevel: LogLevel.Warning);
Configure(app);
}
What I've tried so far
Set Hosting:Environment to Development in the project properties
Set ASPNET_ENV to Development in the project properties
Set Hosting:Environment to Development in launchSettings.json
Set ASPNET_ENV to Development in launchSettings.json
Set ASPNET_ENV to Development in code via Environment.SetEnvironmentVariable("ASPNET_ENV", "Development"); in the Startup method before the call to ConfigurationBuilder.GetEnvironmentVariables()
This is version 1.0.0-rc2-20143 by the way. Am I missing something here or is it just a bug?
The environment variable name has been changed to ASPNETCORE_ENVIRONMENT as part of the name change.
It was announced in this issue and change in this PR.
If you have not done it already, try adding the environment variables to the configuration instance.
public static void Main(string[] args)
{
var builder = new ConfigurationBuilder();
builder.AddJsonFile("appsettings.json");
builder.AddEnvironmentVariables();
var config = builder.Build();
}
Whilst the answer from Henk Mollema is correct for the environment variable name, I still encountered issues where dotnetcore would seem to ignore the environment variable and use appsettings.json when run from command line.
To ensure it uses the correct environment and app settings, try the following:
Change the environment variable via Project Properties -> Debug -> Environment Variables -> ASPNETCORE_ENVIRONMENT -> Value : Change the value to match your intended environment eg Staging
Build Solution
From commandline, type SETX ASPNETCORE_ENVIRONMENT "YOUR-ENVIRONMENT-NAME" where replace "YOUR-ENVIRONMENT-NAME" to match what you set in step 1 eg Staging and ensure you include the "quotes"
IMPORTANT - make sure you all your various appsettings.json eg appsettings.staging.json is present in the project directory where you will run it. (In the case of a published solution, the appsettings.staging.json may not have been copied, so ensure it's there)
From Commandline, go to your project directory (or published directory) and run your project by typing dotnet run (or "dotnet YOURPROJECTNAME.DLL" for published projects)
Observe in the next line that appears in the commandline which states Hosting environment: YOURENVIRONMENTNAME eg Hosting environment:staging
Running a DotNetCore project from Visual Studio always picked the
correct appsettings based on the environment set in your project
properties, however these are the steps I followed to run dotnet core correctly from commandline
working correctly.
This worked for me using .net core 2.0 and building a web api. Notice the 2 different methods of accessing the environment variable.
Program.cs
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}
public static IWebHost BuildWebHost(string[] args){
if(Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT ") == "Production"){
return WebHost.CreateDefaultBuilder(args)
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.Build();
} else {
return WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();
}
}
But in the startup file there is another environment option
Startup.cs
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options => options.AddPolicy("AllowAll", p => p.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()));
var connectionString = "Data Source=tcp:<some-ip>,<some-port>;Initial Catalog=<some database>;Integrated Security=False;User Id=SA;Password=<some password>;MultipleActiveResultSets=True";
services.AddDbContext<myContext>(opt => opt.UseSqlServer(connectionString));
services.AddMvc();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseCors("AllowAll");
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseMvc();
}
}