Skip to content

Commit e58f23c

Browse files
committed
Add RECOMPILE query hint to package dependents SQL (#8096)
Progress on #8078
1 parent 63e79fa commit e58f23c

13 files changed

Lines changed: 185 additions & 7 deletions

File tree

src/NuGetGallery.Core/Entities/EntitiesContext.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,18 @@ public EntitiesContext(DbConnection connection, bool readOnly)
5656
ReadOnly = readOnly;
5757
}
5858

59+
public IDisposable WithQueryHint(string queryHint)
60+
{
61+
if (QueryHint != null)
62+
{
63+
throw new InvalidOperationException("A query hint is already applied.");
64+
}
65+
66+
return new QueryHintScope(this, queryHint);
67+
}
68+
5969
public bool ReadOnly { get; private set; }
70+
public string QueryHint { get; private set; }
6071
public DbSet<Package> Packages { get; set; }
6172
public DbSet<PackageDeprecation> Deprecations { get; set; }
6273
public DbSet<PackageRegistration> PackageRegistrations { get; set; }
@@ -503,5 +514,21 @@ protected override void OnModelCreating(DbModelBuilder modelBuilder)
503514
.WillCascadeOnDelete(false);
504515
}
505516
#pragma warning restore 618
517+
518+
private class QueryHintScope : IDisposable
519+
{
520+
private readonly EntitiesContext _entitiesContext;
521+
522+
public QueryHintScope(EntitiesContext entitiesContext, string queryHint)
523+
{
524+
_entitiesContext = entitiesContext ?? throw new ArgumentNullException(nameof(entitiesContext));
525+
_entitiesContext.QueryHint = queryHint;
526+
}
527+
528+
public void Dispose()
529+
{
530+
_entitiesContext.QueryHint = null;
531+
}
532+
}
506533
}
507534
}

src/NuGetGallery.Core/Entities/IReadOnlyEntitiesContext.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,19 @@ public interface IReadOnlyEntitiesContext : IDisposable
1414
DbSet<T> Set<T>() where T : class;
1515

1616
void SetCommandTimeout(int? seconds);
17+
18+
/// <summary>
19+
/// Sets the <see cref="QueryHint"/> for queries to the provided value until the returned disposable is disposed.
20+
/// Note that this method MUST NOT be called with user input.
21+
/// </summary>
22+
/// <param name="queryHint">The query hint.</param>
23+
/// <example>Provide "RECOMPILE" to disable query plan caching.</example>
24+
/// <returns>A disposable that determines the lifetime of the provided query hint.</returns>
25+
IDisposable WithQueryHint(string queryHint);
26+
27+
/// <summary>
28+
/// The current query hint to use for queries. Can be set using <see cref="WithQueryHint(string)"/>.
29+
/// </summary>
30+
string QueryHint { get; }
1731
}
1832
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Data.Common;
5+
using System.Data.Entity.Infrastructure.Interception;
6+
using System.Diagnostics.CodeAnalysis;
7+
8+
namespace NuGetGallery
9+
{
10+
/// <summary>
11+
/// A global (static) interceptor for Entity Framework queries. This is used for
12+
/// <see cref="IReadOnlyEntitiesContext.WithQueryHint(string)"/> to set a query hint for a short window of time on
13+
/// a specific entity context. Inspired by: https://stackoverflow.com/a/45170243
14+
/// </summary>
15+
public class QueryHintInterceptor : DbCommandInterceptor
16+
{
17+
[SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities", Justification = "The query hint is not customer input.")]
18+
public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
19+
{
20+
foreach (var dbContext in interceptionContext.DbContexts)
21+
{
22+
if (dbContext is IReadOnlyEntitiesContext entitiesContext)
23+
{
24+
var queryHint = entitiesContext.QueryHint;
25+
if (queryHint != null)
26+
{
27+
command.CommandText += $" OPTION ( {queryHint} )";
28+
}
29+
30+
break;
31+
}
32+
}
33+
34+
base.ReaderExecuting(command, interceptionContext);
35+
}
36+
}
37+
}

src/NuGetGallery.Core/Entities/ReadOnlyEntitiesContext.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ public DbSet<Package> Packages
3030
}
3131
}
3232

33+
public string QueryHint => _entitiesContext.QueryHint;
34+
3335
DbSet<T> IReadOnlyEntitiesContext.Set<T>()
3436
{
3537
return _entitiesContext.Set<T>();
@@ -44,5 +46,7 @@ public void Dispose()
4446
{
4547
_entitiesContext.Dispose();
4648
}
49+
50+
public IDisposable WithQueryHint(string queryHint) => _entitiesContext.WithQueryHint(queryHint);
4751
}
4852
}

src/NuGetGallery.Core/NuGetGallery.Core.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@
121121
<Compile Include="DisposableAction.cs" />
122122
<Compile Include="Entities\DatabaseWrapper.cs" />
123123
<Compile Include="Entities\DbContextTransactionWrapper.cs" />
124+
<Compile Include="Entities\QueryHintInterceptor.cs" />
124125
<Compile Include="Entities\ReadOnlyEntitiesContext.cs" />
125126
<Compile Include="Entities\IReadOnlyEntitiesContext.cs" />
126127
<Compile Include="Entities\ReadOnlyEntityRepository.cs" />

src/NuGetGallery.Services/PackageManagement/PackageService.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,13 @@ public PackageDependents GetPackageDependents(string id)
155155
}
156156

157157
PackageDependents result = new PackageDependents();
158-
result.TopPackages = GetListOfDependents(id);
159-
result.TotalPackageCount = GetDependentCount(id);
158+
159+
using (_entitiesContext.WithQueryHint("RECOMPILE"))
160+
{
161+
result.TopPackages = GetListOfDependents(id);
162+
result.TotalPackageCount = GetDependentCount(id);
163+
}
164+
160165
return result;
161166
}
162167

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"PreviewSearchPercentage": 0,
33
"PreviewHijackPercentage": 0,
4-
"DependentsPercentage": 0
4+
"DependentsPercentage": 100
55
}

src/NuGetGallery/App_Data/Files/Content/flags.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@
4848
"Domains": []
4949
},
5050
"NuGetGallery.PackageDependents": {
51-
"All": false,
52-
"SiteAdmins": true,
51+
"All": true,
52+
"SiteAdmins": false,
5353
"Accounts": [],
5454
"Domains": []
5555
}

src/NuGetGallery/App_Start/DefaultDependenciesModule.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using System.Data.Common;
77
using System.Data.Entity;
8+
using System.Data.Entity.Infrastructure.Interception;
89
using System.IO;
910
using System.Linq;
1011
using System.Net;
@@ -172,6 +173,8 @@ protected override void Load(ContainerBuilder builder)
172173
.As<ICacheService>()
173174
.InstancePerLifetimeScope();
174175

176+
DbInterception.Add(new QueryHintInterceptor());
177+
175178
var galleryDbConnectionFactory = CreateDbConnectionFactory(
176179
loggerFactory,
177180
nameof(EntitiesContext),

src/VerifyMicrosoftPackage/Fakes/FakeEntitiesContext.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ class FakeEntitiesContext : IEntitiesContext
2727
public DbSet<Package> Packages { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
2828
public DbSet<PackageRename> PackageRenames { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
2929

30+
public string QueryHint => throw new NotImplementedException();
31+
3032
public void DeleteOnCommit<T>(T entity) where T : class
3133
{
3234
throw new NotImplementedException();
@@ -56,5 +58,10 @@ public void SetCommandTimeout(int? seconds)
5658
{
5759
throw new NotImplementedException();
5860
}
61+
62+
public IDisposable WithQueryHint(string queryHint)
63+
{
64+
throw new NotImplementedException();
65+
}
5966
}
6067
}

0 commit comments

Comments
 (0)