Skip to content

Commit 146d316

Browse files
authored
Add a middleware for browser refresh. (dotnet#24574)
* Add a middleware for browser refresh. * Introduce a middleware that can connect to the dotnet-watch change server * dotnet-watch: Inject the middleware in 3.1 or apps using start hooks \ hosting startup dotnet#23412 * Update src/Tools/dotnet-watch/BrowserRefresh/src/StartupHook.cs * Changes per PR comments * Add a test for reading the script * Changes per PR comments * Updates docs * Fixup test * Add project ref
1 parent b654051 commit 146d316

25 files changed

+1320
-16
lines changed

Diff for: AspNetCore.sln

+30
Original file line numberDiff line numberDiff line change
@@ -1445,6 +1445,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.NET.Sdk.BlazorWeb
14451445
EndProject
14461446
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.NET.Sdk.BlazorWebAssembly.Tools", "src\Components\WebAssembly\Sdk\tools\Microsoft.NET.Sdk.BlazorWebAssembly.Tools.csproj", "{175E5CD8-92D4-46BB-882E-3A930D3302D4}"
14471447
EndProject
1448+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Watch.BrowserRefresh", "src\Tools\dotnet-watch\BrowserRefresh\src\Microsoft.AspNetCore.Watch.BrowserRefresh.csproj", "{A5CE25E9-89E1-4F2C-9B89-0C161707E700}"
1449+
EndProject
1450+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Watch.BrowserRefresh.Tests", "src\Tools\dotnet-watch\BrowserRefresh\test\Microsoft.AspNetCore.Watch.BrowserRefresh.Tests.csproj", "{E6A23627-8D63-4DF1-A4F2-8881172C1FE6}"
1451+
EndProject
14481452
Global
14491453
GlobalSection(SolutionConfigurationPlatforms) = preSolution
14501454
Debug|Any CPU = Debug|Any CPU
@@ -6843,6 +6847,30 @@ Global
68436847
{175E5CD8-92D4-46BB-882E-3A930D3302D4}.Release|x64.Build.0 = Release|Any CPU
68446848
{175E5CD8-92D4-46BB-882E-3A930D3302D4}.Release|x86.ActiveCfg = Release|Any CPU
68456849
{175E5CD8-92D4-46BB-882E-3A930D3302D4}.Release|x86.Build.0 = Release|Any CPU
6850+
{A5CE25E9-89E1-4F2C-9B89-0C161707E700}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
6851+
{A5CE25E9-89E1-4F2C-9B89-0C161707E700}.Debug|Any CPU.Build.0 = Debug|Any CPU
6852+
{A5CE25E9-89E1-4F2C-9B89-0C161707E700}.Debug|x64.ActiveCfg = Debug|Any CPU
6853+
{A5CE25E9-89E1-4F2C-9B89-0C161707E700}.Debug|x64.Build.0 = Debug|Any CPU
6854+
{A5CE25E9-89E1-4F2C-9B89-0C161707E700}.Debug|x86.ActiveCfg = Debug|Any CPU
6855+
{A5CE25E9-89E1-4F2C-9B89-0C161707E700}.Debug|x86.Build.0 = Debug|Any CPU
6856+
{A5CE25E9-89E1-4F2C-9B89-0C161707E700}.Release|Any CPU.ActiveCfg = Release|Any CPU
6857+
{A5CE25E9-89E1-4F2C-9B89-0C161707E700}.Release|Any CPU.Build.0 = Release|Any CPU
6858+
{A5CE25E9-89E1-4F2C-9B89-0C161707E700}.Release|x64.ActiveCfg = Release|Any CPU
6859+
{A5CE25E9-89E1-4F2C-9B89-0C161707E700}.Release|x64.Build.0 = Release|Any CPU
6860+
{A5CE25E9-89E1-4F2C-9B89-0C161707E700}.Release|x86.ActiveCfg = Release|Any CPU
6861+
{A5CE25E9-89E1-4F2C-9B89-0C161707E700}.Release|x86.Build.0 = Release|Any CPU
6862+
{E6A23627-8D63-4DF1-A4F2-8881172C1FE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
6863+
{E6A23627-8D63-4DF1-A4F2-8881172C1FE6}.Debug|Any CPU.Build.0 = Debug|Any CPU
6864+
{E6A23627-8D63-4DF1-A4F2-8881172C1FE6}.Debug|x64.ActiveCfg = Debug|Any CPU
6865+
{E6A23627-8D63-4DF1-A4F2-8881172C1FE6}.Debug|x64.Build.0 = Debug|Any CPU
6866+
{E6A23627-8D63-4DF1-A4F2-8881172C1FE6}.Debug|x86.ActiveCfg = Debug|Any CPU
6867+
{E6A23627-8D63-4DF1-A4F2-8881172C1FE6}.Debug|x86.Build.0 = Debug|Any CPU
6868+
{E6A23627-8D63-4DF1-A4F2-8881172C1FE6}.Release|Any CPU.ActiveCfg = Release|Any CPU
6869+
{E6A23627-8D63-4DF1-A4F2-8881172C1FE6}.Release|Any CPU.Build.0 = Release|Any CPU
6870+
{E6A23627-8D63-4DF1-A4F2-8881172C1FE6}.Release|x64.ActiveCfg = Release|Any CPU
6871+
{E6A23627-8D63-4DF1-A4F2-8881172C1FE6}.Release|x64.Build.0 = Release|Any CPU
6872+
{E6A23627-8D63-4DF1-A4F2-8881172C1FE6}.Release|x86.ActiveCfg = Release|Any CPU
6873+
{E6A23627-8D63-4DF1-A4F2-8881172C1FE6}.Release|x86.Build.0 = Release|Any CPU
68466874
EndGlobalSection
68476875
GlobalSection(SolutionProperties) = preSolution
68486876
HideSolutionNode = FALSE
@@ -7567,6 +7595,8 @@ Global
75677595
{83371889-9A3E-4D16-AE77-EB4F83BC6374} = {FED4267E-E5E4-49C5-98DB-8B3F203596EE}
75687596
{525EBCB4-A870-470B-BC90-845306C337D1} = {FED4267E-E5E4-49C5-98DB-8B3F203596EE}
75697597
{175E5CD8-92D4-46BB-882E-3A930D3302D4} = {FED4267E-E5E4-49C5-98DB-8B3F203596EE}
7598+
{A5CE25E9-89E1-4F2C-9B89-0C161707E700} = {B6118E15-C37A-4B05-B4DF-97FE99790417}
7599+
{E6A23627-8D63-4DF1-A4F2-8881172C1FE6} = {B6118E15-C37A-4B05-B4DF-97FE99790417}
75707600
EndGlobalSection
75717601
GlobalSection(ExtensibilityGlobals) = postSolution
75727602
SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F}

Diff for: Directory.Build.targets

+1-1
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@
147147
<KnownFrameworkReference Condition="'$(UseAspNetCoreSharedRuntime)' != 'true'" Remove="Microsoft.AspNetCore.App" />
148148
<KnownFrameworkReference Remove="Microsoft.WindowsDesktop.App" />
149149

150-
<KnownFrameworkReference Condition="'$(UseAspNetCoreSharedRuntime)' == 'true'" Update="Microsoft.AspNetCore.App">
150+
<KnownFrameworkReference Condition="'$(UseAspNetCoreSharedRuntime)' == 'true' AND '$(DoNotApplyWorkaroundsToMicrosoftAspNetCoreApp)' != 'true'" Update="Microsoft.AspNetCore.App">
151151
<LatestRuntimeFrameworkVersion>$(SharedFxVersion)</LatestRuntimeFrameworkVersion>
152152
<DefaultRuntimeFrameworkVersion Condition="'$(IsServicingBuild)' != 'true'">$(SharedFxVersion)</DefaultRuntimeFrameworkVersion>
153153
<TargetingPackVersion Condition="'$(IsServicingBuild)' != 'true'">$(SharedFxVersion)</TargetingPackVersion>

Diff for: THIRD-PARTY-NOTICES.txt

+51
Original file line numberDiff line numberDiff line change
@@ -217,3 +217,54 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
217217
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
218218
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
219219
SOFTWARE.
220+
221+
License notice for West Wind Live Reload ASP.NET Core Middleware
222+
=============================================
223+
224+
225+
MIT License
226+
-----------
227+
228+
Copyright (c) 2019-2020 West Wind Technologies
229+
230+
Permission is hereby granted, free of charge, to any person obtaining a copy
231+
of this software and associated documentation files (the "Software"), to deal
232+
in the Software without restriction, including without limitation the rights
233+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
234+
copies of the Software, and to permit persons to whom the Software is
235+
furnished to do so, subject to the following conditions:
236+
237+
The above copyright notice and this permission notice shall be included in all
238+
copies or substantial portions of the Software.
239+
240+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
241+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
242+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
243+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
244+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
245+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
246+
SOFTWARE.
247+
248+
License notice for cli-spinners
249+
=============================================
250+
251+
MIT License
252+
253+
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
254+
255+
Permission is hereby granted, free of charge, to any person obtaining a copy
256+
of this software and associated documentation files (the "Software"), to deal
257+
in the Software without restriction, including without limitation the rights
258+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
259+
copies of the Software, and to permit persons to whom the Software is
260+
furnished to do so, subject to the following conditions:
261+
262+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
263+
264+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
265+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
266+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
267+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
268+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
269+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
270+
SOFTWARE.

Diff for: eng/ProjectReferences.props

+1
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
<ProjectReferenceProvider Include="Microsoft.AspNetCore.CookiePolicy" ProjectPath="$(RepoRoot)src\Security\CookiePolicy\src\Microsoft.AspNetCore.CookiePolicy.csproj" />
6767
<ProjectReferenceProvider Include="Microsoft.Web.Xdt.Extensions" ProjectPath="$(RepoRoot)src\SiteExtensions\Microsoft.Web.Xdt.Extensions\src\Microsoft.Web.Xdt.Extensions.csproj" />
6868
<ProjectReferenceProvider Include="dotnet-getdocument" ProjectPath="$(RepoRoot)src\Tools\dotnet-getdocument\src\dotnet-getdocument.csproj" />
69+
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Watch.BrowserRefresh" ProjectPath="$(RepoRoot)src\Tools\dotnet-watch\BrowserRefresh\src\Microsoft.AspNetCore.Watch.BrowserRefresh.csproj" />
6970
<ProjectReferenceProvider Include="Microsoft.Extensions.ApiDescription.Client" ProjectPath="$(RepoRoot)src\Tools\Extensions.ApiDescription.Client\src\Microsoft.Extensions.ApiDescription.Client.csproj" />
7071
<ProjectReferenceProvider Include="Microsoft.Extensions.ApiDescription.Server" ProjectPath="$(RepoRoot)src\Tools\Extensions.ApiDescription.Server\src\Microsoft.Extensions.ApiDescription.Server.csproj" />
7172
<ProjectReferenceProvider Include="Microsoft.AspNetCore.DeveloperCertificates.XPlat" ProjectPath="$(RepoRoot)src\Tools\FirstRunCertGenerator\src\Microsoft.AspNetCore.DeveloperCertificates.XPlat.csproj" />

Diff for: eng/Workarounds.targets

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
The web sdk adds an implicit framework reference. This removes it until we can update our build to use framework references.
2121
-->
2222
<ItemGroup>
23-
<FrameworkReference Remove="Microsoft.AspNetCore.App" />
23+
<FrameworkReference Remove="Microsoft.AspNetCore.App" Condition="'$(DoNotApplyWorkaroundsToMicrosoftAspNetCoreApp)' != 'true'" />
2424
<!-- Required because the Razor SDK will generate attributes -->
2525
<Reference Include="Microsoft.AspNetCore.Mvc" Condition="'$(UsingMicrosoftNETSdkWeb)' == 'true' AND '$(TargetFrameworkIdentifier)' == '.NETCoreApp' AND '$(GenerateRazorAssemblyInfo)' == 'true'" />
2626
</ItemGroup>

Diff for: src/Tools/Tools.slnf

+5-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@
1717
"src\\Tools\\dotnet-user-secrets\\src\\dotnet-user-secrets.csproj",
1818
"src\\Tools\\dotnet-user-secrets\\test\\dotnet-user-secrets.Tests.csproj",
1919
"src\\Tools\\dotnet-watch\\src\\dotnet-watch.csproj",
20-
"src\\Tools\\dotnet-watch\\test\\dotnet-watch.Tests.csproj"
20+
"src\\Tools\\dotnet-watch\\BrowserRefresh\\src\\Microsoft.AspNetCore.Watch.BrowserRefresh.csproj",
21+
"src\\Tools\\dotnet-watch\\BrowserRefresh\\test\\Microsoft.AspNetCore.Watch.BrowserRefresh.Tests.csproj",
22+
"src\\Tools\\dotnet-watch\\test\\dotnet-watch.Tests.csproj",
23+
"src\\Middleware\\StaticFiles\\src\\Microsoft.AspNetCore.StaticFiles.csproj",
24+
"src\\Hosting\\TestHost\\src\\Microsoft.AspNetCore.TestHost.csproj"
2125
]
2226
}
2327
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Threading.Tasks;
7+
using Microsoft.AspNetCore.Http;
8+
using Microsoft.AspNetCore.Http.Features;
9+
using Microsoft.Extensions.Logging;
10+
using Microsoft.Net.Http.Headers;
11+
12+
namespace Microsoft.AspNetCore.Watch.BrowserRefresh
13+
{
14+
public class BrowserRefreshMiddleware
15+
{
16+
private static readonly MediaTypeHeaderValue _textHtmlMediaType = new MediaTypeHeaderValue("text/html");
17+
private readonly RequestDelegate _next;
18+
private readonly ILogger _logger;
19+
20+
public BrowserRefreshMiddleware(RequestDelegate next, ILogger<BrowserRefreshMiddleware> logger) =>
21+
(_next, _logger) = (next, logger);
22+
23+
public async Task InvokeAsync(HttpContext context)
24+
{
25+
// We only need to support this for requests that could be initiated by a browser.
26+
if (IsBrowserRequest(context))
27+
{
28+
// Use a custom StreamWrapper to rewrite output on Write/WriteAsync
29+
using var responseStreamWrapper = new ResponseStreamWrapper(context, _logger);
30+
var originalBodyFeature = context.Features.Get<IHttpResponseBodyFeature>();
31+
context.Features.Set<IHttpResponseBodyFeature>(new StreamResponseBodyFeature(responseStreamWrapper));
32+
33+
try
34+
{
35+
await _next(context);
36+
}
37+
finally
38+
{
39+
context.Features.Set(originalBodyFeature);
40+
}
41+
42+
if (responseStreamWrapper.IsHtmlResponse && _logger.IsEnabled(LogLevel.Debug))
43+
{
44+
if (responseStreamWrapper.ScriptInjectionPerformed)
45+
{
46+
Log.BrowserConfiguredForRefreshes(_logger);
47+
}
48+
else
49+
{
50+
Log.FailedToConfiguredForRefreshes(_logger);
51+
}
52+
}
53+
}
54+
else
55+
{
56+
await _next(context);
57+
}
58+
}
59+
60+
internal static bool IsBrowserRequest(HttpContext context)
61+
{
62+
var request = context.Request;
63+
if (!HttpMethods.IsGet(request.Method) && !HttpMethods.IsPost(request.Method))
64+
{
65+
return false;
66+
}
67+
68+
var typedHeaders = request.GetTypedHeaders();
69+
if (!(typedHeaders.Accept is IList<MediaTypeHeaderValue> acceptHeaders))
70+
{
71+
return false;
72+
}
73+
74+
for (var i = 0; i < acceptHeaders.Count; i++)
75+
{
76+
if (acceptHeaders[i].IsSubsetOf(_textHtmlMediaType))
77+
{
78+
return true;
79+
}
80+
}
81+
82+
return false;
83+
}
84+
85+
internal static class Log
86+
{
87+
private static readonly Action<ILogger, Exception?> _setupResponseForBrowserRefresh = LoggerMessage.Define(
88+
LogLevel.Debug,
89+
new EventId(1, "SetUpResponseForBrowserRefresh"),
90+
"Response markup is scheduled to include browser refresh script injection.");
91+
92+
private static readonly Action<ILogger, Exception?> _browserConfiguredForRefreshes = LoggerMessage.Define(
93+
LogLevel.Debug,
94+
new EventId(2, "BrowserConfiguredForRefreshes"),
95+
"Response markup was updated to include browser refresh script injection.");
96+
97+
private static readonly Action<ILogger, Exception?> _failedToConfigureForRefreshes = LoggerMessage.Define(
98+
LogLevel.Debug,
99+
new EventId(3, "FailedToConfiguredForRefreshes"),
100+
"Unable to configure browser refresh script injection on the response.");
101+
102+
public static void SetupResponseForBrowserRefresh(ILogger logger) => _setupResponseForBrowserRefresh(logger, null);
103+
public static void BrowserConfiguredForRefreshes(ILogger logger) => _browserConfiguredForRefreshes(logger, null);
104+
public static void FailedToConfiguredForRefreshes(ILogger logger) => _failedToConfigureForRefreshes(logger, null);
105+
}
106+
}
107+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using Microsoft.AspNetCore.Builder;
6+
using Microsoft.AspNetCore.Hosting;
7+
using Microsoft.Extensions.DependencyInjection;
8+
using Microsoft.Extensions.DependencyInjection.Extensions;
9+
10+
[assembly: HostingStartup(typeof(Microsoft.AspNetCore.Watch.BrowserRefresh.HostingStartup))]
11+
12+
namespace Microsoft.AspNetCore.Watch.BrowserRefresh
13+
{
14+
internal sealed class HostingStartup : IHostingStartup, IStartupFilter
15+
{
16+
public void Configure(IWebHostBuilder builder)
17+
{
18+
builder.ConfigureServices(services => services.TryAddEnumerable(ServiceDescriptor.Singleton<IStartupFilter>(this)));
19+
}
20+
21+
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
22+
{
23+
return app =>
24+
{
25+
app.UseMiddleware<BrowserRefreshMiddleware>();
26+
next(app);
27+
};
28+
}
29+
}
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<!-- This feature is supported in projects targeting 3.1 or later.-->
4+
<TargetFramework>netcoreapp3.1</TargetFramework>
5+
<IsPackable>false</IsPackable>
6+
<Nullable>enable</Nullable>
7+
<IsShipping>false</IsShipping>
8+
<UseAspNetCoreSharedRuntime>true</UseAspNetCoreSharedRuntime>
9+
<DoNotApplyWorkaroundsToMicrosoftAspNetCoreApp>true</DoNotApplyWorkaroundsToMicrosoftAspNetCoreApp>
10+
<ExcludeFromSourceBuild>false</ExcludeFromSourceBuild>
11+
</PropertyGroup>
12+
13+
<ItemGroup>
14+
<FrameworkReference Include="Microsoft.AspNetCore.App" />
15+
<EmbeddedResource Include="WebSocketScriptInjection.js" />
16+
</ItemGroup>
17+
18+
</Project>

0 commit comments

Comments
 (0)