diff --git a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs index 35c5e006..18d0b5e6 100644 --- a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs +++ b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs @@ -449,7 +449,7 @@ public static PagedResults Search(this RedmineManager redmineManager, st { var parameters = CreateSearchParameters(q, limit, offset, searchFilter); - var response = redmineManager.GetPaginatedObjects(parameters); + var response = redmineManager.GetPaginated(new RequestOptions() {QueryString = parameters}); return response; } diff --git a/src/redmine-net-api/Net/RequestOptions.cs b/src/redmine-net-api/Net/RequestOptions.cs index 10f7c77e..1c31f7a6 100644 --- a/src/redmine-net-api/Net/RequestOptions.cs +++ b/src/redmine-net-api/Net/RequestOptions.cs @@ -43,4 +43,20 @@ public sealed class RequestOptions /// /// public string UserAgent { get; set; } + + /// + /// + /// + /// + public RequestOptions Clone() + { + return new RequestOptions + { + QueryString = QueryString != null ? new NameValueCollection(QueryString) : null, + ImpersonateUser = ImpersonateUser, + ContentType = ContentType, + Accept = Accept, + UserAgent = UserAgent + }; + } } \ No newline at end of file diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index 9ddbcdbe..6e10e7b4 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -140,7 +140,7 @@ public T Get(string id, RequestOptions requestOptions = null) public List Get(RequestOptions requestOptions = null) where T : class, new() { - var uri = RedmineApiUrls.GetListFragment(); + var uri = RedmineApiUrls.GetListFragment(requestOptions); return GetInternal(uri, requestOptions); } @@ -149,7 +149,7 @@ public List Get(RequestOptions requestOptions = null) public PagedResults GetPaginated(RequestOptions requestOptions = null) where T : class, new() { - var url = RedmineApiUrls.GetListFragment(); + var url = RedmineApiUrls.GetListFragment(requestOptions); return GetPaginatedInternal(url, requestOptions); } @@ -289,7 +289,7 @@ internal List GetInternal(string uri, RequestOptions requestOptions = null internal PagedResults GetPaginatedInternal(string uri = null, RequestOptions requestOptions = null) where T : class, new() { - uri = uri.IsNullOrWhiteSpace() ? RedmineApiUrls.GetListFragment() : uri; + uri = uri.IsNullOrWhiteSpace() ? RedmineApiUrls.GetListFragment(requestOptions) : uri; var response= ApiClient.Get(uri, requestOptions); diff --git a/src/redmine-net-api/RedmineManagerAsync.cs b/src/redmine-net-api/RedmineManagerAsync.cs index f7060e5a..72e2d6a6 100644 --- a/src/redmine-net-api/RedmineManagerAsync.cs +++ b/src/redmine-net-api/RedmineManagerAsync.cs @@ -30,8 +30,6 @@ namespace Redmine.Net.Api; public partial class RedmineManager: IRedmineManagerAsync { - - /// public async Task CountAsync(RequestOptions requestOptions, CancellationToken cancellationToken = default) where T : class, new() @@ -55,7 +53,7 @@ public async Task CountAsync(RequestOptions requestOptions, Cancellation public async Task> GetPagedAsync(RequestOptions requestOptions = null, CancellationToken cancellationToken = default) where T : class, new() { - var url = RedmineApiUrls.GetListFragment(); + var url = RedmineApiUrls.GetListFragment(requestOptions); var response = await ApiClient.GetAsync(url, requestOptions, cancellationToken).ConfigureAwait(false); @@ -71,67 +69,57 @@ public async Task> GetAsync(RequestOptions requestOptions = null, Can var isLimitSet = false; List resultList = null; - requestOptions ??= new RequestOptions(); - - if (requestOptions.QueryString == null) + var baseRequestOptions = requestOptions != null ? requestOptions.Clone() : new RequestOptions(); + if (baseRequestOptions.QueryString == null) { - requestOptions.QueryString = new NameValueCollection(); + baseRequestOptions.QueryString = new NameValueCollection(); } else { - isLimitSet = int.TryParse(requestOptions.QueryString[RedmineKeys.LIMIT], out pageSize); - int.TryParse(requestOptions.QueryString[RedmineKeys.OFFSET], out offset); + isLimitSet = int.TryParse(baseRequestOptions.QueryString[RedmineKeys.LIMIT], out pageSize); + int.TryParse(baseRequestOptions.QueryString[RedmineKeys.OFFSET], out offset); } if (pageSize == default) { - pageSize = PageSize > 0 - ? PageSize + pageSize = _redmineManagerOptions.PageSize > 0 + ? _redmineManagerOptions.PageSize : RedmineConstants.DEFAULT_PAGE_SIZE_VALUE; - requestOptions.QueryString.Set(RedmineKeys.LIMIT, pageSize.ToInvariantString()); + baseRequestOptions.QueryString.Set(RedmineKeys.LIMIT, pageSize.ToInvariantString()); } - - var hasOffset = RedmineManager.TypesWithOffset.ContainsKey(typeof(T)); + + var hasOffset = TypesWithOffset.ContainsKey(typeof(T)); if (hasOffset) { - requestOptions.QueryString.Set(RedmineKeys.OFFSET, offset.ToInvariantString()); + var firstPageOptions = baseRequestOptions.Clone(); + firstPageOptions.QueryString.Set(RedmineKeys.OFFSET, offset.ToInvariantString()); + var firstPage = await GetPagedAsync(firstPageOptions, cancellationToken).ConfigureAwait(false); - var tempResult = await GetPagedAsync(requestOptions, cancellationToken).ConfigureAwait(false); - - var totalCount = isLimitSet ? pageSize : tempResult.TotalItems; - - if (tempResult?.Items != null) + if (firstPage == null || firstPage.Items == null) { - resultList = new List(tempResult.Items); + return null; } - - var totalPages = (int)Math.Ceiling((double)totalCount / pageSize); - - var remainingPages = totalPages - offset / pageSize; + + var totalCount = isLimitSet ? pageSize : firstPage.TotalItems; + resultList = new List(firstPage.Items); + var totalPages = (int)Math.Ceiling((double)totalCount / pageSize); + var remainingPages = totalPages - 1 - (offset / pageSize); if (remainingPages <= 0) { return resultList; } - + using (var semaphore = new SemaphoreSlim(MAX_CONCURRENT_TASKS)) { var pageFetchTasks = new List>>(); - - for (int page = 0; page < remainingPages; page++) + for (int page = 1; page <= remainingPages; page++) { await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); - - var innerOffset = (page * pageSize) + offset; - - pageFetchTasks.Add(GetPagedInternalAsync(semaphore, new RequestOptions() - { - QueryString = new NameValueCollection() - { - {RedmineKeys.OFFSET, innerOffset.ToInvariantString()}, - {RedmineKeys.LIMIT, pageSize.ToInvariantString()} - } - }, cancellationToken)); + var pageOffset = (page * pageSize) + offset; + var pageRequestOptions = baseRequestOptions.Clone(); + pageRequestOptions.QueryString.Set(RedmineKeys.OFFSET, pageOffset.ToInvariantString()); + pageFetchTasks.Add(GetPagedInternalAsync(semaphore, pageRequestOptions, cancellationToken)); } var pageResults = await @@ -141,30 +129,27 @@ public async Task> GetAsync(RequestOptions requestOptions = null, Can TaskExtensions.WhenAll(pageFetchTasks) #endif .ConfigureAwait(false); - + foreach (var pageResult in pageResults) { if (pageResult?.Items == null) { continue; } - - resultList ??= new List(); - resultList.AddRange(pageResult.Items); } } } else { - var result = await GetPagedAsync(requestOptions, cancellationToken: cancellationToken) + var result = await GetPagedAsync(baseRequestOptions, cancellationToken: cancellationToken) .ConfigureAwait(false); if (result?.Items != null) { return new List(result.Items); } } - + return resultList; } @@ -245,7 +230,7 @@ private async Task> GetPagedInternalAsync(SemaphoreSlim semap { try { - var url = RedmineApiUrls.GetListFragment(); + var url = RedmineApiUrls.GetListFragment(requestOptions); var response = await ApiClient.GetAsync(url, requestOptions, cancellationToken).ConfigureAwait(false); diff --git a/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs b/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs index f51e38ad..253543c7 100644 --- a/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs +++ b/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs @@ -126,7 +126,7 @@ public string Serialize(T entity) where T : class var limit = xmlReader.ReadAttributeAsInt(RedmineKeys.LIMIT); var result = xmlReader.ReadElementContentAsCollection(); - if (totalItems == 0 && result.Count > 0) + if (totalItems == 0 && result?.Count > 0) { totalItems = result.Count; } diff --git a/tests/redmine-net-api.Tests/Bugs/RedmineApi-371.cs b/tests/redmine-net-api.Tests/Bugs/RedmineApi-371.cs index 08b3049d..d83e4db1 100644 --- a/tests/redmine-net-api.Tests/Bugs/RedmineApi-371.cs +++ b/tests/redmine-net-api.Tests/Bugs/RedmineApi-371.cs @@ -1,5 +1,6 @@ using System.Collections.Specialized; using Padi.DotNet.RedmineAPI.Tests.Tests; +using Redmine.Net.Api; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Net; using Redmine.Net.Api.Types; @@ -19,12 +20,13 @@ public RedmineApi371(RedmineApiUrlsFixture fixture) [Fact] public void Should_Return_IssueCategories_For_Project_Url() { + var projectIdAsString = 1.ToInvariantString(); var result = _fixture.Sut.GetListFragment( new RequestOptions { - QueryString = new NameValueCollection{ { "project_id", 1.ToInvariantString() } } + QueryString = new NameValueCollection{ { RedmineKeys.PROJECT_ID, projectIdAsString } } }); - Assert.Equal($"projects/1/issue_categories.{_fixture.Format}", result); + Assert.Equal($"projects/{projectIdAsString}/issue_categories.{_fixture.Format}", result); } } \ No newline at end of file