ASP.NET
Core MiniProfiler
▌Introduction
MiniProfiler is a simple but effective mini-profiler for .NET, Ruby, Go and
Node.js.
For DOTNET
developers, it can watch the performance of codes in .NET, ASP.NET, ASP.NET
Core, ASP.NET MVC projects.
This article
will show how to use MiniProfiler in a ASP.NET Core + Entity Framework Core application, and
watching the performance of http requests.
▌Environment
▋ASP.NET Core 3.X (.NET Core SDK 3.1.201)
▋MiniProfiler 4.1.0
▌Implement
▋Intall Packages
First install
the Nuget packages:
Name
|
Nuget package
|
Note
|
MySQL
|
-
|
|
Redis
|
-
|
|
SQL
Server
|
-
|
|
SQL
Server CE
|
-
|
|
SQLite
|
-
|
▋Enable Profiling
▋Startup.cs:
Configure
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseMiniProfiler();
// …
skip
}
▋Startup.cs:
ConfigureService
services.AddMiniProfiler(options =>
{
options.RouteBasePath = "/profiler";
#region Storage
// (Optional) Control storage
// (default is 30 minutes in MemoryCacheStorage)
(options.Storage as MemoryCacheStorage).CacheDuration = TimeSpan.FromMinutes(60);
#endregion
#region Styles
// Default: left
options.PopupRenderPosition = RenderPosition.BottomLeft;
// Default: 15
options.PopupMaxTracesToShow = 10;
// (Optional) Control storage
// (default is 30 minutes in MemoryCacheStorage)
(options.Storage as MemoryCacheStorage).CacheDuration = TimeSpan.FromMinutes(60);
// (Optional) Control which SQL formatter to use, InlineFormatter is the default
options.SqlFormatter = new StackExchange.Profiling.SqlFormatters.InlineFormatter();
#endregion
#region Include/Exclude tracking
// (Optional) You can disable "Connection Open()", "Connection Close()" (and async variant) tracking.
// (defaults to true, and connection opening/closing is tracked)
options.TrackConnectionOpenClose = false;
// Ignore tracing any class named "MyClass"
options.ExcludeType("MyClass");
// options.ExcludedTypes.Add("MyClass");
// Ignore tracing the assembly named "AspNetCore.Profiler.Core"
options.ExcludeAssembly("AspNetCore.Profiler.Core");
// options.ExcludedAssemblies.Add("AspNetCore.Profiler.Core");
// Ignore tracing the method(s) named "IgnoreMe"
options.ExcludeMethod("IgnoreMe");
// options.ExcludedMethods.Add("IgnoreMe");
// Ignore tracing the request with the url path
options.IgnorePath("/Home");
#endregion
#region Authorization
// (Optional)To control authorization, you can use the Func<HttpRequest, bool> options:
// (default is everyone can access profilers)
options.ResultsAuthorize = request => MyCheckIfCanAccessMiniProfiler(request);
options.ResultsListAuthorize = request => MyCheckIfCanAccessMiniProfiler(request);
#endregion
})
.AddEntityFramework(); // Enable Entity Framework tracking
Then we can see
the profiling information in these ways:
Url
|
Description
|
|
1
|
https://<host>/profiler/results
|
Profiling
result for the latest request
|
2
|
https://<host>/profiler/results-index
|
Profiling
results for stored requests
|
3
|
https://<host>/profiler/results-list
|
Profiling
results in JSON for stored requests
|
Latest request
profiling: https://localhost:5011/profiler/results
All stored requests’
profiling information in web page: https://localhost:5011/profiler/results-index
PS. We can click
on the link on the Name field to see the details.
The detail will
be show as below with the url like https://localhost:5011/profiler/results?id=1b842295-5d46-41db-aeca-8151dcd8abc8
All stored requests’
profiling information in JSON: https://localhost:5011/profiler/results-list
▋Enable showing real-time profiling result on MVC view
To show the
embedded profiling result on the MVC page as following, there are some
additional steps.
▋Add Tag helper
Open _ViewImports.cshtml
and add MiniProfiler’s tag helper,
@using AspNetCore.Profiler.Mvc
@using AspNetCore.Profiler.Mvc.Models
@using StackExchange.Profiling
@addTagHelper *, MiniProfiler.AspNetCore.Mvc
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
▋Add MiniProfiler
tag to your master layout
Open the master
layout (Views/Shared/_Layout.cshtml
in default), and add <mini-profiler /> inside.
<body>
<!-- ... skip other HTML -->
<mini-profiler />
</body>
Now we will be
able to see the real-time profiling result on every page.
▋ Styles
Here are some of
the options for styling:
▋PopupRenderPosition
options.PopupRenderPosition = RenderPosition.BottomLeft; // Left|Right|BottomLeft|BottomRight
When setting it to RenderPosition.BottomLeft:
When setting it
to RenderPosition.Left:
▋SqlFormatter
When setting it
to InlineFormatter:
When setting it
to SqlServerFormatter:
▋Profiling/Tracking
▋Database
connection
Disable tracking
Database connection open/close by
// (Optional) You can disable "Connection Open()", "Connection Close()" (and async variant) tracking.
// (defaults to true, and connection opening/closing is tracked)
options.TrackConnectionOpenClose = false;
The default
value is “true”, that will result in showing the following information (in red
block):
▋Exclude
Type/Assembly/Method
Furthermore, we
can exclude tracking specified Type/Assembly/Method by setting ExcludeType("MyClass")
/ ExcludeAssembly("MyAssembly")
/ ExcludeMethod("MyMethod")
or use ExcludedTypes.Add("MyClass")
/ ExcludedAssemblies.Add("MyAssembly")
/ ExcludedMethods.Add("MyMethod").
For example,
options.ExcludeType("MyClass");
options.ExcludedTypes.Add("MyClass");
We can chain the
exclude methods like following,
options.ExcludeType("MyClass").ExcludeAssembly("MyAssembly");
▋Ignore path
// Ignore tracing the request with the url path
options.IgnorePath("/Home");
Which will
result not tracking the request to
https://myserver/home/index
https://myserver/home/query
https://myserver/home/....
▋Storage
We can save them
into database as the persistent storage.
I will take
storing to Microsoft SQL Server as example.
First, we have
to create the MiniProfiler’s tables in the database.
▋How to create
required tables for MiniProfiler
There is the “GetTableCreationScripts” method inside MiniProfiler’s source code: dotnet/src/MiniProfiler.Providers.SqlServer/SqlServerStorage.cs, that has the creation SQL script.
You can just
copy the full sql below and execute in your database to create them.
PS. Watch out
the changes for newer MiniProfiler, I am using MiniProfiler.Providers.SqlServer
4.1.0 when writing the code.
CREATE TABLE {MiniProfilersTable}
(
RowId integer not null identity constraint PK_{MiniProfilersTable} primary key clustered, -- Need a clustered primary key for SQL Azure
Id uniqueidentifier not null, -- don't cluster on a guid
RootTimingId uniqueidentifier null,
Name nvarchar(200) null,
Started datetime not null,
DurationMilliseconds decimal(15,1) not null,
[User] nvarchar(100) null,
HasUserViewed bit not null,
MachineName nvarchar(100) null,
CustomLinksJson nvarchar(max),
ClientTimingsRedirectCount int null
);
-- displaying results selects everything based on the main MiniProfilers.Id column
CREATE UNIQUE NONCLUSTERED INDEX IX_{MiniProfilersTable}_Id ON {MiniProfilersTable} (Id);
-- speeds up a query that is called on every .Stop()
CREATE NONCLUSTERED INDEX IX_{MiniProfilersTable}_User_HasUserViewed_Includes ON {MiniProfilersTable} ([User], HasUserViewed) INCLUDE (Id, [Started]);
CREATE TABLE {MiniProfilerTimingsTable}
(
RowId integer not null identity constraint PK_{MiniProfilerTimingsTable} primary key clustered,
Id uniqueidentifier not null,
MiniProfilerId uniqueidentifier not null,
ParentTimingId uniqueidentifier null,
Name nvarchar(200) not null,
DurationMilliseconds decimal(15,3) not null,
StartMilliseconds decimal(15,3) not null,
IsRoot bit not null,
Depth smallint not null,
CustomTimingsJson nvarchar(max) null
);
CREATE UNIQUE NONCLUSTERED INDEX IX_{MiniProfilerTimingsTable}_Id ON {MiniProfilerTimingsTable} (Id);
CREATE NONCLUSTERED INDEX IX_{MiniProfilerTimingsTable}_MiniProfilerId ON {MiniProfilerTimingsTable} (MiniProfilerId);
CREATE TABLE {MiniProfilerClientTimingsTable}
(
RowId integer not null identity constraint PK_{MiniProfilerClientTimingsTable} primary key clustered,
Id uniqueidentifier not null,
MiniProfilerId uniqueidentifier not null,
Name nvarchar(200) not null,
Start decimal(9, 3) not null,
Duration decimal(9, 3) not null
);
CREATE UNIQUE NONCLUSTERED INDEX IX_{MiniProfilerClientTimingsTable}_Id on {MiniProfilerClientTimingsTable} (Id);
CREATE NONCLUSTERED INDEX IX_{MiniProfilerClientTimingsTable}_MiniProfilerId on {MiniProfilerClientTimingsTable} (MiniProfilerId);
";
Or we can do a
little trick to use the GetTableCreationScripts method from StackExchange.Profiling.Storage.SqlServerStorage.
First lets
create a class to inherit class: SqlServerStorage, and expose the
creation SQL script:
public class CustomSqlServerStorage : SqlServerStorage
{
public CustomSqlServerStorage(string connectionString) : base(connectionString)
{
}
public IEnumerable<string> CreateSqls
{
get {
return base.GetTableCreationScripts();
}
}
}
Then we can
create the tables after startup, open Program.cs and update as
following,
public class Program
{
public static void Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
// Create MiniProfiler's profiling table
var configuration = services.GetRequiredService<IConfiguration>();
var connectionString = configuration.GetConnectionString("DefaultConnection");
var dbContext = services.GetRequiredService<DemoDbContext>() as DemoDbContext;
var tableQueryRslt = dbContext.Payments.FromSqlRaw(
"SELECT NEWID() as Id FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'dbo' AND TABLE_NAME = 'MiniProfilers'");
var isExist = tableQueryRslt.Count() > 0;
if (!isExist)
{
using (var sqlserverStorage = new CustomSqlServerStorage(connectionString))
{
IEnumerable<string> createSqls = sqlserverStorage.CreateSqls;
foreach (string sql in createSqls)
{
_ = dbContext.Database.ExecuteSqlRaw(sql);
}
}
}
}
host.Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
Either of the
ways will create the following tables in SQL Server:
▋Enable SQL
Server Storage
After creating
the tables, enable SQL Server Storage in the MiniProfiler options,
services.AddMiniProfiler(options =>
{
// …
skip other options
options.Storage = new SqlServerStorage(Configuration.GetConnectionString("DefaultConnection"));
}
Result:
▋Custom authorization
The profiling
result contains sensitive information, such as Stacktrace, SQL and etc. It’s
better to allow only authorized users to access the profiling result.
MiniProfiler has
2 options to set the authorization callbacks that will check the requests:
·
ResultsAuthorize: which will
restrict accessing to the single profiling result
·
ResultsListAuthorize: which
will restrict accessing to the profiling results(list)
Notice
that if we enable the MiniProfiler tag on the master page, the
ResultsAuthorize’s callback will be triggered not only on the MiniProfiler’s
single result page (i.e. https://<host>/profiler/results/[?id=xxxx])
when viewing any page.
|
services.AddMiniProfiler(options =>
{
options.RouteBasePath = "/profiler";
// …
skip other settings
// (Optional)To control authorization, you can use the Func<HttpRequest, bool> options:
// (default is everyone can access profilers)
options.ResultsAuthorize = request => MyCheckIfCanAccessMiniProfiler(request);
options.ResultsListAuthorize = request => MyCheckIfCanAccessMiniProfiler(request);
})
.AddEntityFramework(); // Enable Entity Framework tracking
Here is an
example that I use JWT to authenticate the request from users.
1. Store the JWT in cookie to have it validated when
viewing profiling result page in browser.
2. Validate the Authorization header (Bearer token) when trying
to send the request without browser (such as thru RESTful API).
▋Create a custom
Authorization Callback
I created the
Authorization callback as HttpRequestExtensions an can be set like this:
services.AddMiniProfiler(options =>
{
options.RouteBasePath = "/profiler";
// …
skip other settings
// (Optional)To control authorization, you can use the Func<HttpRequest, bool> options:
// (default is everyone can access profilers)
options.ResultsAuthorize
= request => request.IsAuthorizedToMiniProfiler();
options.ResultsListAuthorize = request =>
request.IsAuthorizedToMiniProfiler();
})
.AddEntityFramework(); // Enable Entity Framework tracking
The HttpRequestExtensions
implementation:
public static class HttpRequestExtensions
{
/// <summary>
/// Check if the request is authorized to access MiniProfiler
/// </summary>
/// <param name="request">HttpRequest</param>
/// <returns>True(OK)/False(Not allowed)</returns>
public static bool IsAuthorizedToMiniProfiler(this HttpRequest request)
{
if (!request.Path.ToString().StartsWith("/profiler", StringComparison.InvariantCultureIgnoreCase))
{
return true;
}
var bearerTokenPrefix = "Bearer";
var accessToken = string.Empty;
var authorizationHeader = request.Headers["Authorization"].ToString();
// Get Access token from header
if (!string.IsNullOrEmpty(authorizationHeader) &&
authorizationHeader.StartsWith(bearerTokenPrefix, StringComparison.OrdinalIgnoreCase))
{
accessToken = authorizationHeader.Replace(bearerTokenPrefix, string.Empty, StringComparison.OrdinalIgnoreCase).Trim();
}
// Get Access token from cookie
else if (request.Cookies.TryGetValue("access-token", out accessToken))
{
}
else
{
return false;
}
// Validate JWT
return AccessTokenValidator.ValidateAsync(accessToken).Result;
}
}
Key points:
1. Only validating the request when it is trying to access MiniProfiler’s
path: “/profile/…”
2. Validate the access token (JWT) that comes from cookie or Http
header.
▋Demo
Now the
MiniProfiler can only be accessed on valid access token (JWT).
If there is a
valid access token:
Or we will get a
401 Unauthorized response as following:
And if we send a
request with Authorization header and valid access token, we will get a success
response:
Or 401
Unauthorized response without access token or invalid token:
Make sure that
you had protected the profiling information when rocking on production!
▌Reference
沒有留言:
張貼留言