From 1ac1a215e77471887d869c278d44074ad74b3864 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Thu, 5 Aug 2021 09:07:32 +0200 Subject: [PATCH 1/8] Added basic instrumentation framework --- .../Diagnostics/AspNetCodeTimerSession.cs | 81 +++++ .../Diagnostics/CascadingCodeTimer.cs | 282 ++++++++++++++++++ .../Diagnostics/CodeTimingSessionManager.cs | 87 ++++++ .../Diagnostics/DefaultCodeTimerSession.cs | 52 ++++ .../Diagnostics/DisabledCodeTimer.cs | 30 ++ .../Diagnostics/ICodeTimer.cs | 27 ++ .../Diagnostics/ICodeTimerSession.cs | 14 + 7 files changed, 573 insertions(+) create mode 100644 src/JsonApiDotNetCore/Diagnostics/AspNetCodeTimerSession.cs create mode 100644 src/JsonApiDotNetCore/Diagnostics/CascadingCodeTimer.cs create mode 100644 src/JsonApiDotNetCore/Diagnostics/CodeTimingSessionManager.cs create mode 100644 src/JsonApiDotNetCore/Diagnostics/DefaultCodeTimerSession.cs create mode 100644 src/JsonApiDotNetCore/Diagnostics/DisabledCodeTimer.cs create mode 100644 src/JsonApiDotNetCore/Diagnostics/ICodeTimer.cs create mode 100644 src/JsonApiDotNetCore/Diagnostics/ICodeTimerSession.cs diff --git a/src/JsonApiDotNetCore/Diagnostics/AspNetCodeTimerSession.cs b/src/JsonApiDotNetCore/Diagnostics/AspNetCodeTimerSession.cs new file mode 100644 index 0000000000..89d3639844 --- /dev/null +++ b/src/JsonApiDotNetCore/Diagnostics/AspNetCodeTimerSession.cs @@ -0,0 +1,81 @@ +using System; +using Microsoft.AspNetCore.Http; + +namespace JsonApiDotNetCore.Diagnostics +{ + /// + /// Code timing session management intended for use in ASP.NET Web Applications. Uses to isolate concurrent requests. + /// 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 + /// instance through the entire call chain in that case. + /// + internal sealed class AspNetCodeTimerSession : ICodeTimerSession + { + private const string HttpContextItemKey = "CascadingCodeTimer:Session"; + + private readonly HttpContext _httpContext; + private readonly IHttpContextAccessor _httpContextAccessor; + + public ICodeTimer CodeTimer + { + get + { + HttpContext httpContext = GetHttpContext(); + var codeTimer = (ICodeTimer)httpContext.Items[HttpContextItemKey]; + + if (codeTimer == null) + { + codeTimer = new CascadingCodeTimer(); + httpContext.Items[HttpContextItemKey] = codeTimer; + } + + return codeTimer; + } + } + + public event EventHandler Disposed; + + public AspNetCodeTimerSession(IHttpContextAccessor httpContextAccessor) + { + ArgumentGuard.NotNull(httpContextAccessor, nameof(httpContextAccessor)); + + _httpContextAccessor = httpContextAccessor; + } + + public AspNetCodeTimerSession(HttpContext httpContext) + { + ArgumentGuard.NotNull(httpContext, nameof(httpContext)); + + _httpContext = httpContext; + } + + public void Dispose() + { + HttpContext httpContext = TryGetHttpContext(); + var codeTimer = (ICodeTimer)httpContext?.Items[HttpContextItemKey]; + + if (codeTimer != null) + { + codeTimer.Dispose(); + httpContext.Items[HttpContextItemKey] = null; + } + + OnDisposed(); + } + + private void OnDisposed() + { + Disposed?.Invoke(this, EventArgs.Empty); + } + + private HttpContext GetHttpContext() + { + HttpContext httpContext = TryGetHttpContext(); + return httpContext ?? throw new InvalidOperationException("An active HTTP request is required."); + } + + private HttpContext TryGetHttpContext() + { + return _httpContext ?? _httpContextAccessor?.HttpContext; + } + } +} diff --git a/src/JsonApiDotNetCore/Diagnostics/CascadingCodeTimer.cs b/src/JsonApiDotNetCore/Diagnostics/CascadingCodeTimer.cs new file mode 100644 index 0000000000..4d88e4af7f --- /dev/null +++ b/src/JsonApiDotNetCore/Diagnostics/CascadingCodeTimer.cs @@ -0,0 +1,282 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; + +namespace JsonApiDotNetCore.Diagnostics +{ + /// + /// Records execution times for nested code blocks. + /// + internal sealed class CascadingCodeTimer : ICodeTimer + { + private readonly Stopwatch _stopwatch = new(); + private readonly Stack _activeScopeStack = new(); + private readonly List _completedScopes = new(); + + static CascadingCodeTimer() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // Be default, measurements using Stopwatch can differ 25%-30% on the same function on the same computer. + // The steps below ensure to get an accuracy of 0.1%-0.2%. With this accuracy, algorithms can be tested and compared. + // https://www.codeproject.com/Articles/61964/Performance-Tests-Precise-Run-Time-Measurements-wi + + // The most important thing is to prevent switching between CPU cores or processors. Switching dismisses the cache, etc. and has a huge performance impact on the test. + Process.GetCurrentProcess().ProcessorAffinity = new IntPtr(2); + + // To get the CPU core more exclusively, we must prevent that other threads can use this CPU core. We set our process and thread priority to achieve this. + // Note we should NOT set the thread priority, because async/await usage makes the code jump between pooled threads (depending on Synchronization Context). + Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High; + } + } + + /// + public IDisposable Measure(string name, bool excludeInRelativeCost = false) + { + MeasureScope childScope = CreateChildScope(name, excludeInRelativeCost); + _activeScopeStack.Push(childScope); + + return childScope; + } + + private MeasureScope CreateChildScope(string name, bool excludeInRelativeCost) + { + if (_activeScopeStack.TryPeek(out MeasureScope topScope)) + { + return topScope.SpawnChild(this, name, excludeInRelativeCost); + } + + return new MeasureScope(this, name, excludeInRelativeCost); + } + + private void Close(MeasureScope scope) + { + if (!_activeScopeStack.TryPeek(out MeasureScope topScope) || topScope != scope) + { + throw new InvalidOperationException($"Scope '{scope.Name}' cannot be disposed at this time, because it is not the currently active scope."); + } + + _activeScopeStack.Pop(); + + if (!_activeScopeStack.Any()) + { + _completedScopes.Add(scope); + } + } + + /// + public string GetResult() + { + int paddingLength = GetPaddingLength(); + + var builder = new StringBuilder(); + WriteResult(builder, paddingLength); + + return builder.ToString(); + } + + private int GetPaddingLength() + { + int maxLength = 0; + + foreach (MeasureScope scope in _completedScopes) + { + int nextLength = scope.GetPaddingLength(); + maxLength = Math.Max(maxLength, nextLength); + } + + if (_activeScopeStack.Any()) + { + MeasureScope scope = _activeScopeStack.Peek(); + int nextLength = scope.GetPaddingLength(); + maxLength = Math.Max(maxLength, nextLength); + } + + return maxLength + 3; + } + + private void WriteResult(StringBuilder builder, int paddingLength) + { + foreach (MeasureScope scope in _completedScopes) + { + scope.WriteResult(builder, 0, paddingLength); + } + + if (_activeScopeStack.Any()) + { + MeasureScope scope = _activeScopeStack.Peek(); + scope.WriteResult(builder, 0, paddingLength); + } + } + + public void Dispose() + { + if (_stopwatch.IsRunning) + { + _stopwatch.Stop(); + } + + _completedScopes.Clear(); + _activeScopeStack.Clear(); + } + + private sealed class MeasureScope : IDisposable + { + private readonly CascadingCodeTimer _owner; + private readonly IList _children = new List(); + private readonly bool _excludeInRelativeCost; + private readonly TimeSpan _startedAt; + private TimeSpan? _stoppedAt; + + public string Name { get; } + + public MeasureScope(CascadingCodeTimer owner, string name, bool excludeInRelativeCost) + { + _owner = owner; + _excludeInRelativeCost = excludeInRelativeCost; + Name = name; + + EnsureRunning(); + _startedAt = owner._stopwatch.Elapsed; + } + + private void EnsureRunning() + { + if (!_owner._stopwatch.IsRunning) + { + _owner._stopwatch.Start(); + } + } + + public MeasureScope SpawnChild(CascadingCodeTimer owner, string name, bool excludeInRelativeCost) + { + var childScope = new MeasureScope(owner, name, excludeInRelativeCost); + _children.Add(childScope); + return childScope; + } + + public int GetPaddingLength() + { + return GetPaddingLength(0); + } + + private int GetPaddingLength(int indent) + { + int selfLength = indent * 2 + Name.Length; + int maxChildrenLength = 0; + + foreach (MeasureScope child in _children) + { + int nextLength = child.GetPaddingLength(indent + 1); + maxChildrenLength = Math.Max(nextLength, maxChildrenLength); + } + + return Math.Max(selfLength, maxChildrenLength); + } + + private TimeSpan GetElapsedInSelf() + { + return GetElapsedInTotal() - GetElapsedInChildren(); + } + + private TimeSpan GetElapsedInTotal() + { + TimeSpan stoppedAt = _stoppedAt ?? _owner._stopwatch.Elapsed; + return stoppedAt - _startedAt; + } + + private TimeSpan GetElapsedInChildren() + { + TimeSpan elapsedInChildren = TimeSpan.Zero; + + foreach (MeasureScope childScope in _children) + { + elapsedInChildren += childScope.GetElapsedInTotal(); + } + + return elapsedInChildren; + } + + private TimeSpan GetSkippedInTotal() + { + TimeSpan skippedInSelf = _excludeInRelativeCost ? GetElapsedInSelf() : TimeSpan.Zero; + TimeSpan skippedInChildren = GetSkippedInChildren(); + + return skippedInSelf + skippedInChildren; + } + + private TimeSpan GetSkippedInChildren() + { + TimeSpan skippedInChildren = TimeSpan.Zero; + + foreach (MeasureScope childScope in _children) + { + skippedInChildren += childScope.GetSkippedInTotal(); + } + + return skippedInChildren; + } + + public void WriteResult(StringBuilder builder, int indent, int paddingLength) + { + TimeSpan timeElapsedGlobal = GetElapsedInTotal() - GetSkippedInTotal(); + WriteResult(builder, indent, timeElapsedGlobal, paddingLength); + } + + private void WriteResult(StringBuilder builder, int indent, TimeSpan timeElapsedGlobal, int paddingLength) + { + TimeSpan timeElapsedInSelf = GetElapsedInSelf(); + double scaleElapsedInSelf = timeElapsedGlobal != TimeSpan.Zero ? timeElapsedInSelf / timeElapsedGlobal : 0; + + WriteIndent(builder, indent); + builder.Append(Name); + WritePadding(builder, indent, paddingLength); + builder.AppendFormat(CultureInfo.InvariantCulture, "{0,19:G}", timeElapsedInSelf); + + if (!_excludeInRelativeCost) + { + builder.Append(" ... "); + builder.AppendFormat(CultureInfo.InvariantCulture, "{0,7:#0.00%}", scaleElapsedInSelf); + } + + if (_stoppedAt == null) + { + builder.Append(" (active)"); + } + + builder.AppendLine(); + + foreach (MeasureScope child in _children) + { + child.WriteResult(builder, indent + 1, timeElapsedGlobal, paddingLength); + } + } + + private static void WriteIndent(StringBuilder builder, int indent) + { + builder.Append(new string(' ', indent * 2)); + } + + private void WritePadding(StringBuilder builder, int indent, int paddingLength) + { + string padding = new('.', paddingLength - Name.Length - indent * 2); + builder.Append(' '); + builder.Append(padding); + builder.Append(' '); + } + + public void Dispose() + { + if (_stoppedAt == null) + { + _stoppedAt = _owner._stopwatch.Elapsed; + _owner.Close(this); + } + } + } + } +} diff --git a/src/JsonApiDotNetCore/Diagnostics/CodeTimingSessionManager.cs b/src/JsonApiDotNetCore/Diagnostics/CodeTimingSessionManager.cs new file mode 100644 index 0000000000..658739b927 --- /dev/null +++ b/src/JsonApiDotNetCore/Diagnostics/CodeTimingSessionManager.cs @@ -0,0 +1,87 @@ +using System; +using System.Linq; + +#pragma warning disable AV1008 // Class should not be static + +namespace JsonApiDotNetCore.Diagnostics +{ + /// + /// Provides access to the "current" measurement, which removes the need to pass along a instance through the entire + /// call chain. + /// + internal static class CodeTimingSessionManager + { + private static readonly bool IsEnabled; + private static ICodeTimerSession _session; + + public static ICodeTimer Current + { + get + { + if (!IsEnabled) + { + return DisabledCodeTimer.Instance; + } + + AssertHasActiveSession(); + + return _session.CodeTimer; + } + } + + static CodeTimingSessionManager() + { +#if DEBUG + IsEnabled = !IsRunningInTest(); +#else + IsEnabled = false; +#endif + } + + private static bool IsRunningInTest() + { + const string testAssemblyName = "xunit.core"; + + return AppDomain.CurrentDomain.GetAssemblies().Any(assembly => + assembly.FullName != null && assembly.FullName.StartsWith(testAssemblyName, StringComparison.Ordinal)); + } + + private static void AssertHasActiveSession() + { + if (_session == null) + { + throw new InvalidOperationException($"Call {nameof(Capture)} before accessing the current session."); + } + } + + public static void Capture(ICodeTimerSession session) + { + ArgumentGuard.NotNull(session, nameof(session)); + + AssertNoActiveSession(); + + if (IsEnabled) + { + session.Disposed += SessionOnDisposed; + _session = session; + } + } + + private static void AssertNoActiveSession() + { + if (_session != null) + { + throw new InvalidOperationException("Sessions cannot be nested. Dispose the current session first."); + } + } + + private static void SessionOnDisposed(object sender, EventArgs args) + { + if (_session != null) + { + _session.Disposed -= SessionOnDisposed; + _session = null; + } + } + } +} diff --git a/src/JsonApiDotNetCore/Diagnostics/DefaultCodeTimerSession.cs b/src/JsonApiDotNetCore/Diagnostics/DefaultCodeTimerSession.cs new file mode 100644 index 0000000000..2a65e2096a --- /dev/null +++ b/src/JsonApiDotNetCore/Diagnostics/DefaultCodeTimerSession.cs @@ -0,0 +1,52 @@ +using System; +using System.Threading; + +namespace JsonApiDotNetCore.Diagnostics +{ + /// + /// General code timing session management. Can be used with async/wait, but it cannot distinguish between concurrently running threads, so you'll need + /// to pass an instance through the entire call chain in that case. + /// + internal sealed class DefaultCodeTimerSession : ICodeTimerSession + { + private readonly AsyncLocal _codeTimerInContext = new(); + + public ICodeTimer CodeTimer + { + get + { + AssertNotDisposed(); + + return _codeTimerInContext.Value; + } + } + + public event EventHandler Disposed; + + public DefaultCodeTimerSession() + { + _codeTimerInContext.Value = new CascadingCodeTimer(); + } + + private void AssertNotDisposed() + { + if (_codeTimerInContext.Value == null) + { + throw new ObjectDisposedException(nameof(DefaultCodeTimerSession)); + } + } + + public void Dispose() + { + _codeTimerInContext.Value?.Dispose(); + _codeTimerInContext.Value = null; + + OnDisposed(); + } + + private void OnDisposed() + { + Disposed?.Invoke(this, EventArgs.Empty); + } + } +} diff --git a/src/JsonApiDotNetCore/Diagnostics/DisabledCodeTimer.cs b/src/JsonApiDotNetCore/Diagnostics/DisabledCodeTimer.cs new file mode 100644 index 0000000000..020547f851 --- /dev/null +++ b/src/JsonApiDotNetCore/Diagnostics/DisabledCodeTimer.cs @@ -0,0 +1,30 @@ +using System; + +namespace JsonApiDotNetCore.Diagnostics +{ + /// + /// Doesn't record anything. Intended for Release builds and to not break existing tests. + /// + internal sealed class DisabledCodeTimer : ICodeTimer + { + public static readonly DisabledCodeTimer Instance = new(); + + private DisabledCodeTimer() + { + } + + public IDisposable Measure(string name, bool excludeInRelativeCost = false) + { + return this; + } + + public string GetResult() + { + return string.Empty; + } + + public void Dispose() + { + } + } +} diff --git a/src/JsonApiDotNetCore/Diagnostics/ICodeTimer.cs b/src/JsonApiDotNetCore/Diagnostics/ICodeTimer.cs new file mode 100644 index 0000000000..8d3067034d --- /dev/null +++ b/src/JsonApiDotNetCore/Diagnostics/ICodeTimer.cs @@ -0,0 +1,27 @@ +using System; + +namespace JsonApiDotNetCore.Diagnostics +{ + /// + /// Records execution times for code blocks. + /// + internal interface ICodeTimer : IDisposable + { + /// + /// Starts recording the duration of a code block. Wrap this call in a using statement, so the recording stops when the return value goes out of + /// scope. + /// + /// + /// Description of what is being recorded. + /// + /// + /// When set, indicates to exclude this measurement in calculated percentages. false by default. + /// + IDisposable Measure(string name, bool excludeInRelativeCost = false); + + /// + /// Returns intermediate or final results. + /// + string GetResult(); + } +} diff --git a/src/JsonApiDotNetCore/Diagnostics/ICodeTimerSession.cs b/src/JsonApiDotNetCore/Diagnostics/ICodeTimerSession.cs new file mode 100644 index 0000000000..88be1f366c --- /dev/null +++ b/src/JsonApiDotNetCore/Diagnostics/ICodeTimerSession.cs @@ -0,0 +1,14 @@ +using System; + +namespace JsonApiDotNetCore.Diagnostics +{ + /// + /// Removes the need to pass along a instance through the entire call chain when using code timing. + /// + internal interface ICodeTimerSession : IDisposable + { + ICodeTimer CodeTimer { get; } + + event EventHandler Disposed; + } +} From 3dacad2aec3b3bde905d271d61c2446bcc21b26b Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Thu, 5 Aug 2021 12:43:55 +0200 Subject: [PATCH 2/8] Instrumented library and example project --- JsonApiDotNetCore.sln.DotSettings | 1 + .../Startups/EmptyStartup.cs | 3 +- .../Startups/Startup.cs | 78 ++++++++++----- .../JsonApiDotNetCoreExample/appsettings.json | 4 +- .../Diagnostics/AspNetCodeTimerSession.cs | 4 +- .../Diagnostics/CascadingCodeTimer.cs | 2 +- .../Diagnostics/CodeTimingSessionManager.cs | 5 +- .../Diagnostics/DefaultCodeTimerSession.cs | 2 +- .../Diagnostics/DisabledCodeTimer.cs | 2 +- .../Diagnostics/ICodeTimer.cs | 4 +- .../Diagnostics/ICodeTimerSession.cs | 2 +- .../Diagnostics/MeasurementSettings.cs | 10 ++ .../Middleware/JsonApiMiddleware.cs | 74 ++++++++------ .../Queries/Internal/QueryLayerComposer.cs | 5 + .../Internal/QueryStringReader.cs | 4 + .../EntityFrameworkCoreRepository.cs | 96 +++++++++++++------ .../Serialization/BaseDeserializer.cs | 18 +++- .../Serialization/BaseSerializer.cs | 7 ++ .../Serialization/JsonApiReader.cs | 3 + .../Serialization/JsonApiWriter.cs | 3 + .../Serialization/RequestDeserializer.cs | 10 +- .../Services/JsonApiResourceService.cs | 25 ++++- .../ExceptionHandlerTests.cs | 10 +- .../HostingInIIS/HostingStartup.cs | 5 +- .../IntegrationTests/Logging/LoggingTests.cs | 10 +- .../Startups/TestableStartup.cs | 3 +- .../BaseIntegrationTestContext.cs | 47 ++++++--- test/TestBuildingBlocks/FakeLoggerFactory.cs | 24 ++++- .../Internal/ResourceGraphBuilderTests.cs | 2 +- .../Middleware/JsonApiMiddlewareTests.cs | 5 +- .../Middleware/JsonApiRequestTests.cs | 5 +- 31 files changed, 330 insertions(+), 143 deletions(-) create mode 100644 src/JsonApiDotNetCore/Diagnostics/MeasurementSettings.cs diff --git a/JsonApiDotNetCore.sln.DotSettings b/JsonApiDotNetCore.sln.DotSettings index a6443a0787..bc7a56ea08 100644 --- a/JsonApiDotNetCore.sln.DotSettings +++ b/JsonApiDotNetCore.sln.DotSettings @@ -620,6 +620,7 @@ $left$ = $right$; $collection$.IsNullOrEmpty() $collection$ == null || !$collection$.Any() WARNING + True True True True diff --git a/src/Examples/JsonApiDotNetCoreExample/Startups/EmptyStartup.cs b/src/Examples/JsonApiDotNetCoreExample/Startups/EmptyStartup.cs index 19879aef27..065daef7a5 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Startups/EmptyStartup.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Startups/EmptyStartup.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace JsonApiDotNetCoreExample.Startups { @@ -14,7 +15,7 @@ public virtual void ConfigureServices(IServiceCollection services) { } - public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment environment) + public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment environment, ILoggerFactory loggerFactory) { } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs b/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs index 84ddf4a4de..9a149ca9a3 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs @@ -1,5 +1,6 @@ using System; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Diagnostics; using JsonApiDotNetCoreExample.Data; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Builder; @@ -7,6 +8,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Converters; @@ -14,10 +16,14 @@ namespace JsonApiDotNetCoreExample.Startups { public sealed class Startup : EmptyStartup { + private readonly ICodeTimerSession _codeTimingSession; private readonly string _connectionString; public Startup(IConfiguration configuration) { + _codeTimingSession = new DefaultCodeTimerSession(); + CodeTimingSessionManager.Capture(_codeTimingSession); + string postgresPassword = Environment.GetEnvironmentVariable("PGPASSWORD") ?? "postgres"; _connectionString = configuration["Data:DefaultConnection"].Replace("###", postgresPassword); } @@ -25,43 +31,67 @@ public Startup(IConfiguration configuration) // This method gets called by the runtime. Use this method to add services to the container. public override void ConfigureServices(IServiceCollection services) { - services.AddSingleton(); - - services.AddDbContext(options => + using (CodeTimingSessionManager.Current.Measure("Configure other (startup)")) { - options.UseNpgsql(_connectionString); + services.AddSingleton(); + + services.AddDbContext(options => + { + options.UseNpgsql(_connectionString); #if DEBUG - options.EnableSensitiveDataLogging(); - options.EnableDetailedErrors(); + options.EnableSensitiveDataLogging(); + options.EnableDetailedErrors(); #endif - }); + }); - services.AddJsonApi(options => - { - options.Namespace = "api/v1"; - options.UseRelativeLinks = true; - options.ValidateModelState = true; - options.IncludeTotalResourceCount = true; - options.SerializerSettings.Formatting = Formatting.Indented; - options.SerializerSettings.Converters.Add(new StringEnumConverter()); + using (CodeTimingSessionManager.Current.Measure("Configure JSON:API (startup)")) + { + services.AddJsonApi(options => + { + options.Namespace = "api/v1"; + options.UseRelativeLinks = true; + options.ValidateModelState = true; + options.IncludeTotalResourceCount = true; + options.SerializerSettings.Formatting = Formatting.Indented; + options.SerializerSettings.Converters.Add(new StringEnumConverter()); #if DEBUG - options.IncludeExceptionStackTraceInErrors = true; + options.IncludeExceptionStackTraceInErrors = true; #endif - }, discovery => discovery.AddCurrentAssembly()); + }, discovery => discovery.AddCurrentAssembly()); + } + } } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public override void Configure(IApplicationBuilder app, IWebHostEnvironment environment) + public override void Configure(IApplicationBuilder app, IWebHostEnvironment environment, ILoggerFactory loggerFactory) { - using (IServiceScope scope = app.ApplicationServices.CreateScope()) + ILogger logger = loggerFactory.CreateLogger(); + + using (CodeTimingSessionManager.Current.Measure("Initialize other (startup)")) + { + using (IServiceScope scope = app.ApplicationServices.CreateScope()) + { + var appDbContext = scope.ServiceProvider.GetRequiredService(); + appDbContext.Database.EnsureCreated(); + } + + app.UseRouting(); + + using (CodeTimingSessionManager.Current.Measure("Initialize JSON:API (startup)")) + { + app.UseJsonApi(); + } + + app.UseEndpoints(endpoints => endpoints.MapControllers()); + } + + if (CodeTimingSessionManager.IsEnabled) { - var appDbContext = scope.ServiceProvider.GetRequiredService(); - appDbContext.Database.EnsureCreated(); + string timingResults = CodeTimingSessionManager.Current.GetResults(); + logger.LogInformation($"Measurement results for application startup:{Environment.NewLine}{timingResults}"); } - app.UseRouting(); - app.UseJsonApi(); - app.UseEndpoints(endpoints => endpoints.MapControllers()); + _codeTimingSession.Dispose(); } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/appsettings.json b/src/Examples/JsonApiDotNetCoreExample/appsettings.json index 5d13a80bef..ec2ea30102 100644 --- a/src/Examples/JsonApiDotNetCoreExample/appsettings.json +++ b/src/Examples/JsonApiDotNetCoreExample/appsettings.json @@ -7,7 +7,9 @@ "Default": "Warning", "Microsoft.Hosting.Lifetime": "Warning", "Microsoft.EntityFrameworkCore.Update": "Critical", - "Microsoft.EntityFrameworkCore.Database.Command": "Critical" + "Microsoft.EntityFrameworkCore.Database.Command": "Critical", + "JsonApiDotNetCore.Middleware.JsonApiMiddleware": "Information", + "JsonApiDotNetCoreExample": "Information" } }, "AllowedHosts": "*" diff --git a/src/JsonApiDotNetCore/Diagnostics/AspNetCodeTimerSession.cs b/src/JsonApiDotNetCore/Diagnostics/AspNetCodeTimerSession.cs index 89d3639844..2cfca080a1 100644 --- a/src/JsonApiDotNetCore/Diagnostics/AspNetCodeTimerSession.cs +++ b/src/JsonApiDotNetCore/Diagnostics/AspNetCodeTimerSession.cs @@ -1,4 +1,5 @@ using System; +using JetBrains.Annotations; using Microsoft.AspNetCore.Http; namespace JsonApiDotNetCore.Diagnostics @@ -8,7 +9,8 @@ namespace JsonApiDotNetCore.Diagnostics /// 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 /// instance through the entire call chain in that case. /// - internal sealed class AspNetCodeTimerSession : ICodeTimerSession + [PublicAPI] + public sealed class AspNetCodeTimerSession : ICodeTimerSession { private const string HttpContextItemKey = "CascadingCodeTimer:Session"; diff --git a/src/JsonApiDotNetCore/Diagnostics/CascadingCodeTimer.cs b/src/JsonApiDotNetCore/Diagnostics/CascadingCodeTimer.cs index 4d88e4af7f..27bc5db739 100644 --- a/src/JsonApiDotNetCore/Diagnostics/CascadingCodeTimer.cs +++ b/src/JsonApiDotNetCore/Diagnostics/CascadingCodeTimer.cs @@ -69,7 +69,7 @@ private void Close(MeasureScope scope) } /// - public string GetResult() + public string GetResults() { int paddingLength = GetPaddingLength(); diff --git a/src/JsonApiDotNetCore/Diagnostics/CodeTimingSessionManager.cs b/src/JsonApiDotNetCore/Diagnostics/CodeTimingSessionManager.cs index 658739b927..9160791f87 100644 --- a/src/JsonApiDotNetCore/Diagnostics/CodeTimingSessionManager.cs +++ b/src/JsonApiDotNetCore/Diagnostics/CodeTimingSessionManager.cs @@ -9,9 +9,9 @@ namespace JsonApiDotNetCore.Diagnostics /// Provides access to the "current" measurement, which removes the need to pass along a instance through the entire /// call chain. /// - internal static class CodeTimingSessionManager + public static class CodeTimingSessionManager { - private static readonly bool IsEnabled; + public static readonly bool IsEnabled; private static ICodeTimerSession _session; public static ICodeTimer Current @@ -38,6 +38,7 @@ static CodeTimingSessionManager() #endif } + // ReSharper disable once UnusedMember.Local private static bool IsRunningInTest() { const string testAssemblyName = "xunit.core"; diff --git a/src/JsonApiDotNetCore/Diagnostics/DefaultCodeTimerSession.cs b/src/JsonApiDotNetCore/Diagnostics/DefaultCodeTimerSession.cs index 2a65e2096a..b56eeab962 100644 --- a/src/JsonApiDotNetCore/Diagnostics/DefaultCodeTimerSession.cs +++ b/src/JsonApiDotNetCore/Diagnostics/DefaultCodeTimerSession.cs @@ -7,7 +7,7 @@ namespace JsonApiDotNetCore.Diagnostics /// General code timing session management. Can be used with async/wait, but it cannot distinguish between concurrently running threads, so you'll need /// to pass an instance through the entire call chain in that case. /// - internal sealed class DefaultCodeTimerSession : ICodeTimerSession + public sealed class DefaultCodeTimerSession : ICodeTimerSession { private readonly AsyncLocal _codeTimerInContext = new(); diff --git a/src/JsonApiDotNetCore/Diagnostics/DisabledCodeTimer.cs b/src/JsonApiDotNetCore/Diagnostics/DisabledCodeTimer.cs index 020547f851..1739ed3e81 100644 --- a/src/JsonApiDotNetCore/Diagnostics/DisabledCodeTimer.cs +++ b/src/JsonApiDotNetCore/Diagnostics/DisabledCodeTimer.cs @@ -18,7 +18,7 @@ public IDisposable Measure(string name, bool excludeInRelativeCost = false) return this; } - public string GetResult() + public string GetResults() { return string.Empty; } diff --git a/src/JsonApiDotNetCore/Diagnostics/ICodeTimer.cs b/src/JsonApiDotNetCore/Diagnostics/ICodeTimer.cs index 8d3067034d..9e8abfe9e1 100644 --- a/src/JsonApiDotNetCore/Diagnostics/ICodeTimer.cs +++ b/src/JsonApiDotNetCore/Diagnostics/ICodeTimer.cs @@ -5,7 +5,7 @@ namespace JsonApiDotNetCore.Diagnostics /// /// Records execution times for code blocks. /// - internal interface ICodeTimer : IDisposable + public interface ICodeTimer : IDisposable { /// /// Starts recording the duration of a code block. Wrap this call in a using statement, so the recording stops when the return value goes out of @@ -22,6 +22,6 @@ internal interface ICodeTimer : IDisposable /// /// Returns intermediate or final results. /// - string GetResult(); + string GetResults(); } } diff --git a/src/JsonApiDotNetCore/Diagnostics/ICodeTimerSession.cs b/src/JsonApiDotNetCore/Diagnostics/ICodeTimerSession.cs index 88be1f366c..5da473c38d 100644 --- a/src/JsonApiDotNetCore/Diagnostics/ICodeTimerSession.cs +++ b/src/JsonApiDotNetCore/Diagnostics/ICodeTimerSession.cs @@ -5,7 +5,7 @@ namespace JsonApiDotNetCore.Diagnostics /// /// Removes the need to pass along a instance through the entire call chain when using code timing. /// - internal interface ICodeTimerSession : IDisposable + public interface ICodeTimerSession : IDisposable { ICodeTimer CodeTimer { get; } diff --git a/src/JsonApiDotNetCore/Diagnostics/MeasurementSettings.cs b/src/JsonApiDotNetCore/Diagnostics/MeasurementSettings.cs new file mode 100644 index 0000000000..4fd42b2e30 --- /dev/null +++ b/src/JsonApiDotNetCore/Diagnostics/MeasurementSettings.cs @@ -0,0 +1,10 @@ +#pragma warning disable AV1008 // Class should not be static + +namespace JsonApiDotNetCore.Diagnostics +{ + internal static class MeasurementSettings + { + public static readonly bool ExcludeDatabaseInPercentages = bool.Parse(bool.TrueString); + public static readonly bool ExcludeJsonSerializationInPercentages = bool.Parse(bool.FalseString); + } +} diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs b/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs index 3ad6355d2e..efc3613979 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs @@ -6,13 +6,16 @@ using System.Threading.Tasks; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Diagnostics; using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; using Newtonsoft.Json; @@ -29,57 +32,74 @@ public sealed class JsonApiMiddleware private readonly RequestDelegate _next; - public JsonApiMiddleware(RequestDelegate next) + public JsonApiMiddleware(RequestDelegate next, IHttpContextAccessor httpContextAccessor) { _next = next; + + var session = new AspNetCodeTimerSession(httpContextAccessor); + CodeTimingSessionManager.Capture(session); } public async Task InvokeAsync(HttpContext httpContext, IControllerResourceMapping controllerResourceMapping, IJsonApiOptions options, - IJsonApiRequest request, IResourceContextProvider resourceContextProvider) + IJsonApiRequest request, IResourceContextProvider resourceContextProvider, ILogger logger) { ArgumentGuard.NotNull(httpContext, nameof(httpContext)); ArgumentGuard.NotNull(controllerResourceMapping, nameof(controllerResourceMapping)); ArgumentGuard.NotNull(options, nameof(options)); ArgumentGuard.NotNull(request, nameof(request)); ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(logger, nameof(logger)); - if (!await ValidateIfMatchHeaderAsync(httpContext, options.SerializerSettings)) - { - return; - } - - RouteValueDictionary routeValues = httpContext.GetRouteData().Values; - ResourceContext primaryResourceContext = CreatePrimaryResourceContext(httpContext, controllerResourceMapping, resourceContextProvider); - - if (primaryResourceContext != null) + using (CodeTimingSessionManager.Current.Measure("JSON:API middleware")) { - if (!await ValidateContentTypeHeaderAsync(HeaderConstants.MediaType, httpContext, options.SerializerSettings) || - !await ValidateAcceptHeaderAsync(MediaType, httpContext, options.SerializerSettings)) + if (!await ValidateIfMatchHeaderAsync(httpContext, options.SerializerSettings)) { return; } - SetupResourceRequest((JsonApiRequest)request, primaryResourceContext, routeValues, resourceContextProvider, httpContext.Request); + RouteValueDictionary routeValues = httpContext.GetRouteData().Values; + ResourceContext primaryResourceContext = CreatePrimaryResourceContext(httpContext, controllerResourceMapping, resourceContextProvider); - httpContext.RegisterJsonApiRequest(); - } - else if (IsRouteForOperations(routeValues)) - { - if (!await ValidateContentTypeHeaderAsync(HeaderConstants.AtomicOperationsMediaType, httpContext, options.SerializerSettings) || - !await ValidateAcceptHeaderAsync(AtomicOperationsMediaType, httpContext, options.SerializerSettings)) + if (primaryResourceContext != null) { - return; + if (!await ValidateContentTypeHeaderAsync(HeaderConstants.MediaType, httpContext, options.SerializerSettings) || + !await ValidateAcceptHeaderAsync(MediaType, httpContext, options.SerializerSettings)) + { + return; + } + + SetupResourceRequest((JsonApiRequest)request, primaryResourceContext, routeValues, resourceContextProvider, httpContext.Request); + + httpContext.RegisterJsonApiRequest(); } + else if (IsRouteForOperations(routeValues)) + { + if (!await ValidateContentTypeHeaderAsync(HeaderConstants.AtomicOperationsMediaType, httpContext, options.SerializerSettings) || + !await ValidateAcceptHeaderAsync(AtomicOperationsMediaType, httpContext, options.SerializerSettings)) + { + return; + } - SetupOperationsRequest((JsonApiRequest)request, options, httpContext.Request); + SetupOperationsRequest((JsonApiRequest)request, options, httpContext.Request); - httpContext.RegisterJsonApiRequest(); - } + httpContext.RegisterJsonApiRequest(); + } - // Workaround for bug https://github.com/dotnet/aspnetcore/issues/33394 - httpContext.Features.Set(new FixedQueryFeature(httpContext.Features)); + // Workaround for bug https://github.com/dotnet/aspnetcore/issues/33394 + httpContext.Features.Set(new FixedQueryFeature(httpContext.Features)); - await _next(httpContext); + using (CodeTimingSessionManager.Current.Measure("Subsequent middleware")) + { + await _next(httpContext); + } + } + + if (CodeTimingSessionManager.IsEnabled) + { + string timingResults = CodeTimingSessionManager.Current.GetResults(); + string url = httpContext.Request.GetDisplayUrl(); + logger.LogInformation($"Measurement results for {httpContext.Request.Method} {url}:{Environment.NewLine}{timingResults}"); + } } private async Task ValidateIfMatchHeaderAsync(HttpContext httpContext, JsonSerializerSettings serializerSettings) diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs index a61e9c6555..236a3d80f6 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs @@ -4,6 +4,7 @@ using System.Linq; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Diagnostics; using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; @@ -83,6 +84,8 @@ public QueryLayer ComposeFromConstraints(ResourceContext requestResource) private QueryLayer ComposeTopLayer(IEnumerable constraints, ResourceContext resourceContext) { + using IDisposable _ = CodeTimingSessionManager.Current.Measure("Top-level query composition"); + // @formatter:wrap_chained_method_calls chop_always // @formatter:keep_existing_linebreaks true @@ -113,6 +116,8 @@ private QueryLayer ComposeTopLayer(IEnumerable constraints, R private IncludeExpression ComposeChildren(QueryLayer topLayer, ICollection constraints) { + using IDisposable _ = CodeTimingSessionManager.Current.Measure("Nested query composition"); + // @formatter:wrap_chained_method_calls chop_always // @formatter:keep_existing_linebreaks true diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringReader.cs index fe4064a9c2..68f9cc4a6c 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringReader.cs @@ -1,8 +1,10 @@ +using System; using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers.Annotations; +using JsonApiDotNetCore.Diagnostics; using JsonApiDotNetCore.Errors; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; @@ -35,6 +37,8 @@ public QueryStringReader(IJsonApiOptions options, IRequestQueryStringAccessor qu /// public virtual void ReadAll(DisableQueryStringAttribute disableQueryStringAttribute) { + using IDisposable _ = CodeTimingSessionManager.Current.Measure("Parse query string"); + DisableQueryStringAttribute disableQueryStringAttributeNotNull = disableQueryStringAttribute ?? DisableQueryStringAttribute.Empty; foreach ((string parameterName, StringValues parameterValue) in _queryStringAccessor.Query) diff --git a/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs b/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs index 2daa011d82..a86d002b51 100644 --- a/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs +++ b/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Diagnostics; using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Queries; @@ -71,8 +72,15 @@ public virtual async Task> GetAsync(QueryLayer la ArgumentGuard.NotNull(layer, nameof(layer)); - IQueryable query = ApplyQueryLayer(layer); - return await query.ToListAsync(cancellationToken); + using (CodeTimingSessionManager.Current.Measure("Repository - Get resource(s)")) + { + IQueryable query = ApplyQueryLayer(layer); + + using (CodeTimingSessionManager.Current.Measure("Execute SQL (data)", MeasurementSettings.ExcludeDatabaseInPercentages)) + { + return await query.ToListAsync(cancellationToken); + } + } } /// @@ -83,15 +91,22 @@ public virtual async Task CountAsync(FilterExpression topFilter, Cancellati topFilter }); - ResourceContext resourceContext = _resourceGraph.GetResourceContext(); - - var layer = new QueryLayer(resourceContext) + using (CodeTimingSessionManager.Current.Measure("Repository - Count resources")) { - Filter = topFilter - }; + ResourceContext resourceContext = _resourceGraph.GetResourceContext(); + + var layer = new QueryLayer(resourceContext) + { + Filter = topFilter + }; - IQueryable query = ApplyQueryLayer(layer); - return await query.CountAsync(cancellationToken); + IQueryable query = ApplyQueryLayer(layer); + + using (CodeTimingSessionManager.Current.Measure("Execute SQL (count)", MeasurementSettings.ExcludeDatabaseInPercentages)) + { + return await query.CountAsync(cancellationToken); + } + } } protected virtual IQueryable ApplyQueryLayer(QueryLayer layer) @@ -103,33 +118,40 @@ protected virtual IQueryable ApplyQueryLayer(QueryLayer layer) ArgumentGuard.NotNull(layer, nameof(layer)); - IQueryable source = GetAll(); + using (CodeTimingSessionManager.Current.Measure("Convert QueryLayer to System.Expression")) + { + IQueryable source = GetAll(); - // @formatter:wrap_chained_method_calls chop_always - // @formatter:keep_existing_linebreaks true + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true - QueryableHandlerExpression[] queryableHandlers = _constraintProviders - .SelectMany(provider => provider.GetConstraints()) - .Where(expressionInScope => expressionInScope.Scope == null) - .Select(expressionInScope => expressionInScope.Expression) - .OfType() - .ToArray(); + QueryableHandlerExpression[] queryableHandlers = _constraintProviders + .SelectMany(provider => provider.GetConstraints()) + .Where(expressionInScope => expressionInScope.Scope == null) + .Select(expressionInScope => expressionInScope.Expression) + .OfType() + .ToArray(); - // @formatter:keep_existing_linebreaks restore - // @formatter:wrap_chained_method_calls restore + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore - foreach (QueryableHandlerExpression queryableHandler in queryableHandlers) - { - source = queryableHandler.Apply(source); - } + foreach (QueryableHandlerExpression queryableHandler in queryableHandlers) + { + source = queryableHandler.Apply(source); + } - var nameFactory = new LambdaParameterNameFactory(); + var nameFactory = new LambdaParameterNameFactory(); - var builder = new QueryableBuilder(source.Expression, source.ElementType, typeof(Queryable), nameFactory, _resourceFactory, _resourceGraph, - _dbContext.Model); + var builder = new QueryableBuilder(source.Expression, source.ElementType, typeof(Queryable), nameFactory, _resourceFactory, _resourceGraph, + _dbContext.Model); - Expression expression = builder.ApplyQuery(layer); - return source.Provider.CreateQuery(expression); + Expression expression = builder.ApplyQuery(layer); + + using (CodeTimingSessionManager.Current.Measure("Convert System.Expression to IQueryable")) + { + return source.Provider.CreateQuery(expression); + } + } } protected virtual IQueryable GetAll() @@ -158,6 +180,8 @@ public virtual async Task CreateAsync(TResource resourceFromRequest, TResource r ArgumentGuard.NotNull(resourceFromRequest, nameof(resourceFromRequest)); ArgumentGuard.NotNull(resourceForDatabase, nameof(resourceForDatabase)); + using IDisposable _ = CodeTimingSessionManager.Current.Measure("Repository - Create resource"); + using var collector = new PlaceholderResourceCollector(_resourceFactory, _dbContext); foreach (RelationshipAttribute relationship in _targetedFields.Relationships) @@ -210,6 +234,8 @@ await _resourceDefinitionAccessor.OnSetToManyRelationshipAsync(leftResource, has /// public virtual async Task GetForUpdateAsync(QueryLayer queryLayer, CancellationToken cancellationToken) { + using IDisposable _ = CodeTimingSessionManager.Current.Measure("Repository - Get resource for update"); + IReadOnlyCollection resources = await GetAsync(queryLayer, cancellationToken); return resources.FirstOrDefault(); } @@ -226,6 +252,8 @@ public virtual async Task UpdateAsync(TResource resourceFromRequest, TResource r ArgumentGuard.NotNull(resourceFromRequest, nameof(resourceFromRequest)); ArgumentGuard.NotNull(resourceFromDatabase, nameof(resourceFromDatabase)); + using IDisposable _ = CodeTimingSessionManager.Current.Measure("Repository - Update resource"); + using var collector = new PlaceholderResourceCollector(_resourceFactory, _dbContext); foreach (RelationshipAttribute relationship in _targetedFields.Relationships) @@ -295,6 +323,8 @@ public virtual async Task DeleteAsync(TId id, CancellationToken cancellationToke id }); + using IDisposable _ = CodeTimingSessionManager.Current.Measure("Repository - Delete resource"); + // This enables OnWritingAsync() to fetch the resource, which adds it to the change tracker. // If so, we'll reuse the tracked resource instead of a placeholder resource. var emptyResource = _resourceFactory.CreateInstance(); @@ -374,6 +404,8 @@ public virtual async Task SetRelationshipAsync(TResource leftResource, object ri rightValue }); + using IDisposable _ = CodeTimingSessionManager.Current.Measure("Repository - Set relationship"); + RelationshipAttribute relationship = _targetedFields.Relationships.Single(); object rightValueEvaluated = @@ -402,6 +434,8 @@ public virtual async Task AddToToManyRelationshipAsync(TId leftId, ISet(leftId, relationship, rightResourceIds, cancellationToken); @@ -433,6 +467,8 @@ public virtual async Task RemoveFromToManyRelationshipAsync(TResource leftResour ArgumentGuard.NotNull(rightResourceIds, nameof(rightResourceIds)); + using IDisposable _ = CodeTimingSessionManager.Current.Measure("Repository - Remove from to-many relationship"); + var relationship = (HasManyAttribute)_targetedFields.Relationships.Single(); await _resourceDefinitionAccessor.OnRemoveFromRelationshipAsync(leftResource, relationship, rightResourceIds, cancellationToken); @@ -502,6 +538,8 @@ protected virtual async Task SaveChangesAsync(CancellationToken cancellationToke try { + using IDisposable _ = CodeTimingSessionManager.Current.Measure("Persist EF Core changes", MeasurementSettings.ExcludeDatabaseInPercentages); + await _dbContext.SaveChangesAsync(cancellationToken); } catch (Exception exception) when (exception is DbUpdateException || exception is InvalidOperationException) diff --git a/src/JsonApiDotNetCore/Serialization/BaseDeserializer.cs b/src/JsonApiDotNetCore/Serialization/BaseDeserializer.cs index 8cf2653f9e..b329392c9f 100644 --- a/src/JsonApiDotNetCore/Serialization/BaseDeserializer.cs +++ b/src/JsonApiDotNetCore/Serialization/BaseDeserializer.cs @@ -5,6 +5,7 @@ using System.Linq; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Diagnostics; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Resources.Internal; @@ -61,19 +62,28 @@ protected object DeserializeBody(string body) { ArgumentGuard.NotNullNorEmpty(body, nameof(body)); - JToken bodyJToken = LoadJToken(body); - Document = bodyJToken.ToObject(); + using (CodeTimingSessionManager.Current.Measure("Newtonsoft.Deserialize", MeasurementSettings.ExcludeJsonSerializationInPercentages)) + { + JToken bodyJToken = LoadJToken(body); + Document = bodyJToken.ToObject(); + } if (Document != null) { if (Document.IsManyData) { - return Document.ManyData.Select(ParseResourceObject).ToHashSet(IdentifiableComparer.Instance); + using (CodeTimingSessionManager.Current.Measure("Deserializer.Build (list)")) + { + return Document.ManyData.Select(ParseResourceObject).ToHashSet(IdentifiableComparer.Instance); + } } if (Document.SingleData != null) { - return ParseResourceObject(Document.SingleData); + using (CodeTimingSessionManager.Current.Measure("Deserializer.Build (single)")) + { + return ParseResourceObject(Document.SingleData); + } } } diff --git a/src/JsonApiDotNetCore/Serialization/BaseSerializer.cs b/src/JsonApiDotNetCore/Serialization/BaseSerializer.cs index 33ebf99f64..8519d20fd2 100644 --- a/src/JsonApiDotNetCore/Serialization/BaseSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/BaseSerializer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using JsonApiDotNetCore.Diagnostics; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization.Building; @@ -43,6 +44,8 @@ protected BaseSerializer(IResourceObjectBuilder resourceObjectBuilder) protected Document Build(IIdentifiable resource, IReadOnlyCollection attributes, IReadOnlyCollection relationships) { + using IDisposable _ = CodeTimingSessionManager.Current.Measure("Serializer.Build (single)"); + if (resource == null) { return new Document(); @@ -75,6 +78,8 @@ protected Document Build(IReadOnlyCollection resources, IReadOnly { ArgumentGuard.NotNull(resources, nameof(resources)); + using IDisposable _ = CodeTimingSessionManager.Current.Measure("Serializer.Build (list)"); + var data = new List(); foreach (IIdentifiable resource in resources) @@ -92,6 +97,8 @@ protected string SerializeObject(object value, JsonSerializerSettings defaultSet { ArgumentGuard.NotNull(defaultSettings, nameof(defaultSettings)); + using IDisposable _ = CodeTimingSessionManager.Current.Measure("Newtonsoft.Serialize", MeasurementSettings.ExcludeJsonSerializationInPercentages); + var serializer = JsonSerializer.CreateDefault(defaultSettings); changeSerializer?.Invoke(serializer); diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs b/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs index d0dac14349..d9e861d4a2 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Diagnostics; using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Resources; @@ -47,6 +48,8 @@ public async Task ReadAsync(InputFormatterContext context) { ArgumentGuard.NotNull(context, nameof(context)); + using IDisposable _ = CodeTimingSessionManager.Current.Measure("Read request body"); + string body = await GetRequestBodyAsync(context.HttpContext.Request.Body); string url = context.HttpContext.Request.GetEncodedUrl(); diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs b/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs index fe5463def9..08c81da0f5 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs @@ -6,6 +6,7 @@ using System.Text; using System.Threading.Tasks; using JetBrains.Annotations; +using JsonApiDotNetCore.Diagnostics; using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Serialization.Objects; @@ -47,6 +48,8 @@ public async Task WriteAsync(OutputFormatterWriteContext context) { ArgumentGuard.NotNull(context, nameof(context)); + using IDisposable _ = CodeTimingSessionManager.Current.Measure("Write response body"); + HttpRequest request = context.HttpContext.Request; HttpResponse response = context.HttpContext.Response; diff --git a/src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs b/src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs index ab372fea3f..8220936b20 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs @@ -6,6 +6,7 @@ using Humanizer; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Diagnostics; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; @@ -74,8 +75,13 @@ public object Deserialize(string body) private object DeserializeOperationsDocument(string body) { - JToken bodyToken = LoadJToken(body); - var document = bodyToken.ToObject(); + AtomicOperationsDocument document; + + using (CodeTimingSessionManager.Current.Measure("Newtonsoft.Deserialize", MeasurementSettings.ExcludeJsonSerializationInPercentages)) + { + JToken bodyToken = LoadJToken(body); + document = bodyToken.ToObject(); + } if ((document?.Operations).IsNullOrEmpty()) { diff --git a/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs b/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs index 8ae06cd907..0c3d9c521a 100644 --- a/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs +++ b/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Diagnostics; using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Queries; @@ -61,6 +62,8 @@ public virtual async Task> GetAsync(CancellationT { _traceWriter.LogMethodStart(); + using IDisposable _ = CodeTimingSessionManager.Current.Measure("Service - Get resources"); + if (_options.IncludeTotalResourceCount) { FilterExpression topFilter = _queryLayerComposer.GetTopFilterFromConstraints(_request.PrimaryResource); @@ -91,6 +94,8 @@ public virtual async Task GetAsync(TId id, CancellationToken cancella id }); + using IDisposable _ = CodeTimingSessionManager.Current.Measure("Service - Get single resource"); + return await GetPrimaryResourceByIdAsync(id, TopFieldSelection.PreserveExisting, cancellationToken); } @@ -103,6 +108,8 @@ public virtual async Task GetSecondaryAsync(TId id, string relationshipN relationshipName }); + using IDisposable _ = CodeTimingSessionManager.Current.Measure("Service - Get secondary resource(s)"); + AssertHasRelationship(_request.Relationship, relationshipName); QueryLayer secondaryLayer = _queryLayerComposer.ComposeFromConstraints(_request.SecondaryResource); @@ -134,6 +141,8 @@ public virtual async Task GetRelationshipAsync(TId id, string relationsh ArgumentGuard.NotNullNorEmpty(relationshipName, nameof(relationshipName)); + using IDisposable _ = CodeTimingSessionManager.Current.Measure("Service - Get relationship"); + AssertHasRelationship(_request.Relationship, relationshipName); QueryLayer secondaryLayer = _queryLayerComposer.ComposeSecondaryLayerForRelationship(_request.SecondaryResource); @@ -157,6 +166,8 @@ public virtual async Task CreateAsync(TResource resource, Cancellatio ArgumentGuard.NotNull(resource, nameof(resource)); + using IDisposable _ = CodeTimingSessionManager.Current.Measure("Service - Create resource"); + TResource resourceFromRequest = resource; _resourceChangeTracker.SetRequestedAttributeValues(resourceFromRequest); @@ -253,6 +264,8 @@ public virtual async Task AddToToManyRelationshipAsync(TId leftId, string relati ArgumentGuard.NotNullNorEmpty(relationshipName, nameof(relationshipName)); ArgumentGuard.NotNull(rightResourceIds, nameof(rightResourceIds)); + using IDisposable _ = CodeTimingSessionManager.Current.Measure("Service - Add to to-many relationship"); + AssertHasRelationship(_request.Relationship, relationshipName); if (rightResourceIds.Any() && _request.Relationship is HasManyAttribute { IsManyToMany: true } manyToManyRelationship) @@ -268,7 +281,7 @@ public virtual async Task AddToToManyRelationshipAsync(TId leftId, string relati } catch (DataStoreUpdateException) { - _ = await GetPrimaryResourceByIdAsync(leftId, TopFieldSelection.OnlyIdAttribute, cancellationToken); + await GetPrimaryResourceByIdAsync(leftId, TopFieldSelection.OnlyIdAttribute, cancellationToken); await AssertRightResourcesExistAsync(rightResourceIds, cancellationToken); throw; } @@ -318,6 +331,8 @@ public virtual async Task UpdateAsync(TId id, TResource resource, Can ArgumentGuard.NotNull(resource, nameof(resource)); + using IDisposable _ = CodeTimingSessionManager.Current.Measure("Service - Update resource"); + TResource resourceFromRequest = resource; _resourceChangeTracker.SetRequestedAttributeValues(resourceFromRequest); @@ -357,6 +372,8 @@ public virtual async Task SetRelationshipAsync(TId leftId, string relationshipNa ArgumentGuard.NotNullNorEmpty(relationshipName, nameof(relationshipName)); + using IDisposable _ = CodeTimingSessionManager.Current.Measure("Service - Set relationship"); + AssertHasRelationship(_request.Relationship, relationshipName); TResource resourceFromDatabase = await GetPrimaryResourceForUpdateAsync(leftId, cancellationToken); @@ -382,13 +399,15 @@ public virtual async Task DeleteAsync(TId id, CancellationToken cancellationToke id }); + using IDisposable _ = CodeTimingSessionManager.Current.Measure("Repository - Delete resource"); + try { await _repositoryAccessor.DeleteAsync(id, cancellationToken); } catch (DataStoreUpdateException) { - _ = await GetPrimaryResourceByIdAsync(id, TopFieldSelection.OnlyIdAttribute, cancellationToken); + await GetPrimaryResourceByIdAsync(id, TopFieldSelection.OnlyIdAttribute, cancellationToken); throw; } } @@ -407,6 +426,8 @@ public virtual async Task RemoveFromToManyRelationshipAsync(TId leftId, string r ArgumentGuard.NotNullNorEmpty(relationshipName, nameof(relationshipName)); ArgumentGuard.NotNull(rightResourceIds, nameof(rightResourceIds)); + using IDisposable _ = CodeTimingSessionManager.Current.Measure("Repository - Remove from to-many relationship"); + AssertHasRelationship(_request.Relationship, relationshipName); TResource resourceFromDatabase = await GetPrimaryResourceForUpdateAsync(leftId, cancellationToken); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs index 1a9a272c03..b261702075 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs @@ -27,23 +27,17 @@ public ExceptionHandlerTests(ExampleIntegrationTestContext(); testContext.UseController(); - FakeLoggerFactory loggerFactory = null; + var loggerFactory = new FakeLoggerFactory(LogLevel.Warning); testContext.ConfigureLogging(options => { - loggerFactory = new FakeLoggerFactory(); - options.ClearProviders(); options.AddProvider(loggerFactory); - options.SetMinimumLevel(LogLevel.Warning); }); testContext.ConfigureServicesBeforeStartup(services => { - if (loggerFactory != null) - { - services.AddSingleton(_ => loggerFactory); - } + services.AddSingleton(loggerFactory); }); testContext.ConfigureServicesAfterStartup(services => diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/HostingStartup.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/HostingStartup.cs index 2b369b3c6e..4ea765767a 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/HostingStartup.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/HostingStartup.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; namespace JsonApiDotNetCoreExampleTests.IntegrationTests.HostingInIIS { @@ -19,11 +20,11 @@ protected override void SetJsonApiOptions(JsonApiOptions options) options.IncludeTotalResourceCount = true; } - public override void Configure(IApplicationBuilder app, IWebHostEnvironment environment) + public override void Configure(IApplicationBuilder app, IWebHostEnvironment environment, ILoggerFactory loggerFactory) { app.UsePathBase("/iis-application-virtual-directory"); - base.Configure(app, environment); + base.Configure(app, environment, loggerFactory); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Logging/LoggingTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Logging/LoggingTests.cs index f234be59fc..ead491a2a3 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Logging/LoggingTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Logging/LoggingTests.cs @@ -22,24 +22,18 @@ public LoggingTests(ExampleIntegrationTestContext(); - FakeLoggerFactory loggerFactory = null; + var loggerFactory = new FakeLoggerFactory(LogLevel.Trace); testContext.ConfigureLogging(options => { - loggerFactory = new FakeLoggerFactory(); - options.ClearProviders(); options.AddProvider(loggerFactory); options.SetMinimumLevel(LogLevel.Trace); - options.AddFilter((_, _) => true); }); testContext.ConfigureServicesBeforeStartup(services => { - if (loggerFactory != null) - { - services.AddSingleton(_ => loggerFactory); - } + services.AddSingleton(loggerFactory); }); } diff --git a/test/JsonApiDotNetCoreExampleTests/Startups/TestableStartup.cs b/test/JsonApiDotNetCoreExampleTests/Startups/TestableStartup.cs index a486046b4e..ed5272d2c0 100644 --- a/test/JsonApiDotNetCoreExampleTests/Startups/TestableStartup.cs +++ b/test/JsonApiDotNetCoreExampleTests/Startups/TestableStartup.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Converters; @@ -24,7 +25,7 @@ protected virtual void SetJsonApiOptions(JsonApiOptions options) options.SerializerSettings.Converters.Add(new StringEnumConverter()); } - public override void Configure(IApplicationBuilder app, IWebHostEnvironment environment) + public override void Configure(IApplicationBuilder app, IWebHostEnvironment environment, ILoggerFactory loggerFactory) { app.UseRouting(); app.UseJsonApi(); diff --git a/test/TestBuildingBlocks/BaseIntegrationTestContext.cs b/test/TestBuildingBlocks/BaseIntegrationTestContext.cs index 2c57e94b60..2e87eded6b 100644 --- a/test/TestBuildingBlocks/BaseIntegrationTestContext.cs +++ b/test/TestBuildingBlocks/BaseIntegrationTestContext.cs @@ -80,8 +80,10 @@ private WebApplicationFactory CreateFactory() // This is fixed in EF Core 6, tracked at https://github.com/dotnet/efcore/issues/21234. builder.UseQuerySplittingBehavior(QuerySplittingBehavior.SingleQuery)); +#if DEBUG options.EnableSensitiveDataLogging(); options.EnableDetailedErrors(); +#endif }); }); @@ -147,25 +149,40 @@ public void ConfigureServicesAfterStartup(Action servicesCon protected override IHostBuilder CreateHostBuilder() { - return Host.CreateDefaultBuilder(null).ConfigureWebHostDefaults(webBuilder => - { - webBuilder.ConfigureLogging(options => - { - _loggingConfiguration?.Invoke(options); - }); + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true - webBuilder.ConfigureServices(services => + return Host.CreateDefaultBuilder(null) + .ConfigureAppConfiguration(builder => { - _beforeServicesConfiguration?.Invoke(services); - }); - - webBuilder.UseStartup(); - - webBuilder.ConfigureServices(services => + // For tests asserting on log output, we discard the logging settings from appsettings.json. + // But using appsettings.json for all other tests makes it easy to quickly toggle when debugging. + if (_loggingConfiguration != null) + { + builder.Sources.Clear(); + } + }) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.ConfigureServices(services => + { + _beforeServicesConfiguration?.Invoke(services); + }); + + webBuilder.UseStartup(); + + webBuilder.ConfigureServices(services => + { + _afterServicesConfiguration?.Invoke(services); + }); + }) + .ConfigureLogging(options => { - _afterServicesConfiguration?.Invoke(services); + _loggingConfiguration?.Invoke(options); }); - }); + + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore } } } diff --git a/test/TestBuildingBlocks/FakeLoggerFactory.cs b/test/TestBuildingBlocks/FakeLoggerFactory.cs index 653aa9fea5..0bcc047546 100644 --- a/test/TestBuildingBlocks/FakeLoggerFactory.cs +++ b/test/TestBuildingBlocks/FakeLoggerFactory.cs @@ -11,9 +11,9 @@ public sealed class FakeLoggerFactory : ILoggerFactory, ILoggerProvider { public FakeLogger Logger { get; } - public FakeLoggerFactory() + public FakeLoggerFactory(LogLevel minimumLevel) { - Logger = new FakeLogger(); + Logger = new FakeLogger(minimumLevel); } public ILogger CreateLogger(string categoryName) @@ -31,13 +31,19 @@ public void Dispose() public sealed class FakeLogger : ILogger { + private readonly LogLevel _minimumLevel; private readonly ConcurrentBag _messages = new(); public IReadOnlyCollection Messages => _messages; + public FakeLogger(LogLevel minimumLevel) + { + _minimumLevel = minimumLevel; + } + public bool IsEnabled(LogLevel logLevel) { - return true; + return _minimumLevel != LogLevel.None && logLevel >= _minimumLevel; } public void Clear() @@ -47,8 +53,11 @@ public void Clear() public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) { - string message = formatter(state, exception); - _messages.Add(new FakeLogMessage(logLevel, message)); + if (IsEnabled(logLevel)) + { + string message = formatter(state, exception); + _messages.Add(new FakeLogMessage(logLevel, message)); + } } public IDisposable BeginScope(TState state) @@ -67,6 +76,11 @@ public FakeLogMessage(LogLevel logLevel, string text) LogLevel = logLevel; Text = text; } + + public override string ToString() + { + return $"[{LogLevel.ToString().ToUpperInvariant()}] {Text}"; + } } } } diff --git a/test/UnitTests/Internal/ResourceGraphBuilderTests.cs b/test/UnitTests/Internal/ResourceGraphBuilderTests.cs index 890739d43e..2b64b34974 100644 --- a/test/UnitTests/Internal/ResourceGraphBuilderTests.cs +++ b/test/UnitTests/Internal/ResourceGraphBuilderTests.cs @@ -31,7 +31,7 @@ public void AddDbContext_Does_Not_Throw_If_Context_Contains_Members_That_Do_Not_ public void Adding_DbContext_Members_That_Do_Not_Implement_IIdentifiable_Logs_Warning() { // Arrange - var loggerFactory = new FakeLoggerFactory(); + var loggerFactory = new FakeLoggerFactory(LogLevel.Warning); var resourceGraphBuilder = new ResourceGraphBuilder(new JsonApiOptions(), loggerFactory); resourceGraphBuilder.Add(typeof(TestContext)); diff --git a/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs b/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs index f789b8ba0f..22c4f92f57 100644 --- a/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs +++ b/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.Extensions.Logging.Abstractions; using Moq; using Moq.Language; using Xunit; @@ -81,7 +82,7 @@ private Task RunMiddlewareTask(InvokeConfiguration holder) IJsonApiOptions options = holder.Options.Object; JsonApiRequest request = holder.Request; IResourceGraph resourceGraph = holder.ResourceGraph.Object; - return holder.MiddleWare.InvokeAsync(context, controllerResourceMapping, options, request, resourceGraph); + return holder.MiddleWare.InvokeAsync(context, controllerResourceMapping, options, request, resourceGraph, NullLogger.Instance); } private InvokeConfiguration GetConfiguration(string path, string resourceName = "users", string action = "", string id = null, Type relType = null) @@ -94,7 +95,7 @@ private InvokeConfiguration GetConfiguration(string path, string resourceName = var middleware = new JsonApiMiddleware(_ => { return Task.Run(() => Console.WriteLine("finished")); - }); + }, new HttpContextAccessor()); const string forcedNamespace = "api/v1"; var mockMapping = new Mock(); diff --git a/test/UnitTests/Middleware/JsonApiRequestTests.cs b/test/UnitTests/Middleware/JsonApiRequestTests.cs index 7950291a9d..a225522b0f 100644 --- a/test/UnitTests/Middleware/JsonApiRequestTests.cs +++ b/test/UnitTests/Middleware/JsonApiRequestTests.cs @@ -61,10 +61,11 @@ public async Task Sets_request_properties_correctly(string requestMethod, string var request = new JsonApiRequest(); - var middleware = new JsonApiMiddleware(_ => Task.CompletedTask); + var middleware = new JsonApiMiddleware(_ => Task.CompletedTask, new HttpContextAccessor()); // Act - await middleware.InvokeAsync(httpContext, controllerResourceMappingMock.Object, options, request, resourceGraph); + await middleware.InvokeAsync(httpContext, controllerResourceMappingMock.Object, options, request, resourceGraph, + NullLogger.Instance); // Assert request.IsCollection.Should().Be(expectIsCollection); From b1e83c486750bb9424858733d8e38909e01e1595 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Fri, 6 Aug 2021 13:02:40 +0200 Subject: [PATCH 3/8] Fixed warnings "Invalid cref value" from docfx https://github.com/dotnet/docfx/issues/5112#issuecomment-659487694 --- docs/docfx.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/docfx.json b/docs/docfx.json index 6acc17ce7a..98e83101a5 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -8,10 +8,7 @@ } ], "dest": "api", - "disableGitFeatures": false, - "properties": { - "targetFramework": "netcoreapp3.1" - } + "disableGitFeatures": false } ], "build": { From 1fe22b047c9a49a0e8e0a6ac632fcccc5fabcd00 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Thu, 5 Aug 2021 13:58:30 +0200 Subject: [PATCH 4/8] Updated to Resharper/Rider v2021.2.0 --- .config/dotnet-tools.json | 2 +- Build.ps1 | 2 +- JsonApiDotNetCore.sln.DotSettings | 6 ++++++ inspectcode.ps1 | 8 +------- .../ResourcesInRelationshipsNotFoundException.cs | 2 +- src/JsonApiDotNetCore/ObjectExtensions.cs | 4 ++-- .../ResourceObjectBuilderSettingsProvider.cs | 2 +- .../Mixed/AtomicRequestBodyTests.cs | 3 ++- .../QueryStrings/MusicTrackReleaseDefinition.cs | 2 +- .../AtomicAddToToManyRelationshipTests.cs | 2 +- .../AtomicRemoveFromToManyRelationshipTests.cs | 2 +- .../AtomicReplaceToManyRelationshipTests.cs | 4 ++-- .../AtomicReplaceToManyRelationshipTests.cs | 4 ++-- .../ModelState/ModelStateValidationTests.cs | 3 ++- .../Microservices/Messages/OutgoingMessage.cs | 2 +- .../MultiTenancy/MultiTenancyTests.cs | 2 +- .../Relationships/AddToToManyRelationshipTests.cs | 3 ++- .../RemoveFromToManyRelationshipTests.cs | 3 ++- .../ReplaceToManyRelationshipTests.cs | 11 ++++++----- .../Resources/ReplaceToManyRelationshipTests.cs | 9 +++++---- .../RequiredRelationships/DefaultBehaviorTests.cs | 5 +++-- .../SoftDeletion/SoftDeletionTests.cs | 2 +- .../ZeroKeys/EmptyGuidAsKeyTests.cs | 2 +- .../IntegrationTests/ZeroKeys/ZeroAsKeyTests.cs | 3 ++- .../HttpResponseMessageExtensions.cs | 2 +- .../UnitTests/Middleware/JsonApiMiddlewareTests.cs | 14 +++++++++++--- .../Serialization/DeserializerTestsSetup.cs | 4 ++-- 27 files changed, 62 insertions(+), 46 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 1c7cf727cb..9ad5f18ca6 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "jetbrains.resharper.globaltools": { - "version": "2021.1.4", + "version": "2021.2.0", "commands": [ "jb" ] diff --git a/Build.ps1 b/Build.ps1 index ee1dd68cfb..0cca69c095 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -8,7 +8,7 @@ function CheckLastExitCode { function RunInspectCode { $outputPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'jetbrains-inspectcode-results.xml') - dotnet jb inspectcode JsonApiDotNetCore.sln --output="$outputPath" --profile=WarningSeverities.DotSettings --properties:Configuration=Release --severity=WARNING --verbosity=WARN -dsl=GlobalAll -dsl=SolutionPersonal -dsl=ProjectPersonal + dotnet jb inspectcode JsonApiDotNetCore.sln --no-build --output="$outputPath" --profile=WarningSeverities.DotSettings --properties:Configuration=Release --severity=WARNING --verbosity=WARN -dsl=GlobalAll -dsl=SolutionPersonal -dsl=ProjectPersonal CheckLastExitCode [xml]$xml = Get-Content "$outputPath" diff --git a/JsonApiDotNetCore.sln.DotSettings b/JsonApiDotNetCore.sln.DotSettings index bc7a56ea08..595cae92d8 100644 --- a/JsonApiDotNetCore.sln.DotSettings +++ b/JsonApiDotNetCore.sln.DotSettings @@ -78,7 +78,10 @@ JsonApiDotNetCore.ArgumentGuard.NotNull($EXPR$, $NAME$); SUGGESTION WARNING HINT + WARNING WARNING + WARNING + WARNING WARNING <?xml version="1.0" encoding="utf-16"?><Profile name="JADNC Full Cleanup"><XMLReformatCode>True</XMLReformatCode><CSCodeStyleAttributes ArrangeTypeAccessModifier="True" ArrangeTypeMemberAccessModifier="True" SortModifiers="True" RemoveRedundantParentheses="True" AddMissingParentheses="True" ArrangeBraces="True" ArrangeAttributes="True" ArrangeArgumentsStyle="True" ArrangeCodeBodyStyle="True" ArrangeVarStyle="True" ArrangeTrailingCommas="True" ArrangeObjectCreation="True" ArrangeDefaultValue="True" /><CssAlphabetizeProperties>True</CssAlphabetizeProperties><JsInsertSemicolon>True</JsInsertSemicolon><FormatAttributeQuoteDescriptor>True</FormatAttributeQuoteDescriptor><CorrectVariableKindsDescriptor>True</CorrectVariableKindsDescriptor><VariablesToInnerScopesDescriptor>True</VariablesToInnerScopesDescriptor><StringToTemplatesDescriptor>True</StringToTemplatesDescriptor><JsReformatCode>True</JsReformatCode><JsFormatDocComments>True</JsFormatDocComments><RemoveRedundantQualifiersTs>True</RemoveRedundantQualifiersTs><OptimizeImportsTs>True</OptimizeImportsTs><OptimizeReferenceCommentsTs>True</OptimizeReferenceCommentsTs><PublicModifierStyleTs>True</PublicModifierStyleTs><ExplicitAnyTs>True</ExplicitAnyTs><TypeAnnotationStyleTs>True</TypeAnnotationStyleTs><RelativePathStyleTs>True</RelativePathStyleTs><AsInsteadOfCastTs>True</AsInsteadOfCastTs><HtmlReformatCode>True</HtmlReformatCode><AspOptimizeRegisterDirectives>True</AspOptimizeRegisterDirectives><RemoveCodeRedundancies>True</RemoveCodeRedundancies><CSUseAutoProperty>True</CSUseAutoProperty><CSMakeFieldReadonly>True</CSMakeFieldReadonly><CSMakeAutoPropertyGetOnly>True</CSMakeAutoPropertyGetOnly><CSArrangeQualifiers>True</CSArrangeQualifiers><CSFixBuiltinTypeReferences>True</CSFixBuiltinTypeReferences><CssReformatCode>True</CssReformatCode><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings><EmbraceInRegion>False</EmbraceInRegion><RegionName></RegionName></CSOptimizeUsings><CSShortenReferences>True</CSShortenReferences><CSReformatCode>True</CSReformatCode><CSharpFormatDocComments>True</CSharpFormatDocComments><CSReorderTypeMembers>True</CSReorderTypeMembers><XAMLCollapseEmptyTags>False</XAMLCollapseEmptyTags></Profile> JADNC Full Cleanup @@ -623,13 +626,16 @@ $left$ = $right$; True True True + True True True True + True True True True True True True + True diff --git a/inspectcode.ps1 b/inspectcode.ps1 index ab4b9c95dd..6c9d90768e 100644 --- a/inspectcode.ps1 +++ b/inspectcode.ps1 @@ -8,15 +8,9 @@ if ($LASTEXITCODE -ne 0) { throw "Tool restore failed with exit code $LASTEXITCODE" } -dotnet build -c Release - -if ($LASTEXITCODE -ne 0) { - throw "Build failed with exit code $LASTEXITCODE" -} - $outputPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'jetbrains-inspectcode-results.xml') $resultPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'jetbrains-inspectcode-results.html') -dotnet jb inspectcode JsonApiDotNetCore.sln --output="$outputPath" --profile=WarningSeverities.DotSettings --properties:Configuration=Release --severity=WARNING --verbosity=WARN -dsl=GlobalAll -dsl=SolutionPersonal -dsl=ProjectPersonal +dotnet jb inspectcode JsonApiDotNetCore.sln --build --output="$outputPath" --profile=WarningSeverities.DotSettings --properties:Configuration=Release --severity=WARNING --verbosity=WARN -dsl=GlobalAll -dsl=SolutionPersonal -dsl=ProjectPersonal if ($LASTEXITCODE -ne 0) { throw "Code inspection failed with exit code $LASTEXITCODE" diff --git a/src/JsonApiDotNetCore/Errors/ResourcesInRelationshipsNotFoundException.cs b/src/JsonApiDotNetCore/Errors/ResourcesInRelationshipsNotFoundException.cs index d749bca5fa..94503ab343 100644 --- a/src/JsonApiDotNetCore/Errors/ResourcesInRelationshipsNotFoundException.cs +++ b/src/JsonApiDotNetCore/Errors/ResourcesInRelationshipsNotFoundException.cs @@ -19,7 +19,7 @@ public ResourcesInRelationshipsNotFoundException(IEnumerable(this T element) public static List AsList(this T element) { - return new() + return new List { element }; @@ -29,7 +29,7 @@ public static List AsList(this T element) public static HashSet AsHashSet(this T element) { - return new() + return new HashSet { element }; diff --git a/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilderSettingsProvider.cs b/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilderSettingsProvider.cs index 04a5128aed..d28a7591eb 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilderSettingsProvider.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilderSettingsProvider.cs @@ -24,7 +24,7 @@ public ResourceObjectBuilderSettingsProvider(IDefaultsQueryStringParameterReader /// public ResourceObjectBuilderSettings Get() { - return new(_nullsReader.SerializerNullValueHandling, _defaultsReader.SerializerDefaultValueHandling); + return new ResourceObjectBuilderSettings(_nullsReader.SerializerNullValueHandling, _defaultsReader.SerializerDefaultValueHandling); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Mixed/AtomicRequestBodyTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Mixed/AtomicRequestBodyTests.cs index 8b7084b4fe..2ec7a73e9f 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Mixed/AtomicRequestBodyTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Mixed/AtomicRequestBodyTests.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Net; using System.Net.Http; @@ -82,7 +83,7 @@ public async Task Cannot_process_empty_operations_array() // Arrange var requestBody = new { - atomic__operations = new object[0] + atomic__operations = Array.Empty() }; const string route = "/operations"; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/QueryStrings/MusicTrackReleaseDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/QueryStrings/MusicTrackReleaseDefinition.cs index ac3c4b12a3..857f68ef25 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/QueryStrings/MusicTrackReleaseDefinition.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/QueryStrings/MusicTrackReleaseDefinition.cs @@ -24,7 +24,7 @@ public MusicTrackReleaseDefinition(IResourceGraph resourceGraph, ISystemClock sy public override QueryStringParameterHandlers OnRegisterQueryableHandlersForQueryStringParameters() { - return new() + return new QueryStringParameterHandlers { ["isRecentlyReleased"] = FilterOnRecentlyReleased }; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs index 901d6bde71..5a318858e0 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs @@ -927,7 +927,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => id = existingTrack.StringId, relationship = "performers" }, - data = new object[0] + data = Array.Empty() } } }; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs index 57d0239f02..3599300581 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs @@ -890,7 +890,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => id = existingTrack.StringId, relationship = "performers" }, - data = new object[0] + data = Array.Empty() } } }; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs index 29074f742c..eee3c1815b 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs @@ -54,7 +54,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => id = existingTrack.StringId, relationship = "performers" }, - data = new object[0] + data = Array.Empty() } } }; @@ -107,7 +107,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => id = existingPlaylist.StringId, relationship = "tracks" }, - data = new object[0] + data = Array.Empty() } } }; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs index c1331532fe..5e88e74c5f 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs @@ -56,7 +56,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { performers = new { - data = new object[0] + data = Array.Empty() } } } @@ -114,7 +114,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { tracks = new { - data = new object[0] + data = Array.Empty() } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/InputValidation/ModelState/ModelStateValidationTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/InputValidation/ModelState/ModelStateValidationTests.cs index b74bc5fc01..e9abbb60fb 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/InputValidation/ModelState/ModelStateValidationTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/InputValidation/ModelState/ModelStateValidationTests.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Net; using System.Net.Http; @@ -923,7 +924,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => var requestBody = new { - data = new object[0] + data = Array.Empty() }; string route = $"/systemDirectories/{directory.StringId}/relationships/files"; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/Messages/OutgoingMessage.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/Messages/OutgoingMessage.cs index f001514efc..68837f0d12 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/Messages/OutgoingMessage.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/Messages/OutgoingMessage.cs @@ -22,7 +22,7 @@ public T GetContentAs() public static OutgoingMessage CreateFromContent(IMessageContent content) { - return new() + return new OutgoingMessage { Type = content.GetType().Name, FormatVersion = content.FormatVersion, diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/MultiTenancy/MultiTenancyTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/MultiTenancy/MultiTenancyTests.cs index 8265ee945b..d53c4a5859 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/MultiTenancy/MultiTenancyTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/MultiTenancy/MultiTenancyTests.cs @@ -658,7 +658,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => var requestBody = new { - data = new object[0] + data = Array.Empty() }; string route = $"/nld/shops/{existingShop.StringId}/relationships/products"; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs index 2b97784b48..2de5c7ad04 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -646,7 +647,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => var requestBody = new { - data = new object[0] + data = Array.Empty() }; string route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs index 7d36261871..80bc5ecd50 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -644,7 +645,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => var requestBody = new { - data = new object[0] + data = Array.Empty() }; string route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs index c4653f2244..10d9548039 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -40,7 +41,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => var requestBody = new { - data = new object[0] + data = Array.Empty() }; string route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; @@ -76,7 +77,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => var requestBody = new { - data = new object[0] + data = Array.Empty() }; string route = $"/workItems/{existingWorkItem.StringId}/relationships/tags"; @@ -506,7 +507,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => var requestBody = new { - data = new object[0] + data = Array.Empty() }; const string route = "/workItems/99999999/relationships/subscribers"; @@ -746,7 +747,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => var requestBody = new { - data = new object[0] + data = Array.Empty() }; string route = $"/workItems/{existingWorkItem.StringId}/relationships/children"; @@ -784,7 +785,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => var requestBody = new { - data = new object[0] + data = Array.Empty() }; string route = $"/workItems/{existingWorkItem.StringId}/relationships/relatedFrom"; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs index f9ffae837a..f6d555c4cf 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -48,7 +49,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { subscribers = new { - data = new object[0] + data = Array.Empty() } } } @@ -95,7 +96,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { tags = new { - data = new object[0] + data = Array.Empty() } } } @@ -853,7 +854,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { children = new { - data = new object[0] + data = Array.Empty() } } } @@ -902,7 +903,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { relatedFrom = new { - data = new object[0] + data = Array.Empty() } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RequiredRelationships/DefaultBehaviorTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RequiredRelationships/DefaultBehaviorTests.cs index e6d070f258..d83bdb4f1c 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RequiredRelationships/DefaultBehaviorTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/RequiredRelationships/DefaultBehaviorTests.cs @@ -1,3 +1,4 @@ +using System; using System.Net; using System.Net.Http; using System.Threading.Tasks; @@ -277,7 +278,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { orders = new { - data = new object[0] + data = Array.Empty() } } } @@ -317,7 +318,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => var requestBody = new { - data = new object[0] + data = Array.Empty() }; string route = $"/customers/{existingOrder.Customer.Id}/relationships/orders"; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/SoftDeletionTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/SoftDeletionTests.cs index 63b1d86e35..dfe76b3050 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/SoftDeletionTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SoftDeletion/SoftDeletionTests.cs @@ -669,7 +669,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => var requestBody = new { - data = new object[0] + data = Array.Empty() }; string route = $"/companies/{existingCompany.StringId}/relationships/departments"; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/EmptyGuidAsKeyTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/EmptyGuidAsKeyTests.cs index 1d4813c120..207ce9305b 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/EmptyGuidAsKeyTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/EmptyGuidAsKeyTests.cs @@ -330,7 +330,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => var requestBody = new { - data = new object[0] + data = Array.Empty() }; string route = $"/games/{existingGame.StringId}/relationships/maps"; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/ZeroAsKeyTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/ZeroAsKeyTests.cs index 327de5d77d..c00c7c64fc 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/ZeroAsKeyTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ZeroKeys/ZeroAsKeyTests.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -331,7 +332,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => var requestBody = new { - data = new object[0] + data = Array.Empty() }; string route = $"/players/{existingPlayer.StringId}/relationships/recentlyPlayed"; diff --git a/test/TestBuildingBlocks/HttpResponseMessageExtensions.cs b/test/TestBuildingBlocks/HttpResponseMessageExtensions.cs index 681166358a..3edc655e80 100644 --- a/test/TestBuildingBlocks/HttpResponseMessageExtensions.cs +++ b/test/TestBuildingBlocks/HttpResponseMessageExtensions.cs @@ -14,7 +14,7 @@ public static class HttpResponseMessageExtensions { public static HttpResponseMessageAssertions Should(this HttpResponseMessage instance) { - return new(instance); + return new HttpResponseMessageAssertions(instance); } public sealed class HttpResponseMessageAssertions : ReferenceTypeAssertions diff --git a/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs b/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs index 22c4f92f57..faa199c468 100644 --- a/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs +++ b/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs @@ -135,9 +135,17 @@ private static Mock CreateMockOptions(string forcedNamespace) private static DefaultHttpContext CreateHttpContext(string path, bool isRelationship = false, string action = "", string id = null) { - var context = new DefaultHttpContext(); - context.Request.Path = new PathString(path); - context.Response.Body = new MemoryStream(); + var context = new DefaultHttpContext + { + Request = + { + Path = new PathString(path) + }, + Response = + { + Body = new MemoryStream() + } + }; var feature = new RouteValuesFeature { diff --git a/test/UnitTests/Serialization/DeserializerTestsSetup.cs b/test/UnitTests/Serialization/DeserializerTestsSetup.cs index bf851f0f0c..0ffb883b53 100644 --- a/test/UnitTests/Serialization/DeserializerTestsSetup.cs +++ b/test/UnitTests/Serialization/DeserializerTestsSetup.cs @@ -30,7 +30,7 @@ protected Document CreateDocumentWithRelationships(string primaryType, string re protected Document CreateDocumentWithRelationships(string primaryType) { - return new() + return new Document { Data = new ResourceObject { @@ -67,7 +67,7 @@ protected RelationshipEntry CreateRelationshipData(string relatedType = null, bo protected Document CreateTestResourceDocument() { - return new() + return new Document { Data = new ResourceObject { From 0363c0764360815deddecd4486856976c9c13517 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Fri, 6 Aug 2021 13:14:08 +0200 Subject: [PATCH 5/8] Optimization: pkg-restore only for local cleanupcode --- cleanupcode.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cleanupcode.ps1 b/cleanupcode.ps1 index 605ebff705..dccf70ebc1 100644 --- a/cleanupcode.ps1 +++ b/cleanupcode.ps1 @@ -8,10 +8,10 @@ if ($LASTEXITCODE -ne 0) { throw "Tool restore failed with exit code $LASTEXITCODE" } -dotnet build -c Release +dotnet restore if ($LASTEXITCODE -ne 0) { - throw "Build failed with exit code $LASTEXITCODE" + throw "Package restore failed with exit code $LASTEXITCODE" } dotnet regitlint -s JsonApiDotNetCore.sln --print-command --jb --profile --jb --profile='\"JADNC Full Cleanup\"' --jb --properties:Configuration=Release --jb --verbosity=WARN From 1a80bbe388e615be64d8af69e877a732a312de30 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Fri, 6 Aug 2021 18:41:51 +0200 Subject: [PATCH 6/8] Adding debug flags to diagnose cibuild hang --- Build.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Build.ps1 b/Build.ps1 index 0cca69c095..d82f932b85 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -8,7 +8,7 @@ function CheckLastExitCode { function RunInspectCode { $outputPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'jetbrains-inspectcode-results.xml') - dotnet jb inspectcode JsonApiDotNetCore.sln --no-build --output="$outputPath" --profile=WarningSeverities.DotSettings --properties:Configuration=Release --severity=WARNING --verbosity=WARN -dsl=GlobalAll -dsl=SolutionPersonal -dsl=ProjectPersonal + dotnet jb inspectcode JsonApiDotNetCore.sln --debug --no-build --output="$outputPath" --profile=WarningSeverities.DotSettings --properties:Configuration=Release --severity=WARNING --verbosity=TRACE -dsl=GlobalAll -dsl=SolutionPersonal -dsl=ProjectPersonal CheckLastExitCode [xml]$xml = Get-Content "$outputPath" @@ -47,7 +47,7 @@ function RunCleanupCode { $mergeCommitHash = git rev-parse "HEAD" $targetCommitHash = git rev-parse "$env:APPVEYOR_REPO_BRANCH" - dotnet regitlint -s JsonApiDotNetCore.sln --print-command --jb --profile --jb --profile='\"JADNC Full Cleanup\"' --jb --properties:Configuration=Release --jb --verbosity=WARN -f commits -a $mergeCommitHash -b $targetCommitHash --fail-on-diff --print-diff + dotnet regitlint -s JsonApiDotNetCore.sln --print-command --jb --profile --jb --profile='\"JADNC Full Cleanup\"' --jb --properties:Configuration=Release --jb --verbosity=TRACE --jb --debug -f commits -a $mergeCommitHash -b $targetCommitHash --fail-on-diff --print-diff CheckLastExitCode } } From 8f8f8f67b4be9c94050f731dd220d219964c6693 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Tue, 10 Aug 2021 10:19:45 +0200 Subject: [PATCH 7/8] Partial revert of update to Resharper/Rider v2021.2.0 --- .config/dotnet-tools.json | 2 +- Build.ps1 | 4 ++-- cleanupcode.ps1 | 4 ++-- inspectcode.ps1 | 8 +++++++- .../Startups/EmptyStartup.cs | 2 ++ .../ResourcesInRelationshipsNotFoundException.cs | 2 +- src/JsonApiDotNetCore/ObjectExtensions.cs | 4 ++-- .../ResourceObjectBuilderSettingsProvider.cs | 2 +- .../QueryStrings/MusicTrackReleaseDefinition.cs | 2 +- .../Microservices/Messages/OutgoingMessage.cs | 2 +- .../HttpResponseMessageExtensions.cs | 2 +- .../UnitTests/Middleware/JsonApiMiddlewareTests.cs | 14 +++----------- .../Serialization/DeserializerTestsSetup.cs | 4 ++-- 13 files changed, 26 insertions(+), 26 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 9ad5f18ca6..1c7cf727cb 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "jetbrains.resharper.globaltools": { - "version": "2021.2.0", + "version": "2021.1.4", "commands": [ "jb" ] diff --git a/Build.ps1 b/Build.ps1 index d82f932b85..ee1dd68cfb 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -8,7 +8,7 @@ function CheckLastExitCode { function RunInspectCode { $outputPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'jetbrains-inspectcode-results.xml') - dotnet jb inspectcode JsonApiDotNetCore.sln --debug --no-build --output="$outputPath" --profile=WarningSeverities.DotSettings --properties:Configuration=Release --severity=WARNING --verbosity=TRACE -dsl=GlobalAll -dsl=SolutionPersonal -dsl=ProjectPersonal + dotnet jb inspectcode JsonApiDotNetCore.sln --output="$outputPath" --profile=WarningSeverities.DotSettings --properties:Configuration=Release --severity=WARNING --verbosity=WARN -dsl=GlobalAll -dsl=SolutionPersonal -dsl=ProjectPersonal CheckLastExitCode [xml]$xml = Get-Content "$outputPath" @@ -47,7 +47,7 @@ function RunCleanupCode { $mergeCommitHash = git rev-parse "HEAD" $targetCommitHash = git rev-parse "$env:APPVEYOR_REPO_BRANCH" - dotnet regitlint -s JsonApiDotNetCore.sln --print-command --jb --profile --jb --profile='\"JADNC Full Cleanup\"' --jb --properties:Configuration=Release --jb --verbosity=TRACE --jb --debug -f commits -a $mergeCommitHash -b $targetCommitHash --fail-on-diff --print-diff + dotnet regitlint -s JsonApiDotNetCore.sln --print-command --jb --profile --jb --profile='\"JADNC Full Cleanup\"' --jb --properties:Configuration=Release --jb --verbosity=WARN -f commits -a $mergeCommitHash -b $targetCommitHash --fail-on-diff --print-diff CheckLastExitCode } } diff --git a/cleanupcode.ps1 b/cleanupcode.ps1 index dccf70ebc1..605ebff705 100644 --- a/cleanupcode.ps1 +++ b/cleanupcode.ps1 @@ -8,10 +8,10 @@ if ($LASTEXITCODE -ne 0) { throw "Tool restore failed with exit code $LASTEXITCODE" } -dotnet restore +dotnet build -c Release if ($LASTEXITCODE -ne 0) { - throw "Package restore failed with exit code $LASTEXITCODE" + throw "Build failed with exit code $LASTEXITCODE" } dotnet regitlint -s JsonApiDotNetCore.sln --print-command --jb --profile --jb --profile='\"JADNC Full Cleanup\"' --jb --properties:Configuration=Release --jb --verbosity=WARN diff --git a/inspectcode.ps1 b/inspectcode.ps1 index 6c9d90768e..ab4b9c95dd 100644 --- a/inspectcode.ps1 +++ b/inspectcode.ps1 @@ -8,9 +8,15 @@ if ($LASTEXITCODE -ne 0) { throw "Tool restore failed with exit code $LASTEXITCODE" } +dotnet build -c Release + +if ($LASTEXITCODE -ne 0) { + throw "Build failed with exit code $LASTEXITCODE" +} + $outputPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'jetbrains-inspectcode-results.xml') $resultPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'jetbrains-inspectcode-results.html') -dotnet jb inspectcode JsonApiDotNetCore.sln --build --output="$outputPath" --profile=WarningSeverities.DotSettings --properties:Configuration=Release --severity=WARNING --verbosity=WARN -dsl=GlobalAll -dsl=SolutionPersonal -dsl=ProjectPersonal +dotnet jb inspectcode JsonApiDotNetCore.sln --output="$outputPath" --profile=WarningSeverities.DotSettings --properties:Configuration=Release --severity=WARNING --verbosity=WARN -dsl=GlobalAll -dsl=SolutionPersonal -dsl=ProjectPersonal if ($LASTEXITCODE -ne 0) { throw "Code inspection failed with exit code $LASTEXITCODE" diff --git a/src/Examples/JsonApiDotNetCoreExample/Startups/EmptyStartup.cs b/src/Examples/JsonApiDotNetCoreExample/Startups/EmptyStartup.cs index 065daef7a5..04c3a3d551 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Startups/EmptyStartup.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Startups/EmptyStartup.cs @@ -15,6 +15,8 @@ public virtual void ConfigureServices(IServiceCollection services) { } + // ReSharper disable once UnusedMemberInSuper.Global + // ReSharper disable once UnusedParameter.Global public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment environment, ILoggerFactory loggerFactory) { } diff --git a/src/JsonApiDotNetCore/Errors/ResourcesInRelationshipsNotFoundException.cs b/src/JsonApiDotNetCore/Errors/ResourcesInRelationshipsNotFoundException.cs index 94503ab343..d749bca5fa 100644 --- a/src/JsonApiDotNetCore/Errors/ResourcesInRelationshipsNotFoundException.cs +++ b/src/JsonApiDotNetCore/Errors/ResourcesInRelationshipsNotFoundException.cs @@ -19,7 +19,7 @@ public ResourcesInRelationshipsNotFoundException(IEnumerable(this T element) public static List AsList(this T element) { - return new List + return new() { element }; @@ -29,7 +29,7 @@ public static List AsList(this T element) public static HashSet AsHashSet(this T element) { - return new HashSet + return new() { element }; diff --git a/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilderSettingsProvider.cs b/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilderSettingsProvider.cs index d28a7591eb..04a5128aed 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilderSettingsProvider.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilderSettingsProvider.cs @@ -24,7 +24,7 @@ public ResourceObjectBuilderSettingsProvider(IDefaultsQueryStringParameterReader /// public ResourceObjectBuilderSettings Get() { - return new ResourceObjectBuilderSettings(_nullsReader.SerializerNullValueHandling, _defaultsReader.SerializerDefaultValueHandling); + return new(_nullsReader.SerializerNullValueHandling, _defaultsReader.SerializerDefaultValueHandling); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/QueryStrings/MusicTrackReleaseDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/QueryStrings/MusicTrackReleaseDefinition.cs index 857f68ef25..ac3c4b12a3 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/QueryStrings/MusicTrackReleaseDefinition.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/QueryStrings/MusicTrackReleaseDefinition.cs @@ -24,7 +24,7 @@ public MusicTrackReleaseDefinition(IResourceGraph resourceGraph, ISystemClock sy public override QueryStringParameterHandlers OnRegisterQueryableHandlersForQueryStringParameters() { - return new QueryStringParameterHandlers + return new() { ["isRecentlyReleased"] = FilterOnRecentlyReleased }; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/Messages/OutgoingMessage.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/Messages/OutgoingMessage.cs index 68837f0d12..f001514efc 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/Messages/OutgoingMessage.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Microservices/Messages/OutgoingMessage.cs @@ -22,7 +22,7 @@ public T GetContentAs() public static OutgoingMessage CreateFromContent(IMessageContent content) { - return new OutgoingMessage + return new() { Type = content.GetType().Name, FormatVersion = content.FormatVersion, diff --git a/test/TestBuildingBlocks/HttpResponseMessageExtensions.cs b/test/TestBuildingBlocks/HttpResponseMessageExtensions.cs index 3edc655e80..681166358a 100644 --- a/test/TestBuildingBlocks/HttpResponseMessageExtensions.cs +++ b/test/TestBuildingBlocks/HttpResponseMessageExtensions.cs @@ -14,7 +14,7 @@ public static class HttpResponseMessageExtensions { public static HttpResponseMessageAssertions Should(this HttpResponseMessage instance) { - return new HttpResponseMessageAssertions(instance); + return new(instance); } public sealed class HttpResponseMessageAssertions : ReferenceTypeAssertions diff --git a/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs b/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs index faa199c468..22c4f92f57 100644 --- a/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs +++ b/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs @@ -135,17 +135,9 @@ private static Mock CreateMockOptions(string forcedNamespace) private static DefaultHttpContext CreateHttpContext(string path, bool isRelationship = false, string action = "", string id = null) { - var context = new DefaultHttpContext - { - Request = - { - Path = new PathString(path) - }, - Response = - { - Body = new MemoryStream() - } - }; + var context = new DefaultHttpContext(); + context.Request.Path = new PathString(path); + context.Response.Body = new MemoryStream(); var feature = new RouteValuesFeature { diff --git a/test/UnitTests/Serialization/DeserializerTestsSetup.cs b/test/UnitTests/Serialization/DeserializerTestsSetup.cs index 0ffb883b53..bf851f0f0c 100644 --- a/test/UnitTests/Serialization/DeserializerTestsSetup.cs +++ b/test/UnitTests/Serialization/DeserializerTestsSetup.cs @@ -30,7 +30,7 @@ protected Document CreateDocumentWithRelationships(string primaryType, string re protected Document CreateDocumentWithRelationships(string primaryType) { - return new Document + return new() { Data = new ResourceObject { @@ -67,7 +67,7 @@ protected RelationshipEntry CreateRelationshipData(string relatedType = null, bo protected Document CreateTestResourceDocument() { - return new Document + return new() { Data = new ResourceObject { From 390422345ab11e6608eba5493f474d21f4e5a654 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Fri, 13 Aug 2021 13:46:33 +0200 Subject: [PATCH 8/8] Review feedback --- src/JsonApiDotNetCore/Diagnostics/CascadingCodeTimer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/JsonApiDotNetCore/Diagnostics/CascadingCodeTimer.cs b/src/JsonApiDotNetCore/Diagnostics/CascadingCodeTimer.cs index 27bc5db739..5da5a33b01 100644 --- a/src/JsonApiDotNetCore/Diagnostics/CascadingCodeTimer.cs +++ b/src/JsonApiDotNetCore/Diagnostics/CascadingCodeTimer.cs @@ -28,7 +28,7 @@ static CascadingCodeTimer() // The most important thing is to prevent switching between CPU cores or processors. Switching dismisses the cache, etc. and has a huge performance impact on the test. Process.GetCurrentProcess().ProcessorAffinity = new IntPtr(2); - // To get the CPU core more exclusively, we must prevent that other threads can use this CPU core. We set our process and thread priority to achieve this. + // To get the CPU core more exclusively, we must prevent that other processes can use this CPU core. We set our process priority to achieve this. // Note we should NOT set the thread priority, because async/await usage makes the code jump between pooled threads (depending on Synchronization Context). Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High; }