Skip to content
Draft
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: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ This release does not contain security updates.
### Added

- `RootServicesHelper` was added to assist with processing OSLC Root Services documents. It can help with direct lookups (as long as your URI ends with `/rootservices` or `/rootservices.xml`), can look up a standard `/.well-known/oslc/rootservices.xml` location, or fall back to appending `/rootservices` for legacy systems.
- ⚡️ `OslcQuery.SubmitAsync<T>()` method that returns `IAsyncEnumerable<T>` for lazy, async iteration over OSLC query results with automatic pagination handling.
- `OslcQueryResult.NextPageAsync()` method for async retrieval of the next page of query results.
- ⚡️Samples for IBM Jazz ERM (aka Doors NG), ETM, and EWM were migrated to .NET 10 and tested against Jazz.net. You can run them yourself using `OSLC4Net_SDK\Examples\scripts\test-jazz_net.ps1`.


Expand Down
34 changes: 34 additions & 0 deletions OSLC4Net_SDK/OSLC4Net.Client/Oslc/Resources/OslcQuery.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/*******************************************************************************
* Copyright (c) 2013 IBM Corporation.
* Copyright (c) 2024 Andrii Berezovskyi and OSLC4Net contributors.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
Expand All @@ -13,6 +14,7 @@
* Steve Pitschke - initial API and implementation
*******************************************************************************/

using System.Runtime.CompilerServices;
using OSLC4Net.Core.DotNetRdfProvider;

namespace OSLC4Net.Client.Oslc.Resources;
Expand Down Expand Up @@ -198,6 +200,38 @@ public async Task<OslcQueryResult> Submit()
_rdfHelper);
}

/// <summary>
/// Execute the OSLC query asynchronously and return all results as an async enumerable.
/// Pagination is handled internally and lazily - pages are only fetched as needed.
/// </summary>
/// <typeparam name="T">The type of OSLC resource to return</typeparam>
/// <param name="cancellationToken">Optional cancellation token</param>
/// <returns>An async enumerable of all query results across all pages</returns>
public async IAsyncEnumerable<T> SubmitAsync<T>([EnumeratorCancellation] CancellationToken cancellationToken = default)
{
OslcQueryResult? currentResult = await Submit().ConfigureAwait(false);

while (currentResult != null)
{
cancellationToken.ThrowIfCancellationRequested();

foreach (var member in currentResult.GetMembers<T>())
{
cancellationToken.ThrowIfCancellationRequested();
yield return member;
}

if (currentResult.MoveNext())
{
currentResult = await currentResult.NextPageAsync().ConfigureAwait(false);
}
else
{
currentResult = null;
}
}
}

internal Task<HttpResponseMessage> GetResponseRawAsync()
{
return oslcClient.GetResourceRawAsync(QueryUri.ToString(), OSLCConstants.CT_RDF);
Expand Down
34 changes: 32 additions & 2 deletions OSLC4Net_SDK/OSLC4Net.Client/Oslc/Resources/OslcQueryResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,23 @@ public OslcQueryResult(OslcQuery query, HttpResponseMessage response,
}

private OslcQueryResult(OslcQueryResult prev, DotNetRdfHelper rdfHelper)
: this(prev, GetResponseForPrev(prev), rdfHelper)
{
}

// Helper method to get response for the previous result - used to avoid duplicating logic
private static HttpResponseMessage GetResponseForPrev(OslcQueryResult prev)
{
var query = new OslcQuery(prev);
// FIXME: we should split the data from logic - ctor should not be making calls; one of the methods should return a record with the data.
return query.GetResponseRawAsync().Result;
}

private OslcQueryResult(OslcQueryResult prev, HttpResponseMessage response, DotNetRdfHelper rdfHelper)
{
_rdfHelper = rdfHelper;
_query = new OslcQuery(prev);
// FIXME: we should split the data from logic - ctor should not be making calls; one of the methods should return a record with the data.
_response = _query.GetResponseRawAsync().Result;
_response = response;

_pageNumber = prev._pageNumber + 1;
}
Expand Down Expand Up @@ -129,6 +141,24 @@ public void Reset()
throw new InvalidOperationException();
}

/// <summary>
/// Asynchronously gets the next page of query results.
/// </summary>
/// <returns>
/// The next page of results, or null if there are no more pages.
/// </returns>
public async Task<OslcQueryResult?> NextPageAsync()
{
if (!MoveNext())
{
return null;
}

var nextQuery = new OslcQuery(this);
var response = await nextQuery.GetResponseRawAsync().ConfigureAwait(false);
return new OslcQueryResult(this, response, _rdfHelper);
}

private long? GetTotalCount()
{
InitializeRdf();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
<None Update="data\multiResponseQuery.rdf">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="data\singlePageQuery.rdf">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\OSLC4Net.Client\OSLC4Net.Client.csproj" />
Expand Down
29 changes: 29 additions & 0 deletions OSLC4Net_SDK/Tests/OSLC4Net.Client.Tests/OslcQueryResultTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,35 @@ public async Task OslcQueryMultiResponseIterTest()
await Assert.That(oslcQueryResult.GetMembersUrls().Length).IsEqualTo(20);
}

[Test]
public async Task NextPageAsync_ReturnsNull_WhenNoNextPage_WithNoNextPageHeader()
{
// Arrange - load an RDF response without a next page link
var responseText = await File.ReadAllTextAsync("data/singlePageQuery.rdf").ConfigureAwait(false);
var testQuery = new OslcQuery(new OslcClient(LoggerFactory.CreateLogger<OslcClient>()),
"https://example.com/oslc/query");
var httpResponseMessage = new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(responseText)
};
var oslcQueryResult = new OslcQueryResult(testQuery, httpResponseMessage);

// Act
var nextPage = await oslcQueryResult.NextPageAsync();

// Assert
await Assert.That(nextPage).IsNull();
}

[Test]
public async Task TotalCount_ReturnsCorrectValue()
{
var oslcQueryResult = await GetMockOslcQueryResultMulti();

await Assert.That(oslcQueryResult.TotalCount).IsEqualTo(537);
}

private async Task<OslcQueryResult> GetMockOslcQueryResultMulti()
{
var responseText = await File.ReadAllTextAsync("data/multiResponseQuery.rdf").ConfigureAwait(false);
Expand Down
28 changes: 28 additions & 0 deletions OSLC4Net_SDK/Tests/OSLC4Net.Client.Tests/data/singlePageQuery.rdf
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<rdf:RDF
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:dcterms="http://purl.org/dc/terms/"
xmlns:oslc="http://open-services.net/ns/core#"
xmlns:oslc_cm="http://open-services.net/ns/cm#"
xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#" >
<rdf:Description rdf:about="https://example.com/oslc/query">
<rdfs:member rdf:resource="https://example.com/resource/1"/>
<rdfs:member rdf:resource="https://example.com/resource/2"/>
</rdf:Description>
<rdf:Description rdf:about="https://example.com/oslc/query">
<dcterms:title>Work Items</dcterms:title>
<oslc:totalCount>2</oslc:totalCount>
<rdf:type rdf:resource="http://open-services.net/ns/core#ResponseInfo"/>
</rdf:Description>
<rdf:Description rdf:about="https://example.com/resource/1">
<dcterms:title rdf:parseType="Literal">Test Resource 1</dcterms:title>
<dcterms:identifier rdf:datatype="http://www.w3.org/2001/XMLSchema#string">1</dcterms:identifier>
<oslc:shortId rdf:datatype="http://www.w3.org/2001/XMLSchema#string">1</oslc:shortId>
<rdf:type rdf:resource="http://open-services.net/ns/cm#ChangeRequest"/>
</rdf:Description>
<rdf:Description rdf:about="https://example.com/resource/2">
<dcterms:title rdf:parseType="Literal">Test Resource 2</dcterms:title>
<dcterms:identifier rdf:datatype="http://www.w3.org/2001/XMLSchema#string">2</dcterms:identifier>
<oslc:shortId rdf:datatype="http://www.w3.org/2001/XMLSchema#string">2</oslc:shortId>
<rdf:type rdf:resource="http://open-services.net/ns/cm#ChangeRequest"/>
</rdf:Description>
</rdf:RDF>
Loading