Skip to content

Fix #371 & small refactoring #384

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

Merged
merged 1 commit into from
May 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/redmine-net-api/Extensions/RedmineManagerExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,7 @@ public static PagedResults<Search> Search(this RedmineManager redmineManager, st
{
var parameters = CreateSearchParameters(q, limit, offset, searchFilter);

var response = redmineManager.GetPaginatedObjects<Search>(parameters);
var response = redmineManager.GetPaginated<Search>(new RequestOptions() {QueryString = parameters});

return response;
}
Expand Down
16 changes: 16 additions & 0 deletions src/redmine-net-api/Net/RequestOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,20 @@ public sealed class RequestOptions
///
/// </summary>
public string UserAgent { get; set; }

/// <summary>
///
/// </summary>
/// <returns></returns>
public RequestOptions Clone()
{
return new RequestOptions
{
QueryString = QueryString != null ? new NameValueCollection(QueryString) : null,
ImpersonateUser = ImpersonateUser,
ContentType = ContentType,
Accept = Accept,
UserAgent = UserAgent
};
}
}
6 changes: 3 additions & 3 deletions src/redmine-net-api/RedmineManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ public T Get<T>(string id, RequestOptions requestOptions = null)
public List<T> Get<T>(RequestOptions requestOptions = null)
where T : class, new()
{
var uri = RedmineApiUrls.GetListFragment<T>();
var uri = RedmineApiUrls.GetListFragment<T>(requestOptions);

return GetInternal<T>(uri, requestOptions);
}
Expand All @@ -149,7 +149,7 @@ public List<T> Get<T>(RequestOptions requestOptions = null)
public PagedResults<T> GetPaginated<T>(RequestOptions requestOptions = null)
where T : class, new()
{
var url = RedmineApiUrls.GetListFragment<T>();
var url = RedmineApiUrls.GetListFragment<T>(requestOptions);

return GetPaginatedInternal<T>(url, requestOptions);
}
Expand Down Expand Up @@ -289,7 +289,7 @@ internal List<T> GetInternal<T>(string uri, RequestOptions requestOptions = null
internal PagedResults<T> GetPaginatedInternal<T>(string uri = null, RequestOptions requestOptions = null)
where T : class, new()
{
uri = uri.IsNullOrWhiteSpace() ? RedmineApiUrls.GetListFragment<T>() : uri;
uri = uri.IsNullOrWhiteSpace() ? RedmineApiUrls.GetListFragment<T>(requestOptions) : uri;

var response= ApiClient.Get(uri, requestOptions);

Expand Down
77 changes: 31 additions & 46 deletions src/redmine-net-api/RedmineManagerAsync.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ namespace Redmine.Net.Api;

public partial class RedmineManager: IRedmineManagerAsync
{


/// <inheritdoc />
public async Task<int> CountAsync<T>(RequestOptions requestOptions, CancellationToken cancellationToken = default)
where T : class, new()
Expand All @@ -55,7 +53,7 @@ public async Task<int> CountAsync<T>(RequestOptions requestOptions, Cancellation
public async Task<PagedResults<T>> GetPagedAsync<T>(RequestOptions requestOptions = null, CancellationToken cancellationToken = default)
where T : class, new()
{
var url = RedmineApiUrls.GetListFragment<T>();
var url = RedmineApiUrls.GetListFragment<T>(requestOptions);

var response = await ApiClient.GetAsync(url, requestOptions, cancellationToken).ConfigureAwait(false);

Expand All @@ -71,67 +69,57 @@ public async Task<List<T>> GetAsync<T>(RequestOptions requestOptions = null, Can
var isLimitSet = false;
List<T> 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<T>(firstPageOptions, cancellationToken).ConfigureAwait(false);

var tempResult = await GetPagedAsync<T>(requestOptions, cancellationToken).ConfigureAwait(false);

var totalCount = isLimitSet ? pageSize : tempResult.TotalItems;

if (tempResult?.Items != null)
if (firstPage == null || firstPage.Items == null)
{
resultList = new List<T>(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<T>(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<Task<PagedResults<T>>>();

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<T>(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<T>(semaphore, pageRequestOptions, cancellationToken));
}

var pageResults = await
Expand All @@ -141,30 +129,27 @@ public async Task<List<T>> GetAsync<T>(RequestOptions requestOptions = null, Can
TaskExtensions.WhenAll(pageFetchTasks)
#endif
.ConfigureAwait(false);

foreach (var pageResult in pageResults)
{
if (pageResult?.Items == null)
{
continue;
}

resultList ??= new List<T>();

resultList.AddRange(pageResult.Items);
}
}
}
else
{
var result = await GetPagedAsync<T>(requestOptions, cancellationToken: cancellationToken)
var result = await GetPagedAsync<T>(baseRequestOptions, cancellationToken: cancellationToken)
.ConfigureAwait(false);
if (result?.Items != null)
{
return new List<T>(result.Items);
}
}

return resultList;
}

Expand Down Expand Up @@ -245,7 +230,7 @@ private async Task<PagedResults<T>> GetPagedInternalAsync<T>(SemaphoreSlim semap
{
try
{
var url = RedmineApiUrls.GetListFragment<T>();
var url = RedmineApiUrls.GetListFragment<T>(requestOptions);

var response = await ApiClient.GetAsync(url, requestOptions, cancellationToken).ConfigureAwait(false);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ public string Serialize<T>(T entity) where T : class
var limit = xmlReader.ReadAttributeAsInt(RedmineKeys.LIMIT);
var result = xmlReader.ReadElementContentAsCollection<T>();

if (totalItems == 0 && result.Count > 0)
if (totalItems == 0 && result?.Count > 0)
{
totalItems = result.Count;
}
Expand Down
6 changes: 4 additions & 2 deletions tests/redmine-net-api.Tests/Bugs/RedmineApi-371.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<IssueCategory>(
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);
}
}
Loading