diff --git a/src/Microsoft.OpenApi/Interfaces/IStreamLoader.cs b/src/Microsoft.OpenApi/Interfaces/IStreamLoader.cs index c3cb9b256..e6438bac1 100644 --- a/src/Microsoft.OpenApi/Interfaces/IStreamLoader.cs +++ b/src/Microsoft.OpenApi/Interfaces/IStreamLoader.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using System.Threading; using System.Threading.Tasks; using Microsoft.OpenApi.Models; @@ -17,7 +18,8 @@ public interface IStreamLoader /// Use Uri to locate data and convert into an input object. /// /// Identifier of some source of an OpenAPI Description + /// The cancellation token. /// A data object that can be processed by a reader to generate an - Task LoadAsync(Uri uri); + Task LoadAsync(Uri uri, CancellationToken cancellationToken = default); } } diff --git a/src/Microsoft.OpenApi/Reader/Services/DefaultStreamLoader.cs b/src/Microsoft.OpenApi/Reader/Services/DefaultStreamLoader.cs index bb230c4a9..ef00c496f 100644 --- a/src/Microsoft.OpenApi/Reader/Services/DefaultStreamLoader.cs +++ b/src/Microsoft.OpenApi/Reader/Services/DefaultStreamLoader.cs @@ -4,6 +4,8 @@ using System; using System.IO; using System.Net.Http; +using System.Runtime.InteropServices; +using System.Threading; using System.Threading.Tasks; using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Models; @@ -27,28 +29,29 @@ public DefaultStreamLoader(Uri baseUrl) this.baseUrl = baseUrl; } - /// - /// Use Uri to locate data and convert into an input object. - /// - /// Identifier of some source of an OpenAPI Description - /// A data object that can be processed by a reader to generate an - /// - public async Task LoadAsync(Uri uri) + /// + public async Task LoadAsync(Uri uri, CancellationToken cancellationToken = default) { - Uri absoluteUri; - absoluteUri = baseUrl.AbsoluteUri.Equals(OpenApiConstants.BaseRegistryUri) ? new Uri(Directory.GetCurrentDirectory() + uri) - : new Uri(baseUrl, uri); + var absoluteUri = (baseUrl.AbsoluteUri.Equals(OpenApiConstants.BaseRegistryUri), baseUrl.IsAbsoluteUri, uri.IsAbsoluteUri) switch + { + (true, _, _) => new Uri(Path.Combine(Directory.GetCurrentDirectory(), uri.ToString())), + // this overcomes a URI concatenation issue for local paths on linux OSes + (_, true, false) when baseUrl.Scheme.Equals("file", StringComparison.OrdinalIgnoreCase) && !RuntimeInformation.IsOSPlatform(OSPlatform.Windows) => + new Uri(Path.Combine(baseUrl.AbsoluteUri, uri.ToString())), + (_, _, _) => new Uri(baseUrl, uri), + }; - switch (absoluteUri.Scheme) + return absoluteUri.Scheme switch { - case "file": - return File.OpenRead(absoluteUri.AbsolutePath); - case "http": - case "https": - return await _httpClient.GetStreamAsync(absoluteUri); - default: - throw new ArgumentException("Unsupported scheme"); - } + "file" => File.OpenRead(absoluteUri.AbsolutePath), + "http" or "https" => +#if NET5_0_OR_GREATER + await _httpClient.GetStreamAsync(absoluteUri, cancellationToken).ConfigureAwait(false), +#else + await _httpClient.GetStreamAsync(absoluteUri).ConfigureAwait(false), +#endif + _ => throw new ArgumentException("Unsupported scheme"), + }; } } } diff --git a/src/Microsoft.OpenApi/Reader/Services/OpenApiWorkspaceLoader.cs b/src/Microsoft.OpenApi/Reader/Services/OpenApiWorkspaceLoader.cs index 06231e75c..32090231f 100644 --- a/src/Microsoft.OpenApi/Reader/Services/OpenApiWorkspaceLoader.cs +++ b/src/Microsoft.OpenApi/Reader/Services/OpenApiWorkspaceLoader.cs @@ -45,8 +45,8 @@ internal async Task LoadAsync(OpenApiReference reference, // If not already in workspace, load it and process references if (!_workspace.Contains(item.ExternalResource)) { - var input = await _loader.LoadAsync(new(item.ExternalResource, UriKind.RelativeOrAbsolute)); - var result = await OpenApiDocument.LoadAsync(input, format, _readerSettings, cancellationToken); + var input = await _loader.LoadAsync(new(item.ExternalResource, UriKind.RelativeOrAbsolute), cancellationToken).ConfigureAwait(false); + var result = await OpenApiDocument.LoadAsync(input, format, _readerSettings, cancellationToken).ConfigureAwait(false); // Merge diagnostics if (result.Diagnostic != null) { @@ -54,7 +54,7 @@ internal async Task LoadAsync(OpenApiReference reference, } if (result.Document != null) { - var loadDiagnostic = await LoadAsync(item, result.Document, format, diagnostic, cancellationToken); + var loadDiagnostic = await LoadAsync(item, result.Document, format, diagnostic, cancellationToken).ConfigureAwait(false); diagnostic = loadDiagnostic; } } diff --git a/test/Microsoft.OpenApi.Readers.Tests/OpenApiReaderTests/OpenApiDiagnosticTests.cs b/test/Microsoft.OpenApi.Readers.Tests/OpenApiReaderTests/OpenApiDiagnosticTests.cs index 667bedbd1..5e065a1e8 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/OpenApiReaderTests/OpenApiDiagnosticTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/OpenApiReaderTests/OpenApiDiagnosticTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using System.Threading; using System.Threading.Tasks; using System; using Microsoft.OpenApi.Models; @@ -64,7 +65,7 @@ public Stream Load(Uri uri) return null; } - public Task LoadAsync(Uri uri) + public Task LoadAsync(Uri uri, CancellationToken cancellationToken = default) { var path = new Uri(new("http://example.org/OpenApiReaderTests/Samples/OpenApiDiagnosticReportMerged/"), uri).AbsolutePath; path = path[1..]; // remove leading slash diff --git a/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs b/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs index 68ecbe33e..a2badc7c8 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Threading; using System.Threading.Tasks; using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Models; @@ -80,7 +81,7 @@ public Stream Load(Uri uri) return null; } - public Task LoadAsync(Uri uri) + public Task LoadAsync(Uri uri, CancellationToken cancellationToken = default) { return Task.FromResult(null); } @@ -93,7 +94,7 @@ public Stream Load(Uri uri) return null; } - public Task LoadAsync(Uri uri) + public Task LoadAsync(Uri uri, CancellationToken cancellationToken = default) { var path = new Uri(new("http://example.org/V3Tests/Samples/OpenApiWorkspace/"), uri).AbsolutePath; path = path[1..]; // remove leading slash diff --git a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt index 273407001..c1fb18800 100644 --- a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt +++ b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt @@ -246,7 +246,7 @@ namespace Microsoft.OpenApi.Interfaces } public interface IStreamLoader { - System.Threading.Tasks.Task LoadAsync(System.Uri uri); + System.Threading.Tasks.Task LoadAsync(System.Uri uri, System.Threading.CancellationToken cancellationToken = default); } } namespace Microsoft.OpenApi @@ -1554,7 +1554,7 @@ namespace Microsoft.OpenApi.Reader.Services public class DefaultStreamLoader : Microsoft.OpenApi.Interfaces.IStreamLoader { public DefaultStreamLoader(System.Uri baseUrl) { } - public System.Threading.Tasks.Task LoadAsync(System.Uri uri) { } + public System.Threading.Tasks.Task LoadAsync(System.Uri uri, System.Threading.CancellationToken cancellationToken = default) { } } } namespace Microsoft.OpenApi.Services