From 7e017b1f0317eb7020f85662405eabf21182f23f Mon Sep 17 00:00:00 2001 From: Loic Sharma Date: Sun, 5 Jul 2020 23:24:43 -0700 Subject: [PATCH 01/10] Move to System.Text.Json and route-to-code Remove old controllers Start working on removing polymorphic serialization Add tests for all extended models Polish? Remove Newtonsoft.Json from BaGet.Protocol Clean up --- .../Metadata/BaGetPackageMetadata.cs | 15 +- .../Metadata/BaGetRegistrationIndexPage.cs | 39 +++ .../BaGetRegistrationIndexPageItem.cs | 33 +++ .../BaGetRegistrationIndexResponse.cs | 39 ++- .../Metadata/BaGetRegistrationLeafResponse.cs | 15 -- .../Metadata/DefaultPackageMetadataService.cs | 3 +- .../Metadata/IPackageMetadataService.cs | 3 +- .../Metadata/RegistrationBuilder.cs | 11 +- src/BaGet.Core/Search/DependentsResponse.cs | 6 +- src/BaGet.Hosting/BaGet.Hosting.csproj | 4 - src/BaGet.Hosting/BaGetApi.cs | 167 +++++++++--- .../Controllers/PackageContentController.cs | 12 - .../Controllers/PackageMetadataController.cs | 55 ---- .../Controllers/SearchController.cs | 66 ----- .../Controllers/ServiceIndexController.cs | 29 --- .../Extensions/HttpContextExtensions.cs | 71 ++++++ .../IEndpointRouteBuilderExtensions.cs | 12 +- .../IServiceCollectionExtensions.cs | 6 +- src/BaGet.Protocol/BaGet.Protocol.csproj | 2 +- .../Catalog/CatalogProcessor.cs | 23 +- src/BaGet.Protocol/Catalog/FileCursor.cs | 23 +- .../Catalog/RawCatalogClient.cs | 19 +- .../Converters/BaseCatalogLeafConverter.cs | 35 --- .../CatalogLeafItemTypeConverter.cs | 42 ---- .../Converters/CatalogLeafTypeConverter.cs | 51 ---- .../PackageDependencyRangeConverter.cs | 33 --- .../PackageDependencyRangeJsonConverter.cs | 54 ++++ .../Converters/SingleOrListConverter.cs | 38 --- .../StringOrStringArrayJsonConverter.cs | 57 +++++ .../Extensions/CatalogModelExtensions.cs | 28 ++- .../Extensions/HttpClientExtensions.cs | 42 +++- src/BaGet.Protocol/Models/AlternatePackage.cs | 11 +- .../Models/AutocompleteContext.cs | 4 +- .../Models/AutocompleteResponse.cs | 7 +- src/BaGet.Protocol/Models/CatalogIndex.cs | 8 +- src/BaGet.Protocol/Models/CatalogLeaf.cs | 21 +- src/BaGet.Protocol/Models/CatalogLeafItem.cs | 16 +- src/BaGet.Protocol/Models/CatalogLeafType.cs | 22 -- src/BaGet.Protocol/Models/CatalogPage.cs | 10 +- src/BaGet.Protocol/Models/CatalogPageItem.cs | 8 +- .../Models/DependencyGroupItem.cs | 6 +- src/BaGet.Protocol/Models/DependencyItem.cs | 8 +- src/BaGet.Protocol/Models/ICatalogLeafItem.cs | 5 - .../Models/PackageDeprecation.cs | 10 +- .../Models/PackageDetailsCatalogLeaf.cs | 52 ++-- src/BaGet.Protocol/Models/PackageMetadata.cs | 40 +-- .../Models/PackageVersionsResponse.cs | 4 +- .../Models/RegistrationIndexPage.cs | 12 +- .../Models/RegistrationIndexPageItem.cs | 8 +- .../Models/RegistrationIndexResponse.cs | 12 +- .../Models/RegistrationLeafResponse.cs | 14 +- src/BaGet.Protocol/Models/SearchContext.cs | 6 +- src/BaGet.Protocol/Models/SearchResponse.cs | 8 +- src/BaGet.Protocol/Models/SearchResult.cs | 30 +-- .../Models/SearchResultVersion.cs | 8 +- src/BaGet.Protocol/Models/ServiceIndexItem.cs | 8 +- .../Models/ServiceIndexResponse.cs | 6 +- .../src/DisplayPackage/DisplayPackage.tsx | 2 +- .../src/DisplayPackage/Registration.tsx | 2 +- .../src/DisplayPackage/SourceRepository.tsx | 2 +- tests/BaGet.Core.Tests/Metadata/ModelTests.cs | 237 ++++++++++++++++++ .../RawCatalogClientTests.cs | 6 +- tests/BaGet.Tests/ApiIntegrationTests.cs | 22 +- tests/BaGet.Tests/TestData.resx | 58 ++--- 64 files changed, 977 insertions(+), 729 deletions(-) create mode 100644 src/BaGet.Core/Metadata/BaGetRegistrationIndexPage.cs create mode 100644 src/BaGet.Core/Metadata/BaGetRegistrationIndexPageItem.cs delete mode 100644 src/BaGet.Core/Metadata/BaGetRegistrationLeafResponse.cs delete mode 100644 src/BaGet.Hosting/Controllers/PackageMetadataController.cs delete mode 100644 src/BaGet.Hosting/Controllers/SearchController.cs delete mode 100644 src/BaGet.Hosting/Controllers/ServiceIndexController.cs create mode 100644 src/BaGet.Hosting/Extensions/HttpContextExtensions.cs delete mode 100644 src/BaGet.Protocol/Converters/BaseCatalogLeafConverter.cs delete mode 100644 src/BaGet.Protocol/Converters/CatalogLeafItemTypeConverter.cs delete mode 100644 src/BaGet.Protocol/Converters/CatalogLeafTypeConverter.cs delete mode 100644 src/BaGet.Protocol/Converters/PackageDependencyRangeConverter.cs create mode 100644 src/BaGet.Protocol/Converters/PackageDependencyRangeJsonConverter.cs delete mode 100644 src/BaGet.Protocol/Converters/SingleOrListConverter.cs create mode 100644 src/BaGet.Protocol/Converters/StringOrStringArrayJsonConverter.cs delete mode 100644 src/BaGet.Protocol/Models/CatalogLeafType.cs create mode 100644 tests/BaGet.Core.Tests/Metadata/ModelTests.cs diff --git a/src/BaGet.Core/Metadata/BaGetPackageMetadata.cs b/src/BaGet.Core/Metadata/BaGetPackageMetadata.cs index f6dba1d60..f84747d41 100644 --- a/src/BaGet.Core/Metadata/BaGetPackageMetadata.cs +++ b/src/BaGet.Core/Metadata/BaGetPackageMetadata.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; +using System.Text.Json.Serialization; using BaGet.Protocol.Models; -using Newtonsoft.Json; namespace BaGet.Core { @@ -10,26 +10,25 @@ namespace BaGet.Core /// public class BaGetPackageMetadata : PackageMetadata { - [JsonProperty("downloads")] + [JsonPropertyName("downloads")] public long Downloads { get; set; } - [JsonProperty("hasReadme")] + [JsonPropertyName("hasReadme")] public bool HasReadme { get; set; } - [JsonProperty("packageTypes")] + [JsonPropertyName("packageTypes")] public IReadOnlyList PackageTypes { get; set; } /// /// The package's release notes. /// - [JsonProperty("releaseNotes")] + [JsonPropertyName("releaseNotes")] public string ReleaseNotes { get; set; } - [JsonProperty("repositoryUrl")] + [JsonPropertyName("repositoryUrl")] public string RepositoryUrl { get; set; } - [JsonProperty("repositoryType")] + [JsonPropertyName("repositoryType")] public string RepositoryType { get; set; } - } } diff --git a/src/BaGet.Core/Metadata/BaGetRegistrationIndexPage.cs b/src/BaGet.Core/Metadata/BaGetRegistrationIndexPage.cs new file mode 100644 index 000000000..6ade52d0f --- /dev/null +++ b/src/BaGet.Core/Metadata/BaGetRegistrationIndexPage.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; +using BaGet.Protocol.Models; + +namespace BaGet.Core +{ + /// + /// BaGet's extensions to a registration index page. + /// Extends . + /// + /// + /// TODO: After this project is updated to .NET 5, make + /// extend and remove identical properties. + /// Properties that are modified should be marked with the "new" modified. + /// See: https://github.com/dotnet/runtime/pull/32107 + /// + public class BaGetRegistrationIndexPage + { +#region Original properties from RegistrationIndexPage. + [JsonPropertyName("@id")] + public string RegistrationPageUrl { get; set; } + + [JsonPropertyName("count")] + public int Count { get; set; } + + [JsonPropertyName("lower")] + public string Lower { get; set; } + + [JsonPropertyName("upper")] + public string Upper { get; set; } +#endregion + + /// + /// This was modified to use BaGet's extended registration index page item model. + /// + [JsonPropertyName("items")] + public IReadOnlyList ItemsOrNull { get; set; } + } +} diff --git a/src/BaGet.Core/Metadata/BaGetRegistrationIndexPageItem.cs b/src/BaGet.Core/Metadata/BaGetRegistrationIndexPageItem.cs new file mode 100644 index 000000000..3177522e3 --- /dev/null +++ b/src/BaGet.Core/Metadata/BaGetRegistrationIndexPageItem.cs @@ -0,0 +1,33 @@ +using System.Text.Json.Serialization; +using BaGet.Protocol.Models; + +namespace BaGet.Core +{ + /// + /// BaGet's extensions to a registration index page. + /// Extends . + /// + /// + /// TODO: After this project is updated to .NET 5, make + /// extend and remove identical properties. + /// Properties that are modified should be marked with the "new" modified. + /// See: https://github.com/dotnet/runtime/pull/32107 + /// + public class BaGetRegistrationIndexPageItem + { +#region Original properties from RegistrationIndexPageItem. + [JsonPropertyName("@id")] + public string RegistrationLeafUrl { get; set; } + + [JsonPropertyName("packageContent")] + public string PackageContentUrl { get; set; } +#endregion + + /// + /// The catalog entry containing the package metadata. + /// This was modified to use BaGet's extended package metadata model. + /// + [JsonPropertyName("catalogEntry")] + public BaGetPackageMetadata PackageMetadata { get; set; } + } +} diff --git a/src/BaGet.Core/Metadata/BaGetRegistrationIndexResponse.cs b/src/BaGet.Core/Metadata/BaGetRegistrationIndexResponse.cs index d82137755..9ff6ce66c 100644 --- a/src/BaGet.Core/Metadata/BaGetRegistrationIndexResponse.cs +++ b/src/BaGet.Core/Metadata/BaGetRegistrationIndexResponse.cs @@ -1,18 +1,45 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; using BaGet.Protocol.Models; -using Newtonsoft.Json; namespace BaGet.Core { /// - /// BaGet's extensions to a registration index response. These additions - /// are not part of the official protocol. + /// BaGet's extensions to a registration index response. + /// Extends . /// - public class BaGetRegistrationIndexResponse : RegistrationIndexResponse + /// + /// TODO: After this project is updated to .NET 5, make + /// extend and remove identical properties. + /// Properties that are modified should be marked with the "new" modified. + /// See: https://github.com/dotnet/runtime/pull/32107 + /// + public class BaGetRegistrationIndexResponse { +#region Original properties from RegistrationIndexResponse. + [JsonPropertyName("@id")] + public string RegistrationIndexUrl { get; set; } + + [JsonPropertyName("@type")] + public IReadOnlyList Type { get; set; } + + [JsonPropertyName("count")] + public int Count { get; set; } +#endregion + + /// + /// The pages that contain all of the versions of the package, ordered + /// by the package's version. This was modified to use BaGet's extended + /// registration index page model. + /// + [JsonPropertyName("items")] + public IReadOnlyList Pages { get; set; } + /// - /// How many times all versions of this package have been downloaded. + /// The package's total downloads across all versions. + /// This is not part of the official NuGet protocol. /// - [JsonProperty("totalDownloads")] + [JsonPropertyName("totalDownloads")] public long TotalDownloads { get; set; } } } diff --git a/src/BaGet.Core/Metadata/BaGetRegistrationLeafResponse.cs b/src/BaGet.Core/Metadata/BaGetRegistrationLeafResponse.cs deleted file mode 100644 index 4e76a6e0e..000000000 --- a/src/BaGet.Core/Metadata/BaGetRegistrationLeafResponse.cs +++ /dev/null @@ -1,15 +0,0 @@ -using BaGet.Protocol.Models; -using Newtonsoft.Json; - -namespace BaGet.Core -{ - /// - /// BaGet's extensions to a registration leaf response. These additions - /// are not part of the official protocol. - /// - public class BaGetRegistrationLeafResponse : RegistrationLeafResponse - { - [JsonProperty("downloads")] - public long Downloads { get; set; } - } -} diff --git a/src/BaGet.Core/Metadata/DefaultPackageMetadataService.cs b/src/BaGet.Core/Metadata/DefaultPackageMetadataService.cs index d9804a092..4196ba74e 100644 --- a/src/BaGet.Core/Metadata/DefaultPackageMetadataService.cs +++ b/src/BaGet.Core/Metadata/DefaultPackageMetadataService.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using BaGet.Protocol.Models; using NuGet.Packaging.Core; using NuGet.Versioning; @@ -41,7 +42,7 @@ public async Task GetRegistrationIndexOrNullAsyn packages)); } - public async Task GetRegistrationLeafOrNullAsync( + public async Task GetRegistrationLeafOrNullAsync( string id, NuGetVersion version, CancellationToken cancellationToken = default) diff --git a/src/BaGet.Core/Metadata/IPackageMetadataService.cs b/src/BaGet.Core/Metadata/IPackageMetadataService.cs index 50cfb94e7..83bf3fab0 100644 --- a/src/BaGet.Core/Metadata/IPackageMetadataService.cs +++ b/src/BaGet.Core/Metadata/IPackageMetadataService.cs @@ -1,5 +1,6 @@ using System.Threading; using System.Threading.Tasks; +using BaGet.Protocol.Models; using NuGet.Versioning; namespace BaGet.Core @@ -27,7 +28,7 @@ public interface IPackageMetadataService /// The package's version. /// A token to cancel the task. /// The registration leaf, or null if the package does not exist. - Task GetRegistrationLeafOrNullAsync( + Task GetRegistrationLeafOrNullAsync( string packageId, NuGetVersion packageVersion, CancellationToken cancellationToken = default); diff --git a/src/BaGet.Core/Metadata/RegistrationBuilder.cs b/src/BaGet.Core/Metadata/RegistrationBuilder.cs index 515d16f60..4090e9323 100644 --- a/src/BaGet.Core/Metadata/RegistrationBuilder.cs +++ b/src/BaGet.Core/Metadata/RegistrationBuilder.cs @@ -29,7 +29,7 @@ public virtual BaGetRegistrationIndexResponse BuildIndex(PackageRegistration reg TotalDownloads = registration.Packages.Sum(p => p.Downloads), Pages = new[] { - new RegistrationIndexPage + new BaGetRegistrationIndexPage { RegistrationPageUrl = _url.GetRegistrationIndexUrl(registration.PackageId), Count = registration.Packages.Count(), @@ -41,16 +41,15 @@ public virtual BaGetRegistrationIndexResponse BuildIndex(PackageRegistration reg }; } - public virtual BaGetRegistrationLeafResponse BuildLeaf(Package package) + public virtual RegistrationLeafResponse BuildLeaf(Package package) { var id = package.Id; var version = package.Version; - return new BaGetRegistrationLeafResponse + return new RegistrationLeafResponse { Type = RegistrationLeafResponse.DefaultType, Listed = package.Listed, - Downloads = package.Downloads, Published = package.Published, RegistrationLeafUrl = _url.GetRegistrationLeafUrl(id, version), PackageContentUrl = _url.GetPackageDownloadUrl(id, version), @@ -58,8 +57,8 @@ public virtual BaGetRegistrationLeafResponse BuildLeaf(Package package) }; } - private RegistrationIndexPageItem ToRegistrationIndexPageItem(Package package) => - new RegistrationIndexPageItem + private BaGetRegistrationIndexPageItem ToRegistrationIndexPageItem(Package package) => + new BaGetRegistrationIndexPageItem { RegistrationLeafUrl = _url.GetRegistrationLeafUrl(package.Id, package.Version), PackageContentUrl = _url.GetPackageDownloadUrl(package.Id, package.Version), diff --git a/src/BaGet.Core/Search/DependentsResponse.cs b/src/BaGet.Core/Search/DependentsResponse.cs index c25a65530..cf01b677e 100644 --- a/src/BaGet.Core/Search/DependentsResponse.cs +++ b/src/BaGet.Core/Search/DependentsResponse.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Core { @@ -12,13 +12,13 @@ public class DependentsResponse /// /// The total number of matches, disregarding skip and take. /// - [JsonProperty("totalHits")] + [JsonPropertyName("totalHits")] public long TotalHits { get; set; } /// /// The package IDs matched by the dependent query. /// - [JsonProperty("data")] + [JsonPropertyName("data")] public IReadOnlyList Data { get; set; } } } diff --git a/src/BaGet.Hosting/BaGet.Hosting.csproj b/src/BaGet.Hosting/BaGet.Hosting.csproj index 46e5ad4f3..68850c33a 100644 --- a/src/BaGet.Hosting/BaGet.Hosting.csproj +++ b/src/BaGet.Hosting/BaGet.Hosting.csproj @@ -11,10 +11,6 @@ - - - - diff --git a/src/BaGet.Hosting/BaGetApi.cs b/src/BaGet.Hosting/BaGetApi.cs index 4f2965554..971e92e29 100644 --- a/src/BaGet.Hosting/BaGetApi.cs +++ b/src/BaGet.Hosting/BaGetApi.cs @@ -1,7 +1,10 @@ +using BaGet.Core; using BaGet.Hosting; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Constraints; +using Microsoft.Extensions.DependencyInjection; +using NuGet.Versioning; namespace BaGet { @@ -19,10 +22,17 @@ public void MapRoutes(IEndpointRouteBuilder endpoints) public void MapServiceIndexRoutes(IEndpointRouteBuilder endpoints) { - endpoints.MapControllerRoute( - name: Routes.IndexRouteName, - pattern: "v3/index.json", - defaults: new { controller = "ServiceIndex", action = "Get" }); + endpoints + .MapGet("v3/index.json", async context => + { + var cancellationToken = context.RequestAborted; + var serviceIndex = context.RequestServices.GetRequiredService(); + + var response = await serviceIndex.GetAsync(cancellationToken); + + await context.Response.WriteAsJsonAsync(response, cancellationToken); + }) + .WithRouteName(Routes.IndexRouteName); } public void MapPackagePublishRoutes(IEndpointRouteBuilder endpoints) @@ -67,42 +77,139 @@ public void MapSymbolRoutes(IEndpointRouteBuilder endpoints) public void MapSearchRoutes(IEndpointRouteBuilder endpoints) { - endpoints.MapControllerRoute( - name: Routes.SearchRouteName, - pattern: "v3/search", - defaults: new { controller = "Search", action = "Search" }); - - endpoints.MapControllerRoute( - name: Routes.AutocompleteRouteName, - pattern: "v3/autocomplete", - defaults: new { controller = "Search", action = "Autocomplete" }); + endpoints + .MapGet("v3/search", async context => + { + var query = context.Request.ReadFromQuery("q"); + var skip = context.Request.ReadFromQuery("skip", defaultValue: 0); + var take = context.Request.ReadFromQuery("take", defaultValue: 20); + var prerelease = context.Request.ReadFromQuery("prerelease", defaultValue: false); + var semVerLevel = context.Request.ReadFromQuery("semVerLevel", defaultValue: null); + var packageType = context.Request.ReadFromQuery("packageType", defaultValue: null); + var framework = context.Request.ReadFromQuery("framework", defaultValue: null); + var cancellationToken = context.RequestAborted; + + var searchService = context.RequestServices.GetRequiredService(); + var includeSemVer2 = semVerLevel == "2.0.0"; + + var response = await searchService.SearchAsync( + query ?? string.Empty, + skip, + take, + prerelease, + includeSemVer2, + packageType, + framework, + cancellationToken); + + await context.Response.WriteAsJsonAsync(response, cancellationToken); + }) + .WithRouteName(Routes.SearchRouteName); + + endpoints + .MapGet("v3/autocomplete", async context => + { + // TODO: Add other autocomplete parameters + // TODO: Support versions autocomplete. + // See: https://github.com/loic-sharma/BaGet/issues/291 + var query = context.Request.ReadFromQuery("q"); + var cancellationToken = context.RequestAborted; + + var searchService = context.RequestServices.GetRequiredService(); + + var response = await searchService.AutocompleteAsync( + query, + cancellationToken: cancellationToken); + + await context.Response.WriteAsJsonAsync(response, cancellationToken); + }) + .WithRouteName(Routes.AutocompleteRouteName); // This is an unofficial API to find packages that depend on a given package. - endpoints.MapControllerRoute( - name: Routes.DependentsRouteName, - pattern: "v3/dependents", - defaults: new { controller = "Search", action = "Dependents" }); + endpoints + .MapGet("v3/dependents", async context => + { + var packageId = context.Request.ReadFromQuery("packageId"); + var cancellationToken = context.RequestAborted; + + var searchService = context.RequestServices.GetRequiredService(); + + var response = await searchService.FindDependentsAsync( + packageId, + cancellationToken: cancellationToken); + + await context.Response.WriteAsJsonAsync(response, cancellationToken); + }) + .WithRouteName(Routes.DependentsRouteName); } public void MapPackageMetadataRoutes(IEndpointRouteBuilder endpoints) { - endpoints.MapControllerRoute( - name: Routes.RegistrationIndexRouteName, - pattern: "v3/registration/{id}/index.json", - defaults: new { controller = "PackageMetadata", action = "RegistrationIndex" }); - - endpoints.MapControllerRoute( - name: Routes.RegistrationLeafRouteName, - pattern: "v3/registration/{id}/{version}.json", - defaults: new { controller = "PackageMetadata", action = "RegistrationLeaf" }); + endpoints + .MapGet("v3/registration/{id}/index.json", async context => + { + var packageId = context.Request.RouteValues["id"]?.ToString(); + var cancellationToken = context.RequestAborted; + + var metadata = context.RequestServices.GetRequiredService(); + + var index = await metadata.GetRegistrationIndexOrNullAsync(packageId, cancellationToken); + if (index == null) + { + context.Response.NotFound(); + return; + } + + await context.Response.WriteAsJsonAsync(index, cancellationToken); + }) + .WithRouteName(Routes.RegistrationIndexRouteName); + + endpoints + .MapGet("v3/registration/{id}/{version}.json", async context => + { + var packageId = context.Request.RouteValues["id"]?.ToString(); + var version = context.Request.RouteValues["version"]?.ToString(); + var cancellationToken = context.RequestAborted; + + var metadata = context.RequestServices.GetRequiredService(); + + if (!NuGetVersion.TryParse(version, out var nugetVersion)) + { + context.Response.NotFound(); + return; + } + + var leaf = await metadata.GetRegistrationLeafOrNullAsync(packageId, nugetVersion, cancellationToken); + if (leaf == null) + { + context.Response.NotFound(); + return; + } + + await context.Response.WriteAsJsonAsync(leaf, cancellationToken); + }) + .WithRouteName(Routes.RegistrationLeafRouteName); } public void MapPackageContentRoutes(IEndpointRouteBuilder endpoints) { - endpoints.MapControllerRoute( - name: Routes.PackageVersionsRouteName, - pattern: "v3/package/{id}/index.json", - defaults: new { controller = "PackageContent", action = "GetPackageVersions" }); + endpoints + .MapGet("v3/package/{id}/index.json", async context => + { + var packageId = context.Request.RouteValues["id"]?.ToString(); + var cancellationToken = context.RequestAborted; + + var content = context.RequestServices.GetRequiredService(); + var response = await content.GetPackageVersionsOrNullAsync(packageId, cancellationToken); + if (response == null) + { + context.Response.NotFound(); + return; + } + + await context.Response.WriteAsJsonAsync(response, cancellationToken); + }) + .WithRouteName(Routes.PackageVersionsRouteName); endpoints.MapControllerRoute( name: Routes.PackageDownloadRouteName, diff --git a/src/BaGet.Hosting/Controllers/PackageContentController.cs b/src/BaGet.Hosting/Controllers/PackageContentController.cs index 749d00450..ddc75bc05 100644 --- a/src/BaGet.Hosting/Controllers/PackageContentController.cs +++ b/src/BaGet.Hosting/Controllers/PackageContentController.cs @@ -2,7 +2,6 @@ using System.Threading; using System.Threading.Tasks; using BaGet.Core; -using BaGet.Protocol.Models; using Microsoft.AspNetCore.Mvc; using NuGet.Versioning; @@ -21,17 +20,6 @@ public PackageContentController(IPackageContentService content) _content = content ?? throw new ArgumentNullException(nameof(content)); } - public async Task> GetPackageVersionsAsync(string id, CancellationToken cancellationToken) - { - var versions = await _content.GetPackageVersionsOrNullAsync(id, cancellationToken); - if (versions == null) - { - return NotFound(); - } - - return versions; - } - public async Task DownloadPackageAsync(string id, string version, CancellationToken cancellationToken) { if (!NuGetVersion.TryParse(version, out var nugetVersion)) diff --git a/src/BaGet.Hosting/Controllers/PackageMetadataController.cs b/src/BaGet.Hosting/Controllers/PackageMetadataController.cs deleted file mode 100644 index ded05cbef..000000000 --- a/src/BaGet.Hosting/Controllers/PackageMetadataController.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using BaGet.Core; -using BaGet.Protocol.Models; -using Microsoft.AspNetCore.Mvc; -using NuGet.Versioning; - -namespace BaGet.Hosting -{ - /// - /// The Package Metadata resource, used to fetch packages' information. - /// See: https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource - /// - public class PackageMetadataController : Controller - { - private readonly IPackageMetadataService _metadata; - - public PackageMetadataController(IPackageMetadataService metadata) - { - _metadata = metadata ?? throw new ArgumentNullException(nameof(metadata)); - } - - // GET v3/registration/{id}.json - [HttpGet] - public async Task> RegistrationIndexAsync(string id, CancellationToken cancellationToken) - { - var index = await _metadata.GetRegistrationIndexOrNullAsync(id, cancellationToken); - if (index == null) - { - return NotFound(); - } - - return index; - } - - // GET v3/registration/{id}/{version}.json - [HttpGet] - public async Task> RegistrationLeafAsync(string id, string version, CancellationToken cancellationToken) - { - if (!NuGetVersion.TryParse(version, out var nugetVersion)) - { - return NotFound(); - } - - var leaf = await _metadata.GetRegistrationLeafOrNullAsync(id, nugetVersion, cancellationToken); - if (leaf == null) - { - return NotFound(); - } - - return leaf; - } - } -} diff --git a/src/BaGet.Hosting/Controllers/SearchController.cs b/src/BaGet.Hosting/Controllers/SearchController.cs deleted file mode 100644 index 0bb6ed8e2..000000000 --- a/src/BaGet.Hosting/Controllers/SearchController.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using BaGet.Core; -using BaGet.Protocol.Models; -using Microsoft.AspNetCore.Mvc; - -namespace BaGet.Hosting -{ - public class SearchController : Controller - { - private readonly ISearchService _searchService; - - public SearchController(ISearchService searchService) - { - _searchService = searchService ?? throw new ArgumentNullException(nameof(searchService)); - } - - public async Task> SearchAsync( - [FromQuery(Name = "q")] string query = null, - [FromQuery]int skip = 0, - [FromQuery]int take = 20, - [FromQuery]bool prerelease = false, - [FromQuery]string semVerLevel = null, - - // These are unofficial parameters - [FromQuery]string packageType = null, - [FromQuery]string framework = null, - CancellationToken cancellationToken = default) - { - var includeSemVer2 = semVerLevel == "2.0.0"; - - return await _searchService.SearchAsync( - query ?? string.Empty, - skip, - take, - prerelease, - includeSemVer2, - packageType, - framework, - cancellationToken); - } - - public async Task> AutocompleteAsync( - [FromQuery(Name = "q")] string query = null, - CancellationToken cancellationToken = default) - { - // TODO: Add other autocomplete parameters - // TODO: Support versions autocomplete. - // See: https://github.com/loic-sharma/BaGet/issues/291 - return await _searchService.AutocompleteAsync( - query, - cancellationToken: cancellationToken); - } - - public async Task> DependentsAsync( - [FromQuery] string packageId, - CancellationToken cancellationToken = default) - { - // TODO: Add other dependents parameters. - return await _searchService.FindDependentsAsync( - packageId, - cancellationToken: cancellationToken); - } - } -} diff --git a/src/BaGet.Hosting/Controllers/ServiceIndexController.cs b/src/BaGet.Hosting/Controllers/ServiceIndexController.cs deleted file mode 100644 index 707af2c6a..000000000 --- a/src/BaGet.Hosting/Controllers/ServiceIndexController.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using BaGet.Core; -using BaGet.Protocol.Models; -using Microsoft.AspNetCore.Mvc; - -namespace BaGet.Hosting -{ - /// - /// The NuGet Service Index. This aids NuGet client to discover this server's services. - /// - public class ServiceIndexController : Controller - { - private readonly IServiceIndexService _serviceIndex; - - public ServiceIndexController(IServiceIndexService serviceIndex) - { - _serviceIndex = serviceIndex ?? throw new ArgumentNullException(nameof(serviceIndex)); - } - - // GET v3/index - [HttpGet] - public async Task GetAsync(CancellationToken cancellationToken) - { - return await _serviceIndex.GetAsync(cancellationToken); - } - } -} diff --git a/src/BaGet.Hosting/Extensions/HttpContextExtensions.cs b/src/BaGet.Hosting/Extensions/HttpContextExtensions.cs new file mode 100644 index 000000000..023f85994 --- /dev/null +++ b/src/BaGet.Hosting/Extensions/HttpContextExtensions.cs @@ -0,0 +1,71 @@ +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace BaGet.Hosting +{ + internal static class HttpContextExtensions + { + private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions + { + IgnoreNullValues = true, + }; + + public static string ReadFromQuery(this HttpRequest request, string key) + { + var value = request.Query[key].ToString(); + if (string.IsNullOrEmpty(value)) + { + return string.Empty; + } + + return value; + } + + public static string ReadFromQuery(this HttpRequest request, string key, string defaultValue) + { + var value = request.Query[key].ToString(); + if (string.IsNullOrEmpty(value)) + { + return defaultValue; + } + + return value; + } + + public static int ReadFromQuery(this HttpRequest context, string key, int defaultValue) + { + var value = context.Query[key].ToString(); + if (value == null || !int.TryParse(value, out var result)) + { + result = defaultValue; + } + + return result; + } + + public static bool ReadFromQuery(this HttpRequest request, string key, bool defaultValue) + { + var value = request.Query[key].ToString(); + if (value == null || !bool.TryParse(value, out var result)) + { + result = defaultValue; + } + + return result; + } + + public static void NotFound(this HttpResponse response) => response.StatusCode = StatusCodes.Status404NotFound; + + public static async Task WriteAsJsonAsync( + this HttpResponse response, + TValue value, + CancellationToken cancellationToken) + { + response.ContentType = "application/json; charset=utf-8"; + + await JsonSerializer.SerializeAsync(response.Body, value, JsonOptions, cancellationToken); + } + } +} diff --git a/src/BaGet.Hosting/Extensions/IEndpointRouteBuilderExtensions.cs b/src/BaGet.Hosting/Extensions/IEndpointRouteBuilderExtensions.cs index 79a90c43a..ddebb6849 100644 --- a/src/BaGet.Hosting/Extensions/IEndpointRouteBuilderExtensions.cs +++ b/src/BaGet.Hosting/Extensions/IEndpointRouteBuilderExtensions.cs @@ -1,17 +1,15 @@ -using BaGet.Hosting; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; -using Microsoft.AspNetCore.Routing.Constraints; namespace BaGet { - public static class IEndpointRouteBuilderExtensions + public static class EndpointRoutingExtensions { - public static void MapBaGetRoutes(this IEndpointRouteBuilder endpoints) + public static IEndpointConventionBuilder WithRouteName(this IEndpointConventionBuilder endpoints, string name) { - + return endpoints.WithMetadata( + new EndpointNameMetadata(name), + new RouteNameMetadata(name)); } - - } } diff --git a/src/BaGet.Hosting/Extensions/IServiceCollectionExtensions.cs b/src/BaGet.Hosting/Extensions/IServiceCollectionExtensions.cs index 57670e1b9..fc546d7c8 100644 --- a/src/BaGet.Hosting/Extensions/IServiceCollectionExtensions.cs +++ b/src/BaGet.Hosting/Extensions/IServiceCollectionExtensions.cs @@ -16,11 +16,7 @@ public static IServiceCollection AddBaGetWebApplication( services .AddControllers() .AddApplicationPart(typeof(PackageContentController).Assembly) - .SetCompatibilityVersion(CompatibilityVersion.Version_3_0) - .AddNewtonsoftJson(options => - { - options.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Utc; - }); + .SetCompatibilityVersion(CompatibilityVersion.Version_3_0); services.AddHttpContextAccessor(); services.AddTransient(); diff --git a/src/BaGet.Protocol/BaGet.Protocol.csproj b/src/BaGet.Protocol/BaGet.Protocol.csproj index f08bbe059..5711a54bb 100644 --- a/src/BaGet.Protocol/BaGet.Protocol.csproj +++ b/src/BaGet.Protocol/BaGet.Protocol.csproj @@ -9,9 +9,9 @@ - + diff --git a/src/BaGet.Protocol/Catalog/CatalogProcessor.cs b/src/BaGet.Protocol/Catalog/CatalogProcessor.cs index 684d6ddb4..13c5cc274 100644 --- a/src/BaGet.Protocol/Catalog/CatalogProcessor.cs +++ b/src/BaGet.Protocol/Catalog/CatalogProcessor.cs @@ -144,18 +144,19 @@ private async Task ProcessLeafAsync(CatalogLeafItem leafItem, Cancellation bool success; try { - switch (leafItem.Type) + if (leafItem.IsPackageDelete()) { - case CatalogLeafType.PackageDelete: - var packageDelete = await _client.GetPackageDeleteLeafAsync(leafItem.CatalogLeafUrl); - success = await _leafProcessor.ProcessPackageDeleteAsync(packageDelete, cancellationToken); - break; - case CatalogLeafType.PackageDetails: - var packageDetails = await _client.GetPackageDetailsLeafAsync(leafItem.CatalogLeafUrl); - success = await _leafProcessor.ProcessPackageDetailsAsync(packageDetails, cancellationToken); - break; - default: - throw new NotSupportedException($"The catalog leaf type '{leafItem.Type}' is not supported."); + var packageDelete = await _client.GetPackageDeleteLeafAsync(leafItem.CatalogLeafUrl); + success = await _leafProcessor.ProcessPackageDeleteAsync(packageDelete, cancellationToken); + } + else if (leafItem.IsPackageDetails()) + { + var packageDetails = await _client.GetPackageDetailsLeafAsync(leafItem.CatalogLeafUrl); + success = await _leafProcessor.ProcessPackageDetailsAsync(packageDetails, cancellationToken); + } + else + { + throw new NotSupportedException($"The catalog leaf type '{leafItem.Type}' is not supported."); } } catch (Exception exception) diff --git a/src/BaGet.Protocol/Catalog/FileCursor.cs b/src/BaGet.Protocol/Catalog/FileCursor.cs index b1b6a9ddd..9be09c2c9 100644 --- a/src/BaGet.Protocol/Catalog/FileCursor.cs +++ b/src/BaGet.Protocol/Catalog/FileCursor.cs @@ -1,9 +1,10 @@ using System; using System.IO; +using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Newtonsoft.Json; namespace BaGet.Protocol.Catalog { @@ -14,8 +15,6 @@ namespace BaGet.Protocol.Catalog /// public class FileCursor : ICursor { - private static readonly JsonSerializerSettings Settings = HttpClientExtensions.JsonSettings; - private readonly string _path; private readonly ILogger _logger; @@ -25,25 +24,27 @@ public FileCursor(string path, ILogger logger) _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } - public Task GetAsync(CancellationToken cancellationToken) + public async Task GetAsync(CancellationToken cancellationToken) { try { - var jsonString = File.ReadAllText(_path); - var data = JsonConvert.DeserializeObject(jsonString, Settings); - _logger.LogDebug("Read cursor value {cursor:O} from {path}.", data.Value, _path); - return Task.FromResult(data.Value); + using (var file = File.OpenRead(_path)) + { + var data = await JsonSerializer.DeserializeAsync(file, options: null, cancellationToken); + _logger.LogDebug("Read cursor value {cursor:O} from {path}.", data.Value, _path); + return data.Value; + } } catch (Exception e) when (e is FileNotFoundException || e is JsonException) { - return Task.FromResult(null); + return null; } } public Task SetAsync(DateTimeOffset value, CancellationToken cancellationToken) { var data = new Data { Value = value }; - var jsonString = JsonConvert.SerializeObject(data); + var jsonString = JsonSerializer.Serialize(data); File.WriteAllText(_path, jsonString); _logger.LogDebug("Wrote cursor value {cursor:O} to {path}.", data.Value, _path); return Task.CompletedTask; @@ -51,7 +52,7 @@ public Task SetAsync(DateTimeOffset value, CancellationToken cancellationToken) private class Data { - [JsonProperty("value")] + [JsonPropertyName("value")] public DateTimeOffset Value { get; set; } } } diff --git a/src/BaGet.Protocol/Catalog/RawCatalogClient.cs b/src/BaGet.Protocol/Catalog/RawCatalogClient.cs index e5feb6572..a477387e3 100644 --- a/src/BaGet.Protocol/Catalog/RawCatalogClient.cs +++ b/src/BaGet.Protocol/Catalog/RawCatalogClient.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -34,7 +35,7 @@ public async Task GetPageAsync(string pageUrl, CancellationToken ca public async Task GetPackageDeleteLeafAsync(string leafUrl, CancellationToken cancellationToken = default) { return await GetAndValidateLeafAsync( - CatalogLeafType.PackageDelete, + "PackageDelete", leafUrl, cancellationToken); } @@ -42,24 +43,24 @@ public async Task GetPackageDeleteLeafAsync(string lea public async Task GetPackageDetailsLeafAsync(string leafUrl, CancellationToken cancellationToken = default) { return await GetAndValidateLeafAsync( - CatalogLeafType.PackageDetails, + "PackageDetails", leafUrl, cancellationToken); } - private async Task GetAndValidateLeafAsync( - CatalogLeafType type, + private async Task GetAndValidateLeafAsync( + string leafType, string leafUrl, - CancellationToken cancellationToken) where T : CatalogLeaf + CancellationToken cancellationToken) where TCatalogLeaf : CatalogLeaf { - var result = await _httpClient.DeserializeUrlAsync(leafUrl, cancellationToken); + var result = await _httpClient.DeserializeUrlAsync(leafUrl, cancellationToken); var leaf = result.GetResultOrThrow(); - if (leaf.Type != type) + if (leaf.Type.FirstOrDefault() != leafType) { throw new ArgumentException( - $"The leaf type found in the document does not match the expected '{type}' type.", - nameof(type)); + $"The leaf type found in the document does not match the expected '{leafType}' type.", + nameof(leafType)); } return leaf; diff --git a/src/BaGet.Protocol/Converters/BaseCatalogLeafConverter.cs b/src/BaGet.Protocol/Converters/BaseCatalogLeafConverter.cs deleted file mode 100644 index 70ea04cd6..000000000 --- a/src/BaGet.Protocol/Converters/BaseCatalogLeafConverter.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Collections.Generic; -using BaGet.Protocol.Models; -using Newtonsoft.Json; - -namespace BaGet.Protocol.Internal -{ - /// - /// Based off: https://github.com/NuGet/NuGet.Services.Metadata/blob/64af0b59c5a79e0143f0808b39946df9f16cb2e7/src/NuGet.Protocol.Catalog/Serialization/BaseCatalogLeafConverter.cs - /// - internal abstract class BaseCatalogLeafConverter : JsonConverter - { - private readonly IReadOnlyDictionary _fromType; - - public BaseCatalogLeafConverter(IReadOnlyDictionary fromType) - { - _fromType = fromType; - } - - public override bool CanConvert(Type objectType) - { - return objectType == typeof(CatalogLeafType); - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - if (!_fromType.TryGetValue((CatalogLeafType)value, out var output)) - { - throw new NotSupportedException($"The catalog leaf type '{value}' is not supported."); - } - - writer.WriteValue(output); - } - } -} diff --git a/src/BaGet.Protocol/Converters/CatalogLeafItemTypeConverter.cs b/src/BaGet.Protocol/Converters/CatalogLeafItemTypeConverter.cs deleted file mode 100644 index a31c26e40..000000000 --- a/src/BaGet.Protocol/Converters/CatalogLeafItemTypeConverter.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using BaGet.Protocol.Models; -using Newtonsoft.Json; - -namespace BaGet.Protocol.Internal -{ - /// - /// Based off: https://github.com/NuGet/NuGet.Services.Metadata/blob/64af0b59c5a79e0143f0808b39946df9f16cb2e7/src/NuGet.Protocol.Catalog/Serialization/CatalogLeafItemTypeConverter.cs - /// - internal class CatalogLeafItemTypeConverter : BaseCatalogLeafConverter - { - private static readonly Dictionary FromType = new Dictionary - { - { CatalogLeafType.PackageDelete, "nuget:PackageDelete" }, - { CatalogLeafType.PackageDetails, "nuget:PackageDetails" }, - }; - - private static readonly Dictionary FromString = FromType - .ToDictionary(x => x.Value, x => x.Key); - - public CatalogLeafItemTypeConverter() : base(FromType) - { - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - var stringValue = reader.Value as string; - if (stringValue != null) - { - CatalogLeafType output; - if (FromString.TryGetValue(stringValue, out output)) - { - return output; - } - } - - throw new JsonSerializationException($"Unexpected value for a {nameof(CatalogLeafType)}."); - } - } -} diff --git a/src/BaGet.Protocol/Converters/CatalogLeafTypeConverter.cs b/src/BaGet.Protocol/Converters/CatalogLeafTypeConverter.cs deleted file mode 100644 index bc37acf16..000000000 --- a/src/BaGet.Protocol/Converters/CatalogLeafTypeConverter.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using BaGet.Protocol.Models; -using Newtonsoft.Json; - -namespace BaGet.Protocol.Internal -{ - /// - /// Based off: https://github.com/NuGet/NuGet.Services.Metadata/blob/64af0b59c5a79e0143f0808b39946df9f16cb2e7/src/NuGet.Protocol.Catalog/Serialization/CatalogLeafTypeConverter.cs - /// - internal class CatalogLeafTypeConverter : BaseCatalogLeafConverter - { - private static readonly Dictionary FromType = new Dictionary - { - { CatalogLeafType.PackageDelete, "PackageDelete" }, - { CatalogLeafType.PackageDetails, "PackageDetails" }, - }; - - private static readonly Dictionary FromString = FromType - .ToDictionary(x => x.Value, x => x.Key); - - public CatalogLeafTypeConverter() : base(FromType) - { - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - List types; - if (reader.TokenType == JsonToken.StartArray) - { - types = serializer.Deserialize>(reader); - } - else - { - types = new List { reader.Value }; - } - - foreach (var type in types.OfType()) - { - CatalogLeafType output; - if (FromString.TryGetValue(type, out output)) - { - return output; - } - } - - throw new JsonSerializationException($"Unexpected value for a {nameof(CatalogLeafType)}."); - } - } -} diff --git a/src/BaGet.Protocol/Converters/PackageDependencyRangeConverter.cs b/src/BaGet.Protocol/Converters/PackageDependencyRangeConverter.cs deleted file mode 100644 index f199cd608..000000000 --- a/src/BaGet.Protocol/Converters/PackageDependencyRangeConverter.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Linq; -using Newtonsoft.Json; - -namespace BaGet.Protocol.Internal -{ - internal class PackageDependencyRangeConverter : JsonConverter - { - public override bool CanConvert(Type objectType) - { - return objectType == typeof(string); - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.StartArray) - { - // There are some quirky packages with arrays of dependency version ranges. In this case, we take the - // first element. - // Example: https://api.nuget.org/v3/catalog0/data/2016.02.21.11.06.01/dingu.generic.repo.ef7.1.0.0-beta2.json - var array = serializer.Deserialize(reader); - return array.FirstOrDefault(); - } - - return serializer.Deserialize(reader); - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - serializer.Serialize(writer, value); - } - } -} diff --git a/src/BaGet.Protocol/Converters/PackageDependencyRangeJsonConverter.cs b/src/BaGet.Protocol/Converters/PackageDependencyRangeJsonConverter.cs new file mode 100644 index 000000000..a0c7bfbd0 --- /dev/null +++ b/src/BaGet.Protocol/Converters/PackageDependencyRangeJsonConverter.cs @@ -0,0 +1,54 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace BaGet.Protocol.Internal +{ + internal class PackageDependencyRangeJsonConverter : JsonConverter + { + public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String) + { + return reader.GetString(); + } + + // There are some quirky packages with arrays of dependency version ranges. + // In this case, we take the first element. + // Example: https://api.nuget.org/v3/catalog0/data/2016.02.21.11.06.01/dingu.generic.repo.ef7.1.0.0-beta2.json + if (reader.TokenType != JsonTokenType.StartArray) + { + throw new JsonException(); + } + + reader.Read(); + if (reader.TokenType != JsonTokenType.String) + { + throw new JsonException(); + } + + var result = reader.GetString(); + + // Ignore all other strings until we reach the end of the array. + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndArray) + { + break; + } + + if (reader.TokenType != JsonTokenType.String) + { + throw new JsonException(); + } + } + + return result; + } + + public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) + { + writer.WriteStringValue(value); + } + } +} diff --git a/src/BaGet.Protocol/Converters/SingleOrListConverter.cs b/src/BaGet.Protocol/Converters/SingleOrListConverter.cs deleted file mode 100644 index d0f8537ab..000000000 --- a/src/BaGet.Protocol/Converters/SingleOrListConverter.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace BaGet.Protocol.Internal -{ - /// - /// Converts a single value or a list of values into the desired type or . - /// - /// The desired type. - internal class SingleOrListConverter : JsonConverter - { - /// - public override bool CanConvert(Type objectType) - { - return objectType == typeof(IReadOnlyList); - } - - /// - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.StartArray) - { - return serializer.Deserialize>(reader); - } - else - { - return serializer.Deserialize(reader); - } - } - - /// - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - serializer.Serialize(writer, value); - } - } -} diff --git a/src/BaGet.Protocol/Converters/StringOrStringArrayJsonConverter.cs b/src/BaGet.Protocol/Converters/StringOrStringArrayJsonConverter.cs new file mode 100644 index 000000000..495ede7a5 --- /dev/null +++ b/src/BaGet.Protocol/Converters/StringOrStringArrayJsonConverter.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace BaGet.Protocol.Internal +{ + internal class StringOrStringArrayJsonConverter : JsonConverter> + { + public override IReadOnlyList Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + // Try to read a single string first. + if (reader.TokenType == JsonTokenType.String) + { + return new List { reader.GetString() }; + } + + // Try to read an array of strings. + if (reader.TokenType != JsonTokenType.StartArray) + { + throw new JsonException(); + } + + var result = new List(); + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.String) + { + result.Add(reader.GetString()); + } + else if (reader.TokenType == JsonTokenType.EndArray) + { + return result; + } + else + { + break; + } + } + + throw new JsonException(); + } + + public override void Write(Utf8JsonWriter writer, IReadOnlyList values, JsonSerializerOptions options) + { + writer.WriteStartArray(); + + foreach (var value in values) + { + writer.WriteStringValue(value); + } + + writer.WriteEndArray(); + } + } +} diff --git a/src/BaGet.Protocol/Extensions/CatalogModelExtensions.cs b/src/BaGet.Protocol/Extensions/CatalogModelExtensions.cs index adff9c424..66f18ffd3 100644 --- a/src/BaGet.Protocol/Extensions/CatalogModelExtensions.cs +++ b/src/BaGet.Protocol/Extensions/CatalogModelExtensions.cs @@ -142,9 +142,29 @@ public static VersionRange ParseRange(this DependencyItem packageDependency) /// /// The catalog leaf. /// True if the catalog leaf represents a package delete. - public static bool IsPackageDelete(this ICatalogLeafItem leaf) + public static bool IsPackageDelete(this CatalogLeafItem leaf) { - return leaf.Type == CatalogLeafType.PackageDelete; + return leaf.Type == "nuget:PackageDelete"; + } + + /// + /// Determines if the provided catalog leaf is a package delete. + /// + /// The catalog leaf. + /// True if the catalog leaf represents a package delete. + public static bool IsPackageDelete(this CatalogLeaf leaf) + { + return leaf.Type.FirstOrDefault() == "PackageDelete"; + } + + /// + /// Determines if the provided catalog leaf is contains package details. + /// + /// The catalog leaf. + /// True if the catalog leaf contains package details. + public static bool IsPackageDetails(this CatalogLeafItem leaf) + { + return leaf.Type == "nuget:PackageDetails"; } /// @@ -152,9 +172,9 @@ public static bool IsPackageDelete(this ICatalogLeafItem leaf) /// /// The catalog leaf. /// True if the catalog leaf contains package details. - public static bool IsPackageDetails(this ICatalogLeafItem leaf) + public static bool IsPackageDetails(this CatalogLeaf leaf) { - return leaf.Type == CatalogLeafType.PackageDetails; + return leaf.Type.FirstOrDefault() == "PackageDetails"; } /// diff --git a/src/BaGet.Protocol/Extensions/HttpClientExtensions.cs b/src/BaGet.Protocol/Extensions/HttpClientExtensions.cs index 805456cc3..29adefbc9 100644 --- a/src/BaGet.Protocol/Extensions/HttpClientExtensions.cs +++ b/src/BaGet.Protocol/Extensions/HttpClientExtensions.cs @@ -1,24 +1,29 @@ using System.IO; using System.Net; using System.Net.Http; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Newtonsoft.Json; namespace BaGet.Protocol { internal static class HttpClientExtensions { - internal static readonly JsonSerializer Serializer = JsonSerializer.Create(JsonSettings); + //internal static readonly JsonSerializer Serializer = JsonSerializer.Create(JsonSettings); - internal static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings + //internal static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings + //{ + // DateTimeZoneHandling = DateTimeZoneHandling.Utc, + // DateParseHandling = DateParseHandling.DateTimeOffset, + // NullValueHandling = NullValueHandling.Ignore, + //}; + + internal static readonly JsonSerializerOptions Options = new JsonSerializerOptions { - DateTimeZoneHandling = DateTimeZoneHandling.Utc, - DateParseHandling = DateParseHandling.DateTimeOffset, - NullValueHandling = NullValueHandling.Ignore, + IgnoreNullValues = true, }; - public static async Task> DeserializeUrlAsync( + public static async Task> DeserializeUrlAsync( this HttpClient httpClient, string documentUrl, CancellationToken cancellationToken = default) @@ -30,7 +35,7 @@ public static async Task> DeserializeUrlAsync( { if (response.StatusCode != HttpStatusCode.OK) { - return new ResponseAndResult( + return new ResponseAndResult( HttpMethod.Get, documentUrl, response.StatusCode, @@ -40,17 +45,30 @@ public static async Task> DeserializeUrlAsync( } using (var stream = await response.Content.ReadAsStreamAsync()) - using (var textReader = new StreamReader(stream)) - using (var jsonReader = new JsonTextReader(textReader)) { - return new ResponseAndResult( + var result = await JsonSerializer.DeserializeAsync(stream, Options, cancellationToken); + + return new ResponseAndResult( HttpMethod.Get, documentUrl, response.StatusCode, response.ReasonPhrase, hasResult: true, - result: Serializer.Deserialize(jsonReader)); + result: result); } + + //using (var stream = await response.Content.ReadAsStreamAsync()) + //using (var textReader = new StreamReader(stream)) + //using (var jsonReader = new JsonTextReader(textReader)) + //{ + // return new ResponseAndResult( + // HttpMethod.Get, + // documentUrl, + // response.StatusCode, + // response.ReasonPhrase, + // hasResult: true, + // result: Serializer.Deserialize(jsonReader)); + //} } } } diff --git a/src/BaGet.Protocol/Models/AlternatePackage.cs b/src/BaGet.Protocol/Models/AlternatePackage.cs index 6699c2405..a5e272adc 100644 --- a/src/BaGet.Protocol/Models/AlternatePackage.cs +++ b/src/BaGet.Protocol/Models/AlternatePackage.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -10,22 +9,22 @@ namespace BaGet.Protocol.Models /// public class AlternatePackage { - [JsonProperty("@id")] + [JsonPropertyName("@id")] public string Url { get; set; } - [JsonProperty("@type")] + [JsonPropertyName("@type")] public string Type { get; set; } /// /// The ID of the alternate package. /// - [JsonProperty("id")] + [JsonPropertyName("id")] public string Id { get; set; } /// /// The allowed version range, or * if any version is allowed. /// - [JsonProperty("range")] + [JsonPropertyName("range")] public string Range { get; set; } } } diff --git a/src/BaGet.Protocol/Models/AutocompleteContext.cs b/src/BaGet.Protocol/Models/AutocompleteContext.cs index b499be46f..96b711c2f 100644 --- a/src/BaGet.Protocol/Models/AutocompleteContext.cs +++ b/src/BaGet.Protocol/Models/AutocompleteContext.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -9,7 +9,7 @@ public class AutocompleteContext Vocab = "http://schema.nuget.org/schema#" }; - [JsonProperty("@vocab")] + [JsonPropertyName("@vocab")] public string Vocab { get; set; } } } diff --git a/src/BaGet.Protocol/Models/AutocompleteResponse.cs b/src/BaGet.Protocol/Models/AutocompleteResponse.cs index ec186c409..5522656fe 100644 --- a/src/BaGet.Protocol/Models/AutocompleteResponse.cs +++ b/src/BaGet.Protocol/Models/AutocompleteResponse.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -10,18 +10,19 @@ namespace BaGet.Protocol.Models /// public class AutocompleteResponse { + [JsonPropertyName("@context")] public AutocompleteContext Context { get; set; } /// /// The total number of matches, disregarding skip and take. /// - [JsonProperty("totalHits")] + [JsonPropertyName("totalHits")] public long TotalHits { get; set; } /// /// The package IDs matched by the autocomplete query. /// - [JsonProperty("data")] + [JsonPropertyName("data")] public IReadOnlyList Data { get; set; } } } diff --git a/src/BaGet.Protocol/Models/CatalogIndex.cs b/src/BaGet.Protocol/Models/CatalogIndex.cs index fa6664b28..beeea373e 100644 --- a/src/BaGet.Protocol/Models/CatalogIndex.cs +++ b/src/BaGet.Protocol/Models/CatalogIndex.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -17,19 +17,19 @@ public class CatalogIndex /// /// A timestamp of the most recent commit. /// - [JsonProperty("commitTimeStamp")] + [JsonPropertyName("commitTimeStamp")] public DateTimeOffset CommitTimestamp { get; set; } /// /// The number of catalog pages in the catalog index. /// - [JsonProperty("count")] + [JsonPropertyName("count")] public int Count { get; set; } /// /// The items used to discover s. /// - [JsonProperty("items")] + [JsonPropertyName("items")] public List Items { get; set; } } } diff --git a/src/BaGet.Protocol/Models/CatalogLeaf.cs b/src/BaGet.Protocol/Models/CatalogLeaf.cs index cb6476006..f41ce62b9 100644 --- a/src/BaGet.Protocol/Models/CatalogLeaf.cs +++ b/src/BaGet.Protocol/Models/CatalogLeaf.cs @@ -1,6 +1,6 @@ using System; -using BaGet.Protocol.Internal; -using Newtonsoft.Json; +using System.Collections.Generic; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -17,44 +17,43 @@ public class CatalogLeaf : ICatalogLeafItem /// /// The URL to the current catalog leaf. /// - [JsonProperty("@id")] + [JsonPropertyName("@id")] public string CatalogLeafUrl { get; set; } /// /// The type of the current catalog leaf. /// - [JsonProperty("@type")] - [JsonConverter(typeof(CatalogLeafTypeConverter))] - public CatalogLeafType Type { get; set; } + [JsonPropertyName("@type")] + public IReadOnlyList Type { get; set; } /// /// The catalog commit ID associated with this catalog item. /// - [JsonProperty("catalog:commitId")] + [JsonPropertyName("catalog:commitId")] public string CommitId { get; set; } /// /// The commit timestamp of this catalog item. /// - [JsonProperty("catalog:commitTimeStamp")] + [JsonPropertyName("catalog:commitTimeStamp")] public DateTimeOffset CommitTimestamp { get; set; } /// /// The package ID of the catalog item. /// - [JsonProperty("id")] + [JsonPropertyName("id")] public string PackageId { get; set; } /// /// The published date of the package catalog item. /// - [JsonProperty("published")] + [JsonPropertyName("published")] public DateTimeOffset Published { get; set; } /// /// The package version of the catalog item. /// - [JsonProperty("version")] + [JsonPropertyName("version")] public string PackageVersion { get; set; } } } diff --git a/src/BaGet.Protocol/Models/CatalogLeafItem.cs b/src/BaGet.Protocol/Models/CatalogLeafItem.cs index a18441470..36dcb940e 100644 --- a/src/BaGet.Protocol/Models/CatalogLeafItem.cs +++ b/src/BaGet.Protocol/Models/CatalogLeafItem.cs @@ -1,6 +1,5 @@ using System; -using BaGet.Protocol.Internal; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -16,32 +15,31 @@ public class CatalogLeafItem : ICatalogLeafItem /// /// The URL to the current catalog leaf. /// - [JsonProperty("@id")] + [JsonPropertyName("@id")] public string CatalogLeafUrl { get; set; } /// /// The type of the current catalog leaf. /// - [JsonProperty("@type")] - [JsonConverter(typeof(CatalogLeafItemTypeConverter))] - public CatalogLeafType Type { get; set; } + [JsonPropertyName("@type")] + public string Type { get; set; } /// /// The commit timestamp of this catalog item. /// - [JsonProperty("commitTimeStamp")] + [JsonPropertyName("commitTimeStamp")] public DateTimeOffset CommitTimestamp { get; set; } /// /// The package ID of the catalog item. /// - [JsonProperty("nuget:id")] + [JsonPropertyName("nuget:id")] public string PackageId { get; set; } /// /// The package version of the catalog item. /// - [JsonProperty("nuget:version")] + [JsonPropertyName("nuget:version")] public string PackageVersion { get; set; } } } diff --git a/src/BaGet.Protocol/Models/CatalogLeafType.cs b/src/BaGet.Protocol/Models/CatalogLeafType.cs deleted file mode 100644 index b158a2393..000000000 --- a/src/BaGet.Protocol/Models/CatalogLeafType.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace BaGet.Protocol.Models -{ - // This class is based off https://github.com/NuGet/NuGet.Services.Metadata/blob/64af0b59c5a79e0143f0808b39946df9f16cb2e7/src/NuGet.Protocol.Catalog/Models/CatalogLeafType.cs - - /// - /// The type of a . - /// - /// See https://docs.microsoft.com/en-us/nuget/api/catalog-resource#item-types - /// - public enum CatalogLeafType - { - /// - /// The represents the snapshot of a package's metadata. - /// - PackageDetails = 1, - - /// - /// The represents a package that was deleted. - /// - PackageDelete = 2, - } -} diff --git a/src/BaGet.Protocol/Models/CatalogPage.cs b/src/BaGet.Protocol/Models/CatalogPage.cs index 3ad6f1edc..65f9e6d3f 100644 --- a/src/BaGet.Protocol/Models/CatalogPage.cs +++ b/src/BaGet.Protocol/Models/CatalogPage.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -17,25 +17,25 @@ public class CatalogPage /// /// A unique ID associated with the most recent commit in this page. /// - [JsonProperty("commitTimeStamp")] + [JsonPropertyName("commitTimeStamp")] public DateTimeOffset CommitTimestamp { get; set; } /// /// The number of items in the page. /// - [JsonProperty("count")] + [JsonPropertyName("count")] public int Count { get; set; } /// /// The items used to discover s. /// - [JsonProperty("items")] + [JsonPropertyName("items")] public List Items { get; set; } /// /// The URL to the Catalog Index. /// - [JsonProperty("parent")] + [JsonPropertyName("parent")] public string CatalogIndexUrl { get; set; } } } diff --git a/src/BaGet.Protocol/Models/CatalogPageItem.cs b/src/BaGet.Protocol/Models/CatalogPageItem.cs index 4d375d221..0240d9e23 100644 --- a/src/BaGet.Protocol/Models/CatalogPageItem.cs +++ b/src/BaGet.Protocol/Models/CatalogPageItem.cs @@ -1,5 +1,5 @@ using System; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -15,19 +15,19 @@ public class CatalogPageItem /// /// The URL to this item's corresponding . /// - [JsonProperty("@id")] + [JsonPropertyName("@id")] public string CatalogPageUrl { get; set; } /// /// A timestamp of the most recent commit in this page. /// - [JsonProperty("commitTimeStamp")] + [JsonPropertyName("commitTimeStamp")] public DateTimeOffset CommitTimestamp { get; set; } /// /// The number of items in the page. /// - [JsonProperty("count")] + [JsonPropertyName("count")] public int Count { get; set; } } } diff --git a/src/BaGet.Protocol/Models/DependencyGroupItem.cs b/src/BaGet.Protocol/Models/DependencyGroupItem.cs index a5fca2fde..c700a0144 100644 --- a/src/BaGet.Protocol/Models/DependencyGroupItem.cs +++ b/src/BaGet.Protocol/Models/DependencyGroupItem.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -13,13 +13,13 @@ public class DependencyGroupItem /// /// The target framework that these dependencies are applicable to. /// - [JsonProperty("targetFramework")] + [JsonPropertyName("targetFramework")] public string TargetFramework { get; set; } /// /// A list of dependencies. /// - [JsonProperty("dependencies", DefaultValueHandling = DefaultValueHandling.Ignore)] + [JsonPropertyName("dependencies")] // TODO: VERIFY IGNORED IF NULL public List Dependencies { get; set; } } } diff --git a/src/BaGet.Protocol/Models/DependencyItem.cs b/src/BaGet.Protocol/Models/DependencyItem.cs index b197bf117..95af13012 100644 --- a/src/BaGet.Protocol/Models/DependencyItem.cs +++ b/src/BaGet.Protocol/Models/DependencyItem.cs @@ -1,5 +1,5 @@ +using System.Text.Json.Serialization; using BaGet.Protocol.Internal; -using Newtonsoft.Json; namespace BaGet.Protocol.Models { @@ -13,14 +13,14 @@ public class DependencyItem /// /// The ID of the package dependency. /// - [JsonProperty("id")] + [JsonPropertyName("id")] public string Id { get; set; } /// /// The allowed version range of the dependency. /// - [JsonProperty("range")] - [JsonConverter(typeof(PackageDependencyRangeConverter))] + [JsonPropertyName("range")] + [JsonConverter(typeof(PackageDependencyRangeJsonConverter))] public string Range { get; set; } } } diff --git a/src/BaGet.Protocol/Models/ICatalogLeafItem.cs b/src/BaGet.Protocol/Models/ICatalogLeafItem.cs index 99bcb5468..2cd59a37f 100644 --- a/src/BaGet.Protocol/Models/ICatalogLeafItem.cs +++ b/src/BaGet.Protocol/Models/ICatalogLeafItem.cs @@ -26,10 +26,5 @@ public interface ICatalogLeafItem /// The package version of the catalog item. /// string PackageVersion { get; } - - /// - /// The type of the current catalog leaf. - /// - CatalogLeafType Type { get; } } } diff --git a/src/BaGet.Protocol/Models/PackageDeprecation.cs b/src/BaGet.Protocol/Models/PackageDeprecation.cs index c00a986c3..9445ade2d 100644 --- a/src/BaGet.Protocol/Models/PackageDeprecation.cs +++ b/src/BaGet.Protocol/Models/PackageDeprecation.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -13,26 +13,26 @@ public class PackageDeprecation /// /// The URL to the document used to produce this object. /// - [JsonProperty("@id")] + [JsonPropertyName("@id")] public string CatalogLeafUrl { get; set; } /// /// The reasons why the package was deprecated. /// Deprecation reasons include: "Legacy", "CriticalBugs", and "Other". /// - [JsonProperty("reasons")] + [JsonPropertyName("reasons")] public IReadOnlyList Reasons { get; set; } /// /// The additional details about this deprecation. /// - [JsonProperty("message")] + [JsonPropertyName("message")] public string Message { get; set; } /// /// The alternate package that should be used instead. /// - [JsonProperty("alternatePackage")] + [JsonPropertyName("alternatePackage")] public AlternatePackage AlternatePackage { get; set; } } } diff --git a/src/BaGet.Protocol/Models/PackageDetailsCatalogLeaf.cs b/src/BaGet.Protocol/Models/PackageDetailsCatalogLeaf.cs index 0f67b6576..5e8cf2575 100644 --- a/src/BaGet.Protocol/Models/PackageDetailsCatalogLeaf.cs +++ b/src/BaGet.Protocol/Models/PackageDetailsCatalogLeaf.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -17,43 +17,43 @@ public class PackageDetailsCatalogLeaf : CatalogLeaf /// /// The package's authors. /// - [JsonProperty("authors")] + [JsonPropertyName("authors")] public string Authors { get; set; } /// /// The package's copyright. /// - [JsonProperty("copyright")] + [JsonPropertyName("copyright")] public string Copyright { get; set; } /// /// A timestamp of when the package was first created. Fallback property: . /// - [JsonProperty("created")] + [JsonPropertyName("created")] public DateTimeOffset Created { get; set; } /// /// A timestamp of when the package was last edited. /// - [JsonProperty("lastEdited")] + [JsonPropertyName("lastEdited")] public DateTimeOffset LastEdited { get; set; } /// /// The dependencies of the package, grouped by target framework. /// - [JsonProperty("dependencyGroups")] + [JsonPropertyName("dependencyGroups")] public List DependencyGroups { get; set; } /// /// The package's description. /// - [JsonProperty("description")] + [JsonPropertyName("description")] public string Description { get; set; } /// /// The URL to the package's icon. /// - [JsonProperty("iconUrl")] + [JsonPropertyName("iconUrl")] public string IconUrl { get; set; } /// @@ -61,110 +61,110 @@ public class PackageDetailsCatalogLeaf : CatalogLeaf /// Note that the NuGet.org catalog had this wrong in some cases. /// Example: https://api.nuget.org/v3/catalog0/data/2016.03.11.21.02.55/mvid.fody.2.json /// - [JsonProperty("isPrerelease")] + [JsonPropertyName("isPrerelease")] public bool IsPrerelease { get; set; } /// /// The package's language. /// - [JsonProperty("language")] + [JsonPropertyName("language")] public string Language { get; set; } /// /// THe URL to the package's license. /// - [JsonProperty("licenseUrl")] + [JsonPropertyName("licenseUrl")] public string LicenseUrl { get; set; } /// /// Whether the pacakge is listed. /// - [JsonProperty("listed")] + [JsonPropertyName("listed")] public bool? Listed { get; set; } /// /// The minimum NuGet client version needed to use this package. /// - [JsonProperty("minClientVersion")] + [JsonPropertyName("minClientVersion")] public string MinClientVersion { get; set; } /// /// The hash of the package encoded using Base64. /// Hash algorithm can be detected using . /// - [JsonProperty("packageHash")] + [JsonPropertyName("packageHash")] public string PackageHash { get; set; } /// /// The algorithm used to hash . /// - [JsonProperty("packageHashAlgorithm")] + [JsonPropertyName("packageHashAlgorithm")] public string PackageHashAlgorithm { get; set; } /// /// The size of the package .nupkg in bytes. /// - [JsonProperty("packageSize")] + [JsonPropertyName("packageSize")] public long PackageSize { get; set; } /// /// The URL for the package's home page. /// - [JsonProperty("projectUrl")] + [JsonPropertyName("projectUrl")] public string ProjectUrl { get; set; } /// /// The package's release notes. /// - [JsonProperty("releaseNotes")] + [JsonPropertyName("releaseNotes")] public string ReleaseNotes { get; set; } /// /// If true, the package requires its license to be accepted. /// - [JsonProperty("requireLicenseAcceptance")] + [JsonPropertyName("requireLicenseAcceptance")] public bool? RequireLicenseAcceptance { get; set; } /// /// The package's summary. /// - [JsonProperty("summary")] + [JsonPropertyName("summary")] public string Summary { get; set; } /// /// The package's tags. /// - [JsonProperty("tags")] + [JsonPropertyName("tags")] public List Tags { get; set; } /// /// The package's title. /// - [JsonProperty("title")] + [JsonPropertyName("title")] public string Title { get; set; } /// /// The version string as it's originally found in the .nuspec. /// - [JsonProperty("verbatimVersion")] + [JsonPropertyName("verbatimVersion")] public string VerbatimVersion { get; set; } /// /// The package's License Expression. /// - [JsonProperty("licenseExpression")] + [JsonPropertyName("licenseExpression")] public string LicenseExpression { get; set; } /// /// The package's license file. /// - [JsonProperty("licenseFile")] + [JsonPropertyName("licenseFile")] public string LicenseFile { get; set; } /// /// The package's icon file. /// - [JsonProperty("iconFile")] + [JsonPropertyName("iconFile")] public string IconFile { get; set; } } } diff --git a/src/BaGet.Protocol/Models/PackageMetadata.cs b/src/BaGet.Protocol/Models/PackageMetadata.cs index 044e3f51a..2e9563d17 100644 --- a/src/BaGet.Protocol/Models/PackageMetadata.cs +++ b/src/BaGet.Protocol/Models/PackageMetadata.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -14,116 +14,116 @@ public class PackageMetadata /// /// The URL to the document used to produce this object. /// - [JsonProperty("@id")] + [JsonPropertyName("@id")] public string CatalogLeafUrl { get; set; } /// /// The ID of the package. /// - [JsonProperty("id")] + [JsonPropertyName("id")] public string PackageId { get; set; } /// /// The full NuGet version after normalization, including any SemVer 2.0.0 build metadata. /// - [JsonProperty("version")] + [JsonPropertyName("version")] public string Version { get; set; } /// /// The package's authors. /// - [JsonProperty("authors")] + [JsonPropertyName("authors")] public string Authors { get; set; } /// /// The dependencies of the package, grouped by target framework. /// - [JsonProperty("dependencyGroups")] + [JsonPropertyName("dependencyGroups")] public IReadOnlyList DependencyGroups { get; set; } /// /// The deprecation associated with the package, if any. /// - [JsonProperty("deprecation")] + [JsonPropertyName("deprecation")] public PackageDeprecation Deprecation { get; set; } /// /// The package's description. /// - [JsonProperty("description")] + [JsonPropertyName("description")] public string Description { get; set; } /// /// The URL to the package's icon. /// - [JsonProperty("iconUrl")] + [JsonPropertyName("iconUrl")] public string IconUrl { get; set; } /// /// The package's language. /// - [JsonProperty("language")] + [JsonPropertyName("language")] public string Language { get; set; } /// /// The URL to the package's license. /// - [JsonProperty("licenseUrl")] + [JsonPropertyName("licenseUrl")] public string LicenseUrl { get; set; } /// /// Whether the package is listed in search results. /// If , the package should be considered as listed. /// - [JsonProperty("listed")] + [JsonPropertyName("listed")] public bool? Listed { get; set; } /// /// The minimum NuGet client version needed to use this package. /// - [JsonProperty("minClientVersion")] + [JsonPropertyName("minClientVersion")] public string MinClientVersion { get; set; } /// /// The URL to download the package's content. /// - [JsonProperty("packageContent")] + [JsonPropertyName("packageContent")] public string PackageContentUrl { get; set; } /// /// The URL for the package's home page. /// - [JsonProperty("projectUrl")] + [JsonPropertyName("projectUrl")] public string ProjectUrl { get; set; } /// /// The package's publish date. /// - [JsonProperty("published")] + [JsonPropertyName("published")] public DateTimeOffset Published { get; set; } /// /// If true, the package requires its license to be accepted. /// - [JsonProperty("requireLicenseAcceptance")] + [JsonPropertyName("requireLicenseAcceptance")] public bool RequireLicenseAcceptance { get; set; } /// /// The package's summary. /// - [JsonProperty("summary")] + [JsonPropertyName("summary")] public string Summary { get; set; } /// /// The package's tags. /// - [JsonProperty("tags")] + [JsonPropertyName("tags")] public IReadOnlyList Tags { get; set; } /// /// The package's title. /// - [JsonProperty("title")] + [JsonPropertyName("title")] public string Title { get; set; } } } diff --git a/src/BaGet.Protocol/Models/PackageVersionsResponse.cs b/src/BaGet.Protocol/Models/PackageVersionsResponse.cs index e42e8d3bb..4a8a64f93 100644 --- a/src/BaGet.Protocol/Models/PackageVersionsResponse.cs +++ b/src/BaGet.Protocol/Models/PackageVersionsResponse.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -13,7 +13,7 @@ public class PackageVersionsResponse /// /// The versions, lowercased and normalized. /// - [JsonProperty("versions")] + [JsonPropertyName("versions")] public IReadOnlyList Versions { get; set; } } } diff --git a/src/BaGet.Protocol/Models/RegistrationIndexPage.cs b/src/BaGet.Protocol/Models/RegistrationIndexPage.cs index 0f3679670..e7190af5c 100644 --- a/src/BaGet.Protocol/Models/RegistrationIndexPage.cs +++ b/src/BaGet.Protocol/Models/RegistrationIndexPage.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -13,34 +13,34 @@ public class RegistrationIndexPage /// /// The URL to the registration page. /// - [JsonProperty("@id")] + [JsonPropertyName("@id")] public string RegistrationPageUrl { get; set; } /// /// The number of registration leafs in the page. /// - [JsonProperty("count")] + [JsonPropertyName("count")] public int Count { get; set; } /// /// if this package's registration is paged. The items can be found /// by following the page's . /// - [JsonProperty("items")] + [JsonPropertyName("items")] public IReadOnlyList ItemsOrNull { get; set; } /// /// This page's lowest package version. The version should be lowercased, normalized, /// and the SemVer 2.0.0 build metadata removed, if any. /// - [JsonProperty("lower")] + [JsonPropertyName("lower")] public string Lower { get; set; } /// /// This page's highest package version. The version should be lowercased, normalized, /// and the SemVer 2.0.0 build metadata removed, if any. /// - [JsonProperty("upper")] + [JsonPropertyName("upper")] public string Upper { get; set; } } } diff --git a/src/BaGet.Protocol/Models/RegistrationIndexPageItem.cs b/src/BaGet.Protocol/Models/RegistrationIndexPageItem.cs index afca418b0..91668366a 100644 --- a/src/BaGet.Protocol/Models/RegistrationIndexPageItem.cs +++ b/src/BaGet.Protocol/Models/RegistrationIndexPageItem.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -12,19 +12,19 @@ public class RegistrationIndexPageItem /// /// The URL to the registration leaf. /// - [JsonProperty("@id")] + [JsonPropertyName("@id")] public string RegistrationLeafUrl { get; set; } /// /// The catalog entry containing the package metadata. /// - [JsonProperty("catalogEntry")] + [JsonPropertyName("catalogEntry")] public PackageMetadata PackageMetadata { get; set; } /// /// The URL to the package content (.nupkg) /// - [JsonProperty("packageContent")] + [JsonPropertyName("packageContent")] public string PackageContentUrl { get; set; } } } diff --git a/src/BaGet.Protocol/Models/RegistrationIndexResponse.cs b/src/BaGet.Protocol/Models/RegistrationIndexResponse.cs index a4dbc24ce..79cf01bac 100644 --- a/src/BaGet.Protocol/Models/RegistrationIndexResponse.cs +++ b/src/BaGet.Protocol/Models/RegistrationIndexResponse.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -20,26 +20,26 @@ public class RegistrationIndexResponse /// /// The URL to the registration index. /// - [JsonProperty("@id")] + [JsonPropertyName("@id")] public string RegistrationIndexUrl { get; set; } /// /// The registration index's type. /// - [JsonProperty("@type")] + [JsonPropertyName("@type")] public IReadOnlyList Type { get; set; } /// - /// The number of registration pages. See . + /// The number of registration pages. See . /// - [JsonProperty("count")] + [JsonPropertyName("count")] public int Count { get; set; } /// /// The pages that contain all of the versions of the package, ordered /// by the package's version. /// - [JsonProperty("items")] + [JsonPropertyName("items")] public IReadOnlyList Pages { get; set; } } } diff --git a/src/BaGet.Protocol/Models/RegistrationLeafResponse.cs b/src/BaGet.Protocol/Models/RegistrationLeafResponse.cs index 33718ac0f..c95ca7ed1 100644 --- a/src/BaGet.Protocol/Models/RegistrationLeafResponse.cs +++ b/src/BaGet.Protocol/Models/RegistrationLeafResponse.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -20,38 +20,38 @@ public class RegistrationLeafResponse /// /// The URL to the registration leaf. /// - [JsonProperty("@id")] + [JsonPropertyName("@id")] public string RegistrationLeafUrl { get; set; } /// /// The registration leaf's type. /// - [JsonProperty("@type")] + [JsonPropertyName("@type")] public IReadOnlyList Type { get; set; } /// /// Whether the package is listed. /// - [JsonProperty("listed")] + [JsonPropertyName("listed")] public bool Listed { get; set; } /// /// The URL to the package content (.nupkg). /// - [JsonProperty("packageContent")] + [JsonPropertyName("packageContent")] public string PackageContentUrl { get; set; } /// /// The date the package was published. On NuGet.org, /// is set to the year 1900 if the package is unlisted. /// - [JsonProperty("published")] + [JsonPropertyName("published")] public DateTimeOffset Published { get; set; } /// /// The URL to the package's registration index. /// - [JsonProperty("registration")] + [JsonPropertyName("registration")] public string RegistrationIndexUrl { get; set; } } } diff --git a/src/BaGet.Protocol/Models/SearchContext.cs b/src/BaGet.Protocol/Models/SearchContext.cs index 7c63a03cf..6abfb92ea 100644 --- a/src/BaGet.Protocol/Models/SearchContext.cs +++ b/src/BaGet.Protocol/Models/SearchContext.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -13,10 +13,10 @@ public static SearchContext Default(string registrationBaseUrl) }; } - [JsonProperty("@vocab")] + [JsonPropertyName("@vocab")] public string Vocab { get; set; } - [JsonProperty("@base")] + [JsonPropertyName("@base")] public string Base { get; set; } } } diff --git a/src/BaGet.Protocol/Models/SearchResponse.cs b/src/BaGet.Protocol/Models/SearchResponse.cs index 50c2d215b..70fce7393 100644 --- a/src/BaGet.Protocol/Models/SearchResponse.cs +++ b/src/BaGet.Protocol/Models/SearchResponse.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -10,19 +10,19 @@ namespace BaGet.Protocol.Models /// public class SearchResponse { - [JsonProperty("@context")] + [JsonPropertyName("@context")] public SearchContext Context { get; set; } /// /// The total number of matches, disregarding skip and take. /// - [JsonProperty("totalHits")] + [JsonPropertyName("totalHits")] public long TotalHits { get; set; } /// /// The packages that matched the search query. /// - [JsonProperty("data")] + [JsonPropertyName("data")] public IReadOnlyList Data { get; set; } } } diff --git a/src/BaGet.Protocol/Models/SearchResult.cs b/src/BaGet.Protocol/Models/SearchResult.cs index 365736680..4b0bb3681 100644 --- a/src/BaGet.Protocol/Models/SearchResult.cs +++ b/src/BaGet.Protocol/Models/SearchResult.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; +using System.Text.Json.Serialization; using BaGet.Protocol.Internal; -using Newtonsoft.Json; namespace BaGet.Protocol.Models { @@ -14,81 +14,81 @@ public class SearchResult /// /// The ID of the matched package. /// - [JsonProperty("id")] + [JsonPropertyName("id")] public string PackageId { get; set; } /// /// The latest version of the matched pacakge. This is the full NuGet version after normalization, /// including any SemVer 2.0.0 build metadata. /// - [JsonProperty("version")] + [JsonPropertyName("version")] public string Version { get; set; } /// /// The description of the matched package. /// - [JsonProperty("description")] + [JsonPropertyName("description")] public string Description { get; set; } /// /// The authors of the matched package. /// - [JsonProperty("authors")] - [JsonConverter(typeof(SingleOrListConverter))] + [JsonPropertyName("authors")] + [JsonConverter(typeof(StringOrStringArrayJsonConverter))] public IReadOnlyList Authors { get; set; } /// /// The URL of the matched package's icon. /// - [JsonProperty("iconUrl")] + [JsonPropertyName("iconUrl")] public string IconUrl { get; set; } /// /// The URL of the matched package's license. /// - [JsonProperty("licenseUrl")] + [JsonPropertyName("licenseUrl")] public string LicenseUrl { get; set; } /// /// The URL of the matched package's homepage. /// - [JsonProperty("projectUrl")] + [JsonPropertyName("projectUrl")] public string ProjectUrl { get; set; } /// /// The URL for the matched package's registration index. /// - [JsonProperty("registration")] + [JsonPropertyName("registration")] public string RegistrationIndexUrl { get; set; } /// /// The summary of the matched package. /// - [JsonProperty("summary")] + [JsonPropertyName("summary")] public string Summary { get; set; } /// /// The tags of the matched package. /// - [JsonProperty("tags")] + [JsonPropertyName("tags")] public IReadOnlyList Tags { get; set; } /// /// The title of the matched package. /// - [JsonProperty("title")] + [JsonPropertyName("title")] public string Title { get; set; } /// /// The total downloads for all versions of the matched package. /// - [JsonProperty("totalDownloads")] + [JsonPropertyName("totalDownloads")] public long TotalDownloads { get; set; } /// /// The versions of the matched package. /// - [JsonProperty("versions")] + [JsonPropertyName("versions")] public IReadOnlyList Versions { get; set; } } } diff --git a/src/BaGet.Protocol/Models/SearchResultVersion.cs b/src/BaGet.Protocol/Models/SearchResultVersion.cs index b7f6823f1..a3a2cb237 100644 --- a/src/BaGet.Protocol/Models/SearchResultVersion.cs +++ b/src/BaGet.Protocol/Models/SearchResultVersion.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -12,19 +12,19 @@ public class SearchResultVersion /// /// The registration leaf URL for this single version of the matched package. /// - [JsonProperty("@id")] + [JsonPropertyName("@id")] public string RegistrationLeafUrl { get; set; } /// /// The package's full NuGet version after normalization, including any SemVer 2.0.0 build metadata. /// - [JsonProperty("version")] + [JsonPropertyName("version")] public string Version { get; set; } /// /// The downloads for this single version of the matched package. /// - [JsonProperty("downloads")] + [JsonPropertyName("downloads")] public long Downloads { get; set; } } } diff --git a/src/BaGet.Protocol/Models/ServiceIndexItem.cs b/src/BaGet.Protocol/Models/ServiceIndexItem.cs index 76718489e..36ba43881 100644 --- a/src/BaGet.Protocol/Models/ServiceIndexItem.cs +++ b/src/BaGet.Protocol/Models/ServiceIndexItem.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -12,19 +12,19 @@ public class ServiceIndexItem /// /// The resource's base URL. /// - [JsonProperty("@id")] + [JsonPropertyName("@id")] public string ResourceUrl { get; set; } /// /// The resource's type. /// - [JsonProperty("@type")] + [JsonPropertyName("@type")] public string Type { get; set; } /// /// Human readable comments about the resource. /// - [JsonProperty("comment")] + [JsonPropertyName("comment")] public string Comment { get; set; } } } diff --git a/src/BaGet.Protocol/Models/ServiceIndexResponse.cs b/src/BaGet.Protocol/Models/ServiceIndexResponse.cs index 5988a8f4c..4b0327124 100644 --- a/src/BaGet.Protocol/Models/ServiceIndexResponse.cs +++ b/src/BaGet.Protocol/Models/ServiceIndexResponse.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace BaGet.Protocol.Models { @@ -13,13 +13,13 @@ public class ServiceIndexResponse /// /// The service index's version. /// - [JsonProperty("version")] + [JsonPropertyName("version")] public string Version { get; set; } /// /// The resources declared by this service index. /// - [JsonProperty("resources")] + [JsonPropertyName("resources")] public IReadOnlyList Resources { get; set; } } } diff --git a/src/BaGet.UI/src/DisplayPackage/DisplayPackage.tsx b/src/BaGet.UI/src/DisplayPackage/DisplayPackage.tsx index a531e26ab..834bb4264 100644 --- a/src/BaGet.UI/src/DisplayPackage/DisplayPackage.tsx +++ b/src/BaGet.UI/src/DisplayPackage/DisplayPackage.tsx @@ -36,7 +36,7 @@ interface IPackage { licenseUrl: string; downloadUrl: string; repositoryUrl: string; - repositoryType: string; + repositoryType?: string; releaseNotes: string; totalDownloads: number; packageType: PackageType; diff --git a/src/BaGet.UI/src/DisplayPackage/Registration.tsx b/src/BaGet.UI/src/DisplayPackage/Registration.tsx index dea000bbc..fc7d60ee0 100644 --- a/src/BaGet.UI/src/DisplayPackage/Registration.tsx +++ b/src/BaGet.UI/src/DisplayPackage/Registration.tsx @@ -29,7 +29,7 @@ export interface ICatalogEntry { listed: boolean; packageTypes: string[]; repositoryUrl: string; - repositoryType: string; + repositoryType?: string; authors: string; tags: string[]; dependencyGroups: IDependencyGroup[]; diff --git a/src/BaGet.UI/src/DisplayPackage/SourceRepository.tsx b/src/BaGet.UI/src/DisplayPackage/SourceRepository.tsx index e64764a89..6d996553a 100644 --- a/src/BaGet.UI/src/DisplayPackage/SourceRepository.tsx +++ b/src/BaGet.UI/src/DisplayPackage/SourceRepository.tsx @@ -3,7 +3,7 @@ import './SourceRepository.css'; interface ISourceRepositoryProps { url: string; - type: string; + type?: string; } class SourceRepository extends React.Component { diff --git a/tests/BaGet.Core.Tests/Metadata/ModelTests.cs b/tests/BaGet.Core.Tests/Metadata/ModelTests.cs new file mode 100644 index 000000000..61d3fcf28 --- /dev/null +++ b/tests/BaGet.Core.Tests/Metadata/ModelTests.cs @@ -0,0 +1,237 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.Json.Serialization; +using BaGet.Protocol.Models; +using Xunit; + +namespace BaGet.Core.Tests.Metadata +{ + public class ModelTests + { + /// + /// BaGet extends the NuGet protocol to add more functionality. + /// Since System.Text.Json does not support polymorphic serialization, + /// this was implemented by duplicating the protocol's models from the + /// "BaGet.Protocol" project. These tests ensure that the duplicates + /// stay in sync with the original protocol models. + /// + public static IEnumerable ExtendedModelsData() + { + yield return new object[] + { + new ExtendedModelData + { + OriginalType = typeof(RegistrationIndexResponse), + DerivedType = typeof(BaGetRegistrationIndexResponse), + + AddedProperties = new Dictionary + { + { "TotalDownloads", typeof(long) }, + }, + + ModifiedProperties = new Dictionary + { + { + "Pages", + ( + From: typeof(IReadOnlyList), + To: typeof(IReadOnlyList) + ) + }, + } + } + }; + + yield return new object[] + { + new ExtendedModelData + { + OriginalType = typeof(RegistrationIndexPage), + DerivedType = typeof(BaGetRegistrationIndexPage), + + ModifiedProperties = new Dictionary + { + { + "ItemsOrNull", + ( + From: typeof(IReadOnlyList), + To: typeof(IReadOnlyList) + ) + }, + } + } + }; + + yield return new object[] + { + new ExtendedModelData + { + OriginalType = typeof(RegistrationIndexPageItem), + DerivedType = typeof(BaGetRegistrationIndexPageItem), + + ModifiedProperties = new Dictionary + { + { + "PackageMetadata", ( From: typeof(PackageMetadata), To: typeof(BaGetPackageMetadata) ) + }, + } + } + }; + + yield return new object[] + { + new ExtendedModelData + { + OriginalType = typeof(PackageMetadata), + DerivedType = typeof(BaGetPackageMetadata), + + AddedProperties = new Dictionary + { + { "Downloads", typeof(long) }, + { "HasReadme", typeof(bool) }, + { "PackageTypes", typeof(IReadOnlyList) }, + { "ReleaseNotes", typeof(string) }, + { "RepositoryUrl", typeof(string) }, + { "RepositoryType", typeof(string) }, + } + } + }; + } + + [Theory] + [MemberData(nameof(ExtendedModelsData))] + public void ValidateExtendedModels(ExtendedModelData data) + { + IReadOnlyDictionary originalProperties = data + .OriginalType + .GetProperties() + .ToDictionary(p => p.Name, p => p); + IReadOnlyDictionary derivedProperties = data + .DerivedType + .GetProperties() + .ToDictionary(p => p.Name, p => p); + + // Check that all properties on the original model are present on the derived model. + var missingProperties = originalProperties.Keys.Where(name => !derivedProperties.ContainsKey(name)); + + Assert.True( + !missingProperties.Any(), + $"The following properties are missing from the derived type: {string.Join(',', missingProperties)}"); + + // Check that all properties on the derived model are as expected compared to the original model. + foreach (var derivedProperty in derivedProperties.Values) + { + // If the property was added, check that it is not on the original type. + if (data.AddedProperties.TryGetValue(derivedProperty.Name, out var addedType)) + { + Assert.True( + !originalProperties.ContainsKey(derivedProperty.Name), + $"Added property '{derivedProperty.Name}' exists on the original type {data.OriginalType}"); + Assert.True( + addedType == derivedProperty.PropertyType, + $"Added property '{derivedProperty.Name}' on type {data.DerivedType} has unexpected property type\n" + + $"Expected: {addedType}\n" + + $"Actual: {derivedProperty.PropertyType}"); + continue; + } + + // This property should exist on both the original and derived models. + // This property should have the same "JsonPropertyName" attribute values. + var originalProperty = Assert.Contains(derivedProperty.Name, originalProperties); + + var originalJsonName = GetAttributeArgs(originalProperty)?.FirstOrDefault(); + var derivedJsonName = GetAttributeArgs(derivedProperty)?.FirstOrDefault(); + + Assert.True( + originalJsonName != null, + $"Property '{originalProperty.Name}' on type '{data.OriginalType}' " + + "does not have a JsonPropertyName attribute"); + Assert.True( + derivedJsonName != null, + $"Property '{derivedProperty.Name}' on type '{data.DerivedType}' " + + "does not have a JsonPropertyName attribute"); + Assert.True( + originalJsonName.ToString() == derivedJsonName.ToString(), + $"Property '{derivedProperty.Name}' on type '{data.DerivedType}' " + + "has a different JsonPropertyName attribute value than " + + $"on type '{data.OriginalType}'.\nExpected: '{originalJsonName}'\n" + + $"Actual: '{derivedJsonName}'"); + + var originalJsonConverterArgs = GetAttributeArgs(originalProperty); + var derivedJsonConverterArgs = GetAttributeArgs(derivedProperty); + + // If the property was modified, check that the property types are expected. + if (data.ModifiedProperties.TryGetValue(derivedProperty.Name, out var modifiedTypes)) + { + Assert.True( + originalProperty.PropertyType == modifiedTypes.From, + $"Modified property '{originalProperty.Name}' on type {data.OriginalType} has unexpected property type\n" + + $"Expected: {modifiedTypes.From}\n" + + $"Actual: {originalProperty.PropertyType}"); + Assert.True( + derivedProperty.PropertyType == modifiedTypes.To, + $"Modified property '{derivedProperty.Name}' on type {data.DerivedType} has unexpected property type\n" + + $"Expected: {modifiedTypes.To}\n" + + $"Actual: {derivedProperty.PropertyType}"); + + if (originalJsonConverterArgs != null || derivedJsonConverterArgs != null) + { + throw new NotSupportedException( + "JSON converters on modified properties is not supported"); + } + + continue; + } + + // Otherwise, this property should be identical to the original property. + Assert.True( + originalProperty.PropertyType == derivedProperty.PropertyType, + $"Property '{derivedProperty.Name}' on type {data.DerivedType} has unexpected property type\n" + + $"Expected: {originalProperty.PropertyType}\n" + + $"Actual: {derivedProperty.PropertyType}"); + Assert.True( + originalJsonConverterArgs?.First() == derivedJsonConverterArgs?.First(), + $"Property '{derivedProperty.Name}' on type '{data.DerivedType}' " + + "has unexpected JsonConverter value.\n" + + $"Expected: '{originalJsonConverterArgs?.First()}'\n" + + $"Actual: '{derivedJsonConverterArgs?.First()}'"); + } + } + + private IList GetAttributeArgs(PropertyInfo property) + { + return property + .CustomAttributes + ?.SingleOrDefault(x => x.AttributeType == typeof(TAttribute)) + ?.ConstructorArguments; + } + + public class ExtendedModelData + { + /// + /// The model's type in the "BaGet.Protocol" project that was extended. + /// + public Type OriginalType { get; set; } + + /// + /// The model's type in the "BaGet.Core" project that extends a + /// type from the "BaGet.Protocol" project. + /// + public Type DerivedType { get; set; } + + /// + /// The properties added by the model type in the "BaGet.Core" project. + /// + public Dictionary AddedProperties { get; set; } = new Dictionary(); + + /// + /// The properties whose types were modified by the model type in the + /// "BaGet.Core" project. + /// + public Dictionary ModifiedProperties { get; set; } + = new Dictionary(); + } + } +} diff --git a/tests/BaGet.Protocol.Tests/RawCatalogClientTests.cs b/tests/BaGet.Protocol.Tests/RawCatalogClientTests.cs index 5ebb2ccf8..55ecc029a 100644 --- a/tests/BaGet.Protocol.Tests/RawCatalogClientTests.cs +++ b/tests/BaGet.Protocol.Tests/RawCatalogClientTests.cs @@ -45,7 +45,8 @@ public async Task GetPackageDetailsLeaf() var leaf = await _target.GetPackageDetailsLeafAsync(TestData.PackageDetailsCatalogLeafUrl); Assert.Equal(TestData.PackageDetailsCatalogLeafUrl, leaf.CatalogLeafUrl); - Assert.Equal(CatalogLeafType.PackageDetails, leaf.Type); + Assert.Equal("PackageDetails", leaf.Type[0]); + Assert.Equal("catalog:Permalink", leaf.Type[1]); Assert.Equal("Test.Package", leaf.PackageId); Assert.Equal("1.0.0", leaf.PackageVersion); @@ -57,7 +58,8 @@ public async Task GetPackageDeleteLeaf() var leaf = await _target.GetPackageDeleteLeafAsync(TestData.PackageDeleteCatalogLeafUrl); Assert.Equal(TestData.PackageDeleteCatalogLeafUrl, leaf.CatalogLeafUrl); - Assert.Equal(CatalogLeafType.PackageDelete, leaf.Type); + Assert.Equal("PackageDelete", leaf.Type[0]); + Assert.Equal("catalog:Permalink", leaf.Type[1]); Assert.Equal("Deleted.Package", leaf.PackageId); Assert.Equal("1.0.0", leaf.PackageVersion); diff --git a/tests/BaGet.Tests/ApiIntegrationTests.cs b/tests/BaGet.Tests/ApiIntegrationTests.cs index 19850ac56..780a0deef 100644 --- a/tests/BaGet.Tests/ApiIntegrationTests.cs +++ b/tests/BaGet.Tests/ApiIntegrationTests.cs @@ -99,7 +99,7 @@ public async Task AutocompleteReturnsOk() Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(@"{ - ""context"": { + ""@context"": { ""@vocab"": ""http://schema.nuget.org/schema#"" }, ""totalHits"": 1, @@ -118,7 +118,7 @@ public async Task AutocompleteReturnsEmpty() Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(@"{ - ""context"": { + ""@context"": { ""@vocab"": ""http://schema.nuget.org/schema#"" }, ""totalHits"": 0, @@ -188,7 +188,6 @@ public async Task PackageMetadataReturnsOk() Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(@"{ - ""totalDownloads"": 0, ""@id"": ""http://localhost/v3/registration/defaultpackage/index.json"", ""@type"": [ ""catalog:CatalogRoot"", @@ -200,9 +199,12 @@ public async Task PackageMetadataReturnsOk() { ""@id"": ""http://localhost/v3/registration/defaultpackage/index.json"", ""count"": 1, + ""lower"": ""1.2.3"", + ""upper"": ""1.2.3"", ""items"": [ { ""@id"": ""http://localhost/v3/registration/defaultpackage/1.2.3.json"", + ""packageContent"": ""http://localhost/v3/package/defaultpackage/1.2.3/defaultpackage.1.2.3.nupkg"", ""catalogEntry"": { ""downloads"": 0, ""hasReadme"": false, @@ -211,13 +213,10 @@ public async Task PackageMetadataReturnsOk() ], ""releaseNotes"": """", ""repositoryUrl"": """", - ""repositoryType"": null, - ""@id"": null, ""id"": ""DefaultPackage"", ""version"": ""1.2.3"", ""authors"": ""Default package author"", ""dependencyGroups"": [], - ""deprecation"": null, ""description"": ""Default package description"", ""iconUrl"": """", ""language"": """", @@ -231,14 +230,12 @@ public async Task PackageMetadataReturnsOk() ""summary"": """", ""tags"": [], ""title"": """" - }, - ""packageContent"": ""http://localhost/v3/package/defaultpackage/1.2.3/defaultpackage.1.2.3.nupkg"" + } } - ], - ""lower"": ""1.2.3"", - ""upper"": ""1.2.3"" + ] } - ] + ], + ""totalDownloads"": 0 }", json); } @@ -259,7 +256,6 @@ public async Task PackageMetadataLeafReturnsOk() Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(@"{ - ""downloads"": 0, ""@id"": ""http://localhost/v3/registration/defaultpackage/1.2.3.json"", ""@type"": [ ""Package"", diff --git a/tests/BaGet.Tests/TestData.resx b/tests/BaGet.Tests/TestData.resx index e2b63e4f3..e804f8787 100644 --- a/tests/BaGet.Tests/TestData.resx +++ b/tests/BaGet.Tests/TestData.resx @@ -1,17 +1,17 @@  - @@ -118,6 +118,6 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - {"version":"3.0.0","resources":[{"@id":"http://localhost/api/v2/package","@type":"PackagePublish/2.0.0","comment":null},{"@id":"http://localhost/api/v2/symbol","@type":"SymbolPackagePublish/4.9.0","comment":null},{"@id":"http://localhost/v3/search","@type":"SearchQueryService","comment":null},{"@id":"http://localhost/v3/search","@type":"SearchQueryService/3.0.0-beta","comment":null},{"@id":"http://localhost/v3/search","@type":"SearchQueryService/3.0.0-rc","comment":null},{"@id":"http://localhost/v3/registration","@type":"RegistrationsBaseUrl","comment":null},{"@id":"http://localhost/v3/registration","@type":"RegistrationsBaseUrl/3.0.0-rc","comment":null},{"@id":"http://localhost/v3/registration","@type":"RegistrationsBaseUrl/3.0.0-beta","comment":null},{"@id":"http://localhost/v3/package","@type":"PackageBaseAddress/3.0.0","comment":null},{"@id":"http://localhost/v3/autocomplete","@type":"SearchAutocompleteService","comment":null},{"@id":"http://localhost/v3/autocomplete","@type":"SearchAutocompleteService/3.0.0-rc","comment":null},{"@id":"http://localhost/v3/autocomplete","@type":"SearchAutocompleteService/3.0.0-beta","comment":null}]} + {"version":"3.0.0","resources":[{"@id":"http://localhost/api/v2/package","@type":"PackagePublish/2.0.0"},{"@id":"http://localhost/api/v2/symbol","@type":"SymbolPackagePublish/4.9.0"},{"@id":"http://localhost/v3/search","@type":"SearchQueryService"},{"@id":"http://localhost/v3/search","@type":"SearchQueryService/3.0.0-beta"},{"@id":"http://localhost/v3/search","@type":"SearchQueryService/3.0.0-rc"},{"@id":"http://localhost/v3/registration","@type":"RegistrationsBaseUrl"},{"@id":"http://localhost/v3/registration","@type":"RegistrationsBaseUrl/3.0.0-rc"},{"@id":"http://localhost/v3/registration","@type":"RegistrationsBaseUrl/3.0.0-beta"},{"@id":"http://localhost/v3/package","@type":"PackageBaseAddress/3.0.0"},{"@id":"http://localhost/v3/autocomplete","@type":"SearchAutocompleteService"},{"@id":"http://localhost/v3/autocomplete","@type":"SearchAutocompleteService/3.0.0-rc"},{"@id":"http://localhost/v3/autocomplete","@type":"SearchAutocompleteService/3.0.0-beta"}]} - \ No newline at end of file + From 34cbfc5922901b6eb57bb0b8492f8cf4364d457a Mon Sep 17 00:00:00 2001 From: Loic Sharma Date: Sun, 19 Jul 2020 17:07:00 -0700 Subject: [PATCH 02/10] Test json converter; clean upP --- .../PackageDependencyRangeJsonConverter.cs | 5 +- .../StringOrStringArrayJsonConverter.cs | 5 +- .../Extensions/HttpClientExtensions.cs | 22 ---- .../Models/DependencyGroupItem.cs | 2 +- ...ackageDependencyRangeJsonConverterTests.cs | 87 +++++++++++++ .../StringOrStringArrayJsonConverterTests.cs | 123 ++++++++++++++++++ 6 files changed, 219 insertions(+), 25 deletions(-) create mode 100644 tests/BaGet.Protocol.Tests/Converters/PackageDependencyRangeJsonConverterTests.cs create mode 100644 tests/BaGet.Protocol.Tests/Converters/StringOrStringArrayJsonConverterTests.cs diff --git a/src/BaGet.Protocol/Converters/PackageDependencyRangeJsonConverter.cs b/src/BaGet.Protocol/Converters/PackageDependencyRangeJsonConverter.cs index a0c7bfbd0..9514cb5d5 100644 --- a/src/BaGet.Protocol/Converters/PackageDependencyRangeJsonConverter.cs +++ b/src/BaGet.Protocol/Converters/PackageDependencyRangeJsonConverter.cs @@ -4,7 +4,10 @@ namespace BaGet.Protocol.Internal { - internal class PackageDependencyRangeJsonConverter : JsonConverter + /// + /// This is an internal API that may be changed or removed without notice in any release. + /// + public class PackageDependencyRangeJsonConverter : JsonConverter { public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/src/BaGet.Protocol/Converters/StringOrStringArrayJsonConverter.cs b/src/BaGet.Protocol/Converters/StringOrStringArrayJsonConverter.cs index 495ede7a5..d4ea42e04 100644 --- a/src/BaGet.Protocol/Converters/StringOrStringArrayJsonConverter.cs +++ b/src/BaGet.Protocol/Converters/StringOrStringArrayJsonConverter.cs @@ -5,7 +5,10 @@ namespace BaGet.Protocol.Internal { - internal class StringOrStringArrayJsonConverter : JsonConverter> + /// + /// This is an internal API that may be changed or removed without notice in any release. + /// + public class StringOrStringArrayJsonConverter : JsonConverter> { public override IReadOnlyList Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/src/BaGet.Protocol/Extensions/HttpClientExtensions.cs b/src/BaGet.Protocol/Extensions/HttpClientExtensions.cs index 29adefbc9..fdbab36bd 100644 --- a/src/BaGet.Protocol/Extensions/HttpClientExtensions.cs +++ b/src/BaGet.Protocol/Extensions/HttpClientExtensions.cs @@ -9,15 +9,6 @@ namespace BaGet.Protocol { internal static class HttpClientExtensions { - //internal static readonly JsonSerializer Serializer = JsonSerializer.Create(JsonSettings); - - //internal static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings - //{ - // DateTimeZoneHandling = DateTimeZoneHandling.Utc, - // DateParseHandling = DateParseHandling.DateTimeOffset, - // NullValueHandling = NullValueHandling.Ignore, - //}; - internal static readonly JsonSerializerOptions Options = new JsonSerializerOptions { IgnoreNullValues = true, @@ -56,19 +47,6 @@ public static async Task> DeserializeUrlAsync( - // HttpMethod.Get, - // documentUrl, - // response.StatusCode, - // response.ReasonPhrase, - // hasResult: true, - // result: Serializer.Deserialize(jsonReader)); - //} } } } diff --git a/src/BaGet.Protocol/Models/DependencyGroupItem.cs b/src/BaGet.Protocol/Models/DependencyGroupItem.cs index c700a0144..e9626c053 100644 --- a/src/BaGet.Protocol/Models/DependencyGroupItem.cs +++ b/src/BaGet.Protocol/Models/DependencyGroupItem.cs @@ -19,7 +19,7 @@ public class DependencyGroupItem /// /// A list of dependencies. /// - [JsonPropertyName("dependencies")] // TODO: VERIFY IGNORED IF NULL + [JsonPropertyName("dependencies")] public List Dependencies { get; set; } } } diff --git a/tests/BaGet.Protocol.Tests/Converters/PackageDependencyRangeJsonConverterTests.cs b/tests/BaGet.Protocol.Tests/Converters/PackageDependencyRangeJsonConverterTests.cs new file mode 100644 index 000000000..5229bd798 --- /dev/null +++ b/tests/BaGet.Protocol.Tests/Converters/PackageDependencyRangeJsonConverterTests.cs @@ -0,0 +1,87 @@ +using System.Text.Json; +using BaGet.Protocol.Internal; +using Xunit; + +namespace BaGet.Protocol.Tests +{ + public class PackageDependencyRangeJsonConverterTests + { + [Fact] + public void DeserializesNull() + { + var json = @"null"; + + var options = new JsonSerializerOptions(); + options.Converters.Add(new PackageDependencyRangeJsonConverter()); + + var result = JsonSerializer.Deserialize(json, options); + + Assert.Null(result); + } + + [Fact] + public void DeserializesString() + { + var json = @"""Hello"""; + + var options = new JsonSerializerOptions(); + options.Converters.Add(new PackageDependencyRangeJsonConverter()); + + var result = JsonSerializer.Deserialize(json, options); + + Assert.Equal("Hello", result); + } + + [Fact] + public void DeserializesStringArray() + { + var json = @"[""first"", ""second"", ""third""]"; + + var options = new JsonSerializerOptions(); + options.Converters.Add(new PackageDependencyRangeJsonConverter()); + + var result = JsonSerializer.Deserialize(json, options); + + Assert.Equal("first", result); + } + + [Theory] + [InlineData(@"false")] + [InlineData(@"0")] + [InlineData(@"{")] + [InlineData(@"[")] + [InlineData(@"[""hello""")] + [InlineData(@"[""hello""}")] + [InlineData(@"[""hello"", 1")] + public void ThrowsOnInvalidJson(string json) + { + var options = new JsonSerializerOptions(); + options.Converters.Add(new PackageDependencyRangeJsonConverter()); + + Assert.Throws( + () => JsonSerializer.Deserialize(json, options)); + } + + [Fact] + public void SerializesNull() + { + var options = new JsonSerializerOptions(); + options.Converters.Add(new StringOrStringArrayJsonConverter()); + + var json = JsonSerializer.Serialize(null, options); + + Assert.Equal("null", json); + } + + [Fact] + public void SerializesString() + { + var options = new JsonSerializerOptions(); + options.Converters.Add(new StringOrStringArrayJsonConverter()); + + var json = JsonSerializer.Serialize("foo", options); + + Assert.Equal(@"""foo""", json); + } + } +} diff --git a/tests/BaGet.Protocol.Tests/Converters/StringOrStringArrayJsonConverterTests.cs b/tests/BaGet.Protocol.Tests/Converters/StringOrStringArrayJsonConverterTests.cs new file mode 100644 index 000000000..9f4b77df2 --- /dev/null +++ b/tests/BaGet.Protocol.Tests/Converters/StringOrStringArrayJsonConverterTests.cs @@ -0,0 +1,123 @@ +using System.Collections.Generic; +using System.Text.Json; +using BaGet.Protocol.Internal; +using Xunit; + +namespace BaGet.Protocol.Tests +{ + public class StringOrStringArrayJsonConverterTests + { + [Fact] + public void DeserializesEmptyString() + { + var json = @""""""; + + var options = new JsonSerializerOptions(); + options.Converters.Add(new StringOrStringArrayJsonConverter()); + + var result = JsonSerializer.Deserialize>(json, options); + + var first = Assert.Single(result); + Assert.NotNull(first); + Assert.True(string.IsNullOrEmpty(first)); + } + + [Fact] + public void DeserializesString() + { + var json = @"""Foo bar"""; + + var options = new JsonSerializerOptions(); + options.Converters.Add(new StringOrStringArrayJsonConverter()); + + var result = JsonSerializer.Deserialize>(json, options); + + var first = Assert.Single(result); + Assert.Equal("Foo bar", first); + } + + [Fact] + public void DeserializesEmptyArray() + { + var json = "[]"; + + var options = new JsonSerializerOptions(); + options.Converters.Add(new StringOrStringArrayJsonConverter()); + + var result = JsonSerializer.Deserialize>(json, options); + + Assert.Empty(result); + } + + [Fact] + public void DeserializesArray() + { + var json = @"[""Foo"", ""bar""]"; + + var options = new JsonSerializerOptions(); + options.Converters.Add(new StringOrStringArrayJsonConverter()); + + var result = JsonSerializer.Deserialize>(json, options); + + Assert.Equal(2, result.Count); + Assert.Equal("Foo", result[0]); + Assert.Equal("bar", result[1]); + } + + [Theory] + [InlineData(@"false")] + [InlineData(@"0")] + [InlineData(@"{")] + [InlineData(@"[")] + [InlineData(@"[""hello""")] + [InlineData(@"[""hello""}")] + [InlineData(@"[""hello"", 1")] + public void ThrowsOnInvalidJson(string json) + { + var options = new JsonSerializerOptions(); + options.Converters.Add(new StringOrStringArrayJsonConverter()); + + Assert.Throws( + () => JsonSerializer.Deserialize>(json, options)); + } + + [Fact] + public void SerializesNull() + { + var options = new JsonSerializerOptions(); + options.Converters.Add(new StringOrStringArrayJsonConverter()); + + IReadOnlyList list = null; + + var json = JsonSerializer.Serialize(list, options); + + Assert.Equal("null", json); + } + + [Fact] + public void SerializesEmptyString() + { + var options = new JsonSerializerOptions(); + options.Converters.Add(new StringOrStringArrayJsonConverter()); + + IReadOnlyList list = new List { "" }; + + var json = JsonSerializer.Serialize(list, options); + + Assert.Equal(@"[""""]", json); + } + + [Fact] + public void SerializesList() + { + var options = new JsonSerializerOptions(); + options.Converters.Add(new StringOrStringArrayJsonConverter()); + + IReadOnlyList list = new List { "Hello", "World", null }; + + var json = JsonSerializer.Serialize(list, options); + + Assert.Equal(@"[""Hello"",""World"",null]", json); + } + } +} From 2dd3adeea710c757202dd0e1008662bf45081a1c Mon Sep 17 00:00:00 2001 From: Loic Sharma Date: Sun, 19 Jul 2020 22:09:53 -0700 Subject: [PATCH 03/10] Undo route-to-code stuffs --- src/BaGet.Hosting/BaGetApi.cs | 167 ++++-------------- .../Controllers/PackageContentController.cs | 12 ++ .../Controllers/PackageMetadataController.cs | 55 ++++++ .../Controllers/SearchController.cs | 66 +++++++ .../Controllers/ServiceIndexController.cs | 29 +++ .../Extensions/HttpContextExtensions.cs | 71 -------- .../IEndpointRouteBuilderExtensions.cs | 15 -- .../IServiceCollectionExtensions.cs | 6 +- 8 files changed, 197 insertions(+), 224 deletions(-) create mode 100644 src/BaGet.Hosting/Controllers/PackageMetadataController.cs create mode 100644 src/BaGet.Hosting/Controllers/SearchController.cs create mode 100644 src/BaGet.Hosting/Controllers/ServiceIndexController.cs delete mode 100644 src/BaGet.Hosting/Extensions/HttpContextExtensions.cs delete mode 100644 src/BaGet.Hosting/Extensions/IEndpointRouteBuilderExtensions.cs diff --git a/src/BaGet.Hosting/BaGetApi.cs b/src/BaGet.Hosting/BaGetApi.cs index 971e92e29..4f2965554 100644 --- a/src/BaGet.Hosting/BaGetApi.cs +++ b/src/BaGet.Hosting/BaGetApi.cs @@ -1,10 +1,7 @@ -using BaGet.Core; using BaGet.Hosting; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Constraints; -using Microsoft.Extensions.DependencyInjection; -using NuGet.Versioning; namespace BaGet { @@ -22,17 +19,10 @@ public void MapRoutes(IEndpointRouteBuilder endpoints) public void MapServiceIndexRoutes(IEndpointRouteBuilder endpoints) { - endpoints - .MapGet("v3/index.json", async context => - { - var cancellationToken = context.RequestAborted; - var serviceIndex = context.RequestServices.GetRequiredService(); - - var response = await serviceIndex.GetAsync(cancellationToken); - - await context.Response.WriteAsJsonAsync(response, cancellationToken); - }) - .WithRouteName(Routes.IndexRouteName); + endpoints.MapControllerRoute( + name: Routes.IndexRouteName, + pattern: "v3/index.json", + defaults: new { controller = "ServiceIndex", action = "Get" }); } public void MapPackagePublishRoutes(IEndpointRouteBuilder endpoints) @@ -77,139 +67,42 @@ public void MapSymbolRoutes(IEndpointRouteBuilder endpoints) public void MapSearchRoutes(IEndpointRouteBuilder endpoints) { - endpoints - .MapGet("v3/search", async context => - { - var query = context.Request.ReadFromQuery("q"); - var skip = context.Request.ReadFromQuery("skip", defaultValue: 0); - var take = context.Request.ReadFromQuery("take", defaultValue: 20); - var prerelease = context.Request.ReadFromQuery("prerelease", defaultValue: false); - var semVerLevel = context.Request.ReadFromQuery("semVerLevel", defaultValue: null); - var packageType = context.Request.ReadFromQuery("packageType", defaultValue: null); - var framework = context.Request.ReadFromQuery("framework", defaultValue: null); - var cancellationToken = context.RequestAborted; - - var searchService = context.RequestServices.GetRequiredService(); - var includeSemVer2 = semVerLevel == "2.0.0"; - - var response = await searchService.SearchAsync( - query ?? string.Empty, - skip, - take, - prerelease, - includeSemVer2, - packageType, - framework, - cancellationToken); - - await context.Response.WriteAsJsonAsync(response, cancellationToken); - }) - .WithRouteName(Routes.SearchRouteName); - - endpoints - .MapGet("v3/autocomplete", async context => - { - // TODO: Add other autocomplete parameters - // TODO: Support versions autocomplete. - // See: https://github.com/loic-sharma/BaGet/issues/291 - var query = context.Request.ReadFromQuery("q"); - var cancellationToken = context.RequestAborted; - - var searchService = context.RequestServices.GetRequiredService(); - - var response = await searchService.AutocompleteAsync( - query, - cancellationToken: cancellationToken); - - await context.Response.WriteAsJsonAsync(response, cancellationToken); - }) - .WithRouteName(Routes.AutocompleteRouteName); - - // This is an unofficial API to find packages that depend on a given package. - endpoints - .MapGet("v3/dependents", async context => - { - var packageId = context.Request.ReadFromQuery("packageId"); - var cancellationToken = context.RequestAborted; - - var searchService = context.RequestServices.GetRequiredService(); + endpoints.MapControllerRoute( + name: Routes.SearchRouteName, + pattern: "v3/search", + defaults: new { controller = "Search", action = "Search" }); - var response = await searchService.FindDependentsAsync( - packageId, - cancellationToken: cancellationToken); + endpoints.MapControllerRoute( + name: Routes.AutocompleteRouteName, + pattern: "v3/autocomplete", + defaults: new { controller = "Search", action = "Autocomplete" }); - await context.Response.WriteAsJsonAsync(response, cancellationToken); - }) - .WithRouteName(Routes.DependentsRouteName); + // This is an unofficial API to find packages that depend on a given package. + endpoints.MapControllerRoute( + name: Routes.DependentsRouteName, + pattern: "v3/dependents", + defaults: new { controller = "Search", action = "Dependents" }); } public void MapPackageMetadataRoutes(IEndpointRouteBuilder endpoints) { - endpoints - .MapGet("v3/registration/{id}/index.json", async context => - { - var packageId = context.Request.RouteValues["id"]?.ToString(); - var cancellationToken = context.RequestAborted; - - var metadata = context.RequestServices.GetRequiredService(); - - var index = await metadata.GetRegistrationIndexOrNullAsync(packageId, cancellationToken); - if (index == null) - { - context.Response.NotFound(); - return; - } - - await context.Response.WriteAsJsonAsync(index, cancellationToken); - }) - .WithRouteName(Routes.RegistrationIndexRouteName); - - endpoints - .MapGet("v3/registration/{id}/{version}.json", async context => - { - var packageId = context.Request.RouteValues["id"]?.ToString(); - var version = context.Request.RouteValues["version"]?.ToString(); - var cancellationToken = context.RequestAborted; - - var metadata = context.RequestServices.GetRequiredService(); - - if (!NuGetVersion.TryParse(version, out var nugetVersion)) - { - context.Response.NotFound(); - return; - } - - var leaf = await metadata.GetRegistrationLeafOrNullAsync(packageId, nugetVersion, cancellationToken); - if (leaf == null) - { - context.Response.NotFound(); - return; - } - - await context.Response.WriteAsJsonAsync(leaf, cancellationToken); - }) - .WithRouteName(Routes.RegistrationLeafRouteName); + endpoints.MapControllerRoute( + name: Routes.RegistrationIndexRouteName, + pattern: "v3/registration/{id}/index.json", + defaults: new { controller = "PackageMetadata", action = "RegistrationIndex" }); + + endpoints.MapControllerRoute( + name: Routes.RegistrationLeafRouteName, + pattern: "v3/registration/{id}/{version}.json", + defaults: new { controller = "PackageMetadata", action = "RegistrationLeaf" }); } public void MapPackageContentRoutes(IEndpointRouteBuilder endpoints) { - endpoints - .MapGet("v3/package/{id}/index.json", async context => - { - var packageId = context.Request.RouteValues["id"]?.ToString(); - var cancellationToken = context.RequestAborted; - - var content = context.RequestServices.GetRequiredService(); - var response = await content.GetPackageVersionsOrNullAsync(packageId, cancellationToken); - if (response == null) - { - context.Response.NotFound(); - return; - } - - await context.Response.WriteAsJsonAsync(response, cancellationToken); - }) - .WithRouteName(Routes.PackageVersionsRouteName); + endpoints.MapControllerRoute( + name: Routes.PackageVersionsRouteName, + pattern: "v3/package/{id}/index.json", + defaults: new { controller = "PackageContent", action = "GetPackageVersions" }); endpoints.MapControllerRoute( name: Routes.PackageDownloadRouteName, diff --git a/src/BaGet.Hosting/Controllers/PackageContentController.cs b/src/BaGet.Hosting/Controllers/PackageContentController.cs index ddc75bc05..2844c8901 100644 --- a/src/BaGet.Hosting/Controllers/PackageContentController.cs +++ b/src/BaGet.Hosting/Controllers/PackageContentController.cs @@ -2,6 +2,7 @@ using System.Threading; using System.Threading.Tasks; using BaGet.Core; +using BaGet.Protocol.Models; using Microsoft.AspNetCore.Mvc; using NuGet.Versioning; @@ -20,6 +21,17 @@ public PackageContentController(IPackageContentService content) _content = content ?? throw new ArgumentNullException(nameof(content)); } + public async Task> GetPackageVersionsAsync(string id, CancellationToken cancellationToken) + { + var versions = await _content.GetPackageVersionsOrNullAsync(id, cancellationToken); + if (versions == null) + { + return NotFound(); + } + + return versions; + } + public async Task DownloadPackageAsync(string id, string version, CancellationToken cancellationToken) { if (!NuGetVersion.TryParse(version, out var nugetVersion)) diff --git a/src/BaGet.Hosting/Controllers/PackageMetadataController.cs b/src/BaGet.Hosting/Controllers/PackageMetadataController.cs new file mode 100644 index 000000000..0707630c7 --- /dev/null +++ b/src/BaGet.Hosting/Controllers/PackageMetadataController.cs @@ -0,0 +1,55 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using BaGet.Core; +using BaGet.Protocol.Models; +using Microsoft.AspNetCore.Mvc; +using NuGet.Versioning; + +namespace BaGet.Hosting +{ + /// + /// The Package Metadata resource, used to fetch packages' information. + /// See: https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource + /// + public class PackageMetadataController : Controller + { + private readonly IPackageMetadataService _metadata; + + public PackageMetadataController(IPackageMetadataService metadata) + { + _metadata = metadata ?? throw new ArgumentNullException(nameof(metadata)); + } + + // GET v3/registration/{id}.json + [HttpGet] + public async Task> RegistrationIndexAsync(string id, CancellationToken cancellationToken) + { + var index = await _metadata.GetRegistrationIndexOrNullAsync(id, cancellationToken); + if (index == null) + { + return NotFound(); + } + + return index; + } + + // GET v3/registration/{id}/{version}.json + [HttpGet] + public async Task> RegistrationLeafAsync(string id, string version, CancellationToken cancellationToken) + { + if (!NuGetVersion.TryParse(version, out var nugetVersion)) + { + return NotFound(); + } + + var leaf = await _metadata.GetRegistrationLeafOrNullAsync(id, nugetVersion, cancellationToken); + if (leaf == null) + { + return NotFound(); + } + + return leaf; + } + } +} diff --git a/src/BaGet.Hosting/Controllers/SearchController.cs b/src/BaGet.Hosting/Controllers/SearchController.cs new file mode 100644 index 000000000..0bb6ed8e2 --- /dev/null +++ b/src/BaGet.Hosting/Controllers/SearchController.cs @@ -0,0 +1,66 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using BaGet.Core; +using BaGet.Protocol.Models; +using Microsoft.AspNetCore.Mvc; + +namespace BaGet.Hosting +{ + public class SearchController : Controller + { + private readonly ISearchService _searchService; + + public SearchController(ISearchService searchService) + { + _searchService = searchService ?? throw new ArgumentNullException(nameof(searchService)); + } + + public async Task> SearchAsync( + [FromQuery(Name = "q")] string query = null, + [FromQuery]int skip = 0, + [FromQuery]int take = 20, + [FromQuery]bool prerelease = false, + [FromQuery]string semVerLevel = null, + + // These are unofficial parameters + [FromQuery]string packageType = null, + [FromQuery]string framework = null, + CancellationToken cancellationToken = default) + { + var includeSemVer2 = semVerLevel == "2.0.0"; + + return await _searchService.SearchAsync( + query ?? string.Empty, + skip, + take, + prerelease, + includeSemVer2, + packageType, + framework, + cancellationToken); + } + + public async Task> AutocompleteAsync( + [FromQuery(Name = "q")] string query = null, + CancellationToken cancellationToken = default) + { + // TODO: Add other autocomplete parameters + // TODO: Support versions autocomplete. + // See: https://github.com/loic-sharma/BaGet/issues/291 + return await _searchService.AutocompleteAsync( + query, + cancellationToken: cancellationToken); + } + + public async Task> DependentsAsync( + [FromQuery] string packageId, + CancellationToken cancellationToken = default) + { + // TODO: Add other dependents parameters. + return await _searchService.FindDependentsAsync( + packageId, + cancellationToken: cancellationToken); + } + } +} diff --git a/src/BaGet.Hosting/Controllers/ServiceIndexController.cs b/src/BaGet.Hosting/Controllers/ServiceIndexController.cs new file mode 100644 index 000000000..707af2c6a --- /dev/null +++ b/src/BaGet.Hosting/Controllers/ServiceIndexController.cs @@ -0,0 +1,29 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using BaGet.Core; +using BaGet.Protocol.Models; +using Microsoft.AspNetCore.Mvc; + +namespace BaGet.Hosting +{ + /// + /// The NuGet Service Index. This aids NuGet client to discover this server's services. + /// + public class ServiceIndexController : Controller + { + private readonly IServiceIndexService _serviceIndex; + + public ServiceIndexController(IServiceIndexService serviceIndex) + { + _serviceIndex = serviceIndex ?? throw new ArgumentNullException(nameof(serviceIndex)); + } + + // GET v3/index + [HttpGet] + public async Task GetAsync(CancellationToken cancellationToken) + { + return await _serviceIndex.GetAsync(cancellationToken); + } + } +} diff --git a/src/BaGet.Hosting/Extensions/HttpContextExtensions.cs b/src/BaGet.Hosting/Extensions/HttpContextExtensions.cs deleted file mode 100644 index 023f85994..000000000 --- a/src/BaGet.Hosting/Extensions/HttpContextExtensions.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; - -namespace BaGet.Hosting -{ - internal static class HttpContextExtensions - { - private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions - { - IgnoreNullValues = true, - }; - - public static string ReadFromQuery(this HttpRequest request, string key) - { - var value = request.Query[key].ToString(); - if (string.IsNullOrEmpty(value)) - { - return string.Empty; - } - - return value; - } - - public static string ReadFromQuery(this HttpRequest request, string key, string defaultValue) - { - var value = request.Query[key].ToString(); - if (string.IsNullOrEmpty(value)) - { - return defaultValue; - } - - return value; - } - - public static int ReadFromQuery(this HttpRequest context, string key, int defaultValue) - { - var value = context.Query[key].ToString(); - if (value == null || !int.TryParse(value, out var result)) - { - result = defaultValue; - } - - return result; - } - - public static bool ReadFromQuery(this HttpRequest request, string key, bool defaultValue) - { - var value = request.Query[key].ToString(); - if (value == null || !bool.TryParse(value, out var result)) - { - result = defaultValue; - } - - return result; - } - - public static void NotFound(this HttpResponse response) => response.StatusCode = StatusCodes.Status404NotFound; - - public static async Task WriteAsJsonAsync( - this HttpResponse response, - TValue value, - CancellationToken cancellationToken) - { - response.ContentType = "application/json; charset=utf-8"; - - await JsonSerializer.SerializeAsync(response.Body, value, JsonOptions, cancellationToken); - } - } -} diff --git a/src/BaGet.Hosting/Extensions/IEndpointRouteBuilderExtensions.cs b/src/BaGet.Hosting/Extensions/IEndpointRouteBuilderExtensions.cs deleted file mode 100644 index ddebb6849..000000000 --- a/src/BaGet.Hosting/Extensions/IEndpointRouteBuilderExtensions.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Routing; - -namespace BaGet -{ - public static class EndpointRoutingExtensions - { - public static IEndpointConventionBuilder WithRouteName(this IEndpointConventionBuilder endpoints, string name) - { - return endpoints.WithMetadata( - new EndpointNameMetadata(name), - new RouteNameMetadata(name)); - } - } -} diff --git a/src/BaGet.Hosting/Extensions/IServiceCollectionExtensions.cs b/src/BaGet.Hosting/Extensions/IServiceCollectionExtensions.cs index fc546d7c8..5ec8134a8 100644 --- a/src/BaGet.Hosting/Extensions/IServiceCollectionExtensions.cs +++ b/src/BaGet.Hosting/Extensions/IServiceCollectionExtensions.cs @@ -16,7 +16,11 @@ public static IServiceCollection AddBaGetWebApplication( services .AddControllers() .AddApplicationPart(typeof(PackageContentController).Assembly) - .SetCompatibilityVersion(CompatibilityVersion.Version_3_0); + .SetCompatibilityVersion(CompatibilityVersion.Version_3_0) + .AddJsonOptions(options => + { + options.JsonSerializerOptions.IgnoreNullValues = true; + }); services.AddHttpContextAccessor(); services.AddTransient(); From 9f501800e3fa1538befcc89cd7e25dfe795c5a31 Mon Sep 17 00:00:00 2001 From: Loic Sharma Date: Sun, 19 Jul 2020 22:35:18 -0700 Subject: [PATCH 04/10] Clean up --- .../Controllers/PackageContentController.cs | 18 ++++++------- src/BaGet.Protocol/BaGet.Protocol.csproj | 1 + .../Extensions/HttpClientExtensions.cs | 27 +++++++------------ .../Support/TestDataHttpMessageHandler.cs | 5 +++- 4 files changed, 23 insertions(+), 28 deletions(-) diff --git a/src/BaGet.Hosting/Controllers/PackageContentController.cs b/src/BaGet.Hosting/Controllers/PackageContentController.cs index 2844c8901..749d00450 100644 --- a/src/BaGet.Hosting/Controllers/PackageContentController.cs +++ b/src/BaGet.Hosting/Controllers/PackageContentController.cs @@ -21,15 +21,15 @@ public PackageContentController(IPackageContentService content) _content = content ?? throw new ArgumentNullException(nameof(content)); } - public async Task> GetPackageVersionsAsync(string id, CancellationToken cancellationToken) - { - var versions = await _content.GetPackageVersionsOrNullAsync(id, cancellationToken); - if (versions == null) - { - return NotFound(); - } - - return versions; + public async Task> GetPackageVersionsAsync(string id, CancellationToken cancellationToken) + { + var versions = await _content.GetPackageVersionsOrNullAsync(id, cancellationToken); + if (versions == null) + { + return NotFound(); + } + + return versions; } public async Task DownloadPackageAsync(string id, string version, CancellationToken cancellationToken) diff --git a/src/BaGet.Protocol/BaGet.Protocol.csproj b/src/BaGet.Protocol/BaGet.Protocol.csproj index 5711a54bb..db60b65d4 100644 --- a/src/BaGet.Protocol/BaGet.Protocol.csproj +++ b/src/BaGet.Protocol/BaGet.Protocol.csproj @@ -11,6 +11,7 @@ + diff --git a/src/BaGet.Protocol/Extensions/HttpClientExtensions.cs b/src/BaGet.Protocol/Extensions/HttpClientExtensions.cs index fdbab36bd..81d6f0758 100644 --- a/src/BaGet.Protocol/Extensions/HttpClientExtensions.cs +++ b/src/BaGet.Protocol/Extensions/HttpClientExtensions.cs @@ -1,7 +1,6 @@ -using System.IO; using System.Net; using System.Net.Http; -using System.Text.Json; +using System.Net.Http.Json; using System.Threading; using System.Threading.Tasks; @@ -9,11 +8,6 @@ namespace BaGet.Protocol { internal static class HttpClientExtensions { - internal static readonly JsonSerializerOptions Options = new JsonSerializerOptions - { - IgnoreNullValues = true, - }; - public static async Task> DeserializeUrlAsync( this HttpClient httpClient, string documentUrl, @@ -35,18 +29,15 @@ public static async Task> DeserializeUrlAsync(stream, Options, cancellationToken); + var result = await response.Content.ReadFromJsonAsync(); - return new ResponseAndResult( - HttpMethod.Get, - documentUrl, - response.StatusCode, - response.ReasonPhrase, - hasResult: true, - result: result); - } + return new ResponseAndResult( + HttpMethod.Get, + documentUrl, + response.StatusCode, + response.ReasonPhrase, + hasResult: true, + result: result); } } } diff --git a/tests/BaGet.Protocol.Tests/Support/TestDataHttpMessageHandler.cs b/tests/BaGet.Protocol.Tests/Support/TestDataHttpMessageHandler.cs index 735649f25..0705c10c5 100644 --- a/tests/BaGet.Protocol.Tests/Support/TestDataHttpMessageHandler.cs +++ b/tests/BaGet.Protocol.Tests/Support/TestDataHttpMessageHandler.cs @@ -55,7 +55,10 @@ private HttpResponseMessage Send(HttpRequestMessage request) { RequestMessage = request, StatusCode = HttpStatusCode.OK, - Content = new StringContent(getContent()), + Content = new StringContent( + getContent(), + encoding: null, + mediaType: "application/json"), }; } } From 304c4af9e678fb719de8daef30eea7ac79d59cae Mon Sep 17 00:00:00 2001 From: Loic Sharma Date: Sun, 19 Jul 2020 22:50:12 -0700 Subject: [PATCH 05/10] Remove allocations --- .../Catalog/RawCatalogClient.cs | 12 ++--- .../Extensions/HttpClientExtensions.cs | 35 ++++++------- .../Models/ResponseAndResult.cs | 50 ------------------- .../PackageContent/RawPackageContentClient.cs | 10 +--- .../RawPackageMetadataClient.cs | 22 ++------ .../Search/RawAutocompleteClient.cs | 9 ++-- src/BaGet.Protocol/Search/RawSearchClient.cs | 5 +- .../ServiceIndex/RawServiceIndexClient.cs | 5 +- 8 files changed, 32 insertions(+), 116 deletions(-) delete mode 100644 src/BaGet.Protocol/Models/ResponseAndResult.cs diff --git a/src/BaGet.Protocol/Catalog/RawCatalogClient.cs b/src/BaGet.Protocol/Catalog/RawCatalogClient.cs index a477387e3..e25fc5de3 100644 --- a/src/BaGet.Protocol/Catalog/RawCatalogClient.cs +++ b/src/BaGet.Protocol/Catalog/RawCatalogClient.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Net.Http; +using System.Net.Http.Json; using System.Threading; using System.Threading.Tasks; using BaGet.Protocol.Models; @@ -20,16 +21,12 @@ public RawCatalogClient(HttpClient httpClient, string catalogUrl) public async Task GetIndexAsync(CancellationToken cancellationToken = default) { - var response = await _httpClient.DeserializeUrlAsync(_catalogUrl, cancellationToken); - - return response.GetResultOrThrow(); + return await _httpClient.GetFromJsonAsync(_catalogUrl, cancellationToken); } public async Task GetPageAsync(string pageUrl, CancellationToken cancellationToken = default) { - var response = await _httpClient.DeserializeUrlAsync(pageUrl, cancellationToken); - - return response.GetResultOrThrow(); + return await _httpClient.GetFromJsonAsync(pageUrl, cancellationToken); } public async Task GetPackageDeleteLeafAsync(string leafUrl, CancellationToken cancellationToken = default) @@ -53,8 +50,7 @@ private async Task GetAndValidateLeafAsync( string leafUrl, CancellationToken cancellationToken) where TCatalogLeaf : CatalogLeaf { - var result = await _httpClient.DeserializeUrlAsync(leafUrl, cancellationToken); - var leaf = result.GetResultOrThrow(); + var leaf = await _httpClient.GetFromJsonAsync(leafUrl, cancellationToken); if (leaf.Type.FirstOrDefault() != leafType) { diff --git a/src/BaGet.Protocol/Extensions/HttpClientExtensions.cs b/src/BaGet.Protocol/Extensions/HttpClientExtensions.cs index 81d6f0758..043d54d53 100644 --- a/src/BaGet.Protocol/Extensions/HttpClientExtensions.cs +++ b/src/BaGet.Protocol/Extensions/HttpClientExtensions.cs @@ -8,36 +8,31 @@ namespace BaGet.Protocol { internal static class HttpClientExtensions { - public static async Task> DeserializeUrlAsync( + /// + /// Deserialize JSON content. If the HTTP response status code is 404, + /// returns the default value. + /// + /// The JSON type to deserialize. + /// The HTTP client that will perform the request. + /// The request URI. + /// A token to cancel the task. + /// The JSON content, or the default value if the HTTP response status code is 404. + public static async Task GetFromJsonOrDefaultAsync( this HttpClient httpClient, - string documentUrl, + string requestUri, CancellationToken cancellationToken = default) { using (var response = await httpClient.GetAsync( - documentUrl, + requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken)) { - if (response.StatusCode != HttpStatusCode.OK) + if (response.StatusCode == HttpStatusCode.NotFound) { - return new ResponseAndResult( - HttpMethod.Get, - documentUrl, - response.StatusCode, - response.ReasonPhrase, - hasResult: false, - result: default); + return default; } - var result = await response.Content.ReadFromJsonAsync(); - - return new ResponseAndResult( - HttpMethod.Get, - documentUrl, - response.StatusCode, - response.ReasonPhrase, - hasResult: true, - result: result); + return await response.Content.ReadFromJsonAsync(); } } } diff --git a/src/BaGet.Protocol/Models/ResponseAndResult.cs b/src/BaGet.Protocol/Models/ResponseAndResult.cs deleted file mode 100644 index cef4d392c..000000000 --- a/src/BaGet.Protocol/Models/ResponseAndResult.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Net; -using System.Net.Http; -using BaGet.Protocol.Models; - -namespace BaGet.Protocol -{ - internal class ResponseAndResult - { - public ResponseAndResult( - HttpMethod method, - string requestUri, - HttpStatusCode statusCode, - string reasonPhrase, - bool hasResult, - T result) - { - Method = method ?? throw new ArgumentNullException(nameof(method)); - RequestUri = requestUri ?? throw new ArgumentNullException(nameof(requestUri)); - StatusCode = statusCode; - ReasonPhrase = reasonPhrase ?? throw new ArgumentNullException(nameof(reasonPhrase)); - HasResult = hasResult; - Result = result; - } - - public HttpMethod Method { get; } - public string RequestUri { get; } - public HttpStatusCode StatusCode { get; } - public string ReasonPhrase { get; } - public bool HasResult { get; } - public T Result { get; } - - public T GetResultOrThrow() - { - if (!HasResult) - { - throw new ProtocolException( - $"The HTTP request failed.{Environment.NewLine}" + - $"Request: {Method} {RequestUri}{Environment.NewLine}" + - $"Response: {(int)StatusCode} {ReasonPhrase}", - Method, - RequestUri, - StatusCode, - ReasonPhrase); - } - - return Result; - } - } -} diff --git a/src/BaGet.Protocol/PackageContent/RawPackageContentClient.cs b/src/BaGet.Protocol/PackageContent/RawPackageContentClient.cs index 87bf27b30..ea63c8aef 100644 --- a/src/BaGet.Protocol/PackageContent/RawPackageContentClient.cs +++ b/src/BaGet.Protocol/PackageContent/RawPackageContentClient.cs @@ -2,6 +2,7 @@ using System.IO; using System.Net; using System.Net.Http; +using System.Net.Http.Json; using System.Threading; using System.Threading.Tasks; using BaGet.Protocol.Models; @@ -35,16 +36,9 @@ public async Task GetPackageVersionsOrNullAsync( CancellationToken cancellationToken = default) { var id = packageId.ToLowerInvariant(); - var url = $"{_packageContentUrl}/{id}/index.json"; - var response = await _httpClient.DeserializeUrlAsync(url, cancellationToken); - - if (response.StatusCode == HttpStatusCode.NotFound) - { - return null; - } - return response.GetResultOrThrow(); + return await _httpClient.GetFromJsonOrDefaultAsync(url, cancellationToken); } /// diff --git a/src/BaGet.Protocol/PackageMetadata/RawPackageMetadataClient.cs b/src/BaGet.Protocol/PackageMetadata/RawPackageMetadataClient.cs index 8d19023e6..658f72c5c 100644 --- a/src/BaGet.Protocol/PackageMetadata/RawPackageMetadataClient.cs +++ b/src/BaGet.Protocol/PackageMetadata/RawPackageMetadataClient.cs @@ -1,6 +1,7 @@ using System; using System.Net; using System.Net.Http; +using System.Net.Http.Json; using System.Threading; using System.Threading.Tasks; using BaGet.Protocol.Models; @@ -32,14 +33,8 @@ public async Task GetRegistrationIndexOrNullAsync( CancellationToken cancellationToken = default) { var url = $"{_packageMetadataUrl}/{packageId.ToLowerInvariant()}/index.json"; - var response = await _httpClient.DeserializeUrlAsync(url, cancellationToken); - if (response.StatusCode == HttpStatusCode.NotFound) - { - return null; - } - - return response.GetResultOrThrow(); + return await _httpClient.GetFromJsonOrDefaultAsync(url, cancellationToken); } /// @@ -47,9 +42,7 @@ public async Task GetRegistrationPageAsync( string pageUrl, CancellationToken cancellationToken = default) { - var response = await _httpClient.DeserializeUrlAsync(pageUrl, cancellationToken); - - return response.GetResultOrThrow(); + return await _httpClient.GetFromJsonAsync(pageUrl, cancellationToken); } /// @@ -57,14 +50,7 @@ public async Task GetRegistrationLeafAsync( string leafUrl, CancellationToken cancellationToken = default) { - var response = await _httpClient.DeserializeUrlAsync(leafUrl, cancellationToken); - - if (response.StatusCode == HttpStatusCode.NotFound) - { - return null; - } - - return response.GetResultOrThrow(); + return await _httpClient.GetFromJsonAsync(leafUrl, cancellationToken); } } } diff --git a/src/BaGet.Protocol/Search/RawAutocompleteClient.cs b/src/BaGet.Protocol/Search/RawAutocompleteClient.cs index 20602a3d8..6c167b2d0 100644 --- a/src/BaGet.Protocol/Search/RawAutocompleteClient.cs +++ b/src/BaGet.Protocol/Search/RawAutocompleteClient.cs @@ -1,5 +1,6 @@ using System; using System.Net.Http; +using System.Net.Http.Json; using System.Threading; using System.Threading.Tasks; using BaGet.Protocol.Models; @@ -44,9 +45,7 @@ public async Task AutocompleteAsync( includeSemVer2, "q"); - var response = await _httpClient.DeserializeUrlAsync(url, cancellationToken); - - return response.GetResultOrThrow(); + return await _httpClient.GetFromJsonAsync(url, cancellationToken); } public async Task ListPackageVersionsAsync( @@ -64,9 +63,7 @@ public async Task ListPackageVersionsAsync( includeSemVer2, "id"); - var response = await _httpClient.DeserializeUrlAsync(url, cancellationToken); - - return response.GetResultOrThrow(); + return await _httpClient.GetFromJsonAsync(url, cancellationToken); } } } diff --git a/src/BaGet.Protocol/Search/RawSearchClient.cs b/src/BaGet.Protocol/Search/RawSearchClient.cs index 4b721030e..fa84040e7 100644 --- a/src/BaGet.Protocol/Search/RawSearchClient.cs +++ b/src/BaGet.Protocol/Search/RawSearchClient.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Net.Http; +using System.Net.Http.Json; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -39,9 +40,7 @@ public async Task SearchAsync( { var url = AddSearchQueryString(_searchUrl, query, skip, take, includePrerelease, includeSemVer2, "q"); - var response = await _httpClient.DeserializeUrlAsync(url, cancellationToken); - - return response.GetResultOrThrow(); + return await _httpClient.GetFromJsonAsync(url, cancellationToken); } internal static string AddSearchQueryString( diff --git a/src/BaGet.Protocol/ServiceIndex/RawServiceIndexClient.cs b/src/BaGet.Protocol/ServiceIndex/RawServiceIndexClient.cs index 830770300..60497b6c8 100644 --- a/src/BaGet.Protocol/ServiceIndex/RawServiceIndexClient.cs +++ b/src/BaGet.Protocol/ServiceIndex/RawServiceIndexClient.cs @@ -1,5 +1,6 @@ using System; using System.Net.Http; +using System.Net.Http.Json; using System.Threading; using System.Threading.Tasks; using BaGet.Protocol.Models; @@ -30,11 +31,9 @@ public RawServiceIndexClient(HttpClient httpClient, string serviceIndexUrl) /// public async Task GetAsync(CancellationToken cancellationToken = default) { - var response = await _httpClient.DeserializeUrlAsync( + return await _httpClient.GetFromJsonAsync( _serviceIndexUrl, cancellationToken); - - return response.GetResultOrThrow(); } } } From 5155443730820868cb9f27c5967bb23d6aafc0ef Mon Sep 17 00:00:00 2001 From: Loic Sharma Date: Sun, 19 Jul 2020 22:53:52 -0700 Subject: [PATCH 06/10] Clean comment --- src/BaGet.Protocol/Extensions/HttpClientExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BaGet.Protocol/Extensions/HttpClientExtensions.cs b/src/BaGet.Protocol/Extensions/HttpClientExtensions.cs index 043d54d53..7d9d7daf1 100644 --- a/src/BaGet.Protocol/Extensions/HttpClientExtensions.cs +++ b/src/BaGet.Protocol/Extensions/HttpClientExtensions.cs @@ -16,7 +16,7 @@ internal static class HttpClientExtensions /// The HTTP client that will perform the request. /// The request URI. /// A token to cancel the task. - /// The JSON content, or the default value if the HTTP response status code is 404. + /// The JSON content, or, the default value if the HTTP response status code is 404. public static async Task GetFromJsonOrDefaultAsync( this HttpClient httpClient, string requestUri, From 787aeeb7c951ae1524ec55edbdf015fb1a468afc Mon Sep 17 00:00:00 2001 From: Loic Sharma Date: Mon, 20 Jul 2020 21:43:31 -0700 Subject: [PATCH 07/10] Don't validate JSON content type --- src/BaGet.Protocol/BaGet.Protocol.csproj | 1 - .../Extensions/HttpClientExtensions.cs | 22 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/BaGet.Protocol/BaGet.Protocol.csproj b/src/BaGet.Protocol/BaGet.Protocol.csproj index db60b65d4..d84123731 100644 --- a/src/BaGet.Protocol/BaGet.Protocol.csproj +++ b/src/BaGet.Protocol/BaGet.Protocol.csproj @@ -12,7 +12,6 @@ - diff --git a/src/BaGet.Protocol/Extensions/HttpClientExtensions.cs b/src/BaGet.Protocol/Extensions/HttpClientExtensions.cs index 7d9d7daf1..def15cff9 100644 --- a/src/BaGet.Protocol/Extensions/HttpClientExtensions.cs +++ b/src/BaGet.Protocol/Extensions/HttpClientExtensions.cs @@ -1,6 +1,7 @@ using System.Net; using System.Net.Http; using System.Net.Http.Json; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -8,6 +9,27 @@ namespace BaGet.Protocol { internal static class HttpClientExtensions { + public static async Task GetFromJsonAsync( + this HttpClient httpClient, + string requestUri, + CancellationToken cancellationToken = default) + { + using (var response = await httpClient.GetAsync( + requestUri, + HttpCompletionOption.ResponseHeadersRead, + cancellationToken)) + { + // This is similar to System.Net.Http.Json's implementation, however, + // this does not validate that the response's content type indicates JSON content. + response.EnsureSuccessStatusCode(); + + using (var stream = await response.Content.ReadAsStreamAsync()) + { + return await JsonSerializer.DeserializeAsync(stream); + } + } + } + /// /// Deserialize JSON content. If the HTTP response status code is 404, /// returns the default value. From 0da156781870edeb1c3883c2a7f9c149f697fbbe Mon Sep 17 00:00:00 2001 From: Loic Sharma Date: Mon, 20 Jul 2020 21:54:35 -0700 Subject: [PATCH 08/10] Fix --- src/BaGet.Protocol/BaGet.Protocol.csproj | 2 +- src/BaGet.Protocol/Catalog/RawCatalogClient.cs | 1 - .../Extensions/HttpClientExtensions.cs | 16 ++++++++++++++-- .../PackageContent/RawPackageContentClient.cs | 1 - .../PackageMetadata/RawPackageMetadataClient.cs | 2 -- .../Search/RawAutocompleteClient.cs | 1 - src/BaGet.Protocol/Search/RawSearchClient.cs | 1 - .../ServiceIndex/RawServiceIndexClient.cs | 1 - 8 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/BaGet.Protocol/BaGet.Protocol.csproj b/src/BaGet.Protocol/BaGet.Protocol.csproj index d84123731..5711a54bb 100644 --- a/src/BaGet.Protocol/BaGet.Protocol.csproj +++ b/src/BaGet.Protocol/BaGet.Protocol.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/BaGet.Protocol/Catalog/RawCatalogClient.cs b/src/BaGet.Protocol/Catalog/RawCatalogClient.cs index e25fc5de3..d773e3654 100644 --- a/src/BaGet.Protocol/Catalog/RawCatalogClient.cs +++ b/src/BaGet.Protocol/Catalog/RawCatalogClient.cs @@ -1,7 +1,6 @@ using System; using System.Linq; using System.Net.Http; -using System.Net.Http.Json; using System.Threading; using System.Threading.Tasks; using BaGet.Protocol.Models; diff --git a/src/BaGet.Protocol/Extensions/HttpClientExtensions.cs b/src/BaGet.Protocol/Extensions/HttpClientExtensions.cs index def15cff9..572d70119 100644 --- a/src/BaGet.Protocol/Extensions/HttpClientExtensions.cs +++ b/src/BaGet.Protocol/Extensions/HttpClientExtensions.cs @@ -1,6 +1,5 @@ using System.Net; using System.Net.Http; -using System.Net.Http.Json; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -9,6 +8,14 @@ namespace BaGet.Protocol { internal static class HttpClientExtensions { + /// + /// Deserialize JSON content. + /// + /// The JSON type to deserialize. + /// The HTTP client that will perform the request. + /// The request URI. + /// A token to cancel the task. + /// The deserialized JSON content public static async Task GetFromJsonAsync( this HttpClient httpClient, string requestUri, @@ -54,7 +61,12 @@ public static async Task GetFromJsonOrDefaultAsync( return default; } - return await response.Content.ReadFromJsonAsync(); + response.EnsureSuccessStatusCode(); + + using (var stream = await response.Content.ReadAsStreamAsync()) + { + return await JsonSerializer.DeserializeAsync(stream); + } } } } diff --git a/src/BaGet.Protocol/PackageContent/RawPackageContentClient.cs b/src/BaGet.Protocol/PackageContent/RawPackageContentClient.cs index ea63c8aef..1d172844f 100644 --- a/src/BaGet.Protocol/PackageContent/RawPackageContentClient.cs +++ b/src/BaGet.Protocol/PackageContent/RawPackageContentClient.cs @@ -2,7 +2,6 @@ using System.IO; using System.Net; using System.Net.Http; -using System.Net.Http.Json; using System.Threading; using System.Threading.Tasks; using BaGet.Protocol.Models; diff --git a/src/BaGet.Protocol/PackageMetadata/RawPackageMetadataClient.cs b/src/BaGet.Protocol/PackageMetadata/RawPackageMetadataClient.cs index 658f72c5c..75f7071f9 100644 --- a/src/BaGet.Protocol/PackageMetadata/RawPackageMetadataClient.cs +++ b/src/BaGet.Protocol/PackageMetadata/RawPackageMetadataClient.cs @@ -1,7 +1,5 @@ using System; -using System.Net; using System.Net.Http; -using System.Net.Http.Json; using System.Threading; using System.Threading.Tasks; using BaGet.Protocol.Models; diff --git a/src/BaGet.Protocol/Search/RawAutocompleteClient.cs b/src/BaGet.Protocol/Search/RawAutocompleteClient.cs index 6c167b2d0..26f63be00 100644 --- a/src/BaGet.Protocol/Search/RawAutocompleteClient.cs +++ b/src/BaGet.Protocol/Search/RawAutocompleteClient.cs @@ -1,6 +1,5 @@ using System; using System.Net.Http; -using System.Net.Http.Json; using System.Threading; using System.Threading.Tasks; using BaGet.Protocol.Models; diff --git a/src/BaGet.Protocol/Search/RawSearchClient.cs b/src/BaGet.Protocol/Search/RawSearchClient.cs index fa84040e7..914b08b41 100644 --- a/src/BaGet.Protocol/Search/RawSearchClient.cs +++ b/src/BaGet.Protocol/Search/RawSearchClient.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Net.Http; -using System.Net.Http.Json; using System.Text; using System.Threading; using System.Threading.Tasks; diff --git a/src/BaGet.Protocol/ServiceIndex/RawServiceIndexClient.cs b/src/BaGet.Protocol/ServiceIndex/RawServiceIndexClient.cs index 60497b6c8..70d85669c 100644 --- a/src/BaGet.Protocol/ServiceIndex/RawServiceIndexClient.cs +++ b/src/BaGet.Protocol/ServiceIndex/RawServiceIndexClient.cs @@ -1,6 +1,5 @@ using System; using System.Net.Http; -using System.Net.Http.Json; using System.Threading; using System.Threading.Tasks; using BaGet.Protocol.Models; From 2922da7e53aec31f2f23291a4676368c34c862d6 Mon Sep 17 00:00:00 2001 From: Loic Sharma Date: Mon, 20 Jul 2020 22:02:31 -0700 Subject: [PATCH 09/10] WIP - Make verbose test --- .azure/pipelines/ci-official.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.azure/pipelines/ci-official.yml b/.azure/pipelines/ci-official.yml index eb2175de2..85910abdf 100644 --- a/.azure/pipelines/ci-official.yml +++ b/.azure/pipelines/ci-official.yml @@ -56,7 +56,7 @@ jobs: command: custom workingDir: src/BaGet.UI customCommand: run build - + - task: Npm@1 displayName: Test frontend inputs: @@ -72,7 +72,7 @@ jobs: inputs: command: test projects: '**/*Tests/*.csproj' - arguments: '--configuration $(BuildConfiguration) --collect:"XPlat Code Coverage"' + arguments: '--configuration $(BuildConfiguration) --collect:"XPlat Code Coverage" --verbosity detailed' - task: DotNetCoreCLI@2 inputs: From a34e6f03167e92a56415a040c56b23b3c14bfc6c Mon Sep 17 00:00:00 2001 From: Loic Sharma Date: Mon, 20 Jul 2020 22:26:38 -0700 Subject: [PATCH 10/10] Reducing verbosity --- .azure/pipelines/ci-official.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azure/pipelines/ci-official.yml b/.azure/pipelines/ci-official.yml index 85910abdf..9c9b9726a 100644 --- a/.azure/pipelines/ci-official.yml +++ b/.azure/pipelines/ci-official.yml @@ -72,7 +72,7 @@ jobs: inputs: command: test projects: '**/*Tests/*.csproj' - arguments: '--configuration $(BuildConfiguration) --collect:"XPlat Code Coverage" --verbosity detailed' + arguments: '--configuration $(BuildConfiguration) --collect:"XPlat Code Coverage"' - task: DotNetCoreCLI@2 inputs: