Skip to content

Commit

Permalink
Merge pull request #57975 from dotnet/merge/release/9.0-rc2-to-releas…
Browse files Browse the repository at this point in the history
…e/9.0

[automated] Merge branch 'release/9.0-rc2' => 'release/9.0'
  • Loading branch information
wtgodbe authored Sep 24, 2024
2 parents 91ef755 + b77c79e commit 3ec8cf7
Show file tree
Hide file tree
Showing 18 changed files with 246 additions and 112 deletions.
30 changes: 16 additions & 14 deletions src/Components/WebAssembly/DevServer/src/Server/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
Expand Down Expand Up @@ -29,27 +30,28 @@ public static void Configure(IApplicationBuilder app, IConfiguration configurati

app.UseWebAssemblyDebugging();

bool applyCopHeaders = configuration.GetValue<bool>("ApplyCopHeaders");
var webHostEnvironment = app.ApplicationServices.GetRequiredService<IWebHostEnvironment>();
var applyCopHeaders = configuration.GetValue<bool>("ApplyCopHeaders");

if (applyCopHeaders)
app.Use(async (ctx, next) =>
{
app.Use(async (ctx, next) =>
if (ctx.Request.Path.StartsWithSegments("/_framework/blazor.boot.json"))
{
if (ctx.Request.Path.StartsWithSegments("/_framework") && !ctx.Request.Path.StartsWithSegments("/_framework/blazor.server.js") && !ctx.Request.Path.StartsWithSegments("/_framework/blazor.web.js"))
ctx.Response.Headers.Append("Blazor-Environment", webHostEnvironment.EnvironmentName);
}
else if (applyCopHeaders && ctx.Request.Path.StartsWithSegments("/_framework") && !ctx.Request.Path.StartsWithSegments("/_framework/blazor.server.js") && !ctx.Request.Path.StartsWithSegments("/_framework/blazor.web.js"))
{
var fileExtension = Path.GetExtension(ctx.Request.Path);
if (string.Equals(fileExtension, ".js", StringComparison.OrdinalIgnoreCase))
{
string fileExtension = Path.GetExtension(ctx.Request.Path);
if (string.Equals(fileExtension, ".js"))
{
// Browser multi-threaded runtime requires cross-origin policy headers to enable SharedArrayBuffer.
ApplyCrossOriginPolicyHeaders(ctx);
}
// Browser multi-threaded runtime requires cross-origin policy headers to enable SharedArrayBuffer.
ApplyCrossOriginPolicyHeaders(ctx);
}
}
await next(ctx);
});
}
await next(ctx);
});

//app.UseBlazorFrameworkFiles();
app.UseRouting();

