Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 99c9a95

Browse files
Log QueryLayer and LINQ expression at Debug level (#1732)
* Log QueryLayer and Expression at Debug level * Replace debugger visualizer link in docs
1 parent b6b56b0 commit 99c9a95

File tree

12 files changed

+257
-7
lines changed

12 files changed

+257
-7
lines changed

‎docs/usage/faq.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ If there's no LINQ provider available, the example [here](https://github.com/jso
160160
which produces SQL and uses [Dapper](https://github.com/DapperLib/Dapper) for data access.
161161

162162
> [!TIP]
163-
> [ExpressionTreeVisualizer](https://github.com/zspitz/ExpressionTreeVisualizer) is very helpful in trying to debug LINQ expression trees!
163+
> [ReadableExpressions](https://github.com/agileobjects/ReadableExpressions) is very helpful in trying to debug LINQ expression trees!
164164
165165
#### I love JsonApiDotNetCore! How can I support the team?
166166
The best way to express your gratitude is by starring our repository.

‎package-versions.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
<MicrosoftApiServerVersion>9.0.*</MicrosoftApiServerVersion>
2323
<NSwagApiClientVersion>14.4.*</NSwagApiClientVersion>
2424
<NewtonsoftJsonVersion>13.0.*</NewtonsoftJsonVersion>
25+
<ReadableExpressionsVersion>4.1.*</ReadableExpressionsVersion>
2526
<ScalarAspNetCoreVersion>2.3.*</ScalarAspNetCoreVersion>
2627
<SwashbuckleVersion>8.*-*</SwashbuckleVersion>
2728
<SystemTextJsonVersion>9.0.*</SystemTextJsonVersion>

‎src/JsonApiDotNetCore/Middleware/TraceLogWriter.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
using System.Linq.Expressions;
12
using System.Reflection;
23
using System.Runtime.CompilerServices;
34
using System.Text;
45
using System.Text.Encodings.Web;
56
using System.Text.Json;
67
using System.Text.Json.Serialization;
78
using JsonApiDotNetCore.Configuration;
9+
using JsonApiDotNetCore.Queries;
10+
using JsonApiDotNetCore.Queries.QueryableBuilding;
811
using JsonApiDotNetCore.Resources;
912
using JsonApiDotNetCore.Resources.Annotations;
1013
using Microsoft.Extensions.Logging;
@@ -204,9 +207,45 @@ private static string SerializeObject(object? value)
204207
}
205208
}
206209

210+
public void LogDebug(QueryLayer queryLayer)
211+
{
212+
ArgumentNullException.ThrowIfNull(queryLayer);
213+
214+
LogQueryLayer(queryLayer);
215+
}
216+
217+
public void LogDebug(Expression expression)
218+
{
219+
ArgumentNullException.ThrowIfNull(expression);
220+
221+
if (_logger.IsEnabled(LogLevel.Debug))
222+
{
223+
string? text = ExpressionTreeFormatter.Instance.GetText(expression);
224+
225+
if (text != null)
226+
{
227+
LogExpression(text);
228+
}
229+
else
230+
{
231+
LogReadableExpressionsAssemblyUnavailable();
232+
}
233+
}
234+
}
235+
207236
[LoggerMessage(Level = LogLevel.Trace, SkipEnabledCheck = true, Message = "Entering {MemberName}({ParameterValues})")]
208237
private partial void LogEnteringMemberWithParameters(string memberName, string parameterValues);
209238

210239
[LoggerMessage(Level = LogLevel.Trace, SkipEnabledCheck = true, Message = "Entering {MemberName}()")]
211240
private partial void LogEnteringMember(string memberName);
241+
242+
[LoggerMessage(Level = LogLevel.Debug, Message = "QueryLayer: {queryLayer}")]
243+
private partial void LogQueryLayer(QueryLayer queryLayer);
244+
245+
[LoggerMessage(Level = LogLevel.Debug, SkipEnabledCheck = true, Message = "Expression tree: {expression}")]
246+
private partial void LogExpression(string expression);
247+
248+
[LoggerMessage(Level = LogLevel.Debug, SkipEnabledCheck = true,
249+
Message = "Failed to load assembly. To log expression trees, add a NuGet reference to 'AgileObjects.ReadableExpressions' in your project.")]
250+
private partial void LogReadableExpressionsAssemblyUnavailable();
212251
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
using System.Linq.Expressions;
2+
using System.Reflection;
3+
4+
namespace JsonApiDotNetCore.Queries.QueryableBuilding;
5+
6+
/// <summary>
7+
/// Converts a <see cref="Expression" /> to readable text, if the AgileObjects.ReadableExpressions NuGet package is referenced.
8+
/// </summary>
9+
internal sealed class ExpressionTreeFormatter
10+
{
11+
private static readonly Lazy<MethodInvoker?> LazyToReadableStringMethod = new(GetToReadableStringMethod, LazyThreadSafetyMode.ExecutionAndPublication);
12+
13+
public static ExpressionTreeFormatter Instance { get; } = new();
14+
15+
private ExpressionTreeFormatter()
16+
{
17+
}
18+
19+
private static MethodInvoker? GetToReadableStringMethod()
20+
{
21+
Assembly? assembly = TryLoadAssembly();
22+
Type? type = assembly?.GetType("AgileObjects.ReadableExpressions.ExpressionExtensions", false);
23+
MethodInfo? method = type?.GetMethods(BindingFlags.Public | BindingFlags.Static).FirstOrDefault(method => method.Name == "ToReadableString");
24+
return method != null ? MethodInvoker.Create(method) : null;
25+
}
26+
27+
private static Assembly? TryLoadAssembly()
28+
{
29+
try
30+
{
31+
return Assembly.Load("AgileObjects.ReadableExpressions");
32+
}
33+
catch (Exception exception) when (exception is ArgumentException or IOException or BadImageFormatException)
34+
{
35+
}
36+
37+
return null;
38+
}
39+
40+
public string? GetText(Expression expression)
41+
{
42+
ArgumentNullException.ThrowIfNull(expression);
43+
44+
try
45+
{
46+
return LazyToReadableStringMethod.Value?.Invoke(null, expression, null) as string;
47+
}
48+
catch (Exception exception) when (exception is TargetException or InvalidOperationException or TargetParameterCountException or NotSupportedException)
49+
{
50+
return null;
51+
}
52+
}
53+
}

‎src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ protected virtual IQueryable<TResource> ApplyQueryLayer(QueryLayer queryLayer)
122122

123123
ArgumentNullException.ThrowIfNull(queryLayer);
124124

125+
_traceWriter.LogDebug(queryLayer);
126+
125127
using (CodeTimingSessionManager.Current.Measure("Convert QueryLayer to System.Expression"))
126128
{
127129
IQueryable<TResource> source = GetAll();
@@ -151,6 +153,8 @@ protected virtual IQueryable<TResource> ApplyQueryLayer(QueryLayer queryLayer)
151153
var context = QueryableBuilderContext.CreateRoot(source, typeof(Queryable), _dbContext.Model, null);
152154
Expression expression = builder.ApplyQuery(queryLayer, context);
153155

156+
_traceWriter.LogDebug(expression);
157+
154158
using (CodeTimingSessionManager.Current.Measure("Convert System.Expression to IQueryable"))
155159
{
156160
return source.Provider.CreateQuery<TResource>(expression);

‎test/DiscoveryTests/DiscoveryTests.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<TargetFrameworks>net9.0;net8.0</TargetFrameworks>
44
</PropertyGroup>
@@ -15,5 +15,6 @@
1515
<PackageReference Include="GitHubActionsTestLogger" Version="$(GitHubActionsTestLoggerVersion)" PrivateAssets="All" />
1616
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(TestSdkVersion)" />
1717
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Condition="'$(TargetFramework)' != 'net8.0'" Version="$(AspNetCoreVersion)" />
18+
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="$(EntityFrameworkCoreVersion)" />
1819
</ItemGroup>
1920
</Project>

‎test/DiscoveryTests/LoggingTests.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using FluentAssertions;
2+
using JsonApiDotNetCore.Configuration;
3+
using JsonApiDotNetCore.Queries;
4+
using JsonApiDotNetCore.Repositories;
5+
using Microsoft.AspNetCore.Builder;
6+
using Microsoft.AspNetCore.TestHost;
7+
using Microsoft.EntityFrameworkCore;
8+
using Microsoft.Extensions.DependencyInjection;
9+
using Microsoft.Extensions.Logging;
10+
using TestBuildingBlocks;
11+
using Xunit;
12+
13+
namespace DiscoveryTests;
14+
15+
public sealed class LoggingTests
16+
{
17+
[Fact]
18+
public async Task Logs_message_to_add_NuGet_reference()
19+
{
20+
// Arrange
21+
using var loggerProvider =
22+
new CapturingLoggerProvider((category, _) => category.StartsWith("JsonApiDotNetCore.Repositories", StringComparison.Ordinal));
23+
24+
WebApplicationBuilder builder = WebApplication.CreateEmptyBuilder(new WebApplicationOptions());
25+
builder.Logging.AddProvider(loggerProvider);
26+
builder.Logging.SetMinimumLevel(LogLevel.Debug);
27+
builder.Services.AddDbContext<TestDbContext>(options => options.UseInMemoryDatabase(Guid.NewGuid().ToString()));
28+
builder.Services.AddJsonApi<TestDbContext>();
29+
builder.WebHost.UseTestServer();
30+
await using WebApplication app = builder.Build();
31+
32+
var resourceGraph = app.Services.GetRequiredService<IResourceGraph>();
33+
ResourceType resourceType = resourceGraph.GetResourceType<PrivateResource>();
34+
35+
var repository = app.Services.GetRequiredService<IResourceRepository<PrivateResource, int>>();
36+
37+
// Act
38+
_ = await repository.GetAsync(new QueryLayer(resourceType), CancellationToken.None);
39+
40+
// Assert
41+
IReadOnlyList<string> logLines = loggerProvider.GetLines();
42+
43+
logLines.Should().Contain(
44+
"[DEBUG] Failed to load assembly. To log expression trees, add a NuGet reference to 'AgileObjects.ReadableExpressions' in your project.");
45+
}
46+
47+
private sealed class TestDbContext(DbContextOptions options)
48+
: DbContext(options)
49+
{
50+
public DbSet<PrivateResource> PrivateResources => Set<PrivateResource>();
51+
}
52+
}

‎test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicTraceLoggingTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public AtomicTraceLoggingTests(IntegrationTestContext<TestableStartup<Operations
2222
testContext.ConfigureLogging(options =>
2323
{
2424
var loggerProvider = new CapturingLoggerProvider((category, level) =>
25-
level >= LogLevel.Trace && category.StartsWith("JsonApiDotNetCore.", StringComparison.Ordinal));
25+
level == LogLevel.Trace && category.StartsWith("JsonApiDotNetCore.", StringComparison.Ordinal));
2626

2727
options.AddProvider(loggerProvider);
2828
options.SetMinimumLevel(LogLevel.Trace);

‎test/JsonApiDotNetCoreTests/IntegrationTests/Logging/Fruit.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,7 @@ public abstract class Fruit : Identifiable<long>
1010
{
1111
[Attr]
1212
public abstract string Color { get; }
13+
14+
[Attr]
15+
public double WeightInKilograms { get; set; }
1316
}

‎test/JsonApiDotNetCoreTests/IntegrationTests/Logging/LoggingFakers.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@ internal sealed class LoggingFakers
1515

1616
private readonly Lazy<Faker<Banana>> _lazyBananaFaker = new(() => new Faker<Banana>()
1717
.MakeDeterministic()
18+
.RuleFor(banana => banana.WeightInKilograms, faker => faker.Random.Double(.2, .3))
1819
.RuleFor(banana => banana.LengthInCentimeters, faker => faker.Random.Double(10, 25)));
1920

2021
private readonly Lazy<Faker<Peach>> _lazyPeachFaker = new(() => new Faker<Peach>()
2122
.MakeDeterministic()
23+
.RuleFor(peach => peach.WeightInKilograms, faker => faker.Random.Double(.2, .3))
2224
.RuleFor(peach => peach.DiameterInCentimeters, faker => faker.Random.Double(6, 7.5)));
2325

2426
public Faker<AuditEntry> AuditEntry => _lazyAuditEntryFaker.Value;

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /