Skip to content

Commit 6647fe1

Browse files
author
Scott Bommarito
authored
Restructure PackageService to better support endpoints requiring all versions of an ID (#6905)
1 parent 6b081e4 commit 6647fe1

8 files changed

Lines changed: 508 additions & 331 deletions

File tree

src/NuGetGallery.Core/Services/CorePackageService.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -285,15 +285,26 @@ public virtual async Task UpdatePackageSigningCertificateAsync(string packageId,
285285
}
286286
}
287287

288-
protected IQueryable<Package> GetPackagesByIdQueryable(string id)
288+
protected IQueryable<Package> GetPackagesByIdQueryable(string id, bool withDeprecations = false)
289289
{
290-
return _packageRepository
290+
var packages = _packageRepository
291291
.GetAll()
292292
.Include(p => p.LicenseReports)
293293
.Include(p => p.PackageRegistration)
294294
.Include(p => p.User)
295295
.Include(p => p.SymbolPackages)
296296
.Where(p => p.PackageRegistration.Id == id);
297+
298+
if (withDeprecations)
299+
{
300+
packages = packages
301+
.Include(p => p.Deprecations.Select(d => d.AlternatePackage.PackageRegistration))
302+
.Include(p => p.Deprecations.Select(d => d.AlternatePackageRegistration))
303+
.Include(p => p.Deprecations.Select(d => d.Cves))
304+
.Include(p => p.Deprecations.Select(d => d.Cwes));
305+
}
306+
307+
return packages;
297308
}
298309

299310
private static Package FindPackage(IQueryable<Package> packages, Func<IQueryable<Package>, IQueryable<Package>> predicate = null)

src/NuGetGallery/Controllers/JsonApiController.cs

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -41,24 +41,22 @@ public JsonApiController(
4141
}
4242

4343
[HttpGet]
44-
public virtual ActionResult GetPackageOwners(string id, string version)
44+
public virtual ActionResult GetPackageOwners(string id)
4545
{
46-
var package = _packageService.FindPackageByIdAndVersion(id, version);
47-
if (package == null)
46+
var registration = _packageService.FindPackageRegistrationById(id);
47+
if (registration == null)
4848
{
4949
return Json(new { message = Strings.AddOwner_PackageNotFound });
5050
}
5151

5252
var currentUser = GetCurrentUser();
53-
if (ActionsRequiringPermissions.ManagePackageOwnership.CheckPermissionsOnBehalfOfAnyAccount(currentUser, package) != PermissionsCheckResult.Allowed)
53+
if (ActionsRequiringPermissions.ManagePackageOwnership.CheckPermissionsOnBehalfOfAnyAccount(currentUser, registration) != PermissionsCheckResult.Allowed)
5454
{
5555
return new HttpUnauthorizedResult();
5656
}
5757

58-
var packageRegistration = package.PackageRegistration;
59-
var packageRegistrationOwners = package.PackageRegistration.Owners;
60-
var allMatchingNamespaceOwners = package
61-
.PackageRegistration
58+
var packageRegistrationOwners = registration.Owners;
59+
var allMatchingNamespaceOwners = registration
6260
.ReservedNamespaces
6361
.SelectMany(rn => rn.Owners)
6462
.Distinct();
@@ -71,7 +69,7 @@ public virtual ActionResult GetPackageOwners(string id, string version)
7169
.Select(u => new PackageOwnersResultViewModel(
7270
u,
7371
currentUser,
74-
packageRegistration,
72+
registration,
7573
Url,
7674
isPending: false,
7775
isNamespaceOwner: true));
@@ -81,19 +79,19 @@ public virtual ActionResult GetPackageOwners(string id, string version)
8179
.Select(u => new PackageOwnersResultViewModel(
8280
u,
8381
currentUser,
84-
packageRegistration,
82+
registration,
8583
Url,
8684
isPending: false,
8785
isNamespaceOwner: false));
8886

8987
owners = owners.Union(packageOwnersOnlyResultViewModel);
9088

9189
var pending =
92-
_packageOwnershipManagementService.GetPackageOwnershipRequests(package: package.PackageRegistration)
90+
_packageOwnershipManagementService.GetPackageOwnershipRequests(package: registration)
9391
.Select(r => new PackageOwnersResultViewModel(
9492
r.NewOwner,
9593
currentUser,
96-
packageRegistration,
94+
registration,
9795
Url,
9896
isPending: true,
9997
isNamespaceOwner: false));

src/NuGetGallery/Controllers/PackagesController.cs

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -650,14 +650,26 @@ public virtual async Task<ActionResult> DisplayPackage(string id, string version
650650
return RedirectToActionPermanent("DisplayPackage", new { id = id, version = normalized });
651651
}
652652

653-
Package package;
654-
if (version != null && version.Equals(GalleryConstants.AbsoluteLatestUrlString, StringComparison.InvariantCultureIgnoreCase))
653+
Package package = null;
654+
// Load all packages with the ID.
655+
var packages = _packageService.FindPackagesById(id);
656+
if (version != null)
655657
{
656-
package = _packageService.FindAbsoluteLatestPackageById(id, SemVerLevelKey.SemVer2);
658+
if (version.Equals(GalleryConstants.AbsoluteLatestUrlString, StringComparison.InvariantCultureIgnoreCase))
659+
{
660+
// The user is looking for the absolute latest version and not an exact version.
661+
package = packages.FirstOrDefault(p => p.IsLatestSemVer2);
662+
}
663+
else
664+
{
665+
package = packages.SingleOrDefault(p => p.NormalizedVersion == NuGetVersionFormatter.Normalize(version));
666+
}
657667
}
658-
else
668+
669+
if (package == null)
659670
{
660-
package = _packageService.FindPackageByIdAndVersion(id, version, SemVerLevelKey.SemVer2);
671+
// If we cannot find the exact version or no version was provided, fall back to the latest version.
672+
package = _packageService.FilterLatestPackage(packages, SemVerLevelKey.SemVer2, allowPrerelease: true);
661673
}
662674

663675
// Validating packages should be hidden to everyone but the owners and admins.
@@ -669,7 +681,7 @@ public virtual async Task<ActionResult> DisplayPackage(string id, string version
669681
{
670682
return HttpNotFound();
671683
}
672-
684+
673685
var model = new DisplayPackageViewModel(package, currentUser);
674686

675687
model.ValidatingTooLong = _validationService.IsValidatingTooLong(package);
@@ -1382,17 +1394,32 @@ public virtual ActionResult ManagePackageOwners(string id)
13821394
[RequiresAccountConfirmation("manage a package")]
13831395
public virtual async Task<ActionResult> Manage(string id, string version = null)
13841396
{
1385-
var package = _packageService.FindPackageByIdAndVersion(id, version, SemVerLevelKey.SemVer2);
1397+
Package package = null;
1398+
// Load all versions of the package.
1399+
var packages = _packageService.FindPackagesById(id, withDeprecations: true);
1400+
if (version != null)
1401+
{
1402+
// Try to find the exact version if it was specified.
1403+
package = packages.SingleOrDefault(p => p.NormalizedVersion == NuGetVersionFormatter.Normalize(version));
1404+
}
1405+
1406+
if (package == null)
1407+
{
1408+
// If the exact version was not found, fall back to the latest version.
1409+
package = _packageService.FilterLatestPackage(packages, SemVerLevelKey.SemVer2, allowPrerelease: true);
1410+
}
1411+
13861412
if (package == null)
13871413
{
1414+
// If the package has no versions, return not found.
13881415
return HttpNotFound();
13891416
}
13901417

13911418
var model = new ManagePackageViewModel(
1392-
package,
1393-
GetCurrentUser(),
1394-
ReportMyPackageReasons,
1395-
Url,
1419+
package,
1420+
GetCurrentUser(),
1421+
ReportMyPackageReasons,
1422+
Url,
13961423
await _readMeService.GetReadMeMdAsync(package));
13971424

13981425
if (!model.CanEdit && !model.CanManageOwners && !model.CanUnlistOrRelist)

src/NuGetGallery/Services/IPackageService.cs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ namespace NuGetGallery
1717
/// </summary>
1818
public interface IPackageService : ICorePackageService
1919
{
20+
/// <summary>
21+
/// Returns all packages with an <see cref="Package.Id"/> of <paramref name="id"/>.
22+
/// Includes deprecation entities if <paramref name="withDeprecations"/> is true.
23+
/// </summary>
24+
IReadOnlyCollection<Package> FindPackagesById(string id, bool withDeprecations = false);
25+
2026
/// <summary>
2127
/// Gets the package with the given ID and version when exists;
2228
/// otherwise gets the latest package version for the given package ID matching the provided constraints.
@@ -26,9 +32,16 @@ public interface IPackageService : ICorePackageService
2632
/// <param name="semVerLevelKey">The SemVer-level key that determines the SemVer filter to be applied.</param>
2733
/// <param name="allowPrerelease"><c>True</c> indicating pre-release packages are allowed, otherwise <c>false</c>.</param>
2834
/// <returns></returns>
29-
Package FindPackageByIdAndVersion(string id, string version, int? semVerLevelKey = null, bool allowPrerelease = true);
30-
31-
Package FindAbsoluteLatestPackageById(string id, int? semVerLevelKey);
35+
Package FindPackageByIdAndVersion(
36+
string id,
37+
string version,
38+
int? semVerLevelKey = null,
39+
bool allowPrerelease = true);
40+
41+
Package FilterLatestPackage(
42+
IReadOnlyCollection<Package> packages,
43+
int? semVerLevelKey = SemVerLevelKey.SemVer2,
44+
bool allowPrerelease = true);
3245

3346
IEnumerable<Package> FindPackagesByOwner(User user, bool includeUnlisted, bool includeVersions = false);
3447

src/NuGetGallery/Services/PackageService.cs

Lines changed: 54 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,11 @@ public override PackageRegistration FindPackageRegistrationById(string packageId
135135
.SingleOrDefault(pr => pr.Id == packageId);
136136
}
137137

138+
public virtual IReadOnlyCollection<Package> FindPackagesById(string id, bool withDeprecations = false)
139+
{
140+
return GetPackagesByIdQueryable(id, withDeprecations).ToList();
141+
}
142+
138143
public virtual Package FindPackageByIdAndVersion(
139144
string id,
140145
string version,
@@ -170,62 +175,68 @@ public virtual Package FindPackageByIdAndVersion(
170175

171176
var packageVersions = packagesQuery.ToList();
172177

173-
// Fallback behavior: collect the latest version.
174-
// Check SemVer-level and allow-prerelease constraints.
175-
if (semVerLevelKey == SemVerLevelKey.SemVer2)
176-
{
177-
package = packageVersions.FirstOrDefault(p => p.IsLatestStableSemVer2);
178-
179-
if (package == null && allowPrerelease)
180-
{
181-
package = packageVersions.FirstOrDefault(p => p.IsLatestSemVer2);
182-
}
183-
}
184-
185-
// Fallback behavior: collect the latest version.
186-
// If SemVer-level is not defined,
187-
// or SemVer-level = 2.0.0 and no package was marked as SemVer2-latest,
188-
// then check for packages marked as non-SemVer2 latest.
189-
if (semVerLevelKey == SemVerLevelKey.Unknown
190-
|| (semVerLevelKey == SemVerLevelKey.SemVer2 && package == null))
191-
{
192-
package = packageVersions.FirstOrDefault(p => p.IsLatestStable);
193-
194-
if (package == null && allowPrerelease)
195-
{
196-
package = packageVersions.FirstOrDefault(p => p.IsLatest);
197-
}
198-
}
199-
200-
// If we couldn't find a package marked as latest, then
201-
// return the most recent one (prerelease ones were already filtered out if appropriate...)
202-
if (package == null)
203-
{
204-
package = packageVersions.OrderByDescending(p => p.Version).FirstOrDefault();
205-
}
178+
package = FilterLatestPackageHelper(packageVersions, semVerLevelKey, allowPrerelease);
206179
}
207180

208181
return package;
209182
}
210183

211-
public virtual Package FindAbsoluteLatestPackageById(string id, int? semVerLevelKey)
184+
public virtual Package FilterLatestPackage(
185+
IReadOnlyCollection<Package> packages,
186+
int? semVerLevelKey = SemVerLevelKey.SemVer2,
187+
bool allowPrerelease = true)
212188
{
213-
var packageVersions = GetPackagesByIdQueryable(id);
189+
return FilterLatestPackageHelper(
190+
// Filter out prereleases in the list if prereleases are not allowed.
191+
packages?.Where(p => allowPrerelease || !p.IsPrerelease).ToList(),
192+
semVerLevelKey,
193+
allowPrerelease);
194+
}
214195

215-
Package package;
196+
private static Package FilterLatestPackageHelper(
197+
IReadOnlyCollection<Package> packages,
198+
int? semVerLevelKey,
199+
bool allowPrerelease)
200+
{
201+
if (packages == null)
202+
{
203+
throw new ArgumentNullException(nameof(packages));
204+
}
205+
206+
Package package = null;
207+
208+
// Fallback behavior: collect the latest version.
209+
// Check SemVer-level and allow-prerelease constraints.
216210
if (semVerLevelKey == SemVerLevelKey.SemVer2)
217211
{
218-
package = packageVersions.FirstOrDefault(p => p.IsLatestSemVer2);
212+
package = packages.FirstOrDefault(p => p.IsLatestStableSemVer2);
213+
214+
if (package == null && allowPrerelease)
215+
{
216+
package = packages.FirstOrDefault(p => p.IsLatestSemVer2);
217+
}
219218
}
220-
else
219+
220+
// Fallback behavior: collect the latest version.
221+
// If SemVer-level is not defined,
222+
// or SemVer-level = 2.0.0 and no package was marked as SemVer2-latest,
223+
// then check for packages marked as non-SemVer2 latest.
224+
if (semVerLevelKey == SemVerLevelKey.Unknown
225+
|| (semVerLevelKey == SemVerLevelKey.SemVer2 && package == null))
221226
{
222-
package = packageVersions.FirstOrDefault(p => p.IsLatest);
227+
package = packages.FirstOrDefault(p => p.IsLatestStable);
228+
229+
if (package == null && allowPrerelease)
230+
{
231+
package = packages.FirstOrDefault(p => p.IsLatest);
232+
}
223233
}
224234

225-
// If we couldn't find a package marked as latest, then return the most recent one
235+
// If we couldn't find a package marked as latest, then return the most recent one.
236+
// Prereleases were already filtered out if appropriate.
226237
if (package == null)
227238
{
228-
package = packageVersions.OrderByDescending(p => p.Version).FirstOrDefault();
239+
package = packages.OrderByDescending(p => p.Version).FirstOrDefault();
229240
}
230241

231242
return package;
@@ -277,7 +288,7 @@ private IEnumerable<Package> GetPackagesForOwners(IEnumerable<int> ownerKeys, bo
277288
.ThenByDescending(p => p.Key)
278289
.FirstOrDefault());
279290
}
280-
291+
281292
return packages
282293
.Include(p => p.PackageRegistration)
283294
.Include(p => p.PackageRegistration.Owners)
@@ -360,7 +371,7 @@ public async Task RemovePackageOwnerAsync(PackageRegistration package, User user
360371
await _packageRepository.CommitChangesAsync();
361372
}
362373
}
363-
374+
364375
public bool WillPackageBeOrphanedIfOwnerRemoved(PackageRegistration package, User ownerToRemove)
365376
{
366377
return WillPackageBeOrphanedIfOwnerRemovedHelper(package.Owners, ownerToRemove);

tests/NuGetGallery.Facts/Controllers/JsonApiControllerFacts.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -757,7 +757,7 @@ public void ReturnsFailureIfPackageNotFound()
757757
var controller = GetController<JsonApiController>();
758758

759759
// Act
760-
var result = controller.GetPackageOwners("fakeId", "2.0.0");
760+
var result = controller.GetPackageOwners("fakeId");
761761
dynamic data = ((JsonResult)result).Data;
762762

763763
// Assert
@@ -775,7 +775,7 @@ public void ReturnsFailureIfUserIsNotPackageOwner(Func<Fakes, User> getCurrentUs
775775
controller.SetCurrentUser(currentUser);
776776

777777
// Act
778-
var result = controller.GetPackageOwners(fakes.Package.Id, fakes.Package.Packages.First().Version);
778+
var result = controller.GetPackageOwners(fakes.Package.Id);
779779

780780
// Assert
781781
Assert.IsType<HttpUnauthorizedResult>(result);
@@ -808,7 +808,7 @@ private IEnumerable<PackageOwnersResultViewModel> InvokeAsUser(User currentUser)
808808
var controller = GetController<JsonApiController>();
809809
controller.SetCurrentUser(currentUser);
810810

811-
var result = controller.GetPackageOwners("FakePackage", "2.0");
811+
var result = controller.GetPackageOwners("FakePackage");
812812
return ((JsonResult)result).Data as IEnumerable<PackageOwnersResultViewModel>;
813813
}
814814

0 commit comments

Comments
 (0)