app.UseStaticFiles(new StaticFileOptions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ public void WebAssemblyConfiguration_Works()

if (_serverFixture.TestTrimmedOrMultithreadingApps)
{
// Verify that the environment gets detected as 'Production'.
Browser.Equal("Production", () => _appElement.FindElement(By.Id("environment")).Text);

// Verify values overriden by an environment specific 'appsettings.$(Environment).json are read
Assert.Equal("Prod key2-value", _appElement.FindElement(By.Id("key2")).Text);

Expand All @@ -47,6 +50,9 @@ public void WebAssemblyConfiguration_Works()
}
else
{
// Verify that the dev server always correctly serves the 'Blazor-Environment: Development' header.
Browser.Equal("Development", () => _appElement.FindElement(By.Id("environment")).Text);

// Verify values overriden by an environment specific 'appsettings.$(Environment).json are read
Assert.Equal("Development key2-value", _appElement.FindElement(By.Id("key2")).Text);

Expand Down
102 changes: 22 additions & 80 deletions src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,22 +90,12 @@ public static Task WriteAsJsonAsync<TValue>(

response.ContentType = contentType ?? ContentTypeConstants.JsonContentTypeWithCharset;

var startTask = Task.CompletedTask;
if (!response.HasStarted)
{
// Flush headers before starting Json serialization. This avoids an extra layer of buffering before the first flush.
startTask = response.StartAsync(cancellationToken);
}

// if no user provided token, pass the RequestAborted token and ignore OperationCanceledException
if (!startTask.IsCompleted || !cancellationToken.CanBeCanceled)
if (!cancellationToken.CanBeCanceled)
{
return WriteAsJsonAsyncSlow(startTask, response.BodyWriter, value, options,
ignoreOCE: !cancellationToken.CanBeCanceled,
cancellationToken.CanBeCanceled ? cancellationToken : response.HttpContext.RequestAborted);
return WriteAsJsonAsyncSlow(response.BodyWriter, value, options, response.HttpContext.RequestAborted);
}

startTask.GetAwaiter().GetResult();
return JsonSerializer.SerializeAsync(response.BodyWriter, value, options, cancellationToken);
}

Expand All @@ -131,33 +121,22 @@ public static Task WriteAsJsonAsync<TValue>(

response.ContentType = contentType ?? ContentTypeConstants.JsonContentTypeWithCharset;

var startTask = Task.CompletedTask;
if (!response.HasStarted)
{
// Flush headers before starting Json serialization. This avoids an extra layer of buffering before the first flush.
startTask = response.StartAsync(cancellationToken);
}

// if no user provided token, pass the RequestAborted token and ignore OperationCanceledException
if (!startTask.IsCompleted || !cancellationToken.CanBeCanceled)
if (!cancellationToken.CanBeCanceled)
{
return WriteAsJsonAsyncSlow(startTask, response, value, jsonTypeInfo,
ignoreOCE: !cancellationToken.CanBeCanceled,
cancellationToken.CanBeCanceled ? cancellationToken : response.HttpContext.RequestAborted);
return WriteAsJsonAsyncSlow(response, value, jsonTypeInfo, response.HttpContext.RequestAborted);
}

startTask.GetAwaiter().GetResult();
return JsonSerializer.SerializeAsync(response.BodyWriter, value, jsonTypeInfo, cancellationToken);

static async Task WriteAsJsonAsyncSlow(Task startTask, HttpResponse response, TValue value, JsonTypeInfo<TValue> jsonTypeInfo,
bool ignoreOCE, CancellationToken cancellationToken)
static async Task WriteAsJsonAsyncSlow(HttpResponse response, TValue value, JsonTypeInfo<TValue> jsonTypeInfo,
CancellationToken cancellationToken)
{
try
{
await startTask;
await JsonSerializer.SerializeAsync(response.BodyWriter, value, jsonTypeInfo, cancellationToken);
}
catch (OperationCanceledException) when (ignoreOCE) { }
catch (OperationCanceledException) { }
}
}

Expand All @@ -184,52 +163,38 @@ public static Task WriteAsJsonAsync(

response.ContentType = contentType ?? ContentTypeConstants.JsonContentTypeWithCharset;

var startTask = Task.CompletedTask;
if (!response.HasStarted)
{
// Flush headers before starting Json serialization. This avoids an extra layer of buffering before the first flush.
startTask = response.StartAsync(cancellationToken);
}

// if no user provided token, pass the RequestAborted token and ignore OperationCanceledException
if (!startTask.IsCompleted || !cancellationToken.CanBeCanceled)
if (!cancellationToken.CanBeCanceled)
{
return WriteAsJsonAsyncSlow(startTask, response, value, jsonTypeInfo,
ignoreOCE: !cancellationToken.CanBeCanceled,
cancellationToken.CanBeCanceled ? cancellationToken : response.HttpContext.RequestAborted);
return WriteAsJsonAsyncSlow(response, value, jsonTypeInfo, response.HttpContext.RequestAborted);
}

startTask.GetAwaiter().GetResult();
return JsonSerializer.SerializeAsync(response.BodyWriter, value, jsonTypeInfo, cancellationToken);

static async Task WriteAsJsonAsyncSlow(Task startTask, HttpResponse response, object? value, JsonTypeInfo jsonTypeInfo,
bool ignoreOCE, CancellationToken cancellationToken)
static async Task WriteAsJsonAsyncSlow(HttpResponse response, object? value, JsonTypeInfo jsonTypeInfo,
CancellationToken cancellationToken)
{
try
{
await startTask;
await JsonSerializer.SerializeAsync(response.BodyWriter, value, jsonTypeInfo, cancellationToken);
}
catch (OperationCanceledException) when (ignoreOCE) { }
catch (OperationCanceledException) { }
}
}

[RequiresUnreferencedCode(RequiresUnreferencedCodeMessage)]
[RequiresDynamicCode(RequiresDynamicCodeMessage)]
private static async Task WriteAsJsonAsyncSlow<TValue>(
Task startTask,
PipeWriter body,
TValue value,
JsonSerializerOptions? options,
bool ignoreOCE,
CancellationToken cancellationToken)
{
try
{
await startTask;
await JsonSerializer.SerializeAsync(body, value, options, cancellationToken);
}
catch (OperationCanceledException) when (ignoreOCE) { }
catch (OperationCanceledException) { }
}

/// <summary>
Expand Down Expand Up @@ -304,42 +269,30 @@ public static Task WriteAsJsonAsync(

response.ContentType = contentType ?? ContentTypeConstants.JsonContentTypeWithCharset;

var startTask = Task.CompletedTask;
if (!response.HasStarted)
{
// Flush headers before starting Json serialization. This avoids an extra layer of buffering before the first flush.
startTask = response.StartAsync(cancellationToken);
}

// if no user provided token, pass the RequestAborted token and ignore OperationCanceledException
if (!startTask.IsCompleted || !cancellationToken.CanBeCanceled)
if (!cancellationToken.CanBeCanceled)
{
return WriteAsJsonAsyncSlow(startTask, response.BodyWriter, value, type, options,
ignoreOCE: !cancellationToken.CanBeCanceled,
cancellationToken.CanBeCanceled ? cancellationToken : response.HttpContext.RequestAborted);
return WriteAsJsonAsyncSlow(response.BodyWriter, value, type, options,
response.HttpContext.RequestAborted);
}

startTask.GetAwaiter().GetResult();
return JsonSerializer.SerializeAsync(response.BodyWriter, value, type, options, cancellationToken);
}

[RequiresUnreferencedCode(RequiresUnreferencedCodeMessage)]
[RequiresDynamicCode(RequiresDynamicCodeMessage)]
private static async Task WriteAsJsonAsyncSlow(
Task startTask,
PipeWriter body,
object? value,
Type type,
JsonSerializerOptions? options,
bool ignoreOCE,
CancellationToken cancellationToken)
{
try
{
await startTask;
await JsonSerializer.SerializeAsync(body, value, type, options, cancellationToken);
}
catch (OperationCanceledException) when (ignoreOCE) { }
catch (OperationCanceledException) { }
}

/// <summary>
Expand Down Expand Up @@ -367,33 +320,22 @@ public static Task WriteAsJsonAsync(

response.ContentType = contentType ?? ContentTypeConstants.JsonContentTypeWithCharset;

var startTask = Task.CompletedTask;
if (!response.HasStarted)
{
// Flush headers before starting Json serialization. This avoids an extra layer of buffering before the first flush.
startTask = response.StartAsync(cancellationToken);
}

// if no user provided token, pass the RequestAborted token and ignore OperationCanceledException
if (!startTask.IsCompleted || !cancellationToken.CanBeCanceled)
if (!cancellationToken.CanBeCanceled)
{
return WriteAsJsonAsyncSlow(startTask, response.BodyWriter, value, type, context,
ignoreOCE: !cancellationToken.CanBeCanceled,
cancellationToken.CanBeCanceled ? cancellationToken : response.HttpContext.RequestAborted);
return WriteAsJsonAsyncSlow(response.BodyWriter, value, type, context, response.HttpContext.RequestAborted);
}

startTask.GetAwaiter().GetResult();
return JsonSerializer.SerializeAsync(response.BodyWriter, value, type, context, cancellationToken);

static async Task WriteAsJsonAsyncSlow(Task startTask, PipeWriter body, object? value, Type type, JsonSerializerContext context,
bool ignoreOCE, CancellationToken cancellationToken)
static async Task WriteAsJsonAsyncSlow(PipeWriter body, object? value, Type type, JsonSerializerContext context,
CancellationToken cancellationToken)
{
try
{
await startTask;
await JsonSerializer.SerializeAsync(body, value, type, context, cancellationToken);
}
catch (OperationCanceledException) when (ignoreOCE) { }
catch (OperationCanceledException) { }
}
}

Expand Down
70 changes: 69 additions & 1 deletion src/Http/Http.Extensions/test/HttpResponseJsonExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.IO.Pipelines;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.TestHost;

#nullable enable

Expand Down Expand Up @@ -481,6 +484,71 @@ public async Task WriteAsJsonAsync_NullValue_WithJsonTypeInfo_JsonResponse()
Assert.Equal("null", data);
}

// Regression test: https://github.com/dotnet/aspnetcore/issues/57895
[Fact]
public async Task AsyncEnumerableCanSetHeader()
{
var builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();

await using var app = builder.Build();

app.MapGet("/", IAsyncEnumerable<int> (HttpContext httpContext) =>
{
return AsyncEnum();
async IAsyncEnumerable<int> AsyncEnum()
{
await Task.Yield();
httpContext.Response.Headers["Test"] = "t";
yield return 1;
}
});

await app.StartAsync();

var client = app.GetTestClient();

var result = await client.GetAsync("/");
result.EnsureSuccessStatusCode();
var headerValue = Assert.Single(result.Headers.GetValues("Test"));
Assert.Equal("t", headerValue);

await app.StopAsync();
}

// Regression test: https://github.com/dotnet/aspnetcore/issues/57895
[Fact]
public async Task EnumerableCanSetHeader()
{
var builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();

await using var app = builder.Build();

app.MapGet("/", IEnumerable<int> (HttpContext httpContext) =>
{
return Enum();
IEnumerable<int> Enum()
{
httpContext.Response.Headers["Test"] = "t";
yield return 1;
}
});

await app.StartAsync();

var client = app.GetTestClient();

var result = await client.GetAsync("/");
result.EnsureSuccessStatusCode();
var headerValue = Assert.Single(result.Headers.GetValues("Test"));
Assert.Equal("t", headerValue);

await app.StopAsync();
}

public class TestObject
{
public string? StringProperty { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<Reference Include="Microsoft.AspNetCore.Http.Results" />
<Reference Include="Microsoft.AspNetCore.Http.Extensions" />
<Reference Include="Microsoft.AspNetCore.Mvc.Core" />
<Reference Include="Microsoft.AspNetCore.TestHost" />
<Reference Include="Microsoft.Extensions.DependencyInjection" />
<Reference Include="Microsoft.Extensions.DependencyModel" />
</ItemGroup>
Expand Down
7 changes: 6 additions & 1 deletion src/Installers/Windows/SharedFrameworkBundle/Bundle.wxs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi" xmlns:dep="http://schemas.microsoft.com/wix/DependencyExtension" xmlns:bal="http://schemas.microsoft.com/wix/BalExtension">
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi" xmlns:util="http://schemas.microsoft.com/wix/UtilExtension"
xmlns:dep="http://schemas.microsoft.com/wix/DependencyExtension" xmlns:bal="http://schemas.microsoft.com/wix/BalExtension">
<Bundle Name="$(var.BundleName)" Version="$(var.BundleVersion)" Manufacturer="Microsoft Corporation" UpgradeCode="$(var.BundleUpgradeCode)">
<BootstrapperApplicationRef Id="WixStandardBootstrapperApplication.HyperlinkLicense">
<bal:WixStandardBootstrapperApplication LicenseUrl="https://go.microsoft.com/fwlink/?LinkId=329770"
Expand All @@ -11,6 +12,10 @@
<PayloadGroupRef Id="PG_Resources"/>
</BootstrapperApplicationRef>

<util:RegistrySearchRef Id="RemovePreviousVersionRegistryKeySearch"/>
<util:RegistrySearchRef Id="RemoveSpecificPreviousVersionRegistryKeyExistsSearch"/>
<util:RegistrySearchRef Id="RemoveSpecificPreviousVersionRegistryKeySearch"/>

<!-- Ensure upgrades from 3.0.0 preview 1, 2, and 3. Conditioned for the 3.0.0 family. -->
<?if $(var.Version)=3.0.0.0?>
<?if $(var.Platform)=x86?>
Expand Down
Loading

0 comments on commit 3ec8cf7

Please sign in to comment.