dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.2k stars 9.95k forks source link

Unable to call StaticWebAssetsLoader.UseStaticWebAssets after upgrading to VS 2022 official release #38212

Closed JayCallas closed 9 months ago

JayCallas commented 2 years ago

Have a Blazor Server .NET 6 application where my local development environment uses the name "Local" instead of the default "Development". Out-of-the-box, the application is returning 404 for the static content like CSS files. Admittedly, I am having problems finding the official suggestions at this time but the suggestions out there state that you need to manually call StaticWebAssetsLoader.UseStaticWebAssets() for the environments other than Development.

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureAppConfiguration((ctx, cb) => {
    // https://github.com/dotnet/aspnetcore/blob/main/src/DefaultBuilder/src/WebHost.cs#L219
    if (ctx.HostingEnvironment.IsEnvironment("Local")) {
        // This call inserts "StaticWebAssetsFileProvider" into the static file middleware
        StaticWebAssetsLoader.UseStaticWebAssets(ctx.HostingEnvironment, ctx.Configuration);
    }
});

This has been working with both ASP.NET 5 and (up until yesterday) with ASP.NET 6. Once I upgraded from latest Visual Studio 2022 Preview to Visual Studio 2022 Current build, I am getting the following exception on application startup.

Unhandled exception. System.NotSupportedException: The web root changed from "C:\Development\Repos\Staging\ABC\ABC.UI\wwwroot" to "C:\Development\Repos\Staging\ABC\ABC.UI\". Changing the host configuration using WebApplicationBuilder.WebHost is not supported. Use WebApplication.CreateBuilder(WebApplicationOptions) instead.
   at Microsoft.AspNetCore.Builder.ConfigureWebHostBuilder.ConfigureAppConfiguration(Action`2 configureDelegate)
   at Program.<Main>$(String[] args) in C:\Development\Repos\Staging\ABC\ABC.UI\Program.cs:line 25

C:\Development\Repos\Staging\ABC\ABC.UI\bin\Debug\net6.0\ABC.UI.exe (process 27360) exited with code -532462766.

The exception is not being caused by the call to StaticWebAssetsLoader.UseStaticWebAssets(), it seems to be raised due to the outer call to WebHost.ConfigureAppConfiguration(). I understand that the new recommended way of changing options is to create a new instance of WebApplicationOptions and pass that in.

var opt = new WebApplicationOptions() {
    Args = args
};

var builder = WebApplication.CreateBuilder(opt);

But in this case, I am not trying to set any of those options. In essence, how do I call StaticWebAssetsLoader.UseStaticWebAssets() from outside ConfigureAppConfiguration while passing in the appropriate context and configuration as arguments?

Using a global.json file I was able to confirm that this approach works with 6.0.100-rc.2.21505.57 but is now broken with 6.0.100.

JayCallas commented 2 years ago

I seem to have found a solution to this issue but would appreciate feedback on this approach.

Instead of calling this

builder.WebHost.ConfigureAppConfiguration((ctx, cb) => {
    // https://github.com/dotnet/aspnetcore/blob/main/src/DefaultBuilder/src/WebHost.cs#L219
    if (ctx.HostingEnvironment.IsEnvironment("Local")) {
        // This call inserts "StaticWebAssetsFileProvider" into the static file middleware
        StaticWebAssetsLoader.UseStaticWebAssets(ctx.HostingEnvironment, ctx.Configuration);
    }
});

I now have

if (builder.Environment.IsEnvironment("Local")) {
    // Was thrown off since I did not realize that ConfigurationManager implemented IConfiguration and that builder.Configuration can be passed in
    StaticWebAssetsLoader.UseStaticWebAssets(builder.Environment, builder.Configuration);
}

Thoughts?

mkArtakMSFT commented 2 years ago

Thanks for contacting us. @davidfowl, @captainsafia can you please confirm whether the solution @JayCallas has landed on is reasonable? Thanks! //cc @javiercn

adityamandaleeka commented 2 years ago

@captainsafia It sounds like you have started looking at this so I'm assigning this to you. Feel free to redirect as needed.

halter73 commented 2 years ago

Thanks for contacting us. @davidfowl, @captainsafia can you please confirm whether the solution @JayCallas has landed on is reasonable? Thanks! //cc @javiercn

The workaround looks reasonable to me. Both the code with and without builder.WebHost should do the same thing just looking at it.

captainsafia commented 2 years ago

@JayCallas Can you share your complete Program.cs? The exception that you shared seems to indicate that the WebRootPath is being unset at some point. Is there a chance you have another piece of code that is modifying the value of WebRootPath before you invoke ConfigureAppConfiguration?

ghost commented 2 years ago

Hi @JayCallas. We have added the "Needs: Author Feedback" label to this issue, which indicates that we have an open question for you before we can take further action. This issue will be closed automatically in 7 days if we do not hear back from you by then - please feel free to re-open it if you come back to this issue after that time.

JayCallas commented 2 years ago

@captainsafia Unsure if it makes sense to share the entire Program.cs...the call I was making was literally the first lines of the file...but here you go??? Stepping through the code raised the exception at the very beginning...before anything would even have a chance to modify the path (even if we wanted to).

using System.IdentityModel.Tokens.Jwt;

using BlazorDownloadFile;

using Sandbox.Configuration;
using Sandbox.Consumers;
using Sandbox.HealthChecks;
using Sandbox.Services;
using Sandbox.Settings;

using CV.Common.Logging;
using CV.Common.Logging.ApplicationInsights;

using Ganss.XSS;

using MassTransit;

using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Hosting.StaticWebAssets;

using Syncfusion.Blazor;

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureAppConfiguration((ctx, cb) => {
    // https://github.com/dotnet/aspnetcore/blob/main/src/DefaultBuilder/src/WebHost.cs#L219
    if (ctx.HostingEnvironment.IsEnvironment("Local")) {
        // This call inserts "StaticWebAssetsFileProvider" into the static file middleware
        StaticWebAssetsLoader.UseStaticWebAssets(ctx.HostingEnvironment, ctx.Configuration);
    }
});

// ** Settings

var ssoSettings = new SsoSettings();
builder.Configuration.GetSection(SsoSettings.SSO).Bind(ssoSettings);

var rabbitSettings = new RabbitMqSettings();
builder.Configuration.Bind("RabbitMq", rabbitSettings);

var appSettings = new ApplicationSettings();
builder.Configuration.Bind(appSettings);
builder.Services.AddSingleton<ApplicationSettings>(appSettings);

// ** Logging

var loggingSettings = new CvLoggingSettings() {
    ApplicationName = "Sandbox"
};
builder.Services.AddSingleton(loggingSettings);

builder.Services.AddApplicationInsightsTelemetry();

builder.Services.AddSingleton<ITelemetryInitializer, CvTelemetryInitializer>();

builder.Services.AddHttpContextAccessor();
builder.Services.AddSingleton<ITelemetryInitializer, HttpContextUserTelemetryEnrichment>();

// ** Health Checks

builder.Services
    .AddHealthChecks()
    // We have no real way to test Mail API without sending an actual message but since
    // it is only used for feedback, there really is no reason to test
    .AddCheck<AnnouncementsApiHealthCheck>("announcements-api", Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus.Degraded, new[] { "api" })
    .AddCheck<SandboxApiHealthCheck>("sandpits-api", Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus.Degraded, new[] { "api" });

// ** Authentication

JwtSecurityTokenHandler.DefaultMapInboundClaims = false;

builder.Services
    .AddAuthentication(options => {
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options => {
        options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.SignOutScheme = OpenIdConnectDefaults.AuthenticationScheme;

        options.Authority = ssoSettings.Authority;

        options.ClientId = ssoSettings.ClientId;
        options.ClientSecret = ssoSettings.ClientSecret;

        options.Scope.Add("openid");
        options.Scope.Add("profile");

        options.ResponseType = "code id_token";
        options.GetClaimsFromUserInfoEndpoint = true;
        options.SaveTokens = true;

        options.TokenValidationParameters = new() {
            NameClaimType = "name"
        };

        options.Events = new OpenIdConnectEvents {
            OnAccessDenied = context => {
                context.HandleResponse();
                context.Response.Redirect("/");
                return Task.CompletedTask;
            }
        };
    });

// This is not working in that authentication loops but only with K8s deployment
//builder.Services.AddAuthorization(options => {
//  // By default, all incoming requests will be authorized according to the default policy
//  options.FallbackPolicy = options.DefaultPolicy;
//});

builder.Services.AddAuthorization();

// ** HTTP Clients

builder.Services.AddHttpClients();

// ** HtmlSanitizer

builder.Services.AddScoped<IHtmlSanitizer, HtmlSanitizer>(x => {
    // Configure sanitizer rules as needed here.
    // For now, just use default rules + allow class attributes
    var sanitizer = new Ganss.XSS.HtmlSanitizer();
    sanitizer.AllowedAttributes.Add("class");
    return sanitizer;
});

// ** ORM

builder.Services.AddAutoMapper(new[] { typeof(Program) });      // The project where classes that inherit Profile can be found

// ** Syncfusion

Syncfusion.Licensing.SyncfusionLicenseProvider.RegisterLicense("NTI1NzkwQDMxMzkyZTMzMmUzMG1SL2FzZzNwSVZZWEpFcFdaWXl2Z2xDYTNoZnJyTU15Q05DRk04NUdlZVk9");
builder.Services.AddSyncfusionBlazor();

// ** Services

// Singleton

builder.Services.AddSingleton<ISandpitNotificationService, SandpitNotificationService>();

builder.Services.AddSingleton<INotificationService, NotificationService>();

// Scoped

builder.Services.AddScoped<IAnnouncementsService, AnnouncementsService>();

builder.Services.AddScoped<IUserNotificationsService, UserNotificationsService>();

builder.Services.AddScoped<IRdpConnectionService, RdpConnectionService>();

builder.Services.AddBlazorDownloadFile();

// ** Message Bus

builder.Services.AddScoped<SandpitUpdatedConsumer>();

builder.Services.AddMassTransit(cfg => {
    cfg.AddConsumer<SandpitUpdatedConsumer>();

    cfg.UsingRabbitMq((context, rcfg) => {
        rcfg.Host(rabbitSettings.Uri, hostConfig => {
            hostConfig.Username(rabbitSettings.Username);
            hostConfig.Password(rabbitSettings.Password);
        });

        // We need our own queue per application instance so all instances receive a copy of the message (so SignalR clients can be notified)

        var endpointName = $"cvsandbox-ui-{builder.Environment.EnvironmentName}-{System.Environment.MachineName}".ToLower();

        rcfg.ReceiveEndpoint(endpointName, e => {
            e.AutoDelete = true;
            e.Consumer<SandpitUpdatedConsumer>(context);
        });
    });
});

builder.Services.AddMassTransitHostedService();

// Required to use HTTPS for redirect_uri during authentication -- https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-5.0
builder.Services.Configure<ForwardedHeadersOptions>(options => {
    options.ForwardedHeaders = Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.XForwardedProto;
});

builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();

var app = builder.Build();

if (app.Environment.IsEnvironment("Local")) {
    app.UseDeveloperExceptionPage();
    app.UseForwardedHeaders();
}
else {
    app.UseExceptionHandler("/Error");
    app.UseForwardedHeaders();
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

//app.Use((context, next) => {
//  context.Request.Scheme = "https";
//  return next();
//});

app.UseHttpsRedirection();

app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapBlazorHub();
app.MapFallbackToPage("/_Host");

app.Run();
captainsafia commented 2 years ago

Unsure if it makes sense to share the entire Program.cs...the call I was making was literally the first lines of the file...but here you go???

Should've been clearer. I wanted to tease out if there was anything modifying the WebRootPath and wanted to make sure there was a complete sample.

Anyway, I realized why I wasn't able to reproduce the issue. It's because I was trying to validate this in a web API template which did not contain a wwwroot directory and resulting in all the WebRootPath related logic no-oping.

So, the reason that this happens is because when the HostingEnvironment is initialized the WebRootPath is set to the wwwroot directory (but only if it exists) and the ConfigureAppConfiguration compares this path against the one set in HostingContext which is unset unless the WebRootPath has been passed in via WebApplicationOptions.

One thing we can do to alleviate this is default the WebRootPath in WebApplicationOptions to wwwroot if it exists to match the behavior in WebHostEnvironment.

mike-bagheri commented 2 years ago

by using:

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

if (builder.Environment.IsLocal())
{
    StaticWebAssetsLoader.UseStaticWebAssets(builder.Environment, builder.Configuration);
}

static content will work. However, if in the project file I try to change the location

<StaticWebAssetBasePath>_content</StaticWebAssetBasePath>

it still returns 404. This happens after the upgrade to VS 2022 with .Net 6.0.1

CodeFontana commented 2 years ago

Had the same issue with VS 17.1.3, and this resolved it:

var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseWebRoot("wwwroot").UseStaticWebAssets();

Documented in the answers here: https://stackoverflow.com/questions/64833632/blazor-css-isolation-not-working-and-not-adding-scope-identifiers-after-migratin

Found these issues on the same subject: https://github.com/dotnet/aspnetcore/issues/28911 https://github.com/dotnet/aspnetcore/issues/28174

28174, references this article, which probably needs updating for an ASP.NET 6.0 based on minimal-hosting model:

https://docs.microsoft.com/th-th/aspnet/core/razor-pages/ui-class?view=aspnetcore-6.0&tabs=visual-studio#consume-content-from-a-referenced-rcl

halter73 commented 2 years ago

What's wrong with just the following @CodeFontana?

var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseStaticWebAssets();
CodeFontana commented 2 years ago

@halter73 image

captainsafia commented 2 years ago

I outlined the reason the exception happens here.

Using UseWebRoot before UseStaticWebAssets replicates the behavior that the underlying bug fix would do here (setting wwwroot as the default in the host context).

CodeFontana commented 2 years ago

@captainsafia, excellent, and sorry I (we) missed your explanation.

The real purpose for my comment was for others like me to find and tie all this together. Definitely a good sign when folks are moving their ASP.NET/Blazor apps out of the development launch profile :)

halter73 commented 2 years ago

It doesn't look like UseStaticWebAssets changes the web root directory though. Is this the same issue as https://github.com/dotnet/aspnetcore/issues/39546? I'm wondering if this is more evidence we should backport the fix.

captainsafia commented 2 years ago

It doesn't look like UseStaticWebAssets changes the web root directory though. Is this the same issue as https://github.com/dotnet/aspnetcore/issues/39546? I'm wondering if this is more evidence we should backport the fix

Hm, interesting. I feel like I reproed this in 7.0 as well but perhaps that was before the above PR got merged.

@CodeFontana Do you repro this in 7.0-preview3?

CodeFontana commented 2 years ago

@captainsafia works absolutely fine with 7.0-preview3:

var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseStaticWebAssets();

I'm fine with this workaround for 6.0:

var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseWebRoot("wwwroot").UseStaticWebAssets();

The info in this issue should help others find this workaround. A backport to the next 6.0 servicing might be nice to save others time from bumping into this. It was sort of unexpected, something as mundane as creating a launchSettings.json entry for a production profile-- and suddenly an exception you didn't receive under the development profile.

Appreciate all the help and understanding!

rafael-f commented 1 year ago

Just to add more info to this issue, on my project using:

builder.WebHost.UseStaticWebAssets();

Worked fine, published it on a docker container and also worked fine, another dev also had no issues, however a third dev, with similar setup as the other 2 devs, was running into this issue, it makes no sense. Possibly an issue with visual studio version or version of something else?

After we changed it to:

builder.WebHost.UseWebRoot("wwwroot").UseStaticWebAssets();

It started working on his machine too...

Error message was the same as CodeFontana posted above (different folders only).

sam-wheat commented 11 months ago

I wasted a day on this. Guys if a bug is going to remain open for two years or more kindly consider adding a link in the documentation:

https://learn.microsoft.com/en-us/aspnet/core/fundamentals/environments?view=aspnetcore-7.0

captainsafia commented 9 months ago

Filed https://github.com/dotnet/AspNetCore.Docs/issues/31142 to doc this. I eagerly marked this is a candidate for servicing but I think given there is workaround and this doesn't repro in .NET 7 or .NET 8, we should just document it at this point.

I'll close this for now and track documenting in the docs repo. Thanks for your patience everyone.

ghost commented 9 months ago

This issue has been resolved and has not had any activity for 1 day. It will be closed for housekeeping purposes.

See our Issue Management Policies for more information.