From 4b89ea2ed1e7f6b4a162952b9e29ab82a1026c6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Czerwi=C5=84ski?= Date: Wed, 12 May 2021 17:43:46 +0200 Subject: [PATCH 1/5] TagWith overload for using CallerFilePath and CallerLineNumber --- .../EntityFrameworkQueryableExtensions.cs | 38 ++++++++++++++++++- ...yableMethodNormalizingExpressionVisitor.cs | 11 ++++++ src/Shared/SharedTypeExtensions.cs | 7 ++++ .../Query/FromSqlSprocQueryTestBase.cs | 17 +++++++++ 4 files changed, 72 insertions(+), 1 deletion(-) diff --git a/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs b/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs index 9719040342a..2979d030078 100644 --- a/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs +++ b/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.ChangeTracking; @@ -2562,7 +2563,11 @@ public static IQueryable AsTracking( internal static readonly MethodInfo TagWithMethodInfo = typeof(EntityFrameworkQueryableExtensions) - .GetRequiredDeclaredMethod(nameof(TagWith)); + .GetRequiredDeclaredMethod(nameof(TagWith), mi => mi.GetParameters().Length == 2); + + internal static readonly MethodInfo TagWithCallerInfoMethodInfo + = typeof(EntityFrameworkQueryableExtensions) + .GetRequiredDeclaredMethod(nameof(TagWith), mi => mi.GetParameters().Length == 3); /// /// Adds a tag to the collection of tags associated with an EF LINQ query. Tags are query annotations @@ -2594,6 +2599,37 @@ source.Provider is EntityQueryProvider : source; } + /// + /// Adds a tag to the collection of tags associated with an EF LINQ query with source file name and line + /// where method was called that can provide contextual tracing information at different points in the query pipeline. + /// + /// The type of entity being queried. + /// The source query. + /// file name where the method was called + /// file line number where the method was called + /// A new query annotated with the given tag. + /// + /// + /// + public static IQueryable TagWith( + this IQueryable source, + [NotParameterized][CallerFilePath] string? fromFile = null, + [NotParameterized][CallerLineNumber] int onLine = 0) + { + Check.NotNull(source, nameof(source)); + + return + source.Provider is EntityQueryProvider + ? source.Provider.CreateQuery( + Expression.Call( + instance: null, + method: TagWithCallerInfoMethodInfo.MakeGenericMethod(typeof(T)), + arg0: source.Expression, + arg1: Expression.Constant(fromFile), + arg2: Expression.Constant(onLine))) + : source; + } + #endregion #region Load diff --git a/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs b/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs index 2e3831d7126..eb3ed3b0950 100644 --- a/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs @@ -211,6 +211,17 @@ private void VerifyReturnType(Expression expression, ParameterExpression lambdaP return visitedExpression; } + if (genericMethodDefinition == EntityFrameworkQueryableExtensions.TagWithCallerInfoMethodInfo) + { + var visitedExpression = Visit(methodCallExpression.Arguments[0]); + + var fileName = methodCallExpression.Arguments[1].GetConstantValue(); + var lineNo = methodCallExpression.Arguments[2].GetConstantValue(); + _queryCompilationContext.AddTag($"file: {fileName}:{lineNo}"); + + return visitedExpression; + } + if (genericMethodDefinition == EntityFrameworkQueryableExtensions.IgnoreQueryFiltersMethodInfo) { var visitedExpression = Visit(methodCallExpression.Arguments[0]); diff --git a/src/Shared/SharedTypeExtensions.cs b/src/Shared/SharedTypeExtensions.cs index d3bf3df032f..eead0fcaf49 100644 --- a/src/Shared/SharedTypeExtensions.cs +++ b/src/Shared/SharedTypeExtensions.cs @@ -194,6 +194,13 @@ public static MethodInfo GetRequiredDeclaredMethod(this Type type, string name) return method; } + public static MethodInfo GetRequiredDeclaredMethod(this Type type, string name, Func methodSelector) + { + var method = type.GetTypeInfo().GetDeclaredMethods(name).Single(methodSelector); + + return method; + } + public static PropertyInfo GetRequiredDeclaredProperty(this Type type, string name) { var property = type.GetTypeInfo().GetDeclaredProperty(name); diff --git a/test/EFCore.Relational.Specification.Tests/Query/FromSqlSprocQueryTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/FromSqlSprocQueryTestBase.cs index f8f41ade983..e63927384be 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/FromSqlSprocQueryTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/FromSqlSprocQueryTestBase.cs @@ -460,5 +460,22 @@ protected NorthwindContext CreateContext() protected abstract string TenMostExpensiveProductsSproc { get; } protected abstract string CustomerOrderHistorySproc { get; } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual void From_sql_queryable_stored_procedure_with_caller_info_tag(bool async) + { + using var context = CreateContext(); + var query = context + .Set() + .FromSqlRaw(TenMostExpensiveProductsSproc, GetTenMostExpensiveProductsParameters()) + .TagWith(); + + var actual = query.ToQueryString().Split(Environment.NewLine).First(); + + Assert.StartsWith(@"-- file: ", actual); + Assert.EndsWith(@"EFCore.Relational.Specification.Tests\Query\FromSqlSprocQueryTestBase.cs:473", actual); + } } } From 9b2d6568dc620b99be2975b98512294df00ca789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Czerwi=C5=84ski?= Date: Wed, 12 May 2021 19:16:50 +0200 Subject: [PATCH 2/5] fixing failing test in windows environment --- .../Query/FromSqlSprocQueryTestBase.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/EFCore.Relational.Specification.Tests/Query/FromSqlSprocQueryTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/FromSqlSprocQueryTestBase.cs index e63927384be..ae07c2b0327 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/FromSqlSprocQueryTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/FromSqlSprocQueryTestBase.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.IO; using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.Diagnostics; @@ -475,7 +476,7 @@ public virtual void From_sql_queryable_stored_procedure_with_caller_info_tag(boo var actual = query.ToQueryString().Split(Environment.NewLine).First(); Assert.StartsWith(@"-- file: ", actual); - Assert.EndsWith(@"EFCore.Relational.Specification.Tests\Query\FromSqlSprocQueryTestBase.cs:473", actual); + Assert.EndsWith($"EFCore.Relational.Specification.Tests{Path.DirectorySeparatorChar}Query{Path.DirectorySeparatorChar}FromSqlSprocQueryTestBase.cs:474", actual); } } } From 049eca04cb5ad7be2bc5a22d52066564000e83ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Czerwi=C5=84ski?= Date: Wed, 12 May 2021 20:20:54 +0200 Subject: [PATCH 3/5] simplifying unit test a bit to avoid platform specific issues --- .../Query/FromSqlSprocQueryTestBase.cs | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/test/EFCore.Relational.Specification.Tests/Query/FromSqlSprocQueryTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/FromSqlSprocQueryTestBase.cs index ae07c2b0327..e35e378bbb4 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/FromSqlSprocQueryTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/FromSqlSprocQueryTestBase.cs @@ -66,6 +66,22 @@ public virtual async Task From_sql_queryable_stored_procedure_with_tag(bool asyn && mep.UnitPrice == 263.50m); } + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual void From_sql_queryable_stored_procedure_with_caller_info_tag(bool async) + { + using var context = CreateContext(); + var query = context + .Set() + .FromSqlRaw(TenMostExpensiveProductsSproc, GetTenMostExpensiveProductsParameters()) + .TagWith("SampleFileName", 13); + + var actual = query.ToQueryString().Split(Environment.NewLine).First(); + + Assert.Equal("-- file: SampleFileName:13", actual); + } + [ConditionalTheory] [InlineData(false)] [InlineData(true)] @@ -461,22 +477,5 @@ protected NorthwindContext CreateContext() protected abstract string TenMostExpensiveProductsSproc { get; } protected abstract string CustomerOrderHistorySproc { get; } - - [ConditionalTheory] - [InlineData(false)] - [InlineData(true)] - public virtual void From_sql_queryable_stored_procedure_with_caller_info_tag(bool async) - { - using var context = CreateContext(); - var query = context - .Set() - .FromSqlRaw(TenMostExpensiveProductsSproc, GetTenMostExpensiveProductsParameters()) - .TagWith(); - - var actual = query.ToQueryString().Split(Environment.NewLine).First(); - - Assert.StartsWith(@"-- file: ", actual); - Assert.EndsWith($"EFCore.Relational.Specification.Tests{Path.DirectorySeparatorChar}Query{Path.DirectorySeparatorChar}FromSqlSprocQueryTestBase.cs:474", actual); - } } } From c8c11c454d84e30d563b172abac617936e97139e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Czerwi=C5=84ski?= Date: Fri, 14 May 2021 12:59:38 +0200 Subject: [PATCH 4/5] code review fixes --- .../Extensions/EntityFrameworkQueryableExtensions.cs | 4 ++-- .../Query/FromSqlSprocQueryTestBase.cs | 6 +++++- .../Query/FromSqlSprocQuerySqlServerTest.cs | 10 ++++++++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs b/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs index 2979d030078..ac566b9f3a3 100644 --- a/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs +++ b/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs @@ -2605,8 +2605,8 @@ source.Provider is EntityQueryProvider /// /// The type of entity being queried. /// The source query. - /// file name where the method was called - /// file line number where the method was called + /// The file name where the method was called + /// The file line number where the method was called /// A new query annotated with the given tag. /// /// diff --git a/test/EFCore.Relational.Specification.Tests/Query/FromSqlSprocQueryTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/FromSqlSprocQueryTestBase.cs index e35e378bbb4..3472e7959e1 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/FromSqlSprocQueryTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/FromSqlSprocQueryTestBase.cs @@ -69,7 +69,7 @@ public virtual async Task From_sql_queryable_stored_procedure_with_tag(bool asyn [ConditionalTheory] [InlineData(false)] [InlineData(true)] - public virtual void From_sql_queryable_stored_procedure_with_caller_info_tag(bool async) + public virtual async Task From_sql_queryable_stored_procedure_with_caller_info_tag(bool async) { using var context = CreateContext(); var query = context @@ -77,6 +77,10 @@ public virtual void From_sql_queryable_stored_procedure_with_caller_info_tag(boo .FromSqlRaw(TenMostExpensiveProductsSproc, GetTenMostExpensiveProductsParameters()) .TagWith("SampleFileName", 13); + var queryResult = async + ? await query.ToArrayAsync() + : query.ToArray(); + var actual = query.ToQueryString().Split(Environment.NewLine).First(); Assert.Equal("-- file: SampleFileName:13", actual); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/FromSqlSprocQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/FromSqlSprocQuerySqlServerTest.cs index 86dbfe89068..8ead7873d91 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/FromSqlSprocQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/FromSqlSprocQuerySqlServerTest.cs @@ -28,6 +28,16 @@ public override async Task From_sql_queryable_stored_procedure_with_tag(bool asy AssertSql( @"-- Stored Procedure +[dbo].[Ten Most Expensive Products]"); + } + + public override async Task From_sql_queryable_stored_procedure_with_caller_info_tag(bool async) + { + await base.From_sql_queryable_stored_procedure_with_caller_info_tag(async); + + AssertSql( + @"-- file: SampleFileName:13 + [dbo].[Ten Most Expensive Products]"); } From 0e2d40ac6a7055845ce45017f356497af16f57ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Czerwi=C5=84ski?= Date: Mon, 17 May 2021 09:54:55 +0200 Subject: [PATCH 5/5] code review fixes --- .../Extensions/EntityFrameworkQueryableExtensions.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs b/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs index ac566b9f3a3..7ab64e6e692 100644 --- a/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs +++ b/src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs @@ -2563,11 +2563,15 @@ public static IQueryable AsTracking( internal static readonly MethodInfo TagWithMethodInfo = typeof(EntityFrameworkQueryableExtensions) - .GetRequiredDeclaredMethod(nameof(TagWith), mi => mi.GetParameters().Length == 2); + .GetRequiredDeclaredMethod(nameof(TagWith), mi => mi.GetParameters().Length == 2 + && mi.GetParameters().Select(p => p.ParameterType) + .SequenceEqual(new Type[] { typeof(IQueryable<>).MakeGenericType(mi.GetGenericArguments()), typeof(string) })); internal static readonly MethodInfo TagWithCallerInfoMethodInfo = typeof(EntityFrameworkQueryableExtensions) - .GetRequiredDeclaredMethod(nameof(TagWith), mi => mi.GetParameters().Length == 3); + .GetRequiredDeclaredMethod(nameof(TagWith), mi => mi.GetParameters().Length == 3 + && mi.GetParameters().Select(p => p.ParameterType) + .SequenceEqual(new Type[] { typeof(IQueryable<>).MakeGenericType(mi.GetGenericArguments()), typeof(string), typeof(int) })); /// /// Adds a tag to the collection of tags associated with an EF LINQ query. Tags are query annotations