Skip to content

Commit

Permalink
file scoped namespace and other csharp features (#245)
Browse files Browse the repository at this point in the history
  • Loading branch information
bruno-garcia committed Nov 25, 2023
1 parent cdf5cf1 commit a629bea
Show file tree
Hide file tree
Showing 54 changed files with 1,829 additions and 2,121 deletions.
5 changes: 0 additions & 5 deletions .github/workflows/web-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,6 @@ jobs:
- name: Check out code
uses: actions/checkout@v4

- name: Setup .NET SDK
uses: actions/setup-dotnet@v3
with:
dotnet-version: '8.0.x'

- name: Prepare Docker
id: docker-prep
run: |
Expand Down
3 changes: 2 additions & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
<PropertyGroup>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>true</ImplicitUsings>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<SentryVersion>4.0.0-beta.0</SentryVersion>
<SentryVersion>4.0.0-beta.2</SentryVersion>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)' == 'Release'">
Expand Down
194 changes: 90 additions & 104 deletions src/NuGet.Protocol.Catalog/CatalogClient.cs
Original file line number Diff line number Diff line change
@@ -1,140 +1,126 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using NuGet.Protocol.Catalog.Models;
using NuGet.Protocol.Catalog.Serialization;
using Sentry;

namespace NuGet.Protocol.Catalog
namespace NuGet.Protocol.Catalog;

public class CatalogClient(HttpClient httpClient, ILogger<CatalogClient> logger) : ICatalogClient
{
public class CatalogClient : ICatalogClient
{
private static readonly JsonSerializer JsonSerializer = CatalogJsonSerialization.Serializer;
private readonly HttpClient _httpClient;
private readonly ILogger<CatalogClient> _logger;
private static readonly JsonSerializer JsonSerializer = CatalogJsonSerialization.Serializer;
private readonly HttpClient _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
private readonly ILogger<CatalogClient> _logger = logger ?? throw new ArgumentNullException(nameof(logger));

public CatalogClient(HttpClient httpClient, ILogger<CatalogClient> logger)
public Task<CatalogIndex> GetIndexAsync(string indexUrl, CancellationToken token)
{
var index = DeserializeUrlAsync<CatalogIndex>(indexUrl, token);
if (index is null)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
throw new InvalidOperationException($"Url {indexUrl} didn't return a CatalogIndex.");
}

public Task<CatalogIndex> GetIndexAsync(string indexUrl, CancellationToken token)
{
var index = DeserializeUrlAsync<CatalogIndex>(indexUrl, token);
if (index is null)
{
throw new InvalidOperationException($"Url {indexUrl} didn't return a CatalogIndex.");
}
return index!;
}

return index!;
public Task<CatalogPage> GetPageAsync(string pageUrl, CancellationToken token)
{
var page = DeserializeUrlAsync<CatalogPage>(pageUrl, token);
if (page is null)
{
throw new InvalidOperationException($"Url {pageUrl} didn't return a CatalogPage.");
}

public Task<CatalogPage> GetPageAsync(string pageUrl, CancellationToken token)
{
var page = DeserializeUrlAsync<CatalogPage>(pageUrl, token);
if (page is null)
{
throw new InvalidOperationException($"Url {pageUrl} didn't return a CatalogPage.");
}
return page!;
}

return page!;
}
public async Task<CatalogLeaf> GetLeafAsync(string leafUrl, CancellationToken token)
{
// Buffer all of the JSON so we can parse twice. Once to determine the leaf type and once to deserialize
// the entire thing to the proper leaf type.
_logger.LogDebug("Downloading {leafUrl} as a byte array.", leafUrl);
var jsonBytes = await _httpClient.GetByteArrayAsync(leafUrl);
var untypedLeaf = DeserializeBytes<CatalogLeaf>(jsonBytes);

public async Task<CatalogLeaf> GetLeafAsync(string leafUrl, CancellationToken token)
return untypedLeaf.Type switch
{
// Buffer all of the JSON so we can parse twice. Once to determine the leaf type and once to deserialize
// the entire thing to the proper leaf type.
_logger.LogDebug("Downloading {leafUrl} as a byte array.", leafUrl);
var jsonBytes = await _httpClient.GetByteArrayAsync(leafUrl);
var untypedLeaf = DeserializeBytes<CatalogLeaf>(jsonBytes);

return untypedLeaf.Type switch
{
CatalogLeafType.PackageDetails => (CatalogLeaf)DeserializeBytes<PackageDetailsCatalogLeaf>(jsonBytes),
CatalogLeafType.PackageDelete => DeserializeBytes<PackageDeleteCatalogLeaf>(jsonBytes),
_ => throw new NotSupportedException($"The catalog leaf type '{untypedLeaf.Type}' is not supported.")
};
}
CatalogLeafType.PackageDetails => (CatalogLeaf)DeserializeBytes<PackageDetailsCatalogLeaf>(jsonBytes),
CatalogLeafType.PackageDelete => DeserializeBytes<PackageDeleteCatalogLeaf>(jsonBytes),
_ => throw new NotSupportedException($"The catalog leaf type '{untypedLeaf.Type}' is not supported.")
};
}

public Task<CatalogLeaf> GetPackageDeleteLeafAsync(string leafUrl, CancellationToken token)
=> GetAndValidateLeafAsync<PackageDeleteCatalogLeaf>(CatalogLeafType.PackageDelete, leafUrl, token);
public Task<CatalogLeaf> GetPackageDeleteLeafAsync(string leafUrl, CancellationToken token)
=> GetAndValidateLeafAsync<PackageDeleteCatalogLeaf>(CatalogLeafType.PackageDelete, leafUrl, token);

public Task<CatalogLeaf> GetPackageDetailsLeafAsync(string leafUrl, CancellationToken token)
=> GetAndValidateLeafAsync<PackageDetailsCatalogLeaf>(CatalogLeafType.PackageDetails, leafUrl, token);
public Task<CatalogLeaf> GetPackageDetailsLeafAsync(string leafUrl, CancellationToken token)
=> GetAndValidateLeafAsync<PackageDetailsCatalogLeaf>(CatalogLeafType.PackageDetails, leafUrl, token);

public async Task<CatalogLeaf> GetAndValidateLeafAsync<T>(CatalogLeafType type, string leafUrl, CancellationToken token) where T : CatalogLeaf
public async Task<CatalogLeaf> GetAndValidateLeafAsync<T>(CatalogLeafType type, string leafUrl, CancellationToken token) where T : CatalogLeaf
{
using (_logger.BeginScope(new Dictionary<string, string>
{
{ "type", type.ToString()},
{ "leafUrl", leafUrl},
}))
{
using (_logger.BeginScope(new Dictionary<string, string>
{
{ "type", type.ToString()},
{ "leafUrl", leafUrl},
}))
{
_logger.LogInformation("Getting package leaf: {type}, {leafUrl}", type, leafUrl);
var leaf = await DeserializeUrlAsync<T>(leafUrl, token);

if (leaf is null)
{
throw new InvalidOperationException("Leaf URL: {leafUrl} didn't return a valid leaf object.");
}

if (leaf.Type != type)
{
_logger.LogError("The leaf type found in the document does not match the expected '{type}' type.", type);
}
_logger.LogInformation("Getting package leaf: {type}, {leafUrl}", type, leafUrl);
var leaf = await DeserializeUrlAsync<T>(leafUrl, token);

return leaf!;
if (leaf is null)
{
throw new InvalidOperationException("Leaf URL: {leafUrl} didn't return a valid leaf object.");
}
}

private static T DeserializeBytes<T>(byte[] jsonBytes)
where T : class
{
using var stream = new MemoryStream(jsonBytes);
using var textReader = new StreamReader(stream);
using var jsonReader = new JsonTextReader(textReader);
var result = JsonSerializer.Deserialize<T>(jsonReader);
if (result == null)
if (leaf.Type != type)
{
throw new InvalidOperationException("Deserialization resulted in null");
_logger.LogError("The leaf type found in the document does not match the expected '{type}' type.", type);
}

return result;
return leaf!;
}
}

private async Task<T?> DeserializeUrlAsync<T>(string documentUrl, CancellationToken token)
where T : class
private static T DeserializeBytes<T>(byte[] jsonBytes)
where T : class
{
using var stream = new MemoryStream(jsonBytes);
using var textReader = new StreamReader(stream);
using var jsonReader = new JsonTextReader(textReader);
var result = JsonSerializer.Deserialize<T>(jsonReader);
if (result == null)
{
_logger.LogDebug("Downloading {documentUrl} as a stream.", documentUrl);

using var response = await _httpClient.GetAsync(documentUrl, token);
await using var stream = await response.Content.ReadAsStreamAsync();
using var textReader = new StreamReader(stream);
using var jsonReader = new JsonTextReader(textReader);
var deserializingSpan = SentrySdk.GetSpan()
?.StartChild("json.deserialize", "Deserializing response: " + documentUrl);
try
{
var responseOfT = JsonSerializer.Deserialize<T?>(jsonReader);
deserializingSpan?.Finish(SpanStatus.Ok);
return responseOfT;
}
catch (JsonReaderException e)
{
_logger.LogError(new EventId(0, documentUrl), e, "Failed to deserialize.");
deserializingSpan?.Finish(e);
return default!;
}
throw new InvalidOperationException("Deserialization resulted in null");
}

return result;
}

private async Task<T?> DeserializeUrlAsync<T>(string documentUrl, CancellationToken token)
where T : class
{
_logger.LogDebug("Downloading {documentUrl} as a stream.", documentUrl);

using var response = await _httpClient.GetAsync(documentUrl, token);
await using var stream = await response.Content.ReadAsStreamAsync();
using var textReader = new StreamReader(stream);
using var jsonReader = new JsonTextReader(textReader);
var deserializingSpan = SentrySdk.GetSpan()
?.StartChild("json.deserialize", "Deserializing response: " + documentUrl);
try
{
var responseOfT = JsonSerializer.Deserialize<T?>(jsonReader);
deserializingSpan?.Finish(SpanStatus.Ok);
return responseOfT;
}
catch (JsonReaderException e)
{
_logger.LogError(new EventId(0, documentUrl), e, "Failed to deserialize.");
deserializingSpan?.Finish(e);
return default!;
}
}
}
Loading

0 comments on commit a629bea

Please sign in to comment.