From a8138a8fd4a9e3f7861d4e29d7432063161ae964 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Mon, 22 Nov 2021 20:03:47 -0500 Subject: [PATCH 1/5] Add async implementation --- NGitLab.Mock/Clients/ProjectClient.cs | 16 ++ .../Internals/GitLabCollectionResponse.cs | 35 +++++ NGitLab.Tests/AsyncApiValidation.cs | 50 ++++++ .../Docker/GitLabTestContextRequestOptions.cs | 41 +++++ NGitLab.Tests/ProjectsTests.cs | 30 ++++ NGitLab/Extensions/FunctionRetryExtensions.cs | 30 ++++ NGitLab/IHttpRequestor.cs | 10 ++ NGitLab/IProjectClient.cs | 6 + NGitLab/Impl/GitLabCollectionResponse.cs | 15 ++ NGitLab/Impl/HttpRequestor.GitLabRequest.cs | 81 +++++++--- NGitLab/Impl/HttpRequestor.cs | 144 +++++++++--------- NGitLab/Impl/ProjectClient.cs | 23 ++- NGitLab/NGitLab.csproj | 8 +- NGitLab/PublicAPI.Unshipped.txt | 19 ++- NGitLab/RequestOptions.cs | 15 ++ 15 files changed, 427 insertions(+), 96 deletions(-) create mode 100644 NGitLab.Mock/Internals/GitLabCollectionResponse.cs create mode 100644 NGitLab.Tests/AsyncApiValidation.cs create mode 100644 NGitLab/Impl/GitLabCollectionResponse.cs diff --git a/NGitLab.Mock/Clients/ProjectClient.cs b/NGitLab.Mock/Clients/ProjectClient.cs index 16f42f94..4005ad8d 100644 --- a/NGitLab.Mock/Clients/ProjectClient.cs +++ b/NGitLab.Mock/Clients/ProjectClient.cs @@ -1,6 +1,10 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using NGitLab.Mock.Internals; using NGitLab.Models; namespace NGitLab.Mock.Clients @@ -206,6 +210,13 @@ public Models.Project GetById(int id, SingleProjectQuery query) } } + [SuppressMessage("Design", "MA0042:Do not use blocking calls in an async method", Justification = "Would be an infinite recursion")] + public async Task GetByIdAsync(int id, SingleProjectQuery query, CancellationToken cancellationToken = default) + { + await Task.Yield(); + return GetById(id, query); + } + public IEnumerable GetForks(string id, ForkedProjectQuery query) { using (Context.BeginOperationScope()) @@ -279,5 +290,10 @@ public UploadedProjectFile UploadFile(string id, FormDataContent data) { throw new NotImplementedException(); } + + public GitLabCollectionResponse GetAsync(ProjectQuery query) + { + return GitLabCollectionResponse.Create(Get(query)); + } } } diff --git a/NGitLab.Mock/Internals/GitLabCollectionResponse.cs b/NGitLab.Mock/Internals/GitLabCollectionResponse.cs new file mode 100644 index 00000000..44606148 --- /dev/null +++ b/NGitLab.Mock/Internals/GitLabCollectionResponse.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace NGitLab.Mock.Internals +{ + internal static class GitLabCollectionResponse + { + public static GitLabCollectionResponse Create(IEnumerable items) + { + return new GitLabCollectionResponseImpl(items); + } + + private sealed class GitLabCollectionResponseImpl : GitLabCollectionResponse + { + private readonly IEnumerable _items; + + public GitLabCollectionResponseImpl(IEnumerable items) + { + _items = items; + } + + public override async IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) + { + await Task.Yield(); + foreach (var item in _items) + { + yield return item; + } + } + + public override IEnumerator GetEnumerator() => _items.GetEnumerator(); + } + } +} diff --git a/NGitLab.Tests/AsyncApiValidation.cs b/NGitLab.Tests/AsyncApiValidation.cs new file mode 100644 index 00000000..5538eb89 --- /dev/null +++ b/NGitLab.Tests/AsyncApiValidation.cs @@ -0,0 +1,50 @@ +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; + +namespace NGitLab.Tests +{ + public class AsyncApiValidation + { + [Test] + public void ValidateAsyncMethodSignature() + { + var interfaces = typeof(IGitLabClient).Assembly.GetTypes().Where(t => t.IsInterface && t.IsPublic && t != typeof(IHttpRequestor)); + foreach (var iface in interfaces) + { + foreach (var method in iface.GetMethods()) + { + if (typeof(Task).IsAssignableFrom(method.ReturnType)) + { + // Ensure method that returns a Task takes a CancellationToken + var parameterInfo = method.GetParameters().LastOrDefault(); + if (parameterInfo is null) + { + Assert.Fail($"The method '{method}' must have a parameter of type 'CancellationToken'"); + } + + if (parameterInfo.ParameterType != typeof(CancellationToken)) + { + Assert.Fail($"The parameter '{parameterInfo.Name}' of '{method}' of must be of type 'CancellationToken'"); + } + + if (!string.Equals(parameterInfo.Name, "cancellationToken", System.StringComparison.Ordinal)) + { + Assert.Fail($"The parameter '{parameterInfo.Name}' of '{method}' of must be named 'cancellationToken'"); + } + + // Ensure the parameter is optional + var attr = parameterInfo.GetCustomAttribute(); + if (attr is null) + { + Assert.Fail($"The parameter '{parameterInfo.Name}' of '{method}' must be optional"); + } + } + } + } + } + } +} diff --git a/NGitLab.Tests/Docker/GitLabTestContextRequestOptions.cs b/NGitLab.Tests/Docker/GitLabTestContextRequestOptions.cs index 11790649..a17319e6 100644 --- a/NGitLab.Tests/Docker/GitLabTestContextRequestOptions.cs +++ b/NGitLab.Tests/Docker/GitLabTestContextRequestOptions.cs @@ -70,6 +70,47 @@ public override WebResponse GetResponse(HttpWebRequest request) return response; } + public override async Task GetResponseAsync(HttpWebRequest request, CancellationToken cancellationToken) + { + lock (_allRequests) + { + _allRequests.Add(request); + } + + WebResponse response = null; + + // GitLab is unstable, so let's make sure we don't overload it with many concurrent requests + await s_semaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + try + { + response = await base.GetResponseAsync(request, cancellationToken).ConfigureAwait(false); + } + catch (WebException exception) + { + response = exception.Response; + if (response is HttpWebResponse webResponse) + { + response = new LoggableHttpWebResponse(webResponse); + throw new WebException(exception.Message, exception, exception.Status, response); + } + + throw; + } + finally + { + response = LogRequest(request, response); + } + } + finally + { + s_semaphoreSlim.Release(); + } + + return response; + } + private WebResponse LogRequest(HttpWebRequest request, WebResponse response) { byte[] requestContent = null; diff --git a/NGitLab.Tests/ProjectsTests.cs b/NGitLab.Tests/ProjectsTests.cs index 58f9ea94..793fda95 100644 --- a/NGitLab.Tests/ProjectsTests.cs +++ b/NGitLab.Tests/ProjectsTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Threading; using System.Threading.Tasks; using NGitLab.Models; using NGitLab.Tests.Docker; @@ -11,6 +12,35 @@ namespace NGitLab.Tests { public class ProjectsTests { + [Test] + [NGitLabRetry] + public async Task GetProjectByIdAsync() + { + using var context = await GitLabTestContext.CreateAsync(); + var project = context.CreateProject(); + var projectClient = context.Client.Projects; + + var projectResult = await projectClient.GetByIdAsync(project.Id, new SingleProjectQuery(), CancellationToken.None); + Assert.AreEqual(project.Id, projectResult.Id); + } + + [Test] + [NGitLabRetry] + public async Task GetProjectsAsync() + { + using var context = await GitLabTestContext.CreateAsync(); + var project = context.CreateProject(); + var projectClient = context.Client.Projects; + + var projects = new List(); + await foreach (var item in projectClient.GetAsync(new ProjectQuery())) + { + projects.Add(item); + } + + CollectionAssert.IsNotEmpty(projects); + } + [Test] [NGitLabRetry] public async Task GetOwnedProjects() diff --git a/NGitLab/Extensions/FunctionRetryExtensions.cs b/NGitLab/Extensions/FunctionRetryExtensions.cs index f8b72051..4c6b6433 100644 --- a/NGitLab/Extensions/FunctionRetryExtensions.cs +++ b/NGitLab/Extensions/FunctionRetryExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Threading; +using System.Threading.Tasks; namespace NGitLab.Extensions { @@ -38,5 +39,34 @@ public static T Retry(this Func action, Func predica } } } + + /// + /// Do a retry a number of time on the received action if it fails + /// + public static async Task RetryAsync(this Func> action, Func predicate, TimeSpan waitTime, int maxRetryCount, bool useExponentialBackoff) + { + var retriesLeft = maxRetryCount; + while (true) + { + try + { + return await action().ConfigureAwait(false); + } + catch (Exception ex) when (retriesLeft > 0 && predicate(ex, retriesLeft)) + { + var currentRetry = maxRetryCount - retriesLeft + 1; + Logger?.Invoke($"{ex.Message} -> Internal Retry in {waitTime.TotalMilliseconds.ToStringInvariant()} ms ({currentRetry.ToStringInvariant()} of {maxRetryCount.ToStringInvariant()})..."); + + await Task.Delay(waitTime).ConfigureAwait(false); + + if (useExponentialBackoff) + { + waitTime = waitTime.Add(waitTime); + } + + retriesLeft--; + } + } + } } } diff --git a/NGitLab/IHttpRequestor.cs b/NGitLab/IHttpRequestor.cs index 3cd532c7..773a6e41 100644 --- a/NGitLab/IHttpRequestor.cs +++ b/NGitLab/IHttpRequestor.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.IO; +using System.Threading; +using System.Threading.Tasks; namespace NGitLab { @@ -8,12 +10,20 @@ public interface IHttpRequestor { IEnumerable GetAll(string tailUrl); + GitLabCollectionResponse GetAllAsync(string tailUrl); + void Stream(string tailAPIUrl, Action parser); + Task StreamAsync(string tailAPIUrl, Func parser, CancellationToken cancellationToken); + T To(string tailAPIUrl); + Task ToAsync(string tailAPIUrl, CancellationToken cancellationToken); + void Execute(string tailAPIUrl); + Task ExecuteAsync(string tailAPIUrl, CancellationToken cancellationToken); + IHttpRequestor With(object data); } } diff --git a/NGitLab/IProjectClient.cs b/NGitLab/IProjectClient.cs index 066f7496..1e6ab9c6 100644 --- a/NGitLab/IProjectClient.cs +++ b/NGitLab/IProjectClient.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using NGitLab.Models; namespace NGitLab @@ -25,6 +27,8 @@ public interface IProjectClient /// IEnumerable Get(ProjectQuery query); + GitLabCollectionResponse GetAsync(ProjectQuery query); + Project this[int id] { get; } /// @@ -50,6 +54,8 @@ public interface IProjectClient Project GetById(int id, SingleProjectQuery query); + Task GetByIdAsync(int id, SingleProjectQuery query, CancellationToken cancellationToken = default); + Project Fork(string id, ForkProject forkProject); IEnumerable GetForks(string id, ForkedProjectQuery query); diff --git a/NGitLab/Impl/GitLabCollectionResponse.cs b/NGitLab/Impl/GitLabCollectionResponse.cs new file mode 100644 index 00000000..b4765dc0 --- /dev/null +++ b/NGitLab/Impl/GitLabCollectionResponse.cs @@ -0,0 +1,15 @@ +using System.Collections; +using System.Collections.Generic; +using System.Threading; + +namespace NGitLab +{ + public abstract class GitLabCollectionResponse : IEnumerable, IAsyncEnumerable + { + public abstract IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default); + + public abstract IEnumerator GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/NGitLab/Impl/HttpRequestor.GitLabRequest.cs b/NGitLab/Impl/HttpRequestor.GitLabRequest.cs index 9b1a6623..a6dd68af 100644 --- a/NGitLab/Impl/HttpRequestor.GitLabRequest.cs +++ b/NGitLab/Impl/HttpRequestor.GitLabRequest.cs @@ -2,6 +2,8 @@ using System.IO; using System.Net; using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; using NGitLab.Extensions; using NGitLab.Models; @@ -66,6 +68,16 @@ public WebResponse GetResponse(RequestOptions options) options.IsIncremental); } + public Task GetResponseAsync(RequestOptions options, CancellationToken cancellationToken) + { + Func> getResponseImpl = () => GetResponseImplAsync(options, cancellationToken); + + return getResponseImpl.RetryAsync(options.ShouldRetry, + options.RetryInterval, + options.RetryCount, + options.IsIncremental); + } + private WebResponse GetResponseImpl(RequestOptions options) { try @@ -76,36 +88,57 @@ private WebResponse GetResponseImpl(RequestOptions options) catch (WebException wex) { if (wex.Response == null) - { throw; - } - using var errorResponse = (HttpWebResponse)wex.Response; - string jsonString; - using (var reader = new StreamReader(errorResponse.GetResponseStream())) - { - jsonString = reader.ReadToEnd(); - } + HandleWebException(wex); + throw; + } + } + + private async Task GetResponseImplAsync(RequestOptions options, CancellationToken cancellationToken) + { + try + { + var request = CreateRequest(options); + return await options.GetResponseAsync(request, cancellationToken).ConfigureAwait(false); + } + catch (WebException wex) + { + if (wex.Response == null) + throw; - var errorMessage = ExtractErrorMessage(jsonString, out var parsedError); - var exceptionMessage = - $"GitLab server returned an error ({errorResponse.StatusCode}): {errorMessage}. " + - $"Original call: {Method} {Url}"; + HandleWebException(wex); + throw; + } + } - if (JsonData != null) - { - exceptionMessage += $". With data {JsonData}"; - } + private void HandleWebException(WebException ex) + { + using var errorResponse = (HttpWebResponse)ex.Response; + string jsonString; + using (var reader = new StreamReader(errorResponse.GetResponseStream())) + { + jsonString = reader.ReadToEnd(); + } - throw new GitLabException(exceptionMessage) - { - OriginalCall = Url, - ErrorObject = parsedError, - StatusCode = errorResponse.StatusCode, - ErrorMessage = errorMessage, - MethodType = Method, - }; + var errorMessage = ExtractErrorMessage(jsonString, out var parsedError); + var exceptionMessage = + $"GitLab server returned an error ({errorResponse.StatusCode}): {errorMessage}. " + + $"Original call: {Method} {Url}"; + + if (JsonData != null) + { + exceptionMessage += $". With data {JsonData}"; } + + throw new GitLabException(exceptionMessage) + { + OriginalCall = Url, + ErrorObject = parsedError, + StatusCode = errorResponse.StatusCode, + ErrorMessage = errorMessage, + MethodType = Method, + }; } private HttpWebRequest CreateRequest(RequestOptions options) diff --git a/NGitLab/Impl/HttpRequestor.cs b/NGitLab/Impl/HttpRequestor.cs index 00de97ca..c130d3b8 100644 --- a/NGitLab/Impl/HttpRequestor.cs +++ b/NGitLab/Impl/HttpRequestor.cs @@ -1,9 +1,10 @@ using System; -using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; +using System.Threading; +using System.Threading.Tasks; #if NET45 using System.Reflection; #endif @@ -54,6 +55,11 @@ public virtual void Execute(string tailAPIUrl) Stream(tailAPIUrl, parser: null); } + public virtual async Task ExecuteAsync(string tailAPIUrl, CancellationToken cancellationToken) + { + await StreamAsync(tailAPIUrl, parser: null, cancellationToken).ConfigureAwait(false); + } + public virtual T To(string tailAPIUrl) { var result = default(T); @@ -65,6 +71,17 @@ public virtual T To(string tailAPIUrl) return result; } + public virtual async Task ToAsync(string tailAPIUrl, CancellationToken cancellationToken) + { + var result = default(T); + await StreamAsync(tailAPIUrl, async s => + { + var json = await new StreamReader(s).ReadToEndAsync().ConfigureAwait(false); + result = SimpleJson.DeserializeObject(json); + }, cancellationToken).ConfigureAwait(false); + return result; + } + public Uri GetAPIUrl(string tailAPIUrl) { if (!tailAPIUrl.StartsWith("/", StringComparison.Ordinal)) @@ -97,100 +114,91 @@ public virtual void Stream(string tailAPIUrl, Action parser) } } + public virtual async Task StreamAsync(string tailAPIUrl, Func parser, CancellationToken cancellationToken) + { + var request = new GitLabRequest(GetAPIUrl(tailAPIUrl), _methodType, _data, _apiToken, _options.Sudo); + + using var response = await request.GetResponseAsync(_options, cancellationToken).ConfigureAwait(false); + if (parser != null) + { + using var stream = response.GetResponseStream(); + await parser(stream).ConfigureAwait(false); + } + } + public virtual IEnumerable GetAll(string tailUrl) { return new Enumerable(_apiToken, GetAPIUrl(tailUrl), _options); } - private sealed class Enumerable : IEnumerable + public virtual GitLabCollectionResponse GetAllAsync(string tailUrl) + { + return new Enumerable(_apiToken, GetAPIUrl(tailUrl), _options); + } + + internal sealed class Enumerable : GitLabCollectionResponse { private readonly string _apiToken; private readonly RequestOptions _options; private readonly Uri _startUrl; - public Enumerable(string apiToken, Uri startUrl, RequestOptions options) + internal Enumerable(string apiToken, Uri startUrl, RequestOptions options) { _apiToken = apiToken; _startUrl = startUrl; _options = options; } - public IEnumerator GetEnumerator() + public override async IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) { - return new Enumerator(_apiToken, _startUrl, _options); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - private sealed class Enumerator : IEnumerator - { - private readonly string _apiToken; - private readonly RequestOptions _options; - private readonly List _buffer = new(); - - private Uri _nextUrlToLoad; - private int _index; - - public Enumerator(string apiToken, Uri startUrl, RequestOptions options) + var nextUrlToLoad = _startUrl; + while (nextUrlToLoad != null) { - _apiToken = apiToken; - _nextUrlToLoad = startUrl; - _options = options; + var request = new GitLabRequest(nextUrlToLoad, MethodType.Get, data: null, _apiToken, _options.Sudo); + using var response = await request.GetResponseAsync(_options, cancellationToken).ConfigureAwait(false); + nextUrlToLoad = GetNextPageUrl(response); + + var stream = response.GetResponseStream(); + using var streamReader = new StreamReader(stream); + var responseText = await streamReader.ReadToEndAsync().ConfigureAwait(false); + var deserialized = SimpleJson.DeserializeObject(responseText); + foreach (var item in deserialized) + yield return item; } + } - public void Dispose() + public override IEnumerator GetEnumerator() + { + var nextUrlToLoad = _startUrl; + while (nextUrlToLoad != null) { + var request = new GitLabRequest(nextUrlToLoad, MethodType.Get, data: null, _apiToken, _options.Sudo); + using var response = request.GetResponse(_options); + nextUrlToLoad = GetNextPageUrl(response); + + var stream = response.GetResponseStream(); + using var streamReader = new StreamReader(stream); + var responseText = streamReader.ReadToEnd(); + var deserialized = SimpleJson.DeserializeObject(responseText); + foreach (var item in deserialized) + yield return item; } + } - public bool MoveNext() - { - if (++_index < _buffer.Count) - return true; - - if (_nextUrlToLoad == null) - return false; - - // Empty the buffer and get next batch from GitLab, if any - _index = 0; - _buffer.Clear(); - - var request = new GitLabRequest(_nextUrlToLoad, MethodType.Get, data: null, _apiToken, _options.Sudo); - using (var response = request.GetResponse(_options)) - { - // ; rel="next", ; rel="first", ; rel="last" - var link = response.Headers["Link"] ?? response.Headers["Links"]; - - string[] nextLink = null; - if (!string.IsNullOrEmpty(link)) - { - nextLink = link.Split(',') - .Select(l => l.Split(';')) - .FirstOrDefault(pair => pair[1].Contains("next")); - } - - _nextUrlToLoad = (nextLink != null) ? new Uri(nextLink[0].Trim('<', '>', ' ')) : null; - - var stream = response.GetResponseStream(); - var responseText = new StreamReader(stream).ReadToEnd(); - var deserialized = SimpleJson.DeserializeObject(responseText); - - _buffer.AddRange(deserialized); - } - - return _buffer.Count > 0; - } + private static Uri GetNextPageUrl(WebResponse response) + { + // ; rel="next", ; rel="first", ; rel="last" + var link = response.Headers["Link"] ?? response.Headers["Links"]; - public void Reset() + string[] nextLink = null; + if (!string.IsNullOrEmpty(link)) { - throw new NotSupportedException(); + nextLink = link.Split(',') + .Select(l => l.Split(';')) + .FirstOrDefault(pair => pair[1].Contains("next")); } - public T Current => _buffer[_index]; - - object IEnumerator.Current => Current; + return nextLink != null ? new Uri(nextLink[0].Trim('<', '>', ' ')) : null; } } } diff --git a/NGitLab/Impl/ProjectClient.cs b/NGitLab/Impl/ProjectClient.cs index b8775508..cc7117c1 100644 --- a/NGitLab/Impl/ProjectClient.cs +++ b/NGitLab/Impl/ProjectClient.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Net; using System.Threading; +using System.Threading.Tasks; using NGitLab.Extensions; using NGitLab.Models; @@ -41,7 +42,7 @@ private static bool SupportKeysetPagination(ProjectQuery query) return string.IsNullOrEmpty(query.Search); } - public IEnumerable Get(ProjectQuery query) + private static string CreateGetUrl(ProjectQuery query) { var url = Project.Url; @@ -95,9 +96,21 @@ public IEnumerable Get(ProjectQuery query) url = Utils.AddParameter(url, "min_access_level", (int)query.MinAccessLevel.Value); } + return url; + } + + public IEnumerable Get(ProjectQuery query) + { + var url = CreateGetUrl(query); return _api.Get().GetAll(url); } + public GitLabCollectionResponse GetAsync(ProjectQuery query) + { + var url = CreateGetUrl(query); + return _api.Get().GetAllAsync(url); + } + public Project GetById(int id, SingleProjectQuery query) { var url = Project.Url + "/" + id.ToStringInvariant(); @@ -106,6 +119,14 @@ public Project GetById(int id, SingleProjectQuery query) return _api.Get().To(url); } + public async Task GetByIdAsync(int id, SingleProjectQuery query, CancellationToken cancellationToken = default) + { + var url = Project.Url + "/" + id.ToStringInvariant(); + url = Utils.AddParameter(url, "statistics", query.Statistics); + + return await _api.Get().ToAsync(url, cancellationToken).ConfigureAwait(false); + } + public Project Fork(string id, ForkProject forkProject) { return _api.Post().With(forkProject).To(Project.Url + "/" + id + "/fork"); diff --git a/NGitLab/NGitLab.csproj b/NGitLab/NGitLab.csproj index 2b2fcdb7..e84b70e7 100644 --- a/NGitLab/NGitLab.csproj +++ b/NGitLab/NGitLab.csproj @@ -1,11 +1,12 @@  - net45;netstandard2.0 + net461;netstandard2.0 - + + @@ -13,7 +14,10 @@ runtime; build; native; contentfiles; analyzers + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/NGitLab/PublicAPI.Unshipped.txt b/NGitLab/PublicAPI.Unshipped.txt index e5a001a4..a15de0a7 100644 --- a/NGitLab/PublicAPI.Unshipped.txt +++ b/NGitLab/PublicAPI.Unshipped.txt @@ -1,4 +1,6 @@ -const NGitLab.Impl.GroupsClient.Url = "/groups" -> string +abstract NGitLab.GitLabCollectionResponse.GetAsyncEnumerator(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IAsyncEnumerator +abstract NGitLab.GitLabCollectionResponse.GetEnumerator() -> System.Collections.Generic.IEnumerator +const NGitLab.Impl.GroupsClient.Url = "/groups" -> string const NGitLab.Impl.LabelClient.GroupLabelUrl = "/groups/{0}/labels" -> string const NGitLab.Impl.LabelClient.ProjectLabelUrl = "/projects/{0}/labels" -> string const NGitLab.Impl.NamespacesClient.Url = "/namespaces" -> string @@ -75,6 +77,8 @@ NGitLab.GitLabClient.Snippets.get -> NGitLab.ISnippetClient NGitLab.GitLabClient.SystemHooks.get -> NGitLab.ISystemHookClient NGitLab.GitLabClient.Users.get -> NGitLab.IUserClient NGitLab.GitLabClient.Version.get -> NGitLab.IVersionClient +NGitLab.GitLabCollectionResponse +NGitLab.GitLabCollectionResponse.GitLabCollectionResponse() -> void NGitLab.GitLabException NGitLab.GitLabException.ErrorMessage.get -> string NGitLab.GitLabException.ErrorMessage.set -> void @@ -193,9 +197,13 @@ NGitLab.IGroupVariableClient.this[string key].get -> NGitLab.Models.Variable NGitLab.IGroupVariableClient.Update(string key, NGitLab.Models.VariableUpdate model) -> NGitLab.Models.Variable NGitLab.IHttpRequestor NGitLab.IHttpRequestor.Execute(string tailAPIUrl) -> void +NGitLab.IHttpRequestor.ExecuteAsync(string tailAPIUrl, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task NGitLab.IHttpRequestor.GetAll(string tailUrl) -> System.Collections.Generic.IEnumerable +NGitLab.IHttpRequestor.GetAllAsync(string tailUrl) -> NGitLab.GitLabCollectionResponse NGitLab.IHttpRequestor.Stream(string tailAPIUrl, System.Action parser) -> void +NGitLab.IHttpRequestor.StreamAsync(string tailAPIUrl, System.Func parser, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task NGitLab.IHttpRequestor.To(string tailAPIUrl) -> T +NGitLab.IHttpRequestor.ToAsync(string tailAPIUrl, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task NGitLab.IHttpRequestor.With(object data) -> NGitLab.IHttpRequestor NGitLab.IIssueClient NGitLab.IIssueClient.ClosedBy(int projectId, int issueIid) -> System.Collections.Generic.IEnumerable @@ -547,7 +555,9 @@ NGitLab.Impl.ProjectClient.Create(NGitLab.Models.ProjectCreate project) -> NGitL NGitLab.Impl.ProjectClient.Delete(int id) -> void NGitLab.Impl.ProjectClient.Fork(string id, NGitLab.Models.ForkProject forkProject) -> NGitLab.Models.Project NGitLab.Impl.ProjectClient.Get(NGitLab.Models.ProjectQuery query) -> System.Collections.Generic.IEnumerable +NGitLab.Impl.ProjectClient.GetAsync(NGitLab.Models.ProjectQuery query) -> NGitLab.GitLabCollectionResponse NGitLab.Impl.ProjectClient.GetById(int id, NGitLab.Models.SingleProjectQuery query) -> NGitLab.Models.Project +NGitLab.Impl.ProjectClient.GetByIdAsync(int id, NGitLab.Models.SingleProjectQuery query, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task NGitLab.Impl.ProjectClient.GetForks(string id, NGitLab.Models.ForkedProjectQuery query) -> System.Collections.Generic.IEnumerable NGitLab.Impl.ProjectClient.GetLanguages(string id) -> System.Collections.Generic.Dictionary NGitLab.Impl.ProjectClient.Owned.get -> System.Collections.Generic.IEnumerable @@ -703,7 +713,9 @@ NGitLab.IProjectClient.Create(NGitLab.Models.ProjectCreate project) -> NGitLab.M NGitLab.IProjectClient.Delete(int id) -> void NGitLab.IProjectClient.Fork(string id, NGitLab.Models.ForkProject forkProject) -> NGitLab.Models.Project NGitLab.IProjectClient.Get(NGitLab.Models.ProjectQuery query) -> System.Collections.Generic.IEnumerable +NGitLab.IProjectClient.GetAsync(NGitLab.Models.ProjectQuery query) -> NGitLab.GitLabCollectionResponse NGitLab.IProjectClient.GetById(int id, NGitLab.Models.SingleProjectQuery query) -> NGitLab.Models.Project +NGitLab.IProjectClient.GetByIdAsync(int id, NGitLab.Models.SingleProjectQuery query, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task NGitLab.IProjectClient.GetForks(string id, NGitLab.Models.ForkedProjectQuery query) -> System.Collections.Generic.IEnumerable NGitLab.IProjectClient.GetLanguages(string id) -> System.Collections.Generic.Dictionary NGitLab.IProjectClient.Owned.get -> System.Collections.Generic.IEnumerable @@ -2905,8 +2917,13 @@ static NGitLab.Models.QueryAssigneeId.None.get -> NGitLab.Models.QueryAssigneeId static NGitLab.RequestOptions.Default.get -> NGitLab.RequestOptions virtual NGitLab.Impl.API.CreateRequestor(NGitLab.Impl.MethodType methodType) -> NGitLab.IHttpRequestor virtual NGitLab.Impl.HttpRequestor.Execute(string tailAPIUrl) -> void +virtual NGitLab.Impl.HttpRequestor.ExecuteAsync(string tailAPIUrl, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task virtual NGitLab.Impl.HttpRequestor.GetAll(string tailUrl) -> System.Collections.Generic.IEnumerable +virtual NGitLab.Impl.HttpRequestor.GetAllAsync(string tailUrl) -> NGitLab.GitLabCollectionResponse virtual NGitLab.Impl.HttpRequestor.Stream(string tailAPIUrl, System.Action parser) -> void +virtual NGitLab.Impl.HttpRequestor.StreamAsync(string tailAPIUrl, System.Func parser, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task virtual NGitLab.Impl.HttpRequestor.To(string tailAPIUrl) -> T +virtual NGitLab.Impl.HttpRequestor.ToAsync(string tailAPIUrl, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task virtual NGitLab.RequestOptions.GetResponse(System.Net.HttpWebRequest request) -> System.Net.WebResponse +virtual NGitLab.RequestOptions.GetResponseAsync(System.Net.HttpWebRequest request, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task virtual NGitLab.RequestOptions.ShouldRetry(System.Exception ex, int retryNumber) -> bool diff --git a/NGitLab/RequestOptions.cs b/NGitLab/RequestOptions.cs index 454dfab3..5207f901 100644 --- a/NGitLab/RequestOptions.cs +++ b/NGitLab/RequestOptions.cs @@ -1,6 +1,8 @@ using System; using System.IO; using System.Net; +using System.Threading; +using System.Threading.Tasks; namespace NGitLab { @@ -74,6 +76,19 @@ public virtual WebResponse GetResponse(HttpWebRequest request) return request.GetResponse(); } + public virtual async Task GetResponseAsync(HttpWebRequest request, CancellationToken cancellationToken) + { + CancellationTokenRegistration cancellationTokenRegistration = default; + if (cancellationToken.CanBeCanceled) + { + cancellationTokenRegistration = cancellationToken.Register(() => request.Abort()); + } + + var result = await request.GetResponseAsync().ConfigureAwait(false); + cancellationTokenRegistration.Dispose(); + return result; + } + internal virtual Stream GetRequestStream(HttpWebRequest request) { return request.GetRequestStream(); From 2411ac76d775fbe134b9ece3cb3f15c232382980 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Tue, 23 Nov 2021 10:18:30 -0500 Subject: [PATCH 2/5] Add name validation --- NGitLab.Tests/AsyncApiValidation.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/NGitLab.Tests/AsyncApiValidation.cs b/NGitLab.Tests/AsyncApiValidation.cs index 5538eb89..d7ef4be4 100644 --- a/NGitLab.Tests/AsyncApiValidation.cs +++ b/NGitLab.Tests/AsyncApiValidation.cs @@ -19,6 +19,11 @@ public void ValidateAsyncMethodSignature() { if (typeof(Task).IsAssignableFrom(method.ReturnType)) { + if (!method.Name.EndsWith("Async", System.StringComparison.Ordinal)) + { + Assert.Fail($"The method '{method}' must end with 'Async'"); + } + // Ensure method that returns a Task takes a CancellationToken var parameterInfo = method.GetParameters().LastOrDefault(); if (parameterInfo is null) @@ -43,6 +48,14 @@ public void ValidateAsyncMethodSignature() Assert.Fail($"The parameter '{parameterInfo.Name}' of '{method}' must be optional"); } } + + if (method.ReturnType.IsGenericType && typeof(GitLabCollectionResponse<>).IsAssignableFrom(method.ReturnType.GetGenericTypeDefinition())) + { + if (!method.Name.EndsWith("Async", System.StringComparison.Ordinal)) + { + Assert.Fail($"The method '{method}' must end with 'Async'"); + } + } } } } From da0580db82a835bc2c2ff86f6c1af0724d1fa2f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Tue, 23 Nov 2021 12:18:08 -0500 Subject: [PATCH 3/5] Update NGitLab.Tests/AsyncApiValidation.cs Co-authored-by: Louis Zanella --- NGitLab.Tests/AsyncApiValidation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NGitLab.Tests/AsyncApiValidation.cs b/NGitLab.Tests/AsyncApiValidation.cs index d7ef4be4..561c2380 100644 --- a/NGitLab.Tests/AsyncApiValidation.cs +++ b/NGitLab.Tests/AsyncApiValidation.cs @@ -33,7 +33,7 @@ public void ValidateAsyncMethodSignature() if (parameterInfo.ParameterType != typeof(CancellationToken)) { - Assert.Fail($"The parameter '{parameterInfo.Name}' of '{method}' of must be of type 'CancellationToken'"); + Assert.Fail($"The last parameter of method '{method}' must be of type 'CancellationToken' and named 'cancellationToken'"); } if (!string.Equals(parameterInfo.Name, "cancellationToken", System.StringComparison.Ordinal)) From f2566e6bd3be74c5230ebcabb15376b7db92a372 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Tue, 23 Nov 2021 12:18:23 -0500 Subject: [PATCH 4/5] Update NGitLab.Tests/AsyncApiValidation.cs Co-authored-by: Louis Zanella --- NGitLab.Tests/AsyncApiValidation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NGitLab.Tests/AsyncApiValidation.cs b/NGitLab.Tests/AsyncApiValidation.cs index 561c2380..121fdd5a 100644 --- a/NGitLab.Tests/AsyncApiValidation.cs +++ b/NGitLab.Tests/AsyncApiValidation.cs @@ -38,7 +38,7 @@ public void ValidateAsyncMethodSignature() if (!string.Equals(parameterInfo.Name, "cancellationToken", System.StringComparison.Ordinal)) { - Assert.Fail($"The parameter '{parameterInfo.Name}' of '{method}' of must be named 'cancellationToken'"); + Assert.Fail($"The parameter '{parameterInfo.Name}' of '{method}' must be named 'cancellationToken'"); } // Ensure the parameter is optional From 86ef1530c7303d7e5dc7b7482b9ed2a8f427d77f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Tue, 23 Nov 2021 13:16:23 -0500 Subject: [PATCH 5/5] Update NGitLab/Impl/HttpRequestor.cs --- NGitLab/Impl/HttpRequestor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NGitLab/Impl/HttpRequestor.cs b/NGitLab/Impl/HttpRequestor.cs index c130d3b8..2b565dcb 100644 --- a/NGitLab/Impl/HttpRequestor.cs +++ b/NGitLab/Impl/HttpRequestor.cs @@ -55,9 +55,9 @@ public virtual void Execute(string tailAPIUrl) Stream(tailAPIUrl, parser: null); } - public virtual async Task ExecuteAsync(string tailAPIUrl, CancellationToken cancellationToken) + public virtual Task ExecuteAsync(string tailAPIUrl, CancellationToken cancellationToken) { - await StreamAsync(tailAPIUrl, parser: null, cancellationToken).ConfigureAwait(false); + return StreamAsync(tailAPIUrl, parser: null, cancellationToken); } public virtual T To(string tailAPIUrl)