-
Notifications
You must be signed in to change notification settings - Fork 65
New Microsoft.Deployment.DotNet.Releases package #75
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
@NikolaMilosavljevic can you or @dleeapho provide me with perms to add reviewers so I can loop in additional folks that were involved? |
|
@leecow, @terrajobst, @KathleenDollard as an FYI |
src/dotnetreleases/src/Microsoft.Deployment.DotNet.Releases/AspNetCoreReleaseComponent.cs
Outdated
Show resolved
Hide resolved
src/dotnetreleases/src/Microsoft.Deployment.DotNet.Releases/JsonExtensions.cs
Outdated
Show resolved
Hide resolved
src/dotnetreleases/src/Microsoft.Deployment.DotNet.Releases/Product.cs
Outdated
Show resolved
Hide resolved
src/dotnetreleases/src/Microsoft.Deployment.DotNet.Releases/ProductCollection.cs
Show resolved
Hide resolved
src/dotnetreleases/src/Microsoft.Deployment.DotNet.Releases/ReleaseComponent.cs
Outdated
Show resolved
Hide resolved
src/dotnetreleases/src/Microsoft.Deployment.DotNet.Releases/ReleaseVersion.cs
Outdated
Show resolved
Hide resolved
src/dotnetreleases/src/Microsoft.Deployment.DotNet.Releases/ReleaseVersion.cs
Show resolved
Hide resolved
src/dotnetreleases/src/Microsoft.Deployment.DotNet.Releases/ReleaseVersion.cs
Outdated
Show resolved
Hide resolved
|
@NikolaMilosavljevic @MichaelSimons I've finally got round to moving the code/renaming. I had to settle for naming the subset |
|
@joeloff, are the changes ready to be reviewed again? |
I believe so |
| </PropertyGroup> | ||
|
|
||
| <PropertyGroup> | ||
| <LibPrefix Condition="'$(TargetOS)' != 'Windows_NT'">lib</LibPrefix> |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
MichaelSimons
left a comment
There was a problem hiding this 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.
src/Microsoft.Deployment.DotNet.Releases/src/ProductCollection.cs
Outdated
Show resolved
Hide resolved
| } | ||
|
|
||
| using (HttpClient client = new HttpClient()) | ||
| using (var stream = new MemoryStream(await client.GetByteArrayAsync(address))) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
- FileX exists
- User calls DownloadAsync with FileX's path to get a newer version.
- 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.
There was a problem hiding this comment.
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
src/Microsoft.Deployment.DotNet.Releases/tests/ProductCollectionTests.cs
Outdated
Show resolved
Hide resolved
| } | ||
|
|
||
| using (HttpClient client = new HttpClient()) | ||
| using (var stream = new MemoryStream(await client.GetByteArrayAsync(address))) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this 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.Jsoninstead ofNewtonsoft.Json, it would remove a 3rd party dependency - I suggest switching - Are you able to use the
IHttpClientFactoryinstead of newing up anHttpClientand 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
Productclass with static Task-turning methods on it, have anIProductServicethat 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 onIServiceCollectionthat adds the services, so consumers could require anIProductServiceorIReleaseServiceor whatever, into the .ctor of their consuming classes.
|
|
||
| using System; | ||
| using System.Collections.ObjectModel; | ||
| using Newtonsoft.Json.Linq; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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".
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| public async Task<ReadOnlyCollection<ProductRelease>> GetReleasesAsync() | ||
| { | ||
| return await GetReleasesAsync(ReleasesJson, this); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| } = 0; | |
| } |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| } = 0; | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be fixed
| 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; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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; | |
| 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); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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); |
There was a problem hiding this comment.
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
| using (FileStream stream = File.OpenRead(fileName)) | ||
| { | ||
| byte[] checksum = hashAlgorithm.ComputeHash(stream); | ||
|
|
||
| return BitConverter.ToString(checksum).Replace("-", "").ToLowerInvariant(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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(); |
|
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? |
@marcpopMSFT , I agree. We are in good shape API surface-wise and can iterate on follow up issues subsequently. |
|
Yes, @marcpopMSFT and @dleeapho - I would love this, even a preview. 😬 |
|
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? |
I am good with merging. I think we should track the remaining work with issues, gather feedback, and refactor as appropriate. |
|
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 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. |
Preliminary checking for the releases.json API (An immutable DOM for reading release.json data an query .NET releases)