Skip to content

Conversation

@joeloff
Copy link
Member

@joeloff joeloff commented Dec 7, 2020

Preliminary checking for the releases.json API (An immutable DOM for reading release.json data an query .NET releases)

  • net452 has to be supported for now, thus the dependency on Newtonsoft.Json
  • While the releases.json data refers to items such as channels, during the API reviews it was decided to disambiguate some of these overloaded terms.
    • For example, .NET Core 3.1 is considered a Product (in releases-index.json this is called a channel).
    • Each product can have multiple releases, e.g. 3.1.8
    • Each release is composed of multiple components, e.g. runtime, SDKs

@joeloff
Copy link
Member Author

joeloff commented Dec 7, 2020

@NikolaMilosavljevic can you or @dleeapho provide me with perms to add reviewers so I can loop in additional folks that were involved?

@joeloff
Copy link
Member Author

joeloff commented Dec 8, 2020

@leecow, @terrajobst, @KathleenDollard as an FYI

@joeloff
Copy link
Member Author

joeloff commented Jan 13, 2021

@NikolaMilosavljevic @MichaelSimons I've finally got round to moving the code/renaming. I had to settle for naming the subset dotnet_releases as '-' is used to remove subsets and '.' are used to refer to sub-subsets it seems.

@MichaelSimons
Copy link
Member

@joeloff, are the changes ready to be reviewed again?

@joeloff
Copy link
Member Author

joeloff commented Jan 13, 2021

@joeloff, are the changes ready to be reviewed again?

I believe so

</PropertyGroup>

<PropertyGroup>
<LibPrefix Condition="'$(TargetOS)' != 'Windows_NT'">lib</LibPrefix>
Copy link
Member

@eerhardt eerhardt Jan 13, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is all the stuff in this props file necessary? I don't see any usages of these properties from a quick look.

It looks to be duplicated with https://github.com/dotnet/deployment-tools/blob/master/src/installer/Directory.Build.props. Can it be refactored?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's being used right now. @NikolaMilosavljevic when looking at the build pipeline, it seems only WindowsNT debug/release and x86/x64 legs are running, so it should be fine to trim Directory.build.props?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't build any Linux specific native binaries today and don't have a Linux build leg. Feel free to remove this stuff from your props file as you are not building any native projects.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've cleaned up the props file

Copy link
Member

@MichaelSimons MichaelSimons left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes are looking nice. I have a few comments.

}

using (HttpClient client = new HttpClient())
using (var stream = new MemoryStream(await client.GetByteArrayAsync(address)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of var here feels inconsistent with the two surrounding lines which aren't using var. I see this pattern copied in a few other places.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should now be addressed

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still an issue. The use of var here is permitted. What I am pointing out is the inconsistency with the preceding and following lines.


if (!string.Equals(Hash, actualHash, StringComparison.OrdinalIgnoreCase))
{
File.Delete(destinationPath);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The following scenario does not seem to have a desirable behavior.

  1. FileX exists
  2. User calls DownloadAsync with FileX's path to get a newer version.
  3. For some reason the download fails the hash check.

Results: The original FileX was overwritten and deleted.

ExpectedResults: This method should not have any side effects if it failed to download the file. In other words, the original FileX should be preserved.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As per our conversation, I've added an additional method that will download, but not verify and one that will download to a temp file, verify the hash and then either delete or move the temp file depending on the hash results.

I wasn't sure about adding a test because it could turn out to be flaky if there is a problem downloading the file at all

}

using (HttpClient client = new HttpClient())
using (var stream = new MemoryStream(await client.GetByteArrayAsync(address)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still an issue. The use of var here is permitted. What I am pointing out is the inconsistency with the preceding and following lines.

/// <param name="address">The URL pointing to the releases.json file to use.</param>
/// <param name="product">The <see cref="Product"/> to link to the releases.</param>
/// <returns></returns>
public static async Task<ReadOnlyCollection<ProductRelease>> GetReleasesAsync(Uri address, Product product)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the reason this is static? It feels like it should be an product instance method overload which takes an address parameter.

Same comment applies to the next overload.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The non-static one uses the default URL. The static one was intended if users want to mirror the releases.json files on their own server, e.g. an intranet.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that. It is very strange to have a static method of Product that has a Product parameter. What is the reason to not make it an instance method?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way I thought about it is that the instance method would use the default URLs and felt like good symmetry given the static method we have on the ProductCollection

Assuming you're hosting the files on a different server, the releases-index would likely point to local copies of the releases.json.

Maybe we should just remove it for now. The only other use case would be if you knew where a specific release lived and didn't care about the top level products, but only wanted to get the releases information.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see the merit in the scenario. I see this as different than the ProductCollection static method because instance state is not required.

Another option is perhaps the ReleasesJson setter should be public. For local copy scenarios, the user would tweak the ReleasesJson property before calling the instance GetReleasesAsync method.

@terrajobst, can you provide guidance here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ReleasesJson property reflects the URL associated with the product as defined in the releases-index.json. Overriding that seems to defeat the main case of an application working its way through the ProductCollection->Product->Releases.

I'd prefer changing the static to an instance method instead of making the setter public

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@terrajobst this is the last issue we need to close on. Feel free to ping me if you need any detail.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree wit @MichaelSimons. It seems more natural to have them regular instance overloads. It makes sense to me so see some with a URL and some without; this would indicate that URL is optional and if not specified a default is being used.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @terrajobst I'll update the PR

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has now been addressed

/// <param name="product">The <see cref="Product"/> to link to the releases.</param>
/// <returns></returns>
public static async Task<ReadOnlyCollection<ProductRelease>> GetReleasesAsync(Uri address, Product product)
public async Task<ReadOnlyCollection<ProductRelease>> GetReleasesAsync(Uri address, Product product)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was expecting the product parameter would be removed with the change to make this an instance method.

Consider grouping with the other GetReleasesAsync overloads.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are correct. I've fixed this in the last commit.

Copy link
Member

@IEvangelist IEvangelist left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a few nits, and a few larger concerns.

  • Why not use System.Text.Json instead of Newtonsoft.Json, it would remove a 3rd party dependency - I suggest switching
  • Are you able to use the IHttpClientFactory instead of newing up an HttpClient and disposing of it right away, I have concerns about socket exhaustion. Also, what if there is no internet connection - these won't work will it?
  • I have concerns about the static functionality hanging off of what initially look like POCOs, it might be better to separate these into services. For example, instead of a Product class with static Task-turning methods on it, have an IProductService that gets you a collection of products. Also, as a consumer of this library it would be nice if it exposed its services through DI. I'd expect to see an extension method on IServiceCollection that adds the services, so consumers could require an IProductService or IReleaseService or whatever, into the .ctor of their consuming classes.


using System;
using System.Collections.ObjectModel;
using Newtonsoft.Json.Linq;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use System.Text.Json instead of an external 3rd party package?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have to support 4.5.2 for some enterprise scenarios, which takes System.Text.Json off the table for us. If you want the latest copy you can only get that by going, but if you downloaded the files and laid them out on an internal machine you can use the local set of files without having to go online.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you know to download them, that is somewhat unintuitive? How does a consumer know when there is a "latest copy", what if they never have internet connection. The idea of relying on parsing these JSON files is a bit fragile, there are inconsistencies. What about codifying this metadata in the package itself, so that it doesn't need to relying on parsing JSON and making HTTP requests. All the past releases are not changing, maybe this would be a use-case for code generators?

/// The date of the latest release for this product.
/// </summary>
[JsonProperty(PropertyName = "latest-release-date")]
public DateTime? LatestReleaseDate
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious why this is nullable, I'd hope that there is ALWAYS a latest release date? If not, you might want to add something in the property comments as to when consumers might expect that to be null.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of the older data, especially around 1.0 and 1.1 is not always consistent or complete. There's been instances where fields were either absent, empty or explicitly set to null or "null".

Copy link
Member

@IEvangelist IEvangelist Feb 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you're saying, maybe that old data should be cleaned up? If these are JSON files sitting around somewhere, we should update them rather than making fragile models that represent them. Consumers are going to want to know these things.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1.0/1.1 released before the releases.json existed. Some issues in the manifest have been resolved. We could do a scan to see if any quirks remain. @leecow to comment on what our options are around updating the .json files. The other thing to note is that we know there are external consumers that have likely worked around some of the data that is incorrect, so there is risk in fixing the older entries.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, backwards compatibility is always important. This is why I believe relying on the JSON makes this entire approach fragile.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@joeloff - I don't have any material objection to fixing up old data, particularly where it may be leading you to implement problematic workarounds.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @leecow I'll remove the nullable datetime and do a pass over the current data.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've fixed this now.

/// The version of the product, e.g "5.0" or "1.1".
/// </summary>
[JsonProperty(PropertyName = "channel-version")]
public string ProductVersion
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The product version is always {major}.{minor} format, but the other versions follow semantic versioning - is that correct? If so, might be worth calling that out in the comments.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. A specific release e.g. 5.0.0-rc2 will follow semver and will be part of the 5.0 product.

/// The name of the product.
/// </summary>
[JsonProperty(PropertyName = "product")]
public string ProductName
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was curious if this should be an enum instead of a string, thoughts on that?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already rebranded between .NET Core and .NET, and it's certainly possible that could happen again. This data is typically used to drive UIs/ human readable text. If it were an enum, any consumer would need to know what the proper product branding is.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that makes sense - thanks.

Comment on lines 137 to 140
public async Task<ReadOnlyCollection<ProductRelease>> GetReleasesAsync()
{
return await GetReleasesAsync(ReleasesJson, this);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public async Task<ReadOnlyCollection<ProductRelease>> GetReleasesAsync()
{
return await GetReleasesAsync(ReleasesJson, this);
}
public Task<ReadOnlyCollection<ProductRelease>> GetReleasesAsync() =>
GetReleasesAsync(ReleasesJson, this);

You can omit the async and await keywords for single Task returning methods like this. Also, expression bodied member is nice here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should also be fixed now

{
get;
private set;
} = 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
} = 0;
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've addressed this.

{
get;
private set;
} = 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
} = 0;
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be fixed

Comment on lines 42 to 53
using (var httpClient = new HttpClient())
{
HttpRequestMessage httpRequest = new HttpRequestMessage(HttpMethod.Head, address);
HttpResponseMessage httpResponse = await httpClient.SendAsync(httpRequest);

httpResponse.EnsureSuccessStatusCode();

DateTime? onlineLastModified = httpResponse.Content.Headers.LastModified?.DateTime;
FileInfo fileInfo = new FileInfo(fileName);

return fileInfo.LastWriteTime >= onlineLastModified;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
using (var httpClient = new HttpClient())
{
HttpRequestMessage httpRequest = new HttpRequestMessage(HttpMethod.Head, address);
HttpResponseMessage httpResponse = await httpClient.SendAsync(httpRequest);
httpResponse.EnsureSuccessStatusCode();
DateTime? onlineLastModified = httpResponse.Content.Headers.LastModified?.DateTime;
FileInfo fileInfo = new FileInfo(fileName);
return fileInfo.LastWriteTime >= onlineLastModified;
}
using HttpClient httpClient = new();
HttpRequestMessage httpRequest = new(HttpMethod.Head, address);
var httpResponse = await httpClient.SendAsync(httpRequest);
httpResponse.EnsureSuccessStatusCode();
var onlineLastModified = httpResponse.Content.Headers.LastModified?.DateTime;
FileInfo fileInfo = new(fileName);
return fileInfo.LastWriteTime >= onlineLastModified;

Comment on lines 64 to 80
using (var httpClient = new HttpClient())
{
string directory = Path.GetDirectoryName(fileName);

if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}

HttpResponseMessage httpResponse = await httpClient.GetAsync(address);
httpResponse.EnsureSuccessStatusCode();

using (FileStream fileStream = File.Create(fileName))
{
await httpResponse.Content.CopyToAsync(fileStream);
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
using (var httpClient = new HttpClient())
{
string directory = Path.GetDirectoryName(fileName);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
HttpResponseMessage httpResponse = await httpClient.GetAsync(address);
httpResponse.EnsureSuccessStatusCode();
using (FileStream fileStream = File.Create(fileName))
{
await httpResponse.Content.CopyToAsync(fileStream);
}
}
var directory = Path.GetDirectoryName(fileName);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
using HttpClient httpClient = new();
var httpResponse = await httpClient.GetAsync(address);
httpResponse.EnsureSuccessStatusCode();
using FileStream fileStream = File.Create(fileName);
await httpResponse.Content.CopyToAsync(fileStream);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's C# 8.0, which we can't use because of 4.5.2

Comment on lines +111 to +116
using (FileStream stream = File.OpenRead(fileName))
{
byte[] checksum = hashAlgorithm.ComputeHash(stream);

return BitConverter.ToString(checksum).Replace("-", "").ToLowerInvariant();
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
using (FileStream stream = File.OpenRead(fileName))
{
byte[] checksum = hashAlgorithm.ComputeHash(stream);
return BitConverter.ToString(checksum).Replace("-", "").ToLowerInvariant();
}
using FileStream stream = File.OpenRead(fileName);
var checksum = hashAlgorithm.ComputeHash(stream);
return BitConverter.ToString(checksum).Replace("-", "").ToLowerInvariant();

@marcpopMSFT
Copy link
Member

As we have a few folks waiting to get their hands on this package, should we consider checking in with the latest updates from Jacques and filing a bug for any remaining open questions? We should ensure that we aren't going to change the api surface after it's checked in if we can help it but I think most of the feedback has shifted away from the API type changes to coding patterns. @dleeapho, thoughts?

@dleeapho
Copy link

should we consider checking in with the latest updates from Jacques and filing a bug for any remaining open questions? We should ensure that we aren't going to change the api surface after it's checked in if we can help it but I think most of the feedback has shifted away from the API type changes to coding patterns.

@marcpopMSFT , I agree. We are in good shape API surface-wise and can iterate on follow up issues subsequently.

@IEvangelist
Copy link
Member

Yes, @marcpopMSFT and @dleeapho - I would love this, even a preview. 😬

@joeloff
Copy link
Member Author

joeloff commented Feb 12, 2021

I'm going to file issues for the HTTPClient concerns @IEvangelist raised. @NikolaMilosavljevic already opened an issue for Linux builds (we need those for tests).

@MichaelSimons do you have any outstanding concerns or are we ready to sign off for Preview 1 and merge?

@MichaelSimons
Copy link
Member

@MichaelSimons do you have any outstanding concerns or are we ready to sign off for Preview 1 and merge?

I am good with merging. I think we should track the remaining work with issues, gather feedback, and refactor as appropriate.

@IEvangelist
Copy link
Member

IEvangelist commented Feb 12, 2021

One thing that @joeloff and I were just discussing, is that fact that this package doesn't account for or consider .NET Standard releases. For example, it has no knowledge of .NET Standard releases, instead it only knows about .NET Core, .NET and I believe .NET Framework - but .NET Standard is missing. I was somewhat expecting this package to provide instances of ReleaseComponent for each release of .NET Standard 1.0, 1.2, ... 2.1. Does that make sense?

This table is useful, and it would be great if this was somehow codified into the release package - so that consumers could use it to determine compatibility.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants