diff --git a/README.md b/README.md index 555ffbd..b64822b 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,6 @@ # Entity Framework Profiler for ASP.Net Core -A tiny profiler without hassle. - - - ``` ┌────────────────────────────────────────────────────────────────────────────────┐ │ │ @@ -35,10 +31,15 @@ A tiny profiler without hassle. └────────────────────────────────────────────────────────────────────────────────┘ ``` + +A tiny profiler without hassle. + + + ## Installation ``` -dotnet add package NanoDbProfiler.AspNetCore --version 0.1.23-alpha-g1a0a4554d2 --source https://www.myget.org/F/guneysu/api/v3/index.json +dotnet add package NanoDbProfiler.AspNetCore --version 8.0.10 --source https://www.myget.org/F/guneysu/api/v3/index.json ``` ```csharp diff --git a/pakefile.ps1 b/pakefile.ps1 index a033262..304ee4d 100644 --- a/pakefile.ps1 +++ b/pakefile.ps1 @@ -1,5 +1,5 @@ -$msbuild = "C:\Program Files\Microsoft Visual Studio\2022\Community\\MSBuild\Current\Bin\amd64\MSBuild.exe" -$msbuild = "C:\Program Files\Microsoft Visual Studio\2022\Preview\\MSBuild\Current\Bin\amd64\MSBuild.exe" +$msbuild = "C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\amd64\MSBuild.exe" +$msbuild = "C:\Program Files\Microsoft Visual Studio\2022\Preview\MSBuild\Current\Bin\amd64\MSBuild.exe" function Init { # dotnet tool install -g nbgv @@ -42,7 +42,6 @@ function Patch-Version { function Build { - . "${msbuild}" /bl ` .\src\NanoDbProfiler.AspNetCore\NanoDbProfiler.AspNetCore.csproj ` -p:Configuration=Release ` diff --git a/src/Directory.Build.props b/src/Directory.Build.props index a02f3fe..652485a 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -4,6 +4,7 @@ enable enable true + 8.0.11 $(MSBuildThisFileDirectory) diff --git a/src/Example/Example.csproj b/src/Example/Example.csproj index fdfeb03..704001e 100644 --- a/src/Example/Example.csproj +++ b/src/Example/Example.csproj @@ -1,24 +1,38 @@ - + net8.0 enable enable + $(DefineConstants);EFCORE_PROFILER_TOOLBAR - - - + + + - + + + + + + + + + + + + + + - + diff --git a/src/Example/Example.sln b/src/Example/Example.sln new file mode 100644 index 0000000..30ce4c3 --- /dev/null +++ b/src/Example/Example.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Example", "Example.csproj", "{FB0C2DC9-B030-4E94-933E-4D57671CEE08}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {FB0C2DC9-B030-4E94-933E-4D57671CEE08}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FB0C2DC9-B030-4E94-933E-4D57671CEE08}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FB0C2DC9-B030-4E94-933E-4D57671CEE08}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FB0C2DC9-B030-4E94-933E-4D57671CEE08}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {2FD7FBBB-C0F2-4F98-BAD9-BCD8B35FA502} + EndGlobalSection +EndGlobal diff --git a/src/Example/NanoDbProfilerEfCoreQueryInterceptor.cs b/src/Example/NanoDbProfilerEfCoreQueryInterceptor.cs deleted file mode 100644 index 0918e4a..0000000 --- a/src/Example/NanoDbProfilerEfCoreQueryInterceptor.cs +++ /dev/null @@ -1,9 +0,0 @@ - -using System.Data.Common; - -using Microsoft.EntityFrameworkCore.Diagnostics; - -using NanoDbProfiler.AspNetCore; - -namespace Example; - diff --git a/src/Example/Pages/Index.cshtml b/src/Example/Pages/Index.cshtml new file mode 100644 index 0000000..5b31fe2 --- /dev/null +++ b/src/Example/Pages/Index.cshtml @@ -0,0 +1,235 @@ +@page +@model Example.Pages.IndexModel +@{ +} + + + + + + + + + NanoDbProfiler for Entity Framework Core + + + + + + + + + + NanoDB Profiler & Sample Queries + Refresh + Clean + Repeat + + Choose an endpoint: + + + + + + Response: + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Example/Pages/Index.cshtml.cs b/src/Example/Pages/Index.cshtml.cs new file mode 100644 index 0000000..d2466e2 --- /dev/null +++ b/src/Example/Pages/Index.cshtml.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Example.Pages +{ + public class IndexModel : PageModel + { + public void OnGet() + { + } + } +} diff --git a/src/Example/Program.cs b/src/Example/Program.cs index f50b917..984f277 100644 --- a/src/Example/Program.cs +++ b/src/Example/Program.cs @@ -1,31 +1,70 @@ +using System.Diagnostics; + +using DbQueryTrace; + using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace Example; + public class Program { public static void Main(string [] args) { var builder = WebApplication.CreateBuilder(args); - + + builder.Services.AddRazorPages(); builder.Services .AddDbContext(o => o - .UseSqlite("Data Source=db.sqlite") - .AddNanoDbProfilerEfCoreInterceptor()) - - .AddNanoDbProfiler() + .AddInterceptors(QueryToolbarInterceptor.DebugLogger()) + .AddInterceptors(QueryToolbarInterceptor.QueryToolbarLogger()) + .UseSqlite("Data Source=db.sqlite")) + +#if NANODBPROFILER + .AddNanoDbProfiler() +#elif EFCORE_PROFILER_TOOLBAR + .AddEfCoreProfilerToolbar() +#endif ; var app = builder.Build(); - app - .UseNanoDbProfilerPage() - .UseNanoDbProfilerToolbar(); - app.MapGet("/", async (HttpContext h, [FromServices] TodoContext db) => +#if NANODBPROFILER + app.UseNanoDbProfiler(x => + { + x.ToolbarEnabled = true; + x.ProfilerPageUrl = "query-log"; + }); +#endif + + +#if EFCORE_PROFILER_TOOLBAR + app.UseEfCoreQueryToolbar(x => x.DisableOnPaths = new [] { + "graphql" + }); +#endif + +#if ALTERNATIVES + app.UseNanoDbProfiler(x => + { + x.ToolbarEnabled = true; + x.ProfilerPageUrl = "query-log"; + }); + + app.UseNanoDbProfiler(x => x.ToolbarEnabled = true); +#endif + + //app.UseDefaultFiles(); + + //app.UseRouting(); + + app.MapRazorPages(); + + app.MapGet("/hello", async (HttpContext h, [FromServices] TodoContext db) => { await db.Database.EnsureCreatedAsync(); - return Results.Text("Hello World!", "text/html"); + return Results.Text("Hello World!", "text/html; charset=utf-8"); }); app.MapGet("/insert", (HttpContext h, [FromServices] TodoContext db) => @@ -41,6 +80,7 @@ public static void Main(string [] args) return Results.Ok(e.Title); }); + app.MapGet("/select/scalar", (HttpContext h, [FromServices] TodoContext db) => Results.Ok(db.Todos.Select(x => x.Id))); app.MapGet("/select/no-tracking", (HttpContext h, [FromServices] TodoContext db) => Results.Ok(db.Todos.Take(10).AsNoTracking())); @@ -85,13 +125,11 @@ public static void Main(string [] args) return Results.Ok(); }); - app - .Services - .CreateScope() - .ServiceProvider - .GetRequiredService() - .Database - .EnsureCreated(); + using (var scope = app.Services.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + db.Database.EnsureCreated(); + } app.Run(); } diff --git a/src/Example/Properties/launchSettings.json b/src/Example/Properties/launchSettings.json index 1625bb2..609c4d4 100644 --- a/src/Example/Properties/launchSettings.json +++ b/src/Example/Properties/launchSettings.json @@ -1,7 +1,7 @@ { "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { - "http": { + "http (query-log)": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, @@ -10,6 +10,25 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } + }, + "http (/)": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5042", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "http (no browser)": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5042", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } } + } } diff --git a/src/Example/pakefile.ps1 b/src/Example/pakefile.ps1 new file mode 100644 index 0000000..9f3e19f --- /dev/null +++ b/src/Example/pakefile.ps1 @@ -0,0 +1,3 @@ +function Run { + dotnet run -c Debug /p:DefineConstants="EFCORE_PROFILER_TOOLBAR" +} diff --git a/src/Example/wwwroot/default.html b/src/Example/wwwroot/default.html new file mode 100644 index 0000000..ec8e5e8 --- /dev/null +++ b/src/Example/wwwroot/default.html @@ -0,0 +1,224 @@ + + + + + + + NanoDbProfiler for Entity Framework Core + + + + + + + + + + + + NanoDB Profiler & Sample Queries + Refresh + Clean + Repeat + + Choose an endpoint: + + {{ url }} + + + + Response: + {{ responseStatus }} + + + + + + + + {{ summary.query }} + + + + + + + + Total Count: {{ summary.total }} + Duration: {{ summary.p95.toFixed(3) }}ms + + + + + + + + + \ No newline at end of file diff --git a/src/NanoDbProfiler.AspNetCore/AspnetCoreExtensions.cs b/src/NanoDbProfiler.AspNetCore/AspnetCoreExtensions.cs index d2895a8..7fbdc7d 100644 --- a/src/NanoDbProfiler.AspNetCore/AspnetCoreExtensions.cs +++ b/src/NanoDbProfiler.AspNetCore/AspnetCoreExtensions.cs @@ -1,116 +1,133 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; +namespace Microsoft.Extensions.DependencyInjection; -namespace Microsoft.Extensions.DependencyInjection +public static class AspnetCoreExtensions { - public static class AspnetCoreExtensions + private static bool _routeConfigured; + private static bool _toolbarMiddlewareRegistered; + + public static IServiceCollection AddNanoDbProfiler(this IServiceCollection _) { - public static IServiceCollection AddNanoDbProfiler(this IServiceCollection _) - { - const string EfCoreRelationalAssemblyString = "Microsoft.EntityFrameworkCore.Relational"; - const string DiagnosticLoggerFullname = "Microsoft.EntityFrameworkCore.Diagnostics.Internal.RelationalCommandDiagnosticsLogger"; - const string DiagnosticLoggerInterfaceName = "IRelationalCommandDiagnosticsLogger"; + const string EfCoreRelationalAssemblyString = "Microsoft.EntityFrameworkCore.Relational"; + const string DiagnosticLoggerFullname = "Microsoft.EntityFrameworkCore.Diagnostics.Internal.RelationalCommandDiagnosticsLogger"; + const string DiagnosticLoggerInterfaceName = "IRelationalCommandDiagnosticsLogger"; - var efCoreRelationalAsm = Assembly.Load(EfCoreRelationalAssemblyString); - ArgumentNullException.ThrowIfNull(efCoreRelationalAsm); + var efCoreRelationalAsm = Assembly.Load(EfCoreRelationalAssemblyString); + ArgumentNullException.ThrowIfNull(efCoreRelationalAsm); - var h = new Harmony("id"); + var h = new Harmony("id"); - Assembly [] assemblies = AppDomain.CurrentDomain.GetAssemblies(); + Assembly [] assemblies = AppDomain.CurrentDomain.GetAssemblies(); - var diagnosticsLoggerTypes = ( - from t in assemblies.SelectMany(t => t.GetTypes()) - where t.GetInterfaces().Any(t => t.Name.Contains(DiagnosticLoggerInterfaceName)) - select t); + var diagnosticsLoggerTypes = ( + from t in assemblies.SelectMany(t => t.GetTypes()) + where t.GetInterfaces().Any(t => t.Name.Contains(DiagnosticLoggerInterfaceName)) + select t); - var efCoreRelationAsm = ( - from asm in assemblies - where asm.GetName().Name == EfCoreRelationalAssemblyString - select asm).Single(); + var efCoreRelationAsm = ( + from asm in assemblies + where asm.GetName().Name == EfCoreRelationalAssemblyString + select asm).Single(); - Type [] efCoreRelationalTypes = efCoreRelationAsm.GetTypes(); + Type [] efCoreRelationalTypes = efCoreRelationAsm.GetTypes(); - var diagnosticsLoggerType = ( - from t in efCoreRelationalTypes - where t.FullName == DiagnosticLoggerFullname - select t).Single(); + var diagnosticsLoggerType = ( + from t in efCoreRelationalTypes + where t.FullName == DiagnosticLoggerFullname + select t).Single(); - MethodInfo [] diagnosticsLoggerMethods = diagnosticsLoggerType.GetMethods(AccessTools.all); + MethodInfo [] diagnosticsLoggerMethods = diagnosticsLoggerType.GetMethods(AccessTools.all); - var diagLoggerMethods = ( - from m in diagnosticsLoggerMethods - where m.Name.Contains("Executed") - select m); + var diagLoggerMethods = ( + from m in diagnosticsLoggerMethods + where m.Name.Contains("Executed") + select m); - patch("CommandReaderExecuted", nameof(Hooks.CommandReaderExecuted), diagLoggerMethods, h); - patch("CommandScalarExecuted", nameof(Hooks.CommandScalarExecuted), diagLoggerMethods, h); - patch("CommandNonQueryExecuted", nameof(Hooks.CommandNonQueryExecuted), diagLoggerMethods, h); - patch("CommandReaderExecutedAsync", nameof(Hooks.CommandReaderExecutedAsync), diagLoggerMethods, h); - patch("CommandScalarExecutedAsync", nameof(Hooks.CommandScalarExecutedAsync), diagLoggerMethods, h); - patch("CommandNonQueryExecutedAsync", nameof(Hooks.CommandNonQueryExecutedAsync), diagLoggerMethods, h); + patch("CommandReaderExecuted", nameof(Hooks.CommandReaderExecuted), diagLoggerMethods, h); + patch("CommandScalarExecuted", nameof(Hooks.CommandScalarExecuted), diagLoggerMethods, h); + patch("CommandNonQueryExecuted", nameof(Hooks.CommandNonQueryExecuted), diagLoggerMethods, h); + patch("CommandReaderExecutedAsync", nameof(Hooks.CommandReaderExecutedAsync), diagLoggerMethods, h); + patch("CommandScalarExecutedAsync", nameof(Hooks.CommandScalarExecutedAsync), diagLoggerMethods, h); + patch("CommandNonQueryExecutedAsync", nameof(Hooks.CommandNonQueryExecutedAsync), diagLoggerMethods, h); - var relationCommandType = efCoreRelationalTypes.Single(x => x.Name == "RelationalCommand"); - var methods = relationCommandType.GetMethods(AccessTools.all); + var relationCommandType = efCoreRelationalTypes.Single(x => x.Name == "RelationalCommand"); + var methods = relationCommandType.GetMethods(AccessTools.all); - patch("ExecuteReader", methods, h, - prefix: new HarmonyMethod(typeof(Hooks).GetMethod(nameof(Hooks.ExecuteReaderPrefix))), - postfix: new HarmonyMethod(typeof(Hooks).GetMethod(nameof(Hooks.ExecuteReaderPostfix)))); + patch("ExecuteReader", methods, h, + prefix: new HarmonyMethod(typeof(Hooks).GetMethod(nameof(Hooks.ExecuteReaderPrefix))), + postfix: new HarmonyMethod(typeof(Hooks).GetMethod(nameof(Hooks.ExecuteReaderPostfix)))); - return _; - } + return _; + } - private static void patch(string name, IEnumerable methods, Harmony harmony, HarmonyMethod prefix, HarmonyMethod postfix) - { - var method = ( - from m in methods - where m.Name == name - select m).Single(); + private static void patch(string name, IEnumerable methods, Harmony harmony, HarmonyMethod prefix, HarmonyMethod postfix) + { + var method = ( + from m in methods + where m.Name == name + select m).Single(); - var replacement = harmony.Patch(method, prefix: prefix, postfix: postfix); - ArgumentNullException.ThrowIfNull(replacement); - } + var replacement = harmony.Patch(method, prefix: prefix, postfix: postfix); + ArgumentNullException.ThrowIfNull(replacement); + } + + private static void patch(string name, string hookName, IEnumerable methods, Harmony harmony) + { + var replacement = harmony.Patch(methods.Single(x => x.Name == name), new HarmonyMethod(typeof(Hooks).GetMethod(name))); + ArgumentNullException.ThrowIfNull(replacement); + } + + public static WebApplication UseNanoDbProfiler(this WebApplication app, Action config = default) + { + var nanoDbProfilerConfiguration = new NanoDbProfilerConfiguration(); + if (config != null) config(nanoDbProfilerConfiguration); - private static void patch(string name, string hookName, IEnumerable methods, Harmony harmony) + if (!_toolbarMiddlewareRegistered && nanoDbProfilerConfiguration.ToolbarEnabled) { - var replacement = harmony.Patch(methods.Single(x => x.Name == name), new HarmonyMethod(typeof(Hooks).GetMethod(name))); - ArgumentNullException.ThrowIfNull(replacement); + app.UseMiddleware(); + _toolbarMiddlewareRegistered = true; } - public static WebApplication UseNanoDbProfilerPage(this WebApplication app, string route = "query-log") - { - QueryLogMiddleware.QUERY_LOG_ROUTE = "/" + route; + configureProfilerPage(app, nanoDbProfilerConfiguration.ProfilerPageUrl); - EfQueryLog.ServiceScopeFactory = app.Services.GetRequiredService(); + return app; + } - app.MapGet(route, (HttpRequest h) => - { - var metrics = EfCoreMetrics.GetInstance(); - MediaTypeHeaderValue.TryParseList(h.Headers ["Accept"], out var accept); - - IResult resp = accept switch - { - null => EfQueryLog.TextResult(metrics), - var a when a.Any(x => x.MatchesMediaType("text/html")) => EfQueryLog.HtmlResult(metrics), - var a when a.Any(x => x.MatchesMediaType("text/plain")) => EfQueryLog.TextResult(metrics), - var a when a.Any(x => x.MatchesMediaType("application/json")) => EfQueryLog.JsonResult(metrics), - _ => EfQueryLog.TextResult(metrics) - }; - - return resp; - }); - - app.MapDelete(route, (HttpRequest h) => + static WebApplication configureProfilerPage(this WebApplication app, string route) + { + if (_routeConfigured) return app; + + QueryLogMiddleware.QUERY_LOG_ROUTE = "/" + route; + + QueryLog.ServiceScopeFactory = app.Services.GetRequiredService(); + + + app.MapGet(route, (HttpRequest h) => + { + var metrics = EfCoreMetrics.GetInstance(); + MediaTypeHeaderValue.TryParseList(h.Headers ["Accept"], out var accept); + + IResult resp = accept switch { - var metrics = EfCoreMetrics.GetInstance(); + null => QueryLog.TextResult(metrics), + var a when a.Any(x => x.MatchesMediaType("text/html")) => QueryLog.HtmlResult(metrics), + var a when a.Any(x => x.MatchesMediaType("text/plain")) => QueryLog.TextResult(metrics), + var a when a.Any(x => x.MatchesMediaType("application/json")) => QueryLog.JsonResult(metrics), + _ => QueryLog.TextResult(metrics) + }; - metrics.Clear(); - return Results.NoContent(); - }); + return resp; + }); + app.MapDelete(route, (HttpRequest h) => + { + var metrics = EfCoreMetrics.GetInstance(); - return app; - } + metrics.Clear(); + return Results.NoContent(); + }); + + _routeConfigured = true; - public static void UseNanoDbProfilerToolbar(this WebApplication app) => app.UseMiddleware(); + return app; } -} \ No newline at end of file +} diff --git a/src/NanoDbProfiler.AspNetCore/DashboardData.cs b/src/NanoDbProfiler.AspNetCore/DashboardData.cs index 3de34b2..9b35bc5 100644 --- a/src/NanoDbProfiler.AspNetCore/DashboardData.cs +++ b/src/NanoDbProfiler.AspNetCore/DashboardData.cs @@ -1,57 +1,4 @@ - -using System.Data.Common; - -using Microsoft.EntityFrameworkCore.Diagnostics; - -namespace NanoDbProfiler.AspNetCore; - -internal class NanoDbProfilerEfCoreQueryInterceptor : DbCommandInterceptor -{ - private static Metric metricFactory(CommandExecutedEventData eventData) - { - return new Metric() - { - Duration = eventData.Duration.TotalMilliseconds, - Query = eventData.Command.CommandText - }; - } - - public override int NonQueryExecuted(DbCommand command, CommandExecutedEventData eventData, int result) - { - EfCoreMetrics.GetInstance().Add(metricFactory(eventData)); - return base.NonQueryExecuted(command, eventData, result); - } - - public override ValueTask NonQueryExecutedAsync(DbCommand command, CommandExecutedEventData eventData, int result, CancellationToken cancellationToken = default) - { - EfCoreMetrics.GetInstance().Add(metricFactory(eventData)); - return base.NonQueryExecutedAsync(command, eventData, result, cancellationToken); - } - - public override DbDataReader ReaderExecuted(DbCommand command, CommandExecutedEventData eventData, DbDataReader result) - { - EfCoreMetrics.GetInstance().Add(metricFactory(eventData)); - return base.ReaderExecuted(command, eventData, result); - } - - public override ValueTask ScalarExecutedAsync(DbCommand command, CommandExecutedEventData eventData, object? result, CancellationToken cancellationToken = default) - { - EfCoreMetrics.GetInstance().Add(metricFactory(eventData)); - return base.ScalarExecutedAsync(command, eventData, result, cancellationToken); - } - - public override object? ScalarExecuted(DbCommand command, CommandExecutedEventData eventData, object? result) - { - EfCoreMetrics.GetInstance().Add(metricFactory(eventData)); - return base.ScalarExecuted(command, eventData, result); - } - - public override ValueTask ReaderExecutedAsync(DbCommand command, CommandExecutedEventData eventData, DbDataReader result, CancellationToken cancellationToken = default) - { - EfCoreMetrics.GetInstance().Add(metricFactory(eventData)); - return base.ReaderExecutedAsync(command, eventData, result, cancellationToken); - } -} +namespace NanoDbProfiler.AspNetCore; public struct DashboardData { diff --git a/src/NanoDbProfiler.AspNetCore/EfCoreDbContextOptionsBuilderExtensions.cs b/src/NanoDbProfiler.AspNetCore/EfCoreDbContextOptionsBuilderExtensions.cs index c469b4c..532231f 100644 --- a/src/NanoDbProfiler.AspNetCore/EfCoreDbContextOptionsBuilderExtensions.cs +++ b/src/NanoDbProfiler.AspNetCore/EfCoreDbContextOptionsBuilderExtensions.cs @@ -2,8 +2,42 @@ public static class EfCoreDbContextOptionsBuilderExtensions { - public static DbContextOptionsBuilder AddNanoDbProfilerEfCoreInterceptor(this DbContextOptionsBuilder o) + public static object AddNanoDbProfilerEfCoreInterceptor(object dbContextOptionsBuilder) { - return o.AddInterceptors(new NanoDbProfilerEfCoreQueryInterceptor()); + // Load the DbContextOptionsBuilder type dynamically + var dbContextOptionsBuilderType = dbContextOptionsBuilder.GetType(); + + // Load the DbCommandInterceptor type dynamically + var dbCommandInterceptorType = Type.GetType("Microsoft.EntityFrameworkCore.Diagnostics.DbCommandInterceptor"); + + if (dbCommandInterceptorType == null) + { + throw new InvalidOperationException("Unable to find DbCommandInterceptor type."); + } + + // Check if your class NanoDbProfilerEfCoreQueryInterceptor inherits from DbCommandInterceptor + var nanoDbProfilerEfCoreQueryInterceptorType = typeof(NanoDbProfilerEfCoreQueryInterceptor); + + if (!dbCommandInterceptorType.IsAssignableFrom(nanoDbProfilerEfCoreQueryInterceptorType)) + { + throw new InvalidOperationException($"{nanoDbProfilerEfCoreQueryInterceptorType.Name} does not inherit from DbCommandInterceptor."); + } + + // Create an instance of NanoDbProfilerEfCoreQueryInterceptor dynamically + var interceptorInstance = Activator.CreateInstance(nanoDbProfilerEfCoreQueryInterceptorType); + + // Get the AddInterceptors method of DbContextOptionsBuilder dynamically + var addInterceptorsMethod = dbContextOptionsBuilderType + .GetMethods(BindingFlags.Public | BindingFlags.Instance) + .FirstOrDefault(m => m.Name == "AddInterceptors" && m.GetParameters().Length == 1); + + if (addInterceptorsMethod == null) + { + throw new InvalidOperationException("Unable to find the AddInterceptors method."); + } + + // Invoke the AddInterceptors method dynamically + return addInterceptorsMethod.Invoke(dbContextOptionsBuilder, new [] { interceptorInstance }); } } + diff --git a/src/NanoDbProfiler.AspNetCore/Hooks.cs b/src/NanoDbProfiler.AspNetCore/Hooks.cs index 25d8be5..9d52027 100644 --- a/src/NanoDbProfiler.AspNetCore/Hooks.cs +++ b/src/NanoDbProfiler.AspNetCore/Hooks.cs @@ -52,7 +52,7 @@ private static void processLog(TimeSpan duration, object command) #endif }; - EfQueryLog.Add(metric); + QueryLog.Add(metric); } public static void ExecuteReaderPostfix(object __instance, @@ -106,7 +106,7 @@ public static void ExecuteReaderPostfix(object __instance, #endif }; - EfQueryLog.Add(metric); + QueryLog.Add(metric); } } diff --git a/src/NanoDbProfiler.AspNetCore/NanoDbProfiler.AspNetCore.csproj b/src/NanoDbProfiler.AspNetCore/NanoDbProfiler.AspNetCore.csproj index 54569c0..1b71c8e 100644 --- a/src/NanoDbProfiler.AspNetCore/NanoDbProfiler.AspNetCore.csproj +++ b/src/NanoDbProfiler.AspNetCore/NanoDbProfiler.AspNetCore.csproj @@ -40,7 +40,7 @@ - + diff --git a/src/NanoDbProfiler.AspNetCore/NanoDbProfilerConfiguration.cs b/src/NanoDbProfiler.AspNetCore/NanoDbProfilerConfiguration.cs new file mode 100644 index 0000000..e296b73 --- /dev/null +++ b/src/NanoDbProfiler.AspNetCore/NanoDbProfilerConfiguration.cs @@ -0,0 +1,7 @@ +namespace Microsoft.Extensions.DependencyInjection; + +public class NanoDbProfilerConfiguration +{ + public bool ToolbarEnabled { get; set; } + public string ProfilerPageUrl { get; set; } = "query-log"; +} \ No newline at end of file diff --git a/src/NanoDbProfiler.AspNetCore/NanoDbProfilerEfCoreQueryInterceptor.cs b/src/NanoDbProfiler.AspNetCore/NanoDbProfilerEfCoreQueryInterceptor.cs new file mode 100644 index 0000000..8d6532a --- /dev/null +++ b/src/NanoDbProfiler.AspNetCore/NanoDbProfilerEfCoreQueryInterceptor.cs @@ -0,0 +1,118 @@ +using System.Data.Common; + +namespace NanoDbProfiler.AspNetCore; + +internal class NanoDbProfilerEfCoreQueryInterceptor +{ + private static Metric metricFactory(object eventData) + { + var metric = new Metric(); + + // Use reflection to get the type of the eventData object + var eventDataType = eventData.GetType(); + + // Get the "Duration" property + var durationProperty = eventDataType.GetProperty("Duration"); + if (durationProperty != null) + { + var duration = durationProperty.GetValue(eventData); // Get the value of the Duration property + if (duration is TimeSpan durationTimeSpan) + { + metric.Duration = durationTimeSpan.TotalMilliseconds; // Convert to milliseconds + } + } + + // Get the "Command" property + var commandProperty = eventDataType.GetProperty("Command"); + if (commandProperty != null) + { + var command = commandProperty.GetValue(eventData); // Get the value of the Command property + if (command is DbCommand dbCommand) + { + metric.Query = dbCommand.CommandText; // Extract the CommandText from the DbCommand + } + } + + return metric; + } + + public int NonQueryExecuted(DbCommand command, object eventData, int result) + { + // Use reflection to pass the eventData to the metric factory + var metric = metricFactory(eventData); + + // Call the singleton instance and add the metric + EfCoreMetrics.GetInstance().Add(metric); + + // Return the result directly without calling base method + return result; + } + + public ValueTask NonQueryExecutedAsync(DbCommand command, object eventData, int result, CancellationToken cancellationToken = default) + { + return ExecuteWithReflection>("NonQueryExecutedAsync", command, eventData, result, cancellationToken); + } + + public DbDataReader ReaderExecuted(DbCommand command, object eventData, DbDataReader result) + { + return ExecuteWithReflection("ReaderExecuted", command, eventData, result); + } + + public ValueTask ScalarExecutedAsync(DbCommand command, object eventData, object? result, CancellationToken cancellationToken = default) + { + return ExecuteWithReflection>("ScalarExecutedAsync", command, eventData, result, cancellationToken); + } + + public object? ScalarExecuted(DbCommand command, object eventData, object? result) + { + return ExecuteWithReflection("ScalarExecuted", command, eventData, result); + } + + public ValueTask ReaderExecutedAsync(DbCommand command, object eventData, DbDataReader result, CancellationToken cancellationToken = default) + { + return ExecuteWithReflection>("ReaderExecutedAsync", command, eventData, result, cancellationToken); + } + + private T ExecuteWithReflection(string methodName, params object [] parameters) + { + // Get the method by name from the current type + var method = this.GetType().GetMethod(methodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + + // If the method is not found, throw an exception + if (method == null) + { + throw new InvalidOperationException($"Method {methodName} not found."); + } + + // Dynamically access properties of eventData (assuming eventData is the second parameter) + var eventData = parameters [1]; + var eventDataType = eventData.GetType(); + + // Get the "metricFactory" method using reflection + var metricFactoryMethod = this.GetType().GetMethod("metricFactory", BindingFlags.NonPublic | BindingFlags.Static); + + if (metricFactoryMethod == null) + { + throw new InvalidOperationException("metricFactory method not found."); + } + + // Invoke the metricFactory method and get the metric + var metric = metricFactoryMethod.Invoke(null, new [] { eventData }); + + // Cast the returned object to the expected type (Metric) + if (metric is Metric validMetric) + { + // Add the metric to the EfCoreMetrics instance + EfCoreMetrics.GetInstance().Add(validMetric); + } + else + { + throw new InvalidOperationException("The metric factory did not return the expected type."); + } + + // Invoke the original method with the parameters and return the result + return (T) method.Invoke(this, parameters); + } + +} + diff --git a/src/NanoDbProfiler.AspNetCore/EfQueryLog.cs b/src/NanoDbProfiler.AspNetCore/QueryLog.cs similarity index 97% rename from src/NanoDbProfiler.AspNetCore/EfQueryLog.cs rename to src/NanoDbProfiler.AspNetCore/QueryLog.cs index 73d9aa7..86358ba 100644 --- a/src/NanoDbProfiler.AspNetCore/EfQueryLog.cs +++ b/src/NanoDbProfiler.AspNetCore/QueryLog.cs @@ -1,6 +1,6 @@ namespace Microsoft.Extensions.DependencyInjection; -public static class EfQueryLog +public static class QueryLog { public static IServiceScopeFactory? ServiceScopeFactory { get; internal set; } diff --git a/src/NanoDbProfiler.AspNetCore/QueryLogMiddleware.cs b/src/NanoDbProfiler.AspNetCore/QueryLogMiddleware.cs index 89d3dc2..88e49c3 100644 --- a/src/NanoDbProfiler.AspNetCore/QueryLogMiddleware.cs +++ b/src/NanoDbProfiler.AspNetCore/QueryLogMiddleware.cs @@ -13,54 +13,60 @@ public QueryLogMiddleware(RequestDelegate next) public async Task InvokeAsync(HttpContext context) { - try + if (!string.IsNullOrEmpty(QUERY_LOG_ROUTE) && context.Request.Path == QUERY_LOG_ROUTE) { - var path = context.Request.Path; - - if (path == QUERY_LOG_ROUTE) - { - await _next(context); - return; - } + await _next(context); + return; + } - var originalBodyStream = context.Response.Body; - using var newBodyStream = new MemoryStream(); - context.Response.Body = newBodyStream; + var originalBodyStream = context.Response.Body; + await using var newBodyStream = new MemoryStream(); + context.Response.Body = newBodyStream; + try + { + // Call the next middleware await _next(context); - newBodyStream.Seek(0, SeekOrigin.Begin); - var body = await new StreamReader(newBodyStream).ReadToEndAsync(); - - // Inject toolbar before closing body tag if the response is HTML - if (context.Response.ContentType != null && context.Response.ContentType.Contains("text/html")) + // If the response is HTML, modify it + if (context.Response.ContentType?.StartsWith("text/html", StringComparison.OrdinalIgnoreCase) == true) { - string? toolbarHtml = default; + newBodyStream.Seek(0, SeekOrigin.Begin); + var body = await new StreamReader(newBodyStream).ReadToEndAsync(); - toolbarHtml = EmbeddedResourceHelpers.GetResource("Toolbar.html"); + var toolbarHtml = EmbeddedResourceHelpers.GetResource("Toolbar.html"); - if (body.Contains("")) + if (body.Contains("", StringComparison.OrdinalIgnoreCase)) { - body = body.Replace("
{{ summary.query }}