AB#5820 - Introduced WebProfiler for asp.net core

This commit is contained in:
Bjarke Berg
2020-03-24 18:18:25 +01:00
parent 919271b2c6
commit 9339e1d999
19 changed files with 431 additions and 18 deletions
@@ -1,3 +1,5 @@
using System;
namespace Umbraco.Net
{
public interface IUmbracoApplicationLifetime
@@ -10,5 +12,7 @@ namespace Umbraco.Net
/// Terminates the current application. The application restarts the next time a request is received for it.
/// </summary>
void Restart();
event EventHandler ApplicationInit;
}
}
+1 -1
View File
@@ -146,7 +146,7 @@ namespace Umbraco.Core
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
internal static bool IsClientSideRequest(this Uri url)
public static bool IsClientSideRequest(this Uri url)
{
try
{
+1 -1
View File
@@ -99,7 +99,7 @@
<PackageReference Include="Microsoft.Owin.Security" Version="4.0.1" />
<PackageReference Include="Microsoft.Owin.Testing" Version="4.0.1" />
<PackageReference Include="Microsoft.Web.Infrastructure" Version="1.0.0.0" />
<PackageReference Include="MiniProfiler" Version="4.0.138" />
<PackageReference Include="MiniProfiler" Version="4.1.0" />
<PackageReference Include="Moq" Version="4.13.1" />
<PackageReference Include="NPoco" Version="4.0.2" />
<PackageReference Include="NUnit" Version="3.12.0" />
@@ -1,3 +1,4 @@
using System;
using System.Threading;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Hosting;
@@ -14,6 +15,11 @@ namespace Umbraco.Web.AspNet
{
_httpContextAccessor = httpContextAccessor;
_hostApplicationLifetime = hostApplicationLifetime;
hostApplicationLifetime.ApplicationStarted.Register(() =>
{
ApplicationInit?.Invoke(this, EventArgs.Empty);
});
}
public bool IsRestarting { get; set; }
@@ -32,5 +38,7 @@ namespace Umbraco.Web.AspNet
Thread.CurrentPrincipal = null;
_hostApplicationLifetime.StopApplication();
}
public event EventHandler ApplicationInit;
}
}
@@ -0,0 +1,21 @@
using System;
using Microsoft.AspNetCore.Builder;
using Umbraco.Web.Common.Middleware;
namespace Umbraco.Web.Common.Extensions
{
public static class UmbracoRequestApplicationBuilderExtensions
{
public static IApplicationBuilder UseUmbracoRequest(this IApplicationBuilder app)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
app.UseMiddleware<UmbracoRequestMiddleware>();
return app;
}
}
}
@@ -0,0 +1,20 @@
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Web.Common.Middleware;
namespace Umbraco.Web.Common.Extensions
{
public static class UmbracoRequestServiceCollectionExtensions
{
public static IServiceCollection AddUmbracoRequest(this IServiceCollection services)
{
var umbracoRequestLifetime = new UmbracoRequestLifetime();
services.AddSingleton<IUmbracoRequestLifetimeManager>(umbracoRequestLifetime);
services.AddSingleton<IUmbracoRequestLifetime>(umbracoRequestLifetime);
return services;
}
}
}
@@ -0,0 +1,52 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace Umbraco.Web.Common.Middleware
{
public class UmbracoRequestMiddleware
{
private readonly RequestDelegate _next;
private readonly IUmbracoRequestLifetimeManager _umbracoRequestLifetimeManager;
public UmbracoRequestMiddleware(RequestDelegate next, IUmbracoRequestLifetimeManager umbracoRequestLifetimeManager)
{
_next = next;
_umbracoRequestLifetimeManager = umbracoRequestLifetimeManager;
}
public async Task InvokeAsync(HttpContext context)
{
_umbracoRequestLifetimeManager.InitRequest(context);
await _next(context);
_umbracoRequestLifetimeManager.EndRequest(context);
}
}
public interface IUmbracoRequestLifetime
{
event EventHandler<HttpContext> RequestStart;
event EventHandler<HttpContext> RequestEnd;
}
public class UmbracoRequestLifetime : IUmbracoRequestLifetime, IUmbracoRequestLifetimeManager
{
public event EventHandler<HttpContext> RequestStart;
public event EventHandler<HttpContext> RequestEnd;
public void InitRequest(HttpContext context)
{
RequestStart?.Invoke(this, context);
}
public void EndRequest(HttpContext context)
{
RequestEnd?.Invoke(this, context);
}
}
public interface IUmbracoRequestLifetimeManager
{
void InitRequest(HttpContext context);
void EndRequest(HttpContext context);
}
}
@@ -0,0 +1,81 @@
using System;
using System.Threading;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using StackExchange.Profiling;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Logging;
using Umbraco.Web.Common.Middleware;
namespace Umbraco.Web.Common.Runtime.Profiler
{
public class WebProfiler : IProfiler
{
private const string BootRequestItemKey = "Umbraco.Core.Logging.WebProfiler__isBootRequest";
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly WebProfilerProvider _provider;
private int _first;
public WebProfiler(IHttpContextAccessor httpContextAccessor)
{
// create our own provider, which can provide a profiler even during boot
_provider = new WebProfilerProvider();
_httpContextAccessor = httpContextAccessor;
}
public string Render() => MiniProfiler.Current
.RenderIncludes(_httpContextAccessor.HttpContext, RenderPosition.Right).ToString();
public IDisposable Step(string name) => MiniProfiler.Current?.Step(name);
public void Start()
{
MiniProfiler.StartNew();
}
public void Stop(bool discardResults = false)
{
MiniProfiler.Current?.Stop(discardResults);
}
public void UmbracoApplicationBeginRequest(HttpContext context)
{
// if this is the first request, notify our own provider that this request is the boot request
var first = Interlocked.Exchange(ref _first, 1) == 0;
if (first)
{
_provider.BeginBootRequest();
context.Items[BootRequestItemKey] = true;
// and no need to start anything, profiler is already there
}
// else start a profiler, the normal way
else if (ShouldProfile(context.Request))
Start();
}
public void UmbracoApplicationEndRequest(HttpContext context)
{
// if this is the boot request, or if we should profile this request, stop
// (the boot request is always profiled, no matter what)
if (context.Items.TryGetValue(BootRequestItemKey, out var isBoot) && isBoot.Equals(true))
{
_provider.EndBootRequest();
Stop();
}
else if (ShouldProfile(context.Request))
{
Stop();
}
}
private static bool ShouldProfile(HttpRequest request)
{
if (new Uri(request.GetEncodedUrl(), UriKind.RelativeOrAbsolute).IsClientSideRequest()) return false;
if (bool.TryParse(request.Query["umbDebug"], out var umbDebug)) return umbDebug;
if (bool.TryParse(request.Headers["X-UMB-DEBUG"], out var xUmbDebug)) return xUmbDebug;
if (bool.TryParse(request.Cookies["UMB-DEBUG"], out var cUmbDebug)) return cUmbDebug;
return false;
}
}
}
@@ -0,0 +1,55 @@
using System;
using Umbraco.Core.Composing;
using Umbraco.Core.Logging;
using Umbraco.Net;
using Umbraco.Web.Common.Middleware;
namespace Umbraco.Web.Common.Runtime.Profiler
{
internal sealed class WebProfilerComponent : IComponent
{
private readonly bool _profile;
private readonly WebProfiler _profiler;
private readonly IUmbracoApplicationLifetime _umbracoApplicationLifetime;
private readonly IUmbracoRequestLifetime _umbracoRequestLifetime;
public WebProfilerComponent(IProfiler profiler, ILogger logger, IUmbracoRequestLifetime umbracoRequestLifetime,
IUmbracoApplicationLifetime umbracoApplicationLifetime)
{
_umbracoRequestLifetime = umbracoRequestLifetime;
_umbracoApplicationLifetime = umbracoApplicationLifetime;
_profile = true;
// although registered in WebRuntime.Compose, ensure that we have not
// been replaced by another component, and we are still "the" profiler
_profiler = profiler as WebProfiler;
if (_profiler != null) return;
// if VoidProfiler was registered, let it be known
if (profiler is VoidProfiler)
logger.Info<WebProfilerComponent>(
"Profiler is VoidProfiler, not profiling (must run debug mode to profile).");
_profile = false;
}
public void Initialize()
{
if (!_profile) return;
// bind to ApplicationInit - ie execute the application initialization for *each* application
// it would be a mistake to try and bind to the current application events
_umbracoApplicationLifetime.ApplicationInit += InitializeApplication;
}
public void Terminate()
{
}
private void InitializeApplication(object sender, EventArgs args)
{
_umbracoRequestLifetime.RequestStart +=
(sender, context) => _profiler.UmbracoApplicationBeginRequest(context);
_umbracoRequestLifetime.RequestEnd += (sender, context) => _profiler.UmbracoApplicationEndRequest(context);
}
}
}
@@ -0,0 +1,8 @@
using Umbraco.Core.Composing;
namespace Umbraco.Web.Common.Runtime.Profiler
{
internal class WebProfilerComposer : ComponentComposer<WebProfilerComponent>, ICoreComposer
{
}
}
@@ -0,0 +1,119 @@
using System;
using System.Threading;
using StackExchange.Profiling;
using StackExchange.Profiling.Internal;
using Umbraco.Core.Cache;
namespace Umbraco.Web.Common.Runtime.Profiler
{
public class WebProfilerProvider : DefaultProfilerProvider
{
private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim();
private volatile BootPhase _bootPhase;
private int _first;
private MiniProfiler _startupProfiler;
public WebProfilerProvider()
{
// booting...
_bootPhase = BootPhase.Boot;
}
/// <summary>
/// Gets the current profiler.
/// </summary>
/// <remarks>
/// If the boot phase is not Booted, then this will return the startup profiler (this), otherwise
/// returns the base class
/// </remarks>
public override MiniProfiler CurrentProfiler
{
get
{
// if not booting then just use base (fast)
// no lock, _bootPhase is volatile
if (_bootPhase == BootPhase.Booted)
return base.CurrentProfiler;
// else
try
{
var current = base.CurrentProfiler;
return current ?? _startupProfiler;
}
catch
{
return _startupProfiler;
}
}
}
public void BeginBootRequest()
{
_locker.EnterWriteLock();
try
{
if (_bootPhase != BootPhase.Boot)
throw new InvalidOperationException("Invalid boot phase.");
_bootPhase = BootPhase.BootRequest;
// assign the profiler to be the current MiniProfiler for the request
// is's already active, starting and all
CurrentProfiler = _startupProfiler;
}
finally
{
_locker.ExitWriteLock();
}
}
public void EndBootRequest()
{
_locker.EnterWriteLock();
try
{
if (_bootPhase != BootPhase.BootRequest)
throw new InvalidOperationException("Invalid boot phase.");
_bootPhase = BootPhase.Booted;
_startupProfiler = null;
}
finally
{
_locker.ExitWriteLock();
}
}
/// <summary>
/// Starts a new MiniProfiler.
/// </summary>
/// <remarks>
/// <para>
/// This is called when WebProfiler calls MiniProfiler.Start() so,
/// - as a result of WebRuntime starting the WebProfiler, and
/// - assuming profiling is enabled, on every BeginRequest that should be profiled,
/// - except for the very first one which is the boot request.
/// </para>
/// </remarks>
public override MiniProfiler Start(string profilerName, MiniProfilerBaseOptions options)
{
var first = Interlocked.Exchange(ref _first, 1) == 0;
if (first == false) return base.Start(profilerName, options);
_startupProfiler = new MiniProfiler("StartupProfiler", options);
CurrentProfiler = _startupProfiler;
return _startupProfiler;
}
/// <summary>
/// Indicates the boot phase.
/// </summary>
private enum BootPhase
{
Boot = 0, // boot phase (before the 1st request even begins)
BootRequest = 1, // request boot phase (during the 1st request)
Booted = 2 // done booting
}
}
}
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<OutputType>Library</OutputType>
<LangVersion>8</LangVersion>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Umbraco.Core\Umbraco.Core.csproj" />
<ProjectReference Include="..\Umbraco.Infrastructure\Umbraco.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MiniProfiler.AspNetCore.Mvc" Version="4.1.0" />
</ItemGroup>
</Project>
+4
View File
@@ -9,6 +9,7 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Umbraco.Web.BackOffice.AspNetCore;
using Umbraco.Web.Common.Extensions;
using Umbraco.Web.Website.AspNetCore;
@@ -21,7 +22,9 @@ namespace Umbraco.Web.UI.BackOffice
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddUmbracoConfiguration();
services.AddUmbracoRequest();
services.AddUmbracoWebsite();
services.AddUmbracoBackOffice();
}
@@ -29,6 +32,7 @@ namespace Umbraco.Web.UI.BackOffice
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseUmbracoRequest();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
@@ -7,6 +7,7 @@
<ItemGroup>
<ProjectReference Include="..\Umbraco.Web.BackOffice\Umbraco.Web.BackOffice.csproj" />
<ProjectReference Include="..\Umbraco.Web.Common\Umbraco.Web.Common.csproj" />
<ProjectReference Include="..\Umbraco.Web.Website\Umbraco.Web.Website.csproj" />
</ItemGroup>
+1 -1
View File
@@ -101,7 +101,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="MiniProfiler" Version="4.0.138" />
<PackageReference Include="MiniProfiler" Version="4.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="SecurityCodeScan">
<Version>3.4.0</Version>
@@ -1,3 +1,4 @@
using System;
using System.Threading;
using System.Web;
using Umbraco.Net;
@@ -11,6 +12,8 @@ namespace Umbraco.Web.AspNet
public AspNetUmbracoApplicationLifetime(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
UmbracoApplicationBase.ApplicationInit += ApplicationInit;
}
public bool IsRestarting { get; set; }
@@ -30,5 +33,7 @@ namespace Umbraco.Web.AspNet
Thread.CurrentPrincipal = null;
HttpRuntime.UnloadAppDomain();
}
public event EventHandler ApplicationInit;
}
}
@@ -56,20 +56,7 @@ namespace Umbraco.Web.Install.InstallSteps
throw new InvalidOperationException("Could not find the super user!");
}
var userManager = _httpContextAccessor.GetRequiredHttpContext().GetOwinContext().GetBackOfficeUserManager();
var membershipUser = await userManager.FindByIdAsync(Constants.Security.SuperUserId);
if (membershipUser == null)
{
throw new InvalidOperationException($"No user found in membership provider with id of {Constants.Security.SuperUserId}.");
}
//To change the password here we actually need to reset it since we don't have an old one to use to change
var resetToken = await userManager.GeneratePasswordResetTokenAsync(membershipUser.Id);
var resetResult = await userManager.ChangePasswordWithResetAsync(membershipUser.Id, resetToken, user.Password.Trim());
if (!resetResult.Succeeded)
{
throw new InvalidOperationException("Could not reset password: " + string.Join(", ", resetResult.Errors));
}
await ResetAdminPassword(user.Password);
admin.Email = user.Email.Trim();
admin.Name = user.Name.Trim();
@@ -95,6 +82,26 @@ namespace Umbraco.Web.Install.InstallSteps
return null;
}
private async Task ResetAdminPassword(string clearTextPassword)
{
var userManager = _httpContextAccessor.GetRequiredHttpContext().GetOwinContext().GetBackOfficeUserManager();
var membershipUser = await userManager.FindByIdAsync(Constants.Security.SuperUserId);
if (membershipUser == null)
{
throw new InvalidOperationException(
$"No user found in membership provider with id of {Constants.Security.SuperUserId}.");
}
//To change the password here we actually need to reset it since we don't have an old one to use to change
var resetToken = await userManager.GeneratePasswordResetTokenAsync(membershipUser.Id);
var resetResult =
await userManager.ChangePasswordWithResetAsync(membershipUser.Id, resetToken, clearTextPassword.Trim());
if (!resetResult.Succeeded)
{
throw new InvalidOperationException("Could not reset password: " + string.Join(", ", resetResult.Errors));
}
}
/// <summary>
/// Return a custom view model for this step
/// </summary>
+1 -1
View File
@@ -92,7 +92,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="MiniProfiler" Version="4.0.138" />
<PackageReference Include="MiniProfiler" Version="4.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="NPoco" Version="4.0.2" />
<PackageReference Include="SecurityCodeScan">
+6
View File
@@ -123,6 +123,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Web.Website", "Umbr
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Tests.Common", "Umbraco.Tests.Common\Umbraco.Tests.Common.csproj", "{A499779C-1B3B-48A8-B551-458E582E6E96}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Web.Common", "Umbraco.Web.Common\Umbraco.Web.Common.csproj", "{839D3048-9718-4907-BDE0-7CD63D364383}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -195,6 +197,10 @@ Global
{A499779C-1B3B-48A8-B551-458E582E6E96}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A499779C-1B3B-48A8-B551-458E582E6E96}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A499779C-1B3B-48A8-B551-458E582E6E96}.Release|Any CPU.Build.0 = Release|Any CPU
{839D3048-9718-4907-BDE0-7CD63D364383}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{839D3048-9718-4907-BDE0-7CD63D364383}.Debug|Any CPU.Build.0 = Debug|Any CPU
{839D3048-9718-4907-BDE0-7CD63D364383}.Release|Any CPU.ActiveCfg = Release|Any CPU
{839D3048-9718-4907-BDE0-7CD63D364383}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE