Skip to content

Commit 3dacad2

Browse files
author
Bart Koelman
committed
Instrumented library and example project
1 parent 1ac1a21 commit 3dacad2

31 files changed

+330
-143
lines changed

JsonApiDotNetCore.sln.DotSettings

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,7 @@ $left$ = $right$;</s:String>
620620
<s:String x:Key="/Default/PatternsAndTemplates/StructuralSearch/Pattern/=B3D9EE6B4EC62A4F961EB15F9ADEC2C6/ReplacePattern/@EntryValue">$collection$.IsNullOrEmpty()</s:String>
621621
<s:String x:Key="/Default/PatternsAndTemplates/StructuralSearch/Pattern/=B3D9EE6B4EC62A4F961EB15F9ADEC2C6/SearchPattern/@EntryValue">$collection$ == null || !$collection$.Any()</s:String>
622622
<s:String x:Key="/Default/PatternsAndTemplates/StructuralSearch/Pattern/=B3D9EE6B4EC62A4F961EB15F9ADEC2C6/Severity/@EntryValue">WARNING</s:String>
623+
<s:Boolean x:Key="/Default/UserDictionary/Words/=appsettings/@EntryIndexedValue">True</s:Boolean>
623624
<s:Boolean x:Key="/Default/UserDictionary/Words/=Assignee/@EntryIndexedValue">True</s:Boolean>
624625
<s:Boolean x:Key="/Default/UserDictionary/Words/=Injectables/@EntryIndexedValue">True</s:Boolean>
625626
<s:Boolean x:Key="/Default/UserDictionary/Words/=linebreaks/@EntryIndexedValue">True</s:Boolean>

src/Examples/JsonApiDotNetCoreExample/Startups/EmptyStartup.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Microsoft.AspNetCore.Builder;
22
using Microsoft.AspNetCore.Hosting;
33
using Microsoft.Extensions.DependencyInjection;
4+
using Microsoft.Extensions.Logging;
45

56
namespace JsonApiDotNetCoreExample.Startups
67
{
@@ -14,7 +15,7 @@ public virtual void ConfigureServices(IServiceCollection services)
1415
{
1516
}
1617

17-
public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment environment)
18+
public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment environment, ILoggerFactory loggerFactory)
1819
{
1920
}
2021
}
Lines changed: 54 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,97 @@
11
using System;
22
using JsonApiDotNetCore.Configuration;
3+
using JsonApiDotNetCore.Diagnostics;
34
using JsonApiDotNetCoreExample.Data;
45
using Microsoft.AspNetCore.Authentication;
56
using Microsoft.AspNetCore.Builder;
67
using Microsoft.AspNetCore.Hosting;
78
using Microsoft.EntityFrameworkCore;
89
using Microsoft.Extensions.Configuration;
910
using Microsoft.Extensions.DependencyInjection;
11+
using Microsoft.Extensions.Logging;
1012
using Newtonsoft.Json;
1113
using Newtonsoft.Json.Converters;
1214

1315
namespace JsonApiDotNetCoreExample.Startups
1416
{
1517
public sealed class Startup : EmptyStartup
1618
{
19+
private readonly ICodeTimerSession _codeTimingSession;
1720
private readonly string _connectionString;
1821

1922
public Startup(IConfiguration configuration)
2023
{
24+
_codeTimingSession = new DefaultCodeTimerSession();
25+
CodeTimingSessionManager.Capture(_codeTimingSession);
26+
2127
string postgresPassword = Environment.GetEnvironmentVariable("PGPASSWORD") ?? "postgres";
2228
_connectionString = configuration["Data:DefaultConnection"].Replace("###", postgresPassword);
2329
}
2430

2531
// This method gets called by the runtime. Use this method to add services to the container.
2632
public override void ConfigureServices(IServiceCollection services)
2733
{
28-
services.AddSingleton<ISystemClock, SystemClock>();
29-
30-
services.AddDbContext<AppDbContext>(options =>
34+
using (CodeTimingSessionManager.Current.Measure("Configure other (startup)"))
3135
{
32-
options.UseNpgsql(_connectionString);
36+
services.AddSingleton<ISystemClock, SystemClock>();
37+
38+
services.AddDbContext<AppDbContext>(options =>
39+
{
40+
options.UseNpgsql(_connectionString);
3341
#if DEBUG
34-
options.EnableSensitiveDataLogging();
35-
options.EnableDetailedErrors();
42+
options.EnableSensitiveDataLogging();
43+
options.EnableDetailedErrors();
3644
#endif
37-
});
45+
});
3846

39-
services.AddJsonApi<AppDbContext>(options =>
40-
{
41-
options.Namespace = "api/v1";
42-
options.UseRelativeLinks = true;
43-
options.ValidateModelState = true;
44-
options.IncludeTotalResourceCount = true;
45-
options.SerializerSettings.Formatting = Formatting.Indented;
46-
options.SerializerSettings.Converters.Add(new StringEnumConverter());
47+
using (CodeTimingSessionManager.Current.Measure("Configure JSON:API (startup)"))
48+
{
49+
services.AddJsonApi<AppDbContext>(options =>
50+
{
51+
options.Namespace = "api/v1";
52+
options.UseRelativeLinks = true;
53+
options.ValidateModelState = true;
54+
options.IncludeTotalResourceCount = true;
55+
options.SerializerSettings.Formatting = Formatting.Indented;
56+
options.SerializerSettings.Converters.Add(new StringEnumConverter());
4757
#if DEBUG
48-
options.IncludeExceptionStackTraceInErrors = true;
58+
options.IncludeExceptionStackTraceInErrors = true;
4959
#endif
50-
}, discovery => discovery.AddCurrentAssembly());
60+
}, discovery => discovery.AddCurrentAssembly());
61+
}
62+
}
5163
}
5264

5365
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
54-
public override void Configure(IApplicationBuilder app, IWebHostEnvironment environment)
66+
public override void Configure(IApplicationBuilder app, IWebHostEnvironment environment, ILoggerFactory loggerFactory)
5567
{
56-
using (IServiceScope scope = app.ApplicationServices.CreateScope())
68+
ILogger<Startup> logger = loggerFactory.CreateLogger<Startup>();
69+
70+
using (CodeTimingSessionManager.Current.Measure("Initialize other (startup)"))
71+
{
72+
using (IServiceScope scope = app.ApplicationServices.CreateScope())
73+
{
74+
var appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
75+
appDbContext.Database.EnsureCreated();
76+
}
77+
78+
app.UseRouting();
79+
80+
using (CodeTimingSessionManager.Current.Measure("Initialize JSON:API (startup)"))
81+
{
82+
app.UseJsonApi();
83+
}
84+
85+
app.UseEndpoints(endpoints => endpoints.MapControllers());
86+
}
87+
88+
if (CodeTimingSessionManager.IsEnabled)
5789
{
58-
var appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
59-
appDbContext.Database.EnsureCreated();
90+
string timingResults = CodeTimingSessionManager.Current.GetResults();
91+
logger.LogInformation($"Measurement results for application startup:{Environment.NewLine}{timingResults}");
6092
}
6193

62-
app.UseRouting();
63-
app.UseJsonApi();
64-
app.UseEndpoints(endpoints => endpoints.MapControllers());
94+
_codeTimingSession.Dispose();
6595
}
6696
}
6797
}

src/Examples/JsonApiDotNetCoreExample/appsettings.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
"Default": "Warning",
88
"Microsoft.Hosting.Lifetime": "Warning",
99
"Microsoft.EntityFrameworkCore.Update": "Critical",
10-
"Microsoft.EntityFrameworkCore.Database.Command": "Critical"
10+
"Microsoft.EntityFrameworkCore.Database.Command": "Critical",
11+
"JsonApiDotNetCore.Middleware.JsonApiMiddleware": "Information",
12+
"JsonApiDotNetCoreExample": "Information"
1113
}
1214
},
1315
"AllowedHosts": "*"

src/JsonApiDotNetCore/Diagnostics/AspNetCodeTimerSession.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using JetBrains.Annotations;
23
using Microsoft.AspNetCore.Http;
34

45
namespace JsonApiDotNetCore.Diagnostics
@@ -8,7 +9,8 @@ namespace JsonApiDotNetCore.Diagnostics
89
/// Can be used with async/wait, but it cannot distinguish between concurrently running threads within a single HTTP request, so you'll need to pass an
910
/// <see cref="CascadingCodeTimer" /> instance through the entire call chain in that case.
1011
/// </summary>
11-
internal sealed class AspNetCodeTimerSession : ICodeTimerSession
12+
[PublicAPI]
13+
public sealed class AspNetCodeTimerSession : ICodeTimerSession
1214
{
1315
private const string HttpContextItemKey = "CascadingCodeTimer:Session";
1416

src/JsonApiDotNetCore/Diagnostics/CascadingCodeTimer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ private void Close(MeasureScope scope)
6969
}
7070

7171
/// <inheritdoc />
72-
public string GetResult()
72+
public string GetResults()
7373
{
7474
int paddingLength = GetPaddingLength();
7575

src/JsonApiDotNetCore/Diagnostics/CodeTimingSessionManager.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ namespace JsonApiDotNetCore.Diagnostics
99
/// Provides access to the "current" measurement, which removes the need to pass along a <see cref="CascadingCodeTimer" /> instance through the entire
1010
/// call chain.
1111
/// </summary>
12-
internal static class CodeTimingSessionManager
12+
public static class CodeTimingSessionManager
1313
{
14-
private static readonly bool IsEnabled;
14+
public static readonly bool IsEnabled;
1515
private static ICodeTimerSession _session;
1616

1717
public static ICodeTimer Current
@@ -38,6 +38,7 @@ static CodeTimingSessionManager()
3838
#endif
3939
}
4040

41+
// ReSharper disable once UnusedMember.Local
4142
private static bool IsRunningInTest()
4243
{
4344
const string testAssemblyName = "xunit.core";

src/JsonApiDotNetCore/Diagnostics/DefaultCodeTimerSession.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace JsonApiDotNetCore.Diagnostics
77
/// General code timing session management. Can be used with async/wait, but it cannot distinguish between concurrently running threads, so you'll need
88
/// to pass an <see cref="CascadingCodeTimer" /> instance through the entire call chain in that case.
99
/// </summary>
10-
internal sealed class DefaultCodeTimerSession : ICodeTimerSession
10+
public sealed class DefaultCodeTimerSession : ICodeTimerSession
1111
{
1212
private readonly AsyncLocal<ICodeTimer> _codeTimerInContext = new();
1313

src/JsonApiDotNetCore/Diagnostics/DisabledCodeTimer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public IDisposable Measure(string name, bool excludeInRelativeCost = false)
1818
return this;
1919
}
2020

21-
public string GetResult()
21+
public string GetResults()
2222
{
2323
return string.Empty;
2424
}

src/JsonApiDotNetCore/Diagnostics/ICodeTimer.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace JsonApiDotNetCore.Diagnostics
55
/// <summary>
66
/// Records execution times for code blocks.
77
/// </summary>
8-
internal interface ICodeTimer : IDisposable
8+
public interface ICodeTimer : IDisposable
99
{
1010
/// <summary>
1111
/// Starts recording the duration of a code block. Wrap this call in a <c>using</c> statement, so the recording stops when the return value goes out of
@@ -22,6 +22,6 @@ internal interface ICodeTimer : IDisposable
2222
/// <summary>
2323
/// Returns intermediate or final results.
2424
/// </summary>
25-
string GetResult();
25+
string GetResults();
2626
}
2727
}

src/JsonApiDotNetCore/Diagnostics/ICodeTimerSession.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace JsonApiDotNetCore.Diagnostics
55
/// <summary>
66
/// Removes the need to pass along a <see cref="CascadingCodeTimer" /> instance through the entire call chain when using code timing.
77
/// </summary>
8-
internal interface ICodeTimerSession : IDisposable
8+
public interface ICodeTimerSession : IDisposable
99
{
1010
ICodeTimer CodeTimer { get; }
1111

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#pragma warning disable AV1008 // Class should not be static
2+
3+
namespace JsonApiDotNetCore.Diagnostics
4+
{
5+
internal static class MeasurementSettings
6+
{
7+
public static readonly bool ExcludeDatabaseInPercentages = bool.Parse(bool.TrueString);
8+
public static readonly bool ExcludeJsonSerializationInPercentages = bool.Parse(bool.FalseString);
9+
}
10+
}

src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs

Lines changed: 47 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@
66
using System.Threading.Tasks;
77
using JetBrains.Annotations;
88
using JsonApiDotNetCore.Configuration;
9+
using JsonApiDotNetCore.Diagnostics;
910
using JsonApiDotNetCore.Resources.Annotations;
1011
using JsonApiDotNetCore.Serialization;
1112
using JsonApiDotNetCore.Serialization.Objects;
1213
using Microsoft.AspNetCore.Http;
14+
using Microsoft.AspNetCore.Http.Extensions;
1315
using Microsoft.AspNetCore.Http.Features;
1416
using Microsoft.AspNetCore.Mvc.Controllers;
1517
using Microsoft.AspNetCore.Routing;
18+
using Microsoft.Extensions.Logging;
1619
using Microsoft.Net.Http.Headers;
1720
using Newtonsoft.Json;
1821

@@ -29,57 +32,74 @@ public sealed class JsonApiMiddleware
2932

3033
private readonly RequestDelegate _next;
3134

32-
public JsonApiMiddleware(RequestDelegate next)
35+
public JsonApiMiddleware(RequestDelegate next, IHttpContextAccessor httpContextAccessor)
3336
{
3437
_next = next;
38+
39+
var session = new AspNetCodeTimerSession(httpContextAccessor);
40+
CodeTimingSessionManager.Capture(session);
3541
}
3642

3743
public async Task InvokeAsync(HttpContext httpContext, IControllerResourceMapping controllerResourceMapping, IJsonApiOptions options,
38-
IJsonApiRequest request, IResourceContextProvider resourceContextProvider)
44+
IJsonApiRequest request, IResourceContextProvider resourceContextProvider, ILogger<JsonApiMiddleware> logger)
3945
{
4046
ArgumentGuard.NotNull(httpContext, nameof(httpContext));
4147
ArgumentGuard.NotNull(controllerResourceMapping, nameof(controllerResourceMapping));
4248
ArgumentGuard.NotNull(options, nameof(options));
4349
ArgumentGuard.NotNull(request, nameof(request));
4450
ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider));
51+
ArgumentGuard.NotNull(logger, nameof(logger));
4552

46-
if (!await ValidateIfMatchHeaderAsync(httpContext, options.SerializerSettings))
47-
{
48-
return;
49-
}
50-
51-
RouteValueDictionary routeValues = httpContext.GetRouteData().Values;
52-
ResourceContext primaryResourceContext = CreatePrimaryResourceContext(httpContext, controllerResourceMapping, resourceContextProvider);
53-
54-
if (primaryResourceContext != null)
53+
using (CodeTimingSessionManager.Current.Measure("JSON:API middleware"))
5554
{
56-
if (!await ValidateContentTypeHeaderAsync(HeaderConstants.MediaType, httpContext, options.SerializerSettings) ||
57-
!await ValidateAcceptHeaderAsync(MediaType, httpContext, options.SerializerSettings))
55+
if (!await ValidateIfMatchHeaderAsync(httpContext, options.SerializerSettings))
5856
{
5957
return;
6058
}
6159

62-
SetupResourceRequest((JsonApiRequest)request, primaryResourceContext, routeValues, resourceContextProvider, httpContext.Request);
60+
RouteValueDictionary routeValues = httpContext.GetRouteData().Values;
61+
ResourceContext primaryResourceContext = CreatePrimaryResourceContext(httpContext, controllerResourceMapping, resourceContextProvider);
6362

64-
httpContext.RegisterJsonApiRequest();
65-
}
66-
else if (IsRouteForOperations(routeValues))
67-
{
68-
if (!await ValidateContentTypeHeaderAsync(HeaderConstants.AtomicOperationsMediaType, httpContext, options.SerializerSettings) ||
69-
!await ValidateAcceptHeaderAsync(AtomicOperationsMediaType, httpContext, options.SerializerSettings))
63+
if (primaryResourceContext != null)
7064
{
71-
return;
65+
if (!await ValidateContentTypeHeaderAsync(HeaderConstants.MediaType, httpContext, options.SerializerSettings) ||
66+
!await ValidateAcceptHeaderAsync(MediaType, httpContext, options.SerializerSettings))
67+
{
68+
return;
69+
}
70+
71+
SetupResourceRequest((JsonApiRequest)request, primaryResourceContext, routeValues, resourceContextProvider, httpContext.Request);
72+
73+
httpContext.RegisterJsonApiRequest();
7274
}
75+
else if (IsRouteForOperations(routeValues))
76+
{
77+
if (!await ValidateContentTypeHeaderAsync(HeaderConstants.AtomicOperationsMediaType, httpContext, options.SerializerSettings) ||
78+
!await ValidateAcceptHeaderAsync(AtomicOperationsMediaType, httpContext, options.SerializerSettings))
79+
{
80+
return;
81+
}
7382

74-
SetupOperationsRequest((JsonApiRequest)request, options, httpContext.Request);
83+
SetupOperationsRequest((JsonApiRequest)request, options, httpContext.Request);
7584

76-
httpContext.RegisterJsonApiRequest();
77-
}
85+
httpContext.RegisterJsonApiRequest();
86+
}
7887

79-
// Workaround for bug https://github.com/dotnet/aspnetcore/issues/33394
80-
httpContext.Features.Set<IQueryFeature>(new FixedQueryFeature(httpContext.Features));
88+
// Workaround for bug https://github.com/dotnet/aspnetcore/issues/33394
89+
httpContext.Features.Set<IQueryFeature>(new FixedQueryFeature(httpContext.Features));
8190

82-
await _next(httpContext);
91+
using (CodeTimingSessionManager.Current.Measure("Subsequent middleware"))
92+
{
93+
await _next(httpContext);
94+
}
95+
}
96+
97+
if (CodeTimingSessionManager.IsEnabled)
98+
{
99+
string timingResults = CodeTimingSessionManager.Current.GetResults();
100+
string url = httpContext.Request.GetDisplayUrl();
101+
logger.LogInformation($"Measurement results for {httpContext.Request.Method} {url}:{Environment.NewLine}{timingResults}");
102+
}
83103
}
84104

85105
private async Task<bool> ValidateIfMatchHeaderAsync(HttpContext httpContext, JsonSerializerSettings serializerSettings)

0 commit comments

Comments
 (0